diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..be4f6f2
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+__pycache__
+*.pyc
+node_modules
+/frontend/dist/
+/backend/server.pem
+/testdata/
+/backend/supervisord/supervisord.log
+/dockerfiles/wba/files/wba/dist
+/package-lock.json
diff --git a/backend/keephelper/__main__.py b/backend/keephelper/__main__.py
new file mode 100644
index 0000000..4906ba6
--- /dev/null
+++ b/backend/keephelper/__main__.py
@@ -0,0 +1,77 @@
+import aiohttp
+from aiohttp import web
+from argparse import ArgumentParser
+import ssl
+import json
+
+
+def options_fetch_blocks(request):
+ return web.Response(headers={ 'Access-Control-Allow-Origin': '*' })
+
+
+async def post_fetch_blocks(request):
+ body = await request.read()
+ body = json.loads(body)
+ proxy_host = body['keepProxyHost']
+ arv_token = body['arvToken']
+ segments = body['segments']
+ use_ssl = body['useSsl'] \
+ if 'useSsl' in body \
+ else True
+ protocol = 'https://' \
+ if use_ssl \
+ else 'http://'
+ name = body['name'] \
+ if 'name' in body \
+ else None
+ content_type = body['contentType'] \
+ if 'contentType' in body \
+ else 'application/octet-stream'
+ res = web.StreamResponse()
+ res.content_type = content_type
+ if name:
+ res.headers['Content-Disposition'] = 'attachment; filename*=UTF-8\'\'"' + name + '"'
+ else:
+ res.headers['Content-Disposition'] = 'inline'
+ res.headers['Access-Control-Allow-Origin'] = '*'
+ await res.prepare(request)
+ async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(verify_ssl=False)) as session:
+ for seg in segments:
+ url = protocol + proxy_host + '/' + seg[0]
+ async with session.get(url, headers={ 'Authorization': 'OAuth2 ' + arv_token }) as response:
+ block = await response.read()
+ block = block[seg[1]:seg[2]]
+ await res.write(block)
+ return res
+
+
+def get_index(request):
+ return web.Response(text='Use /fetch-blocks to stream files from Keep')
+
+
+def create_parser():
+ parser = ArgumentParser()
+ parser.add_argument('--port', type=int, default=50080)
+ parser.add_argument('--ssl-cert', type=str, default=None)
+ return parser
+
+
+def main():
+ parser = create_parser()
+ args = parser.parse_args()
+ app = web.Application()
+ app.add_routes([
+ web.get('/', get_index),
+ web.post('/fetch-blocks', post_fetch_blocks),
+ web.options('/fetch-blocks', options_fetch_blocks)
+ ])
+ if args.ssl_cert:
+ ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
+ ssl_context.load_cert_chain(args.ssl_cert)
+ else:
+ ssl_context = None
+ web.run_app(app, port=args.port, ssl_context=ssl_context)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/backend/nginx/conf/nginx.conf b/backend/nginx/conf/nginx.conf
new file mode 100644
index 0000000..5a530be
--- /dev/null
+++ b/backend/nginx/conf/nginx.conf
@@ -0,0 +1,42 @@
+error_log /dev/null;
+pid nginx.pid;
+daemon off;
+
+events {
+
+}
+
+http {
+ include /pstore/data/data_science/app/modules/nginx-1.17.8/conf/mime.types;
+
+ access_log /dev/null;
+
+
+ server {
+ listen 50080;
+ server_name rkalbhpc002.kau.roche.com;
+
+ client_body_temp_path tmp/client_body_temp;
+ fastcgi_temp_path tmp/fastcgi_temp;
+ proxy_temp_path tmp/proxy_temp;
+ scgi_temp_path tmp/scgi_temp;
+ uwsgi_temp_path tmp/uwsgi_temp;
+
+ if ( $request_uri ~ \.(js|css|html|woff|ttf|svg|woff2|eot|svg)$ ) {
+ break;
+ }
+ if ( $request_uri ~ ^/fetch-blocks$ ) {
+ break;
+ }
+ rewrite .* /index.html;
+
+ location / {
+ root /pstore/home/adaszews/workspace/arvados-workbench-advanced/frontend/dist;
+ index index.html;
+ }
+
+ location /fetch-blocks {
+ proxy_pass http://localhost:12358/fetch-blocks;
+ }
+ }
+}
diff --git a/backend/srv.py b/backend/srv.py
new file mode 100644
index 0000000..b1bae77
--- /dev/null
+++ b/backend/srv.py
@@ -0,0 +1,15 @@
+import BaseHTTPServer, SimpleHTTPServer
+import ssl
+
+class RequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+ def do_GET(self):
+ print(self.path)
+ if '.' not in self.path:
+ self.path = '/'
+ SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
+ # RequestHandler, self).do_GET()
+
+httpd = BaseHTTPServer.HTTPServer(('0.0.0.0', 4445), RequestHandler)
+httpd.socket = ssl.wrap_socket (httpd.socket, certfile='/pstore/home/adaszews/workspace/arvados-workbench-advanced/backend/server.pem', server_side=True)
+httpd.serve_forever()
+
diff --git a/backend/supervisord/supervisord.conf b/backend/supervisord/supervisord.conf
new file mode 100644
index 0000000..9089d9e
--- /dev/null
+++ b/backend/supervisord/supervisord.conf
@@ -0,0 +1,4 @@
+[supervisord]
+
+[program:wba-nginx]
+command=/pstore/data/data_science/app/modules/nginx-1.17.8/sbin/nginx -p /pstore/home/adaszews/workspace/arvados-workbench-advanced/backend/nginx
diff --git a/dockerfiles/wba/Dockerfile b/dockerfiles/wba/Dockerfile
new file mode 100644
index 0000000..62bbf90
--- /dev/null
+++ b/dockerfiles/wba/Dockerfile
@@ -0,0 +1,10 @@
+FROM alpine:3.11.3
+
+RUN apk update && \
+ apk add nginx gettext supervisor
+
+COPY files/wba /wba
+
+RUN chown -R nginx /wba/nginx/tmp /wba/nginx/run
+
+ENTRYPOINT /bin/sh -c "cd /wba/supervisord/run && /usr/bin/supervisord -c /wba/supervisord/supervisord.conf"
diff --git a/dockerfiles/wba/files/default.conf b/dockerfiles/wba/files/default.conf
new file mode 100644
index 0000000..cebeb6e
--- /dev/null
+++ b/dockerfiles/wba/files/default.conf
@@ -0,0 +1,18 @@
+# This is a default site configuration which will simply return 404, preventing
+# chance access to any other virtualhost.
+
+server {
+ listen ${PORT0} default_server;
+ listen [::]:${PORT0} default_server;
+
+ # Everything is a 404
+ location / {
+ return 404;
+ }
+
+ # You may need this to prevent return 404 recursion.
+ location = /404.html {
+ internal;
+ }
+}
+
diff --git a/dockerfiles/wba/files/wba/nginx/conf/nginx.conf b/dockerfiles/wba/files/wba/nginx/conf/nginx.conf
new file mode 100644
index 0000000..751c07d
--- /dev/null
+++ b/dockerfiles/wba/files/wba/nginx/conf/nginx.conf
@@ -0,0 +1,41 @@
+error_log /dev/null;
+pid run/nginx.pid;
+daemon off;
+
+events {
+
+}
+
+http {
+ include /etc/nginx/mime.types;
+
+ access_log /dev/null;
+
+ server {
+ listen ${PORT0};
+ server_name wba.ecaas.emea.roche.com;
+
+ client_body_temp_path tmp/client_body_temp;
+ fastcgi_temp_path tmp/fastcgi_temp;
+ proxy_temp_path tmp/proxy_temp;
+ scgi_temp_path tmp/scgi_temp;
+ uwsgi_temp_path tmp/uwsgi_temp;
+
+ if ( $request_uri ~ \.(js|css|html|woff|ttf|svg|woff2|eot|svg)$ ) {
+ break;
+ }
+ if ( $request_uri ~ ^/fetch-blocks$ ) {
+ break;
+ }
+ rewrite .* /index.html;
+
+ location / {
+ root /wba/dist;
+ index index.html;
+ }
+
+ location /fetch-blocks {
+ proxy_pass http://localhost:12358/fetch-blocks;
+ }
+ }
+}
diff --git a/dockerfiles/wba/files/wba/nginx/run/.keep b/dockerfiles/wba/files/wba/nginx/run/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/dockerfiles/wba/files/wba/nginx/tmp/.keep b/dockerfiles/wba/files/wba/nginx/tmp/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/dockerfiles/wba/files/wba/supervisord/run/.keep b/dockerfiles/wba/files/wba/supervisord/run/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/dockerfiles/wba/files/wba/supervisord/supervisord.conf b/dockerfiles/wba/files/wba/supervisord/supervisord.conf
new file mode 100644
index 0000000..ec5975a
--- /dev/null
+++ b/dockerfiles/wba/files/wba/supervisord/supervisord.conf
@@ -0,0 +1,8 @@
+[supervisord]
+logfile=/dev/null
+nodaemon=true
+user=root
+
+[program:wba-nginx]
+command=/usr/sbin/nginx -p /wba/nginx -c conf/nginx.conf
+user=nginx
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..e671a40
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,731 @@
+{
+ "requires": true,
+ "lockfileVersion": 1,
+ "dependencies": {
+ "@babel/code-frame": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz",
+ "integrity": "sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g==",
+ "requires": {
+ "@babel/highlight": "7.8.3"
+ }
+ },
+ "@babel/highlight": {
+ "version": "7.8.3",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.8.3.tgz",
+ "integrity": "sha512-PX4y5xQUvy0fnEVHrYOarRPXVWafSjTW9T0Hab8gVIawpl2Sj0ORyrygANq+KjcNlSSTw0YCLSNA8OyZ1I4yEg==",
+ "requires": {
+ "chalk": "2.4.2",
+ "esutils": "2.0.3",
+ "js-tokens": "4.0.0"
+ }
+ },
+ "@babel/runtime": {
+ "version": "7.8.4",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz",
+ "integrity": "sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==",
+ "requires": {
+ "regenerator-runtime": "0.13.3"
+ }
+ },
+ "@fortawesome/fontawesome-free": {
+ "version": "5.12.0",
+ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-5.12.0.tgz",
+ "integrity": "sha512-vKDJUuE2GAdBERaQWmmtsciAMzjwNrROXA5KTGSZvayAsmuTGjam5z6QNqNPCwDfVljLWuov1nEC3mEQf/n6fQ=="
+ },
+ "@types/estree": {
+ "version": "0.0.39",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz",
+ "integrity": "sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw=="
+ },
+ "@types/node": {
+ "version": "13.7.2",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.7.2.tgz",
+ "integrity": "sha512-uvilvAQbdJvnSBFcKJ2td4016urcGvsiR+N4dHGU87ml8O2Vl6l+ErOi9w0kXSPiwJ1AYlIW+0pDXDWWMOiWbw=="
+ },
+ "acorn": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz",
+ "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw=="
+ },
+ "acorn-dynamic-import": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/acorn-dynamic-import/-/acorn-dynamic-import-4.0.0.tgz",
+ "integrity": "sha512-d3OEjQV4ROpoflsnUA8HozoIR504TFxNivYEUi6uwz0IYhBkTDXGuWlNdMtybRt3nqVx/L6XqMt0FxkXuWKZhw=="
+ },
+ "acorn-jsx": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.1.0.tgz",
+ "integrity": "sha512-tMUqwBWfLFbJbizRmEcWSLw6HnFzfdJs2sOJEOwwtVPMoH/0Ay+E703oZz78VSXZiiDcZrQ5XKjPIUQixhmgVw=="
+ },
+ "align-text": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz",
+ "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=",
+ "requires": {
+ "kind-of": "3.2.2",
+ "longest": "1.0.1",
+ "repeat-string": "1.6.1"
+ }
+ },
+ "ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "requires": {
+ "color-convert": "1.9.3"
+ }
+ },
+ "argparse": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+ "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+ "requires": {
+ "sprintf-js": "1.0.3"
+ }
+ },
+ "bootstrap": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.4.1.tgz",
+ "integrity": "sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA=="
+ },
+ "buble": {
+ "version": "0.19.8",
+ "resolved": "https://registry.npmjs.org/buble/-/buble-0.19.8.tgz",
+ "integrity": "sha512-IoGZzrUTY5fKXVkgGHw3QeXFMUNBFv+9l8a4QJKG1JhG3nCMHTdEX1DCOg8568E2Q9qvAQIiSokv6Jsgx8p2cA==",
+ "requires": {
+ "acorn": "6.4.0",
+ "acorn-dynamic-import": "4.0.0",
+ "acorn-jsx": "5.1.0",
+ "chalk": "2.4.2",
+ "magic-string": "0.25.6",
+ "minimist": "1.2.0",
+ "os-homedir": "2.0.0",
+ "regexpu-core": "4.6.0"
+ }
+ },
+ "builtin-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-2.0.0.tgz",
+ "integrity": "sha512-3U5kUA5VPsRUA3nofm/BXX7GVHKfxz0hOBAPxXrIvHzlDRkQVqEn6yi8QJegxl4LzOHLdvb7XF5dVawa/VVYBg=="
+ },
+ "camelcase": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
+ "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk="
+ },
+ "center-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz",
+ "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=",
+ "requires": {
+ "align-text": "0.1.4",
+ "lazy-cache": "1.0.4"
+ }
+ },
+ "chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "requires": {
+ "ansi-styles": "3.2.1",
+ "escape-string-regexp": "1.0.5",
+ "supports-color": "5.5.0"
+ }
+ },
+ "cliui": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz",
+ "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=",
+ "requires": {
+ "center-align": "0.1.3",
+ "right-align": "0.1.3",
+ "wordwrap": "0.0.2"
+ }
+ },
+ "color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "requires": {
+ "color-name": "1.1.3"
+ }
+ },
+ "color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
+ },
+ "colors": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz",
+ "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="
+ },
+ "commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
+ },
+ "commenting": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/commenting/-/commenting-1.0.5.tgz",
+ "integrity": "sha512-U7qGbcDLSNpOcV3RQRKHp7hFpy9WUmfawbkPdS4R2RhrSu4dOF85QQpx/Zjcv7uLF6tWSUKEKUIkxknPCrVjwg=="
+ },
+ "crypto-js": {
+ "version": "3.1.9-1",
+ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz",
+ "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg="
+ },
+ "decamelize": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
+ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
+ },
+ "escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
+ },
+ "esprima": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
+ "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
+ },
+ "estree-walker": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-0.6.1.tgz",
+ "integrity": "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="
+ },
+ "esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
+ },
+ "exec-sh": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.2.2.tgz",
+ "integrity": "sha512-FIUCJz1RbuS0FKTdaAafAByGS0CPvU3R0MeHxgtl+djzCc//F8HakL8GzmVNZanasTbTAY/3DRFA0KpVqj/eAw==",
+ "requires": {
+ "merge": "1.2.1"
+ }
+ },
+ "filesize": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/filesize/-/filesize-6.0.1.tgz",
+ "integrity": "sha512-u4AYWPgbI5GBhs6id1KdImZWn5yfyFrrQ8OWZdN7ZMfA8Bf4HcO0BGo9bmUIEV8yrp8I1xVfJ/dn90GtFNNJcg=="
+ },
+ "fs-extra": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz",
+ "integrity": "sha1-N5TzeMWLNC6n27sjCVEJxLO2IpE=",
+ "requires": {
+ "graceful-fs": "4.2.3",
+ "jsonfile": "3.0.1",
+ "universalify": "0.1.2"
+ }
+ },
+ "graceful-fs": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.3.tgz",
+ "integrity": "sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ=="
+ },
+ "has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
+ },
+ "history": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz",
+ "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==",
+ "requires": {
+ "@babel/runtime": "7.8.4",
+ "loose-envify": "1.4.0",
+ "resolve-pathname": "3.0.0",
+ "tiny-invariant": "1.1.0",
+ "tiny-warning": "1.0.3",
+ "value-equal": "1.0.1"
+ }
+ },
+ "is-buffer": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz",
+ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="
+ },
+ "is-module": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz",
+ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE="
+ },
+ "jest-worker": {
+ "version": "24.9.0",
+ "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-24.9.0.tgz",
+ "integrity": "sha512-51PE4haMSXcHohnSMdM42anbvZANYTqMrr52tVKPqqsPJMzoP6FYYDVqahX/HrAoKEKz3uUPzSvKs9A3qR4iVw==",
+ "requires": {
+ "merge-stream": "2.0.0",
+ "supports-color": "6.1.0"
+ },
+ "dependencies": {
+ "supports-color": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
+ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ }
+ }
+ },
+ "jquery": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz",
+ "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw=="
+ },
+ "js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
+ },
+ "js-uuid": {
+ "version": "0.0.6",
+ "resolved": "https://registry.npmjs.org/js-uuid/-/js-uuid-0.0.6.tgz",
+ "integrity": "sha1-uxb2gkeOnvrCYrRczCqa31Mypb4="
+ },
+ "js-yaml": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+ "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+ "requires": {
+ "argparse": "1.0.10",
+ "esprima": "4.0.1"
+ }
+ },
+ "jsesc": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
+ "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0="
+ },
+ "jsonfile": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz",
+ "integrity": "sha1-pezG9l9T9mLEQVx2daAzHQmS7GY=",
+ "requires": {
+ "graceful-fs": "4.2.3"
+ }
+ },
+ "kind-of": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz",
+ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=",
+ "requires": {
+ "is-buffer": "1.1.6"
+ }
+ },
+ "lazy-cache": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz",
+ "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4="
+ },
+ "linkstate": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/linkstate/-/linkstate-1.1.1.tgz",
+ "integrity": "sha512-5SICdxQG9FpWk44wSEoM2WOCUNuYfClp10t51XAIV5E7vUILF/dTYxf0vJw6bW2dUd2wcQon+LkNtRijpNLrig=="
+ },
+ "lodash": {
+ "version": "4.17.9",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.9.tgz",
+ "integrity": "sha512-vuRLquvot5sKUldMBumG0YqLvX6m/RGBBOmqb3CWR/MC/QvvD1cTH1fOqxz2FJAQeoExeUdX5Gu9vP2EP6ik+Q=="
+ },
+ "longest": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz",
+ "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc="
+ },
+ "loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "requires": {
+ "js-tokens": "4.0.0"
+ }
+ },
+ "magic-string": {
+ "version": "0.25.6",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.6.tgz",
+ "integrity": "sha512-3a5LOMSGoCTH5rbqobC2HuDNRtE2glHZ8J7pK+QZYppyWA36yuNpsX994rIY2nCuyP7CZYy7lQq/X2jygiZ89g==",
+ "requires": {
+ "sourcemap-codec": "1.4.8"
+ }
+ },
+ "merge": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/merge/-/merge-1.2.1.tgz",
+ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ=="
+ },
+ "merge-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
+ "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="
+ },
+ "minimist": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
+ "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ="
+ },
+ "mkdirp": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
+ "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
+ "requires": {
+ "minimist": "0.0.8"
+ },
+ "dependencies": {
+ "minimist": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
+ "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
+ }
+ }
+ },
+ "moment": {
+ "version": "2.22.2",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.22.2.tgz",
+ "integrity": "sha1-PCV/mDn8DpP/UxSWMiOeuQeD/2Y="
+ },
+ "os-homedir": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-2.0.0.tgz",
+ "integrity": "sha512-saRNz0DSC5C/I++gFIaJTXoFJMRwiP5zHar5vV3xQ2TkgEw6hDCcU5F272JjUylpiVgBrZNQHnfjkLabTfb92Q=="
+ },
+ "papaya-viewer": {
+ "version": "1.0.1449",
+ "resolved": "https://registry.npmjs.org/papaya-viewer/-/papaya-viewer-1.0.1449.tgz",
+ "integrity": "sha512-LdbvmsXlPkKfKB/BiHHdmtutHnps+erm81tnwBr2sNnise65o9T2td8pMGqRfV4FxFScuMEawCNUALLj2+qAKg=="
+ },
+ "path-parse": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
+ "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
+ },
+ "popper.js": {
+ "version": "1.16.1",
+ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz",
+ "integrity": "sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ=="
+ },
+ "preact": {
+ "version": "8.5.3",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-8.5.3.tgz",
+ "integrity": "sha512-O3kKP+1YdgqHOFsZF2a9JVdtqD+RPzCQc3rP+Ualf7V6rmRDchZ9MJbiGTT7LuyqFKZqlHSOyO/oMFmI2lVTsw=="
+ },
+ "preact-router": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/preact-router/-/preact-router-2.6.1.tgz",
+ "integrity": "sha512-Ql3fptQ8hiioIw5zUcWUq5NShl7yFR4e6KBUzLbGI7+HKMIgBnH+aOITN5IrY1rbr2vhKXBdHdd9nLbbjcJTOQ=="
+ },
+ "regenerate": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.0.tgz",
+ "integrity": "sha512-1G6jJVDWrt0rK99kBjvEtziZNCICAuvIPkSiUFIQxVP06RCVpq3dmDo2oi6ABpYaDYaTRr67BEhL8r1wgEZZKg=="
+ },
+ "regenerate-unicode-properties": {
+ "version": "8.1.0",
+ "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.1.0.tgz",
+ "integrity": "sha512-LGZzkgtLY79GeXLm8Dp0BVLdQlWICzBnJz/ipWUgo59qBaZ+BHtq51P2q1uVZlppMuUAT37SDk39qUbjTWB7bA==",
+ "requires": {
+ "regenerate": "1.4.0"
+ }
+ },
+ "regenerator-runtime": {
+ "version": "0.13.3",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
+ "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw=="
+ },
+ "regexpu-core": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.6.0.tgz",
+ "integrity": "sha512-YlVaefl8P5BnFYOITTNzDvan1ulLOiXJzCNZxduTIosN17b87h3bvG9yHMoHaRuo88H4mQ06Aodj5VtYGGGiTg==",
+ "requires": {
+ "regenerate": "1.4.0",
+ "regenerate-unicode-properties": "8.1.0",
+ "regjsgen": "0.5.1",
+ "regjsparser": "0.6.2",
+ "unicode-match-property-ecmascript": "1.0.4",
+ "unicode-match-property-value-ecmascript": "1.1.0"
+ }
+ },
+ "regjsgen": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.1.tgz",
+ "integrity": "sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg=="
+ },
+ "regjsparser": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.2.tgz",
+ "integrity": "sha512-E9ghzUtoLwDekPT0DYCp+c4h+bvuUpe6rRHCTYn6eGoqj1LgKXxT6I0Il4WbjhQkOghzi/V+y03bPKvbllL93Q==",
+ "requires": {
+ "jsesc": "0.5.0"
+ }
+ },
+ "repeat-string": {
+ "version": "1.6.1",
+ "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
+ "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc="
+ },
+ "resolve": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.15.0.tgz",
+ "integrity": "sha512-+hTmAldEGE80U2wJJDC1lebb5jWqvTYAfm3YZ1ckk1gBr0MnCqUKlwK1e+anaFljIl+F5tR5IoZcm4ZDA1zMQw==",
+ "requires": {
+ "path-parse": "1.0.6"
+ }
+ },
+ "resolve-pathname": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz",
+ "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng=="
+ },
+ "right-align": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz",
+ "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=",
+ "requires": {
+ "align-text": "0.1.4"
+ }
+ },
+ "rollup": {
+ "version": "0.66.6",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.66.6.tgz",
+ "integrity": "sha512-J7/SWanrcb83vfIHqa8+aVVGzy457GcjA6GVZEnD0x2u4OnOd0Q1pCrEoNe8yLwM6z6LZP02zBT2uW0yh5TqOw==",
+ "requires": {
+ "@types/estree": "0.0.39",
+ "@types/node": "13.7.2"
+ }
+ },
+ "rollup-plugin-buble": {
+ "version": "0.19.8",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-buble/-/rollup-plugin-buble-0.19.8.tgz",
+ "integrity": "sha512-8J4zPk2DQdk3rxeZvxgzhHh/rm5nJkjwgcsUYisCQg1QbT5yagW+hehYEW7ZNns/NVbDCTv4JQ7h4fC8qKGOKw==",
+ "requires": {
+ "buble": "0.19.8",
+ "rollup-pluginutils": "2.8.2"
+ }
+ },
+ "rollup-plugin-copy": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-copy/-/rollup-plugin-copy-0.2.3.tgz",
+ "integrity": "sha1-2sGrgdHyILrrmOXEwBCCUuHtu5g=",
+ "requires": {
+ "colors": "1.4.0",
+ "fs-extra": "3.0.1"
+ }
+ },
+ "rollup-plugin-includepaths": {
+ "version": "0.2.3",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-includepaths/-/rollup-plugin-includepaths-0.2.3.tgz",
+ "integrity": "sha512-4QbSIZPDT+FL4SViEVCRi4cGCA64zQJu7u5qmCkO3ecHy+l9EQBsue15KfCpddfb6Br0q47V/v2+E2YUiqts9g=="
+ },
+ "rollup-plugin-license": {
+ "version": "0.7.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-license/-/rollup-plugin-license-0.7.0.tgz",
+ "integrity": "sha512-KoZxV+UxBUaubo3mu7IHjMFryCuZIU8Q9tm8GLUl/lz6DQCEJUEgcp+urItEuux8xa7M0Qx7Fjoe4g3s9hsUFg==",
+ "requires": {
+ "commenting": "1.0.5",
+ "lodash": "4.17.9",
+ "magic-string": "0.25.0",
+ "mkdirp": "0.5.1",
+ "moment": "2.22.2"
+ },
+ "dependencies": {
+ "magic-string": {
+ "version": "0.25.0",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.0.tgz",
+ "integrity": "sha512-Msbwa9oNYNPjwVh9ury5X2BHbTFWoirTlzuf4X+pIoSOQVKNRJHXTx1WmKYuXzRM4QZFv8dGXyZvhDMmWhGLPw==",
+ "requires": {
+ "sourcemap-codec": "1.4.8"
+ }
+ }
+ }
+ },
+ "rollup-plugin-minify": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-minify/-/rollup-plugin-minify-1.0.3.tgz",
+ "integrity": "sha1-PGTb4ytVJXDrJg+gIflAEOWRkLI=",
+ "requires": {
+ "uglify-js": "2.8.29"
+ }
+ },
+ "rollup-plugin-node-resolve": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-node-resolve/-/rollup-plugin-node-resolve-3.4.0.tgz",
+ "integrity": "sha512-PJcd85dxfSBWih84ozRtBkB731OjXk0KnzN0oGp7WOWcarAFkVa71cV5hTJg2qpVsV2U8EUwrzHP3tvy9vS3qg==",
+ "requires": {
+ "builtin-modules": "2.0.0",
+ "is-module": "1.0.0",
+ "resolve": "1.15.0"
+ }
+ },
+ "rollup-plugin-uglify": {
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/rollup-plugin-uglify/-/rollup-plugin-uglify-6.0.4.tgz",
+ "integrity": "sha512-ddgqkH02klveu34TF0JqygPwZnsbhHVI6t8+hGTcYHngPkQb5MIHI0XiztXIN/d6V9j+efwHAqEL7LspSxQXGw==",
+ "requires": {
+ "@babel/code-frame": "7.8.3",
+ "jest-worker": "24.9.0",
+ "serialize-javascript": "2.1.2",
+ "uglify-js": "3.8.0"
+ },
+ "dependencies": {
+ "source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
+ },
+ "uglify-js": {
+ "version": "3.8.0",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.0.tgz",
+ "integrity": "sha512-ugNSTT8ierCsDHso2jkBHXYrU8Y5/fY2ZUprfrJUiD7YpuFvV4jODLFmb3h4btQjqr5Nh4TX4XtgDfCU1WdioQ==",
+ "requires": {
+ "commander": "2.20.3",
+ "source-map": "0.6.1"
+ }
+ }
+ }
+ },
+ "rollup-pluginutils": {
+ "version": "2.8.2",
+ "resolved": "https://registry.npmjs.org/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz",
+ "integrity": "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==",
+ "requires": {
+ "estree-walker": "0.6.1"
+ }
+ },
+ "serialize-javascript": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
+ "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ=="
+ },
+ "source-map": {
+ "version": "0.5.7",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
+ "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
+ },
+ "sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="
+ },
+ "sprintf-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+ "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
+ },
+ "streamsaver": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/streamsaver/-/streamsaver-2.0.3.tgz",
+ "integrity": "sha512-IpXeZ67YxcsrfZHe3yg/IyZ5KPfRSn1teDy5mRX2e8M6K410NcJNcR+SFQ2Z92DO36VBUArQP4Vy3Qu33MwIOQ=="
+ },
+ "supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "requires": {
+ "has-flag": "3.0.0"
+ }
+ },
+ "tiny-invariant": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz",
+ "integrity": "sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw=="
+ },
+ "tiny-warning": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
+ "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
+ },
+ "uglify-js": {
+ "version": "2.8.29",
+ "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz",
+ "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=",
+ "requires": {
+ "source-map": "0.5.7",
+ "uglify-to-browserify": "1.0.2",
+ "yargs": "3.10.0"
+ }
+ },
+ "uglify-to-browserify": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
+ "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=",
+ "optional": true
+ },
+ "unicode-canonical-property-names-ecmascript": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz",
+ "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ=="
+ },
+ "unicode-match-property-ecmascript": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz",
+ "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==",
+ "requires": {
+ "unicode-canonical-property-names-ecmascript": "1.0.4",
+ "unicode-property-aliases-ecmascript": "1.0.5"
+ }
+ },
+ "unicode-match-property-value-ecmascript": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.1.0.tgz",
+ "integrity": "sha512-hDTHvaBk3RmFzvSl0UVrUmC3PuW9wKVnpoUDYH0JDkSIovzw+J5viQmeYHxVSBptubnr7PbH2e0fnpDRQnQl5g=="
+ },
+ "unicode-property-aliases-ecmascript": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.0.5.tgz",
+ "integrity": "sha512-L5RAqCfXqAwR3RriF8pM0lU0w4Ryf/GgzONwi6KnL1taJQa7x1TCxdJnILX59WIGOwR57IVxn7Nej0fz1Ny6fw=="
+ },
+ "universalify": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz",
+ "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="
+ },
+ "value-equal": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
+ "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw=="
+ },
+ "watch": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/watch/-/watch-1.0.2.tgz",
+ "integrity": "sha1-NApxe952Vyb6CqB9ch4BR6VR3ww=",
+ "requires": {
+ "exec-sh": "0.2.2",
+ "minimist": "1.2.0"
+ }
+ },
+ "web-streams-polyfill": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-2.0.6.tgz",
+ "integrity": "sha512-nXOi4fBykO4LzyQhZX3MAGib635KGZBoNTkNXrNIkz0zthEf2QokEWxRb0H632xNLDWtHFb1R6dFGzksjYMSDw=="
+ },
+ "window-size": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
+ "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0="
+ },
+ "wordwrap": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
+ "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8="
+ },
+ "yargs": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
+ "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
+ "requires": {
+ "camelcase": "1.2.1",
+ "cliui": "2.1.0",
+ "decamelize": "1.2.0",
+ "window-size": "0.1.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100755
index 0000000..1d7576b
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,32 @@
+{
+ "dependencies": {
+ "@fortawesome/fontawesome-free": "^5.12.0",
+ "bootstrap": "^4.4.1",
+ "crypto-js": "^3.1.9-1",
+ "filesize": "^6.0.1",
+ "history": "^4.10.1",
+ "jquery": "^3.4.1",
+ "js-uuid": "0.0.6",
+ "js-yaml": "^3.13.1",
+ "linkstate": "^1.1.1",
+ "papaya-viewer": "^1.0.1449",
+ "popper.js": "^1.16.1",
+ "preact": "^8.2.9",
+ "preact-router": "^2.6.1",
+ "rollup": "^0.66.6",
+ "rollup-plugin-buble": "^0.19.2",
+ "rollup-plugin-copy": "^0.2.3",
+ "rollup-plugin-includepaths": "^0.2.3",
+ "rollup-plugin-license": "^0.7.0",
+ "rollup-plugin-minify": "^1.0.3",
+ "rollup-plugin-node-resolve": "^3.3.0",
+ "rollup-plugin-uglify": "^6.0.4",
+ "streamsaver": "^2.0.3",
+ "watch": "^1.0.2",
+ "web-streams-polyfill": "^2.0.6"
+ },
+ "scripts": {
+ "rollup": "rollup -c",
+ "watch": "watch \"rollup -c\" src"
+ }
+}
diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js
new file mode 100755
index 0000000..e23c411
--- /dev/null
+++ b/frontend/rollup.config.js
@@ -0,0 +1,67 @@
+import resolve from 'rollup-plugin-node-resolve'
+import buble from 'rollup-plugin-buble';
+import copy from 'rollup-plugin-copy';
+import includePaths from 'rollup-plugin-includepaths';
+import license from 'rollup-plugin-license';
+import { uglify } from "rollup-plugin-uglify";
+
+export default {
+ // dest: 'dist/js/app.js',
+ input: 'src/js/index.js',
+ output: {
+ file: 'dist/js/app.min.js',
+ name: 'WBADV',
+ format: 'umd',
+ sourceMap: true
+ },
+ plugins: [
+ includePaths({
+ paths: ['src/js', 'src/js/widget', 'src/js/misc', 'src/js/component',
+ 'src/js/page', 'src/js/dialog', 'src/js/arvados/base',
+ 'src/js/arvados/collection', 'src/js/arvados/process']
+ }),
+ copy({
+ 'src/html/index.html': 'dist/index.html',
+ 'src/css/index.css': 'dist/css/index.css',
+ 'node_modules/bootstrap/dist/css/bootstrap.min.css': 'dist/css/bootstrap.min.css',
+ 'node_modules/bootstrap/dist/js/bootstrap.min.js': 'dist/js/bootstrap.min.js',
+ 'node_modules/jquery/dist/jquery.min.js': 'dist/js/jquery.min.js',
+ 'node_modules/@fortawesome/fontawesome-free/js/fontawesome.min.js': 'dist/js/fontawesome.min.js',
+ 'node_modules/@fortawesome/fontawesome-free/css/all.min.css': 'dist/css/all.min.css',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.eot': 'dist/webfonts/fa-regular-400.eot',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.svg': 'dist/webfonts/fa-regular-400.svg',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.ttf': 'dist/webfonts/fa-regular-400.ttf',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff': 'dist/webfonts/fa-regular-400.woff',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-regular-400.woff2': 'dist/webfonts/fa-regular-400.woff2',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.eot': 'dist/webfonts/fa-solid-900.eot',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.svg': 'dist/webfonts/fa-solid-900.svg',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.ttf': 'dist/webfonts/fa-solid-900.ttf',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff': 'dist/webfonts/fa-solid-900.woff',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2': 'dist/webfonts/fa-solid-900.woff2',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.eot': 'dist/webfonts/fa-brands-400.eot',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.svg': 'dist/webfonts/fa-brands-400.svg',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.ttf': 'dist/webfonts/fa-brands-400.ttf',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff': 'dist/webfonts/fa-brands-400.woff',
+ 'node_modules/@fortawesome/fontawesome-free/webfonts/fa-brands-400.woff2': 'dist/webfonts/fa-brands-400.woff2',
+ 'node_modules/js-uuid/js-uuid.js': 'dist/js/js-uuid.js',
+ 'node_modules/filesize/lib/filesize.js': 'dist/js/filesize.js',
+ 'node_modules/crypto-js/core.js': 'dist/js/crypto-js/core.js',
+ 'node_modules/crypto-js/md5.js': 'dist/js/crypto-js/md5.js',
+ 'src/js/worker/wb-manifest-worker.js': 'dist/js/wb-manifest-worker.js',
+ 'node_modules/js-yaml/dist/js-yaml.min.js': 'dist/js/js-yaml.min.js',
+ 'node_modules/streamsaver/mitm.html': 'dist/mitm.html',
+ 'node_modules/streamsaver/sw.js': 'dist/sw.js',
+ 'src/js/thirdparty/StreamSaver.js': 'dist/js/StreamSaver.js',
+ 'node_modules/web-streams-polyfill/dist/ponyfill.js': 'dist/js/web-streams-polyfill/ponyfill.js',
+ 'node_modules/papaya-viewer/release/current/standard/papaya.js': 'dist/js/papaya.js',
+ 'node_modules/papaya-viewer/release/current/standard/papaya.css': 'dist/css/papaya.css',
+ verbose: true
+ }),
+ buble({jsx: 'h', objectAssign: 'Object.assign'}),
+ resolve({}),
+ license({
+ banner: 'Copyright (C) Stanislaw Adaszewski, 2020.\nContact: s.adaszewski@gmail.com\nAll Rights Reserved.',
+ }),
+ // uglify()
+ ]
+}
diff --git a/frontend/src/css/index.css b/frontend/src/css/index.css
new file mode 100755
index 0000000..2c49b82
--- /dev/null
+++ b/frontend/src/css/index.css
@@ -0,0 +1,27 @@
+pre.word-wrap {
+ white-space: pre-wrap; /* Since CSS 2.1 */
+ white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
+ white-space: -pre-wrap; /* Opera 4-6 */
+ white-space: -o-pre-wrap; /* Opera 7 */
+ word-wrap: break-word; /* Internet Explorer 5.5+ */
+}
+
+pre.terminal {
+ background: black;
+ color: #aaa;
+ height: 600px;
+ width: 100%;
+}
+
+.w-1 {
+ width: 1px !important;
+}
+
+div.wb-json-viewer {
+ font-family: "Courier New", fixed-width;
+ white-space: pre-wrap;
+}
+
+textarea.wb-json-editor {
+ font-family: "Courier New", fixed-width;
+}
diff --git a/frontend/src/html/index.html b/frontend/src/html/index.html
new file mode 100755
index 0000000..e7e1be6
--- /dev/null
+++ b/frontend/src/html/index.html
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/frontend/src/js/arvados/base/arvados-object-name.js b/frontend/src/js/arvados/base/arvados-object-name.js
new file mode 100644
index 0000000..d39bb02
--- /dev/null
+++ b/frontend/src/js/arvados/base/arvados-object-name.js
@@ -0,0 +1,20 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import arvadosTypeName from 'arvados-type-name';
+
+function arvadosObjectName(item) {
+ let typeName = arvadosTypeName(item['uuid']);
+ if (typeName === 'user')
+ return (item.first_name + ' ' + item.last_name);
+ else if (typeName === 'container')
+ return ('Container running image ' + item.container_image);
+ else
+ return item.name;
+}
+
+export default arvadosObjectName;
diff --git a/frontend/src/js/arvados/base/arvados-type-name.js b/frontend/src/js/arvados/base/arvados-type-name.js
new file mode 100644
index 0000000..19750ce
--- /dev/null
+++ b/frontend/src/js/arvados/base/arvados-type-name.js
@@ -0,0 +1,28 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+const typeIdToName = {
+ 'tpzed': 'user',
+ 'j7d0g': 'group',
+ 'xvhdp': 'container_request',
+ 'dz642': 'container',
+ '7fd4e': 'workflow',
+ 'ozdt8': 'api_client',
+ '4zz18': 'collection'
+};
+
+function arvadosTypeName(id) {
+ if (!id)
+ return;
+
+ if (id.length === 5)
+ return typeIdToName[id];
+ else
+ return typeIdToName[id.split('-')[1]];
+}
+
+export default arvadosTypeName;
diff --git a/frontend/src/js/arvados/base/detect-hashes.js b/frontend/src/js/arvados/base/detect-hashes.js
new file mode 100644
index 0000000..4b94ae2
--- /dev/null
+++ b/frontend/src/js/arvados/base/detect-hashes.js
@@ -0,0 +1,34 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function detectHashes(obj) {
+ let Q = [ obj ];
+ let matches = {};
+
+ while (Q.length > 0) {
+ let item = Q.pop();
+
+ if (!item)
+ continue;
+
+ if (typeof(item) === 'string') {
+ // use regexes
+ let rx = /[a-f0-9]{32}\+[0-9]+/g;
+ for (let m = rx.exec(item); m; m = rx.exec(item))
+ matches[m[0]] = true;
+
+ } else if (typeof(item) === 'object') {
+ Object.keys(item).map(k => Q.push(item[k]));
+ }
+ }
+
+ matches = Object.keys(matches);
+
+ return matches;
+}
+
+export default detectHashes;
diff --git a/frontend/src/js/arvados/base/detect-uuids.js b/frontend/src/js/arvados/base/detect-uuids.js
new file mode 100644
index 0000000..600c284
--- /dev/null
+++ b/frontend/src/js/arvados/base/detect-uuids.js
@@ -0,0 +1,34 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function detectUuids(obj) {
+ let Q = [ obj ];
+ let matches = {};
+
+ while (Q.length > 0) {
+ let item = Q.pop();
+
+ if (!item)
+ continue;
+
+ if (typeof(item) === 'string') {
+ // use regexes
+ let rx = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/g;
+ for (let m = rx.exec(item); m; m = rx.exec(item))
+ matches[m[0]] = true;
+
+ } else if (typeof(item) === 'object') {
+ Object.keys(item).map(k => Q.push(item[k]));
+ }
+ }
+
+ matches = Object.keys(matches);
+
+ return matches;
+}
+
+export default detectUuids;
diff --git a/frontend/src/js/arvados/base/fetch-object-parents.js b/frontend/src/js/arvados/base/fetch-object-parents.js
new file mode 100644
index 0000000..64bc0f0
--- /dev/null
+++ b/frontend/src/js/arvados/base/fetch-object-parents.js
@@ -0,0 +1,51 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+
+function fetchObjectParents(arvHost, arvToken, uuid) {
+ let parents = [];
+
+ let cb = xhr => {
+ const item = xhr.response.items[0];
+ if (!item)
+ return parents.reverse();
+
+ item.name = arvadosObjectName(item);
+ parents.push(item);
+
+ if (!item.owner_uuid ||
+ item.owner_uuid.endsWith('-tpzed-000000000000000')) {
+
+ return parents.reverse();
+ }
+
+ const objectType = arvadosTypeName(item.owner_uuid);
+ const filters = [
+ ['uuid', '=', item.owner_uuid]
+ ];
+
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + objectType + 's' +
+ '?filters=' + encodeURIComponent(JSON.stringify(filters))).then(cb);
+ };
+
+ const objectType = arvadosTypeName(uuid);
+ const filters = [
+ ['uuid', '=', uuid]
+ ];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + objectType + 's' +
+ '?filters=' + encodeURIComponent(JSON.stringify(filters)));
+ prom = prom.then(cb);
+
+ return prom;
+}
+
+export default fetchObjectParents;
diff --git a/frontend/src/js/arvados/base/make-arvados-request.js b/frontend/src/js/arvados/base/make-arvados-request.js
new file mode 100644
index 0000000..1da9d77
--- /dev/null
+++ b/frontend/src/js/arvados/base/make-arvados-request.js
@@ -0,0 +1,67 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import wbApplyPromiseOrdering from 'wb-apply-promise-ordering';
+
+const requestPromiseOrdering = {};
+
+function makeArvadosRequest(arvHost, arvToken, endpoint, params={}) {
+ const defaultParams = {
+ 'method': 'GET',
+ 'data': null,
+ 'contentType': 'application/json;charset=utf-8',
+ 'responseType': 'json',
+ 'useSsl': true,
+ 'requireToken': true,
+ 'onProgress': () => {},
+ 'promiseOrdering': true,
+ 'expectedStatus': 200
+ };
+
+ Object.keys(defaultParams).map(k => (params[k] =
+ (k in params ? params[k] : defaultParams[k])));
+ let { method, data, contentType, responseType,
+ useSsl, requireToken, onProgress, promiseOrdering,
+ expectedStatus } = params;
+
+ if (!(arvHost && (arvToken || !requireToken)))
+ return new Promise((accept, reject) => reject());
+
+ let xhr = new XMLHttpRequest();
+ xhr.open(method, (useSsl ? 'https://' : 'http://') + arvHost + endpoint);
+ if (arvToken)
+ xhr.setRequestHeader('Authorization', 'OAuth2 ' + arvToken);
+ if (data !== null)
+ xhr.setRequestHeader('Content-Type', contentType);
+ xhr.responseType = responseType;
+
+ xhr.onprogress = onProgress;
+
+ let prom = new Promise((accept, reject) => {
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState !== 4)
+ return;
+
+ if ((expectedStatus instanceof Array) &&
+ expectedStatus.indexOf(xhr.status) !== -1) {
+ accept(xhr);
+ } else if (expectedStatus === xhr.status) {
+ accept(xhr);
+ } else {
+ reject(xhr);
+ }
+ };
+ xhr.send(data);
+ });
+
+ if (promiseOrdering)
+ prom = wbApplyPromiseOrdering(prom, requestPromiseOrdering);
+
+ return prom;
+}
+
+export default makeArvadosRequest;
diff --git a/frontend/src/js/arvados/base/url-for-object.js b/frontend/src/js/arvados/base/url-for-object.js
new file mode 100644
index 0000000..d8f208a
--- /dev/null
+++ b/frontend/src/js/arvados/base/url-for-object.js
@@ -0,0 +1,35 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import arvadosTypeName from 'arvados-type-name';
+
+function urlForObject(item, mode='primary') {
+ let objectType = arvadosTypeName(item.uuid.split('-')[1]);
+ if (objectType === 'user')
+ return ('/browse/' + item.uuid);
+ else if (objectType === 'group' && item.group_class === 'project') {
+ if (mode === 'properties')
+ return ('/project/' + item.uuid);
+ else
+ return ('/browse/' + item.uuid);
+ } else if (objectType === 'container_request')
+ return ('/process/' + item.uuid);
+ else if (objectType === 'workflow') {
+ if (mode === 'launch')
+ return ('/workflow-launch/' + item.uuid)
+ else
+ return ('/workflow/' + item.uuid);
+ } else if (objectType === 'collection') {
+ if (mode === 'primary' || mode === 'browse')
+ return ('/collection-browse/' + item.uuid);
+ else
+ return ('/collection/' + item.uuid);
+ } else if (objectType === 'container')
+ return ('/container/' + item.uuid);
+}
+
+export default urlForObject;
diff --git a/frontend/src/js/arvados/base/wb-delete-object.js b/frontend/src/js/arvados/base/wb-delete-object.js
new file mode 100644
index 0000000..6492ce3
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-delete-object.js
@@ -0,0 +1,18 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+function wbDeleteObject(arvHost, arvToken, uuid) {
+ const typeName = arvadosTypeName(uuid);
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' +
+ uuid, { 'method': 'DELETE' });
+}
+
+export default wbDeleteObject;
diff --git a/frontend/src/js/arvados/base/wb-fetch-objects.js b/frontend/src/js/arvados/base/wb-fetch-objects.js
new file mode 100644
index 0000000..627bb65
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-fetch-objects.js
@@ -0,0 +1,45 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import arvadosTypeName from 'arvados-type-name';
+import makeArvadosRequest from 'make-arvados-request';
+
+function wbFetchObjects(arvHost, arvToken, uuids) {
+ const unique = {};
+ uuids.map(u => (unique[u] = true));
+ uuids = {};
+ Object.keys(unique).map(u => {
+ let typeName = arvadosTypeName(u);
+ if (!typeName) {
+ console.log('Unknown type name for UUID: ' + u);
+ return;
+ }
+ if (!(typeName in uuids))
+ uuids[typeName] = [];
+ uuids[typeName].push(u);
+ });
+
+ const lookup = {};
+ let prom = new Promise(accept => accept());
+ for (let typeName in uuids) {
+ let filters = [
+ ['uuid', 'in', uuids[typeName]]
+ ];
+ prom = prom.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's?filters=' +
+ encodeURIComponent(JSON.stringify(filters))));
+ prom = prom.then(xhr => xhr.response.items.map(it => (
+ lookup[it.uuid] = it)));
+ prom = prom.catch(xhr => console.log(xhr.responseURL + ': ' + xhr.statusText));
+ }
+
+ prom = prom.then(() => lookup);
+
+ return prom;
+}
+
+export default wbFetchObjects;
diff --git a/frontend/src/js/arvados/base/wb-id-tools.js b/frontend/src/js/arvados/base/wb-id-tools.js
new file mode 100644
index 0000000..6d7b58c
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-id-tools.js
@@ -0,0 +1,79 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import arvadosTypeName from 'arvados-type-name';
+
+const UUID_PATTERN = '[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}';
+const PDH_PATTERN = '[a-f0-9]{32}\\+[0-9]+';
+
+const UUID_REGEX = new RegExp(UUID_PATTERN);
+const PDH_REGEX = new RegExp(PDH_PATTERN);
+
+const UUID_REGEX_G = new RegExp(UUID_PATTERN, 'g');
+const PDH_REGEX_G = new RegExp(PDH_PATTERN, 'g');
+
+class WBIdTools {
+ static isIdentifier(value) {
+ return ( this.isUuid(value) || this.isPDH(value) );
+ }
+
+ static isUuid(value) {
+ const m = UUID_REGEX.exec(value);
+ return (m && m[0] === value);
+ }
+
+ static isPDH(value) {
+ const m = PDH_REGEX.exec(value);
+ return (m && m[0] === value);
+ }
+
+ static startsWithIdentifier(value) {
+ return ( this.startsWithUuid(value) || this.startsWithPDH(value) );
+ }
+
+ static startsWithUuid(value) {
+ const m = UUID_REGEX.exec(value);
+ return ( m && m.index === 0 );
+ }
+
+ static startsWithPDH(value) {
+ const m = PDH_REGEX.exec(value);
+ return ( m && m.index === 0 );
+ }
+
+ static detectIdentifiers(value) {
+ return this.detectUuids(value).concat(this.detectPDHs(value));
+ }
+
+ static detectUuids(value) {
+ let m;
+ const res = [];
+ while (m = UUID_REGEX_G.exec(value)) {
+ res.push(m);
+ }
+ return res;
+ }
+
+ static detectPDHs(value) {
+ let m;
+ const res = [];
+ while (m = PDH_REGEX_G.exec(value)) {
+ res.push(m);
+ }
+ return res;
+ }
+
+ static typeName(value) {
+ if (this.isPDH(value))
+ return 'collection';
+ if (this.isUuid(value))
+ return arvadosTypeName(value);
+ throw Error('Given value is neither an UUID nor a PDH: ' + value);
+ }
+}
+
+export default WBIdTools;
diff --git a/frontend/src/js/arvados/base/wb-move-object.js b/frontend/src/js/arvados/base/wb-move-object.js
new file mode 100644
index 0000000..96f1521
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-move-object.js
@@ -0,0 +1,20 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+function wbMoveObject(arvHost, arvToken, uuid, newOwnerUuid) {
+ const typeName = arvadosTypeName(uuid);
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' +
+ uuid, { 'method': 'PUT', 'data': JSON.stringify({
+ 'owner_uuid': newOwnerUuid
+ }) });
+}
+
+export default wbMoveObject;
diff --git a/frontend/src/js/arvados/base/wb-rename-object.js b/frontend/src/js/arvados/base/wb-rename-object.js
new file mode 100644
index 0000000..379ef60
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-rename-object.js
@@ -0,0 +1,23 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+function wbRenameObject(arvHost, arvToken, uuid, newName) {
+ const update = {
+ 'name': newName
+ };
+ const typeName = arvadosTypeName(uuid);
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' +
+ uuid + '?' + typeName + '=' +
+ encodeURIComponent(JSON.stringify(update)),
+ { 'method': 'PUT' });
+}
+
+export default wbRenameObject;
diff --git a/frontend/src/js/arvados/base/wb-update-field.js b/frontend/src/js/arvados/base/wb-update-field.js
new file mode 100644
index 0000000..537a834
--- /dev/null
+++ b/frontend/src/js/arvados/base/wb-update-field.js
@@ -0,0 +1,23 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+function wbUpdateField(arvHost, arvToken, uuid, fieldName, fieldValue) {
+ const typeName = arvadosTypeName(uuid);
+ const data = {};
+ data[fieldName] = fieldValue;
+ const prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' + uuid, {
+ method: 'PUT',
+ data: JSON.stringify(data)
+ });
+ return prom;
+}
+
+export default wbUpdateField;
diff --git a/frontend/src/js/arvados/collection/wb-copy-collection.js b/frontend/src/js/arvados/collection/wb-copy-collection.js
new file mode 100644
index 0000000..3b6a7bd
--- /dev/null
+++ b/frontend/src/js/arvados/collection/wb-copy-collection.js
@@ -0,0 +1,32 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+function wbCopyCollection(arvHost, arvToken, uuid, newOwnerUuid) {
+ const typeName = arvadosTypeName(uuid);
+ if (typeName !== 'collection')
+ throw Error('Specified UUID does not refer to a collection');
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections/' + uuid);
+ prom = prom.then(xhr => {
+ const { name, description, properties, portable_data_hash,
+ manifest_text } = xhr.response;
+ const dup = { name: name + ' (Copied at ' + new Date().toISOString() + ')',
+ description, properties, portable_data_hash, manifest_text,
+ owner_uuid: newOwnerUuid };
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections', {
+ 'method': 'POST',
+ 'data': JSON.stringify(dup)
+ });
+ });
+ return prom;
+}
+
+export default wbCopyCollection;
diff --git a/frontend/src/js/arvados/collection/wb-download-file.js b/frontend/src/js/arvados/collection/wb-download-file.js
new file mode 100644
index 0000000..4b85a4b
--- /dev/null
+++ b/frontend/src/js/arvados/collection/wb-download-file.js
@@ -0,0 +1,64 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+
+function wbDownloadFile(arvHost, arvToken,
+ manifestReader, path) {
+
+ const file = manifestReader.getFile(path);
+ const name = path.split('/').reverse()[0];
+ const blockRefs = file[0];
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/keep_services');
+ prom = prom.then(xhr => {
+ const services = xhr.response['items'];
+ const proxies = services.filter(svc => (svc.service_type === 'proxy'));
+ const n = Math.floor(Math.random() * proxies.length);
+ const proxy = proxies[n];
+ const blocks = [];
+
+ let prom_2 = new Promise(accept => accept());
+
+ for (let i = 0; i < blockRefs.length; i++) {
+ const [ locator, position, size ] = blockRefs[i];
+
+ const prom_1 = makeArvadosRequest(
+ proxy.service_host + ':' + proxy.service_port,
+ arvToken,
+ '/' + locator,
+ { 'useSsl': proxy.service_ssl_flag,
+ 'responseType': 'arraybuffer' }
+ );
+
+ prom_2 = prom_2.then(() => prom_1);
+
+ prom_2 = prom_2.then(xhr => (blocks.push(xhr.response.slice(position,
+ position + size))));
+ }
+
+ prom_2 = prom_2.then(() => blocks);
+
+ return prom_2;
+ });
+
+
+
+ /* prom = prom.then(() => {
+ const blob = new Blob(blocks);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = name;
+ a.click();
+ }); */
+
+ return prom;
+}
+
+export default wbDownloadFile;
diff --git a/frontend/src/js/arvados/collection/wb-manifest-worker-wrapper.js b/frontend/src/js/arvados/collection/wb-manifest-worker-wrapper.js
new file mode 100644
index 0000000..30672f9
--- /dev/null
+++ b/frontend/src/js/arvados/collection/wb-manifest-worker-wrapper.js
@@ -0,0 +1,35 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+class WBManifestWorkerWrapper {
+ constructor() {
+ this.worker = new Worker('/js/wb-manifest-worker.js');
+ this.worker.onmessage = e => this.onMessage(e);
+ this.worker.onerror = e => console.log(e);
+ this.queue = [];
+ }
+
+ onMessage(e) {
+ if (this.queue.length === 0)
+ throw Error('Unexpected message from worker');
+ const [msgType, accept, reject] = this.queue.splice(0, 1)[0];
+ if (e.data[0] === msgType + 'Result')
+ accept(e);
+ else
+ reject(e);
+ }
+
+ postMessage(m) {
+ const prom = new Promise((accept, reject) => {
+ this.queue.push([ m[0], accept, reject ]);
+ this.worker.postMessage(m);
+ });
+ return prom;
+ }
+}
+
+export default WBManifestWorkerWrapper;
diff --git a/frontend/src/js/arvados/process/wb-input-spec-info.js b/frontend/src/js/arvados/process/wb-input-spec-info.js
new file mode 100644
index 0000000..12e421a
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-input-spec-info.js
@@ -0,0 +1,21 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function wbInputSpecInfo(inputSpec) {
+ const isFile = (inputSpec.type === 'File' || inputSpec.type === 'File[]' ||
+ (inputSpec.type.type === 'array' && [].concat(inputSpec.type.items).indexOf('File') !== -1));
+
+ const isDirectory = (inputSpec.type === 'Directory' || inputSpec.type === 'Directory[]' ||
+ (inputSpec.type.type === 'array' && [].concat(inputSpec.type.items).indexOf('Directory') !== -1));
+
+ const isArray = (inputSpec.type === 'File[]' || inputSpec.type === 'Directory[]' ||
+ inputSpec.type.type === 'array');
+
+ return { isFile, isDirectory, isArray };
+}
+
+export default wbInputSpecInfo;
diff --git a/frontend/src/js/arvados/process/wb-parse-workflow-def.js b/frontend/src/js/arvados/process/wb-parse-workflow-def.js
new file mode 100644
index 0000000..a6f5b8c
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-parse-workflow-def.js
@@ -0,0 +1,18 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function wbParseWorkflowDef(text) {
+ let definition;
+ try {
+ definition = JSON.parse(text);
+ } catch (_) {
+ definition = jsyaml.load(text);
+ }
+ return definition;
+}
+
+export default wbParseWorkflowDef;
diff --git a/frontend/src/js/arvados/process/wb-process-misc.js b/frontend/src/js/arvados/process/wb-process-misc.js
new file mode 100644
index 0000000..86169d5
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-process-misc.js
@@ -0,0 +1,18 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function encodeURIComponentIncludingDots(s) {
+ return encodeURIComponent(s).replace('.', '%2E');
+}
+
+function parseKeepRef(value) {
+ if (value && typeof(value) === 'object' && 'location' in value && value.location.startsWith('keep:'))
+ return value.location.substr(5);
+ return value;
+}
+
+export { encodeURIComponentIncludingDots, parseKeepRef }
diff --git a/frontend/src/js/arvados/process/wb-process-state-name.js b/frontend/src/js/arvados/process/wb-process-state-name.js
new file mode 100644
index 0000000..d1f89c4
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-process-state-name.js
@@ -0,0 +1,27 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function wbProcessStateName(containerRequest, container) {
+ const cr = containerRequest;
+ const c = container;
+ if (!c)
+ return cr.state;
+ if (cr.state !== 'Uncommitted' && cr.priority === 0)
+ return 'Cancelled';
+ if (cr.state === 'Uncommitted' || !cr.container_uuid || c.state === 'Queued' || c.state === 'Locked')
+ return 'Pending';
+ if (c.state === 'Running')
+ return 'Running';
+ if (c.state === 'Complete' && c.exit_code === 0)
+ return 'Complete';
+ if (c.state === 'Complete' && c.exit_code !== 0)
+ return 'Failed';
+ if (c.state === 'Cancelled')
+ return 'Cancelled';
+}
+
+export default wbProcessStateName;
diff --git a/frontend/src/js/arvados/process/wb-submit-container-request.js b/frontend/src/js/arvados/process/wb-submit-container-request.js
new file mode 100644
index 0000000..0f41515
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-submit-container-request.js
@@ -0,0 +1,169 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+import wbUuidsToCwl from 'wb-uuids-to-cwl';
+
+function wbParseWorkflowInputs(workflowDefinition, userInputs, errors) {
+ // first see if all inputs are parseable
+ const inputs = {};
+
+ const main = workflowDefinition['$graph'].find(a => (a.id === '#main'));
+
+ for (let k in userInputs) {
+ try {
+ let val = jsyaml.safeLoad(userInputs[k]);
+ val = wbUuidsToCwl(val);
+ k = k.split('/').slice(1).join('/');
+ inputs[k] = (val === undefined ? null : val);
+ } catch (exc) {
+ errors.push('Error parsing ' + k + ': ' + exc.message);
+ }
+ }
+
+ return inputs;
+}
+
+function ensureSubProject(arvHost, arvToken, projectUuid) {
+ const filters = [
+ [ 'group_class', '=', 'project' ],
+ [ 'owner_uuid', '=', projectUuid ],
+ [ 'properties.type', '=', 'daily_process_subproject_container' ]
+ ];
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups?filters=' + encodeURIComponent(JSON.stringify(filters)));
+
+ prom = prom.then(xhr => {
+ if (xhr.response.items.length === 0) {
+ let prom_1 = new Promise(accept => accept());
+ prom_1 = prom_1.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups', { method: 'POST',
+ data: JSON.stringify({ owner_uuid: projectUuid,
+ group_class: 'project',
+ name: 'Container for daily sub-projects for processes',
+ properties: { type: 'daily_process_subproject_container' } }) }));
+ prom_1 = prom_1.then(xhr_1 => xhr_1.response.uuid);
+ return prom_1;
+ }
+ return xhr.response.items[0].uuid;
+ });
+
+ let date = new Date();
+ date = (date.getYear() + 1900) + '-' +
+ ('00' + (date.getMonth() + 1)).slice(-2) + '-' +
+ ('00' + date.getDate()).slice(-2);
+
+ let containerUuid;
+
+ prom = prom.then(uuid => {
+ containerUuid = uuid;
+
+ const filters_1 = [
+ [ 'group_class', '=', 'project'],
+ [ 'owner_uuid', '=', containerUuid ],
+ [ 'properties.type', '=', 'daily_process_subproject' ],
+ [ 'properties.date', '=', date ]
+ ];
+
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups?filters=' + encodeURIComponent(JSON.stringify(filters_1)));
+ });
+
+ prom = prom.then(xhr => {
+ if (xhr.response.items.length === 0) {
+ let prom_1 = new Promise(accept => accept());
+ prom_1 = prom_1.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups', { method: 'POST',
+ data: JSON.stringify({ owner_uuid: containerUuid,
+ group_class: 'project',
+ name: 'Daily processes sub-project for ' + date,
+ properties: { type: 'daily_process_subproject', date } }) }));
+ prom_1 = prom_1.then(xhr => xhr.response.uuid);
+ return prom_1;
+ }
+ return xhr.response.items[0].uuid;
+ });
+ return prom;
+}
+
+// params:
+// arvHost, arvToken, inputs,
+// projectUuid, workflowDefinition, workflowUuid
+// processName, processDescription, placeInSubProject
+function wbSubmitContainerRequest(params) {
+
+ const { workflowDefinition, workflowUuid,
+ processName, processDescription, inputs,
+ arvHost, arvToken,
+ placeInSubProject } = params;
+
+ let { projectUuid } = params;
+
+ let prom = new Promise(accept => accept());
+
+ if (placeInSubProject) {
+ prom = prom.then(() => ensureSubProject(arvHost, arvToken, projectUuid));
+ prom = prom.then(subProjUuid => (projectUuid = subProjUuid));
+ }
+
+ prom = prom.then(() => {
+ // prepare a request
+ const req = {
+ name: processName,
+ description: processDescription,
+ owner_uuid: projectUuid,
+ container_image: 'arvados/jobs',
+ properties: {
+ template_uuid: workflowUuid
+ },
+ runtime_constraints: {
+ API: true,
+ vcpus: 1,
+ ram: 1073741824
+ },
+ cwd: '/var/spool/cwl',
+ command: [
+ 'arvados-cwl-runner',
+ '--local',
+ '--api=containers',
+ '--project-uuid=' + projectUuid,
+ '--collection-cache-size=256',
+ '/var/lib/cwl/workflow.json#main',
+ '/var/lib/cwl/cwl.input.json'],
+ output_path: '/var/spool/cwl',
+ priority: 1,
+ state: 'Committed',
+ mounts: {
+ 'stdout': {
+ kind: 'file',
+ path: '/var/spool/cwl/cwl.output.json'
+ },
+ '/var/spool/cwl': {
+ kind: 'collection',
+ writable: true
+ },
+ '/var/lib/cwl/workflow.json': {
+ kind: 'json',
+ content: workflowDefinition
+ },
+ '/var/lib/cwl/cwl.input.json': {
+ kind: 'json',
+ content: inputs
+ }
+ }
+ };
+
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests',
+ { method: 'POST', data: JSON.stringify(req) });
+ });
+
+ return prom;
+}
+
+export { wbParseWorkflowInputs, wbSubmitContainerRequest };
diff --git a/frontend/src/js/arvados/process/wb-uuids-to-cwl.js b/frontend/src/js/arvados/process/wb-uuids-to-cwl.js
new file mode 100644
index 0000000..73aa1ef
--- /dev/null
+++ b/frontend/src/js/arvados/process/wb-uuids-to-cwl.js
@@ -0,0 +1,32 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function wbUuidsToCwl(obj) {
+ if (obj instanceof Array) {
+ const res = [];
+ for (let k in obj) {
+ res[k] = wbUuidsToCwl(obj[k]);
+ }
+ return res;
+ }
+
+ if (typeof(obj) === 'string' &&
+ (/^[0-9a-z]{5}-[0-9a-z]{5}-[0-9a-z]{15}/.exec(obj) ||
+ /^[0-9a-f]{32}\+[0-9]+/.exec(obj))) {
+
+ const isDirectory = obj.endsWith('/');
+
+ return {
+ 'class': (isDirectory ? 'Directory' : 'File'),
+ 'location': 'keep:' + (isDirectory ? obj.substr(0, obj.length - 1) : obj)
+ };
+ }
+
+ throw Error('Expected Arvados path or array of paths');
+}
+
+export default wbUuidsToCwl;
diff --git a/frontend/src/js/component/wb-arvados-crumbs.js b/frontend/src/js/component/wb-arvados-crumbs.js
new file mode 100644
index 0000000..de741ff
--- /dev/null
+++ b/frontend/src/js/component/wb-arvados-crumbs.js
@@ -0,0 +1,55 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBBreadcrumbs from 'wb-breadcrumbs';
+import fetchObjectParents from 'fetch-object-parents';
+
+class WBArvadosCrumbs extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.items = [ { 'name': 'All Projects' } ];
+ }
+
+ fetchCrumbs() {
+ const { mode, uuid, app } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ if (mode === 'shared-with-me') {
+ this.setState({ 'items': [ { 'name': 'Shared with Me' } ] });
+ return;
+ }
+
+ if (!uuid) {
+ this.setState({ 'items': [ { 'name': 'All Projects' } ] });
+ return;
+ }
+
+ let prom = fetchObjectParents(arvHost, arvToken, uuid);
+ prom = prom.then(parents => {
+ this.setState({ 'items': parents });
+ });
+ }
+
+ componentDidMount() {
+ this.fetchCrumbs();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchCrumbs();
+ }
+
+ render({ app }, { items }) {
+ return (
+ app.breadcrumbClicked(item) } />
+ );
+ }
+}
+
+export default WBArvadosCrumbs;
diff --git a/frontend/src/js/component/wb-collection-content.js b/frontend/src/js/component/wb-collection-content.js
new file mode 100644
index 0000000..c68fdb5
--- /dev/null
+++ b/frontend/src/js/component/wb-collection-content.js
@@ -0,0 +1,251 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import WBBreadcrumbs from 'wb-breadcrumbs';
+import WBPagination from 'wb-pagination';
+import makeArvadosRequest from 'make-arvados-request';
+import wbDownloadFile from 'wb-download-file';
+import WBManifestWorkerWrapper from 'wb-manifest-worker-wrapper';
+
+function unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g,
+ (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+}
+
+function encodeURIComponentIncludingDots(s) {
+ return encodeURIComponent(s).replace('.', '%2E');
+}
+
+function endsWith(what, endings) {
+ if (typeof(endings) === 'string')
+ return what.endsWith(endings);
+ if (endings instanceof Array)
+ return endings.map(a => what.endsWith(a)).reduce((a, b) => (a || b));
+ throw Error('Expected second argument to be either a string or an array');
+}
+
+function maskRows(rows) {
+ return rows.map(r => r.map(c => '-'));
+}
+
+class WBCollectionContent extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.manifestWorker = new WBManifestWorkerWrapper();
+ this.state.loaded = 0;
+ this.state.total = 0;
+ this.state.mode = 'manifestDownload';
+ this.state.parsedStreams = 0;
+ this.state.totalStreams = 1;
+ }
+
+ getUrl(params) {
+ let res = '/collection-browse/' +
+ ('uuid' in params ? params.uuid : this.props.uuid) + '/' +
+ encodeURIComponentIncludingDots('collectionPath' in params ? params.collectionPath : this.props.collectionPath) + '/' +
+ ('page' in params ? params.page : this.props.page);
+ return res;
+ }
+
+ componentDidMount() {
+ let { arvHost, arvToken } = this.props.app.state;
+ let { uuid, collectionPath } = this.props;
+ let { manifestWorker } = this.state;
+
+ let select = [ 'manifest_text' ];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections/' + uuid +
+ '?select=' + encodeURIComponent(JSON.stringify(select)),
+ { 'onProgress': e => {
+ this.setState({ 'loaded': e.loaded, 'total': e.total });
+ } });
+
+ prom = prom.then(xhr => {
+ const streams = xhr.response.manifest_text.split('\n');
+ const paths = streams.filter(s => s).map(s => {
+ const n = s.indexOf(' ');
+ return unescapeName(s.substr(0, n));
+ });
+
+ let prom_1 = new Promise(accept => accept());
+
+ prom_1 = prom_1.then(() => {
+ this.setState({
+ 'totalStreams': streams.length,
+ 'parsedStreams': 0,
+ 'mode': 'manifestParse'
+ });
+ return manifestWorker.postMessage([ 'precreatePaths', paths ]);
+ });
+
+ let lastListingTimestamp = new Date(0);
+
+ for (let i = 0; i < streams.length; i++) {
+ prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'parseStream', streams[i] ]));
+
+ prom_1 = prom_1.then(() => {
+ if (new Date() - lastListingTimestamp < 1000)
+ return;
+
+ lastListingTimestamp = new Date();
+
+ let prom_2 = new Promise(accept => accept());
+
+ prom_2 = prom_2.then(() => manifestWorker.postMessage([
+ 'listDirectory', '.' + this.props.collectionPath, true
+ ]));
+
+ prom_2 = prom_2.then(e => {
+ this.prepareRows(e.data[1]);
+ this.setState({ 'parsedStreams': (i + 1) });
+ });
+
+ return prom_2;
+ });
+ }
+
+ prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'listDirectory',
+ '.' + this.props.collectionPath, true ]));
+
+ prom_1 = prom_1.then(e => {
+ this.state.mode = 'browsingReady';
+ this.prepareRows(e.data[1]);
+ });
+
+ return prom_1;
+ });
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+
+ this.setState({ rows: maskRows(this.state.rows) });
+
+ const { manifestWorker, mode } = this.state;
+ const { collectionPath } = this.props;
+
+ if (mode === 'browsingReady') {
+ let prom = manifestWorker.postMessage([ 'listDirectory', '.' + collectionPath, true ]);
+ prom = prom.then(e => this.prepareRows(e.data[1]));
+ }
+ }
+
+ prepareRows(listing) {
+ let { manifestWorker, mode } = this.state;
+ let { collectionPath, page, itemsPerPage, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ const numPages = Math.ceil(listing.length / itemsPerPage);
+ listing = listing.slice(page * itemsPerPage,
+ page * itemsPerPage + itemsPerPage);
+
+ this.setState({
+ 'numPages': numPages,
+ 'rows': listing.map(item => (
+ (item[0] === 'd') ? [
+ ({ item[1] }/),
+ 'Directory',
+ null,
+ ()
+ ] : [
+ item[1],
+ 'File',
+ filesize(item[2]),
+ ( (mode === 'browsingReady') ? (
+
+
+
+
+
+ { endsWith(item[1].toLowerCase(), ['.nii', '.nii.gz']) ? (
+
+ ) : null }
+
+ ) : null)
+ ]
+ ))
+ });
+ }
+
+ render({ collectionPath, page }, { manifestReader, rows,
+ numPages, loaded, total, mode, parsedStreams, totalStreams }) {
+
+ return (
+
+
({ name, index })) }
+ getItemUrl={ it => this.getUrl({
+ collectionPath: ('.' + collectionPath).split('/').slice(0, it.index + 1).join('/').substr(1),
+ page: 0
+ }) } />
+
+ { (mode === 'manifestDownload') ?
+ (
+
+
Downloading manifest: { filesize(loaded) }
+
+
+
+ ) : (
+
+ { mode === 'manifestParse' ? (
+
+
Parsing manifest: { parsedStreams }/{ totalStreams }
+
+
+ ) : null }
+
+
+
+
this.getUrl({ 'page': page }) } />
+
+ ) }
+
+ );
+ }
+}
+
+WBCollectionContent.defaultProps = {
+ 'collectionPath': '',
+ 'page': 0,
+ 'itemsPerPage': 20
+};
+
+export default WBCollectionContent;
diff --git a/frontend/src/js/component/wb-collection-fields.js b/frontend/src/js/component/wb-collection-fields.js
new file mode 100644
index 0000000..bdaf69d
--- /dev/null
+++ b/frontend/src/js/component/wb-collection-fields.js
@@ -0,0 +1,99 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+import urlForObject from 'url-for-object';
+import wbFormatDate from 'wb-format-date';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import WBAccordion from 'wb-accordion';
+import wbFormatSpecialValue from 'wb-format-special-value';
+import WBJsonViewer from 'wb-json-viewer';
+import WBJsonEditor from 'wb-json-editor';
+import wbUpdateField from 'wb-update-field';
+
+class WBCollectionFields extends Component {
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchData();
+ }
+
+ prepareRows(item) {
+ const { app } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ let rows = [
+ [ 'Name', item.name ],
+ [ 'Description', wbFormatSpecialValue(item.description) ],
+ [ 'Properties', (
+ wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value)
+ .then(() => { item.properties = value; this.prepareRows(item); }) } />
+ ) ],
+ [ 'Portable Data Hash', item.portable_data_hash ],
+ [ 'Replication Desired', item.replication_desired ? item.replication_desired : (
+ { String(item.replication_desired) }
+ ) ],
+ [ 'Replication Confirmed', item.replication_confirmed ? item.replication_confirmed : (
+ { String(item.replication_confirmed) }
+ ) ],
+ [ 'Replication Confirmed At', wbFormatDate(item.replication_confirmed_at) ],
+ [ 'Trash At', wbFormatDate(item.trash_at) ],
+ [ 'Delete At', wbFormatDate(item.delete_at) ],
+ [ 'Is Trashed', String(item.is_trashed) ],
+ [ 'Current Version UUID', (
+
+ ) ],
+ [ 'Version', item.version ],
+ [ 'Preserve Version', String(item.preserve_version) ],
+ [ 'File Count', item.file_count ],
+ [ 'Total Size', filesize(item.file_size_total) ]
+ ];
+ this.setState({ 'rows': rows });
+ }
+
+ fetchData() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ const filters = [
+ ['uuid', '=', uuid]
+ ];
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections?filters=' + encodeURIComponent(JSON.stringify(filters)));
+
+ prom = prom.then(xhr => {
+ const item = xhr.response.items[0];
+ if (!item)
+ throw Error('Item not found');
+ this.prepareRows(item);
+ });
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBCollectionFields;
diff --git a/frontend/src/js/component/wb-collection-listing.js b/frontend/src/js/component/wb-collection-listing.js
new file mode 100644
index 0000000..1c743e6
--- /dev/null
+++ b/frontend/src/js/component/wb-collection-listing.js
@@ -0,0 +1,191 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+import makeArvadosRequest from 'make-arvados-request';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import urlForObject from 'url-for-object';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+import WBCheckboxes from 'wb-checkboxes';
+
+class WBCollectionListing extends Component {
+
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.numPages = 0;
+ this.state.orderStream = uuid.v4();
+ this.state.collectionTypes = [ 'Intermediate', 'Output', 'Log', 'Other' ];
+ this.state.collectionTypeMask = [ true, true, true, true ];
+ }
+
+ componentDidMount() {
+ this.fetchItems();
+ }
+
+ prepareRows(items, ownerLookup) {
+ let { app, renderRenameLink, renderDeleteButton,
+ renderSelectionCell, renderSharingButton,
+ renderEditDescription } = this.props;
+
+ return items.map(item => [
+ renderSelectionCell(item),
+ (),
+ (
+ { item['description'] } { renderEditDescription(item, () => this.fetchItems()) }
+
),
+ (
+
+
{ item.owner_uuid }
+
),
+ item['file_count'],
+ filesize(item['file_size_total']),
+ (
+
+
+
+
+ { renderDeleteButton(item, () => this.fetchItems()) }
+
+ { renderSharingButton(item) }
+
)
+ ]);
+ }
+
+ fetchItems() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { activePage, itemsPerPage, ownerUuid, textSearch } = this.props;
+ const { collectionTypes, collectionTypeMask } = this.state;
+
+ let filters = [];
+
+ if (ownerUuid)
+ filters.push([ 'owner_uuid', '=', ownerUuid ]);
+
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]);
+
+ if (collectionTypeMask.filter(a => (!a)).length != 0) {
+ if (collectionTypeMask[3]) {
+ for (let i = 0; i < 3; i++)
+ if (!collectionTypeMask[i])
+ filters.push([ 'properties.type', '!=', collectionTypes[i].toLowerCase() ]);
+ } else {
+ filters.push([ 'properties.type', 'in',
+ collectionTypes.filter((_, k) => collectionTypeMask[k]).map(a => a.toLowerCase()) ]);
+ }
+ }
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + encodeURIComponent(itemsPerPage) +
+ '&offset=' + encodeURIComponent(itemsPerPage * activePage));
+
+ let collections;
+ let numPages
+
+ prom = prom.then(xhr => {
+ collections = xhr.response['items'];
+ numPages = Math.ceil(xhr.response['items_available'] / xhr.response['limit']);
+
+ let owners = {};
+ collections.map(c => {
+ let typeName = arvadosTypeName(c.owner_uuid);
+ if (!(typeName in owners))
+ owners[typeName] = [];
+ owners[typeName].push(c.owner_uuid);
+ });
+
+ let lookup = {};
+ let prom_1 = new Promise(accept => accept());
+ for (let typeName in owners) {
+ let filters_1 = [
+ ['uuid', 'in', owners[typeName]]
+ ];
+ prom_1 = prom_1.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's?filters=' +
+ encodeURIComponent(JSON.stringify(filters_1))));
+ prom_1 = prom_1.then(xhr => xhr.response.items.map(item => (
+ lookup[item.uuid] = item)));
+ }
+
+ prom_1 = prom_1.then(() => lookup);
+
+ return prom_1;
+ });
+
+ //let ownerLookup = {};
+ //prom = prom.then(lookup => (ownerLookup = lookup));
+
+ prom = prom.then(ownerLookup =>
+ this.setState({
+ 'numPages': numPages,
+ 'rows': this.prepareRows(collections, ownerLookup),
+ 'error': null
+ }));
+
+ prom = prom.catch(() => this.setState({
+ 'error': 'An error occured querying the Arvados API',
+ 'rows': []
+ }));
+ }
+
+ componentWillReceiveProps(nextProps, nextState) {
+ this.props = nextProps;
+ this.fetchItems();
+ }
+
+ render({ app, ownerUuid, activePage, getPageUrl }, { rows, numPages, error,
+ collectionTypes, collectionTypeMask }) {
+
+ return (
+
+ { error ? (
+ { error }
+
) : null }
+
+
route(getPageUrl(0)) } />
+
+
+
+
+
+ );
+ }
+}
+
+WBCollectionListing.defaultProps = {
+ 'itemsPerPage': 100,
+ 'ownerUuid': null,
+ 'renderSharingButton': () => null,
+ 'renderEditDescription': () => null
+};
+
+export default WBCollectionListing;
diff --git a/frontend/src/js/component/wb-common-fields.js b/frontend/src/js/component/wb-common-fields.js
new file mode 100644
index 0000000..4b76f0c
--- /dev/null
+++ b/frontend/src/js/component/wb-common-fields.js
@@ -0,0 +1,79 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+import urlForObject from 'url-for-object';
+import wbFormatDate from 'wb-format-date';
+import WBNameAndUuid from 'wb-name-and-uuid';
+
+class WBCommonFields extends Component {
+ componentDidMount() {
+ this.prepareRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ // this.setState({ 'rows': null });
+ this.prepareRows();
+ }
+
+ prepareRows() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ const typeName = arvadosTypeName(uuid);
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' +
+ encodeURIComponent(uuid));
+
+ prom = prom.then(xhr => {
+ const item = xhr.response;
+ let rows = [
+ [ 'UUID', item.uuid ],
+ [ 'Kind', item.kind ],
+ [ 'Owner', (
+
+ ) ],
+ [ 'Created at', wbFormatDate(item.created_at) ],
+ [ 'Modified at', wbFormatDate(item.modified_at) ],
+ [ 'Modified by User', (
+ item.modified_by_user_uuid ? () : '-'
+ ) ],
+ [ 'Modified by Client', (
+ item.modified_by_client_uuid ? () : '-'
+ ) ],
+ [ 'API Url', (
+
+ { 'https://' + app.state.arvHost + '/arvados/v1/' + typeName + 's/' + uuid }
+
+ ) ],
+ [ 'ETag', item.etag ]
+ ];
+ this.setState({ 'rows': rows });
+ });
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBCommonFields;
diff --git a/frontend/src/js/component/wb-container-fields.js b/frontend/src/js/component/wb-container-fields.js
new file mode 100644
index 0000000..b4b3eaa
--- /dev/null
+++ b/frontend/src/js/component/wb-container-fields.js
@@ -0,0 +1,114 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+import urlForObject from 'url-for-object';
+import wbFormatDate from 'wb-format-date';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import WBAccordion from 'wb-accordion';
+import WBJsonViewer from 'wb-json-viewer';
+
+class WBContainerFields extends Component {
+ componentDidMount() {
+ this.prepareRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.prepareRows();
+ }
+
+ prepareRows() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ let item;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/containers/' + uuid);
+ prom = prom.then(xhr => (item = xhr.response));
+
+ prom = prom.then(() => {
+ let rows = [
+ [ 'State', item.state ],
+ [ 'Started At', wbFormatDate(item.started_at) ],
+ [ 'Finished At', wbFormatDate(item.started_at) ],
+ [ 'Log', item.log ? (
+
+ ) : ( { String(item.log) } ) ],
+ [ 'Environment', (
+
+
+
+ ) ],
+ [ 'Working Directory', item.cwd ],
+ [ 'Command', (
+
+ ) ],
+ [ 'Output Path', item.output_path ],
+ [ 'Mounts', (
+
+ { Object.keys(item.mounts).map(k => (
+
+ )) }
+
+ ) ],
+ [ 'Runtime Constraints', (
+
+
+
+ ) ],
+ [ 'Runtime Status', (
+
+
+
+ ) ],
+ [ 'Scheduling Parameters', (
+
+
+
+ ) ],
+ [ 'Output', item.output ? (
+
+ ) : ( { String(item.output) } )],
+ [ 'Container Image', (
+
+ ) ],
+ [ 'Progress', item.progress ],
+ [ 'Priority', item.priority ],
+ [ 'Exit Code', item.exit_code === null ? ( null ) : item.exit_code ],
+ [ 'Auth UUID', item.auth_uuid === null ? ( null ) : item.auth_uuid ],
+ [ 'Locked by UUID', item.locked_by_uuid === null ? ( null ) : item.locked_by_uuid ]
+ ];
+ rows = rows.map(r => [r[0], r[1] ? r[1] : ({ String(r[1]) })]);
+ this.setState({ 'rows': rows });
+ });
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBContainerFields;
diff --git a/frontend/src/js/component/wb-container-request-fields.js b/frontend/src/js/component/wb-container-request-fields.js
new file mode 100644
index 0000000..13e666d
--- /dev/null
+++ b/frontend/src/js/component/wb-container-request-fields.js
@@ -0,0 +1,139 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+import arvadosObjectName from 'arvados-object-name';
+import urlForObject from 'url-for-object';
+import wbFormatDate from 'wb-format-date';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import WBAccordion from 'wb-accordion';
+import WBJsonViewer from 'wb-json-viewer';
+import wbUpdateField from 'wb-update-field';
+import WBJsonEditor from 'wb-json-editor';
+
+class WBContainerRequestFields extends Component {
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchData();
+ }
+
+ prepareRows(item) {
+ const { app } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ let rows = [
+ [ 'Name', item.name ],
+ [ 'Description', item.description || ({ String(item.description) }) ],
+ [ 'Properties', (
+ wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value)
+ .then(() => { item.properties = value; this.prepareRows(item); }) } />
+ ) ],
+ [ 'State', item.state ],
+ [ 'Requesting Container', (
+
+ ) ],
+ [ 'Container', (
+
+ ) ],
+ [ 'Container Count Max', item.container_count_max ],
+ [ 'Mounts', (
+
+ { Object.keys(item.mounts).map(k => (
+
+ )) }
+
+ ) ],
+ [ 'Runtime Constraints', (
+
+
+
+ ) ],
+ [ 'Scheduling Parameters', (
+
+
+
+ ) ],
+ [ 'Container Image', (
+
+ ) ],
+ [ 'Environment', (
+
+
+
+ ) ],
+ [ 'Working Directory', item.cwd ],
+ [ 'Command', (
+
+ ) ],
+ [ 'Output Path', item.output_path ],
+ [ 'Output Name', item.output_name ],
+ [ 'Output TTL', item.output_ttl ],
+ [ 'Priority', item.priority ],
+ [ 'Expires At', wbFormatDate(item.expires_at) ],
+ [ 'Use Existing', String(item.use_existing) ],
+ [ 'Log', (
+
+ ) ],
+ [ 'Output', (
+
+ ) ],
+ [ 'Filters', (
+ item.filters ? () : ({ String(item.filters) })
+ ) ],
+ [ 'Runtime Token', item.runtime_token || ({ String(item.runtime_token) }) ],
+ [ 'Runtime User', (
+
+ ) ],
+ [ 'Runtime Auth Scopes', (
+ item.runtime_auth_scopes ? (
+
+ ) : (
+ { String(item.runtime_auth_scopes) }
+ )
+ ) ]
+ ];
+ rows = rows.map(r => [r[0], r[1] ? r[1] : ({ String(r[1]) })]);
+ this.setState({ rows });
+ }
+
+ fetchData() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ let item;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests/' + uuid);
+ prom = prom.then(xhr => this.prepareRows(xhr.response));
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBContainerRequestFields;
diff --git a/frontend/src/js/component/wb-inline-search.js b/frontend/src/js/component/wb-inline-search.js
new file mode 100644
index 0000000..457017c
--- /dev/null
+++ b/frontend/src/js/component/wb-inline-search.js
@@ -0,0 +1,22 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBInlineSearch extends Component {
+ render({ textSearch, navigate }) {
+ return (
+
+ navigate(e.target.value)) : null } />
+
+
+ );
+ }
+}
+
+export default WBInlineSearch;
diff --git a/frontend/src/js/component/wb-json-editor.js b/frontend/src/js/component/wb-json-editor.js
new file mode 100644
index 0000000..2f6798a
--- /dev/null
+++ b/frontend/src/js/component/wb-json-editor.js
@@ -0,0 +1,85 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBJsonViewer from 'wb-json-viewer';
+import WBAccordion from 'wb-accordion';
+import WBDialog from 'wb-dialog';
+
+class WbJsonEditorDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ }
+
+ render({ name, onChange }, { editValue, parseError }) {
+ return (
+ {
+ onChange(JSON.parse(editValue));
+ } }
+ canAccept={ () => {
+ try { JSON.parse(editValue) }
+ catch (exc) { this.setState({ parseError: exc.message }); return false; }
+ return true;
+ } }>
+
+
+ );
+ }
+
+ show() {
+ this.dialogRef.current.show();
+ }
+}
+
+class WBJsonEditor extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ }
+
+ render({ app, name, value, stringify, pretty, onChange }, { editValue, parseError }) {
+ return (
+
+
+ {
+ const dlg = this.dialogRef.current;
+ dlg.setState({ parseError: null,
+ editValue: stringify ?
+ pretty ? JSON.stringify(value, null, 2)
+ : JSON.stringify(value) : value });
+ dlg.show();
+ } }>
+
+
+ ) ] } cardHeaderClass="card-header-sm">
+
+
+
+ );
+ }
+}
+
+WBJsonEditor.defaultProps = {
+ stringify: true,
+ pretty: true,
+ onChange: () => {}
+};
+
+export default WBJsonEditor;
diff --git a/frontend/src/js/component/wb-json-viewer.js b/frontend/src/js/component/wb-json-viewer.js
new file mode 100644
index 0000000..a77c8dd
--- /dev/null
+++ b/frontend/src/js/component/wb-json-viewer.js
@@ -0,0 +1,50 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBIdTools from 'wb-id-tools';
+import urlForObject from 'url-for-object';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosObjectName from 'arvados-object-name';
+import WBLazyInlineName from 'wb-lazy-inline-name';
+
+function detectIds(value, app) {
+ const matches = WBIdTools.detectIdentifiers(value);
+ matches.sort((a, b) => (a.index - b.index));
+ const res = [];
+ let ofs = 0;
+ for (let i = 0; i < matches.length; i++) {
+ const { index } = matches[i];
+ const id = matches[i][0];
+ const typeName = WBIdTools.typeName(id);
+ const url = (typeName === 'group' ? '/browse/' + id :
+ typeName === 'collection' ? '/collection-browse/' + id :
+ urlForObject({ uuid: id }));
+ res.push(value.substring(ofs, index));
+ res.push(h(WBLazyInlineName, { identifier: id, app }, id));
+ ofs = index + id.length;
+ }
+ res.push(value.substring(ofs));
+ return res;
+}
+
+class WBJsonViewer extends Component {
+ render({ value, stringify, app, pretty }) {
+ if (stringify)
+ value = pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value);
+ return (
+ { detectIds(value, app) }
+ );
+ }
+}
+
+WBJsonViewer.defaultProps = {
+ stringify: true,
+ pretty: true
+};
+
+export default WBJsonViewer;
diff --git a/frontend/src/js/component/wb-lazy-inline-name.js b/frontend/src/js/component/wb-lazy-inline-name.js
new file mode 100644
index 0000000..295736c
--- /dev/null
+++ b/frontend/src/js/component/wb-lazy-inline-name.js
@@ -0,0 +1,66 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import WBIdTools from 'wb-id-tools';
+import urlForObject from 'url-for-object';
+import arvadosObjectName from 'arvados-object-name';
+
+class WBLazyInlineName extends Component {
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.identifier === this.props.identifier)
+ return;
+ this.setState({ item: null });
+ }
+
+ fetchData() {
+ const { app, identifier } = this.props;
+ const { arvHost, arvToken } = app.state;
+ const typeName = WBIdTools.typeName(identifier);
+ if (WBIdTools.isPDH(identifier)) {
+ const filters = [
+ [ 'portable_data_hash', '=', identifier ]
+ ];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections?filters=' + encodeURIComponent(JSON.stringify(filters)));
+ prom = prom.then(xhr => this.setState({ item: {
+ uuid: xhr.response.items.length > 0 ? xhr.response.items[0].uuid : '',
+ name: xhr.response.items.length > 0 ? xhr.response.items[0].name : 'Not Found' +
+ ( xhr.response.items_available > 1 ? ' (+' + (xhr.response.items_available - 1) + ' others)' : '' )
+ }}));
+ return;
+ }
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName + 's/' + identifier);
+ prom = prom.then(xhr => this.setState({ item: xhr.response }));
+ prom = prom.catch(() => this.setState({ item: { name: 'Not Found' }}));
+ }
+
+ render({ identifier }, { item }) {
+ if (item) {
+ return (
+ { arvadosObjectName(item) }
+ );
+ }
+
+ const typeName = WBIdTools.typeName(identifier);
+ const url = (typeName === 'group' ? '/browse/' + identifier :
+ typeName === 'collection' ? '/collection-browse/' + identifier :
+ urlForObject({ uuid: identifier }));
+ return (
+
+ { identifier } { e.preventDefault(); this.fetchData(); } }>
+
+
+
+ );
+ }
+}
+
+export default WBLazyInlineName;
diff --git a/frontend/src/js/component/wb-live-logs.js b/frontend/src/js/component/wb-live-logs.js
new file mode 100644
index 0000000..99d4c6c
--- /dev/null
+++ b/frontend/src/js/component/wb-live-logs.js
@@ -0,0 +1,82 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import WBPagination from 'wb-pagination';
+
+class WBLiveLogs extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.page = 0;
+ this.state.moreItemsPerPage = false;
+ this.terminalRef = createRef();
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.uuid === this.props.uuid);
+ return;
+ this.props = nextProps;
+ this.state.page = 0;
+ this.fetchData();
+ }
+
+ fetchData() {
+ const { uuid, app } = this.props;
+ let { itemsPerPage } = this.props;
+ const { page, moreItemsPerPage } = this.state;
+ if (moreItemsPerPage)
+ itemsPerPage *= 10;
+ const { arvHost, arvToken } = app.state;
+ const filters = [
+ [ 'object_uuid', '=', uuid ]
+ ];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/logs?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&offset=' + (itemsPerPage * page) +
+ '&limit=' + itemsPerPage);
+ prom = prom.then(xhr => {
+ const { items } = xhr.response;
+ this.setState({
+ content: items
+ .filter(a => ('text' in a.properties))
+ .map(a => a.properties.text.trim()).join('\n'),
+ numPages: Math.ceil(xhr.response.items_available / itemsPerPage)
+ });
+ this.terminalRef.current.scrollTo(0, 0);
+ });
+ }
+
+ render({}, { content, page, numPages, moreItemsPerPage }) {
+ return (
+
+
+ { this.state.moreItemsPerPage = e.target.checked;
+ this.state.page = 0; this.fetchData(); } } />
+
+
+
{ this.state.page = page; this.fetchData(); } } />
+
+ { content }
+
+
+ );
+ }
+}
+
+WBLiveLogs.defaultProps = {
+ itemsPerPage: 100
+};
+
+export default WBLiveLogs;
diff --git a/frontend/src/js/component/wb-name-and-uuid.js b/frontend/src/js/component/wb-name-and-uuid.js
new file mode 100644
index 0000000..e63c166
--- /dev/null
+++ b/frontend/src/js/component/wb-name-and-uuid.js
@@ -0,0 +1,130 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import urlForObject from 'url-for-object';
+import arvadosObjectName from 'arvados-object-name';
+import arvadosTypeName from 'arvados-type-name';
+
+class WBNameAndUuid extends Component {
+ fetchData() {
+ const { uuid, app, lookup } = this.props;
+
+ if (!uuid)
+ return;
+
+ if (lookup && (uuid in lookup)) {
+ this.setState({ 'item': lookup[uuid]});
+ return;
+ }
+
+ const { arvHost, arvToken } = app.state;
+
+ let prom = new Promise(accept => accept());
+
+ if (/[0-9a-f]{32}\+[0-9]+/g.exec(uuid)) {
+ let filters = [
+ ['portable_data_hash', '=', uuid]
+ ];
+ prom = prom.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections?filters=' +
+ encodeURIComponent(JSON.stringify(filters))));
+ prom = prom.then(xhr => {
+ if (xhr.response.items.length === 0) {
+ this.setState({
+ 'item': {
+ 'uuid': uuid,
+ 'name': 'Collection with portable data hash ' + uuid
+ }
+ });
+ return;
+ }
+ let item = xhr.response.items[0];
+ if (xhr.response.items.length > 1)
+ item.name += ' +' + (xhr.response.items.length - 1) + ' others';
+ this.setState({ item });
+ });
+
+ } else if (/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(uuid)) {
+ let typeName = arvadosTypeName(uuid);
+ const filters = [
+ ['uuid', '=', uuid]
+ ];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + typeName +
+ 's?filters=' + encodeURIComponent(JSON.stringify(filters)));
+ prom = prom.then(xhr => {
+ const item = xhr.response.items[0];
+ if (!item)
+ this.setState({ 'error': 'Item not found' });
+ else
+ this.setState({
+ 'item': item
+ });
+ });
+ prom = prom.catch(xhr => {
+ this.setState({
+ 'error': 'Unable to retrieve: ' + xhr.status + ' (' + xhr.statusText + ')'
+ });
+ });
+
+ } else {
+ this.setState({
+ 'item': {
+ 'uuid': uuid
+ }
+ });
+ }
+ }
+
+ componentDidMount() {
+ if (this.props.lazy)
+ ;//this.setState({ item: { uuid: this.props.uuid }});
+ else
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.uuid === nextProps.uuid)
+ return;
+
+ if (nextProps.lazy) {
+ this.setState({ item: null });
+
+ } else {
+ this.props = nextProps;
+ this.fetchData();
+ }
+ }
+
+ render({ uuid, onLinkClicked, lazy }, { error, item }) {
+ if (!uuid)
+ return (
+ { String(uuid) }
+ );
+
+ return (
+
+ );
+ }
+}
+
+export default WBNameAndUuid;
diff --git a/frontend/src/js/component/wb-navbar-common.js b/frontend/src/js/component/wb-navbar-common.js
new file mode 100644
index 0000000..c691eec
--- /dev/null
+++ b/frontend/src/js/component/wb-navbar-common.js
@@ -0,0 +1,38 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbar from 'wb-navbar';
+import WBInlineSearch from 'wb-inline-search';
+
+class WBNavbarCommon extends Component {
+ render({ app, items, activeItem, textSearch, textSearchNavigate }) {
+ return (
+ What's New Info), id: 'whatsnew' }
+ ].concat(items) }
+ rhs={ textSearchNavigate ? (
+
+ ) : null }
+ titleUrl = { '/browse/' + app.state.currentUser.uuid }
+ getItemUrl={ item => app.navbarItemUrl(item) }
+ activeItem={ activeItem } />
+ );
+ }
+}
+
+WBNavbarCommon.defaultProps = {
+ 'items': []
+};
+
+export default WBNavbarCommon;
diff --git a/frontend/src/js/component/wb-path-display.js b/frontend/src/js/component/wb-path-display.js
new file mode 100644
index 0000000..e61780c
--- /dev/null
+++ b/frontend/src/js/component/wb-path-display.js
@@ -0,0 +1,60 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import { encodeURIComponentIncludingDots } from 'wb-process-misc';
+
+class WBPathDisplay extends Component {
+ fetchData() {
+ const { app } = this.props;
+ const { arvHost, arvToken } = app.state;
+ let { path } = this.props;
+ if (path.endsWith('/'))
+ path = path.substr(0, path.length - 1);
+ let m;
+ if (m = /^[0-9a-f]{32}\+[0-9]+/.exec(path));
+ else if (m = /^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(path));
+ else return;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections/' + m[0]);
+ prom = prom.then(xhr => this.setState({
+ item: xhr.response,
+ tail: path.substr(m[0].length)
+ }));
+ prom = prom.catch(() => this.setState({ 'error': 'Cannot load' }));
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchData();
+ }
+
+ render({}, { item, tail, error }) {
+ if (error)
+ return error;
+
+ if (!item)
+ return 'Loading...';
+
+ return (
+
+
+ { item.name || item.uuid }
+
+ { tail }
+
+
+ );
+ }
+}
+
+export default WBPathDisplay;
diff --git a/frontend/src/js/component/wb-process-dashboard.js b/frontend/src/js/component/wb-process-dashboard.js
new file mode 100644
index 0000000..1114101
--- /dev/null
+++ b/frontend/src/js/component/wb-process-dashboard.js
@@ -0,0 +1,141 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import wbProcessStateName from 'wb-process-state-name';
+
+function getAll(makeRequest) {
+ let prom = makeRequest(0);
+ prom = prom.then(xhr => {
+ const { items, limit, items_available } = xhr.response;
+ let res = [].concat(items);
+ let prom_1 = new Promise(accept => accept());
+ for (let ofs = limit; ofs < items_available; ofs += limit) {
+ prom_1 = prom_1.then(() => makeRequest(ofs));
+ prom_1 = prom_1.then(xhr_1 => {
+ res = res.concat(xhr_1.response.items);
+ });
+ }
+ prom_1 = prom_1.then(() => res);
+ return prom_1;
+ });
+ return prom;
+}
+
+class WBProcessDashboard extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = Array(5).fill(Array(6).fill('-'));
+ }
+
+ fetchData() {
+ const { app, parentProcessUuid } = this.props;
+ const { arvHost, arvToken } = app.state;
+ let prom = new Promise(accept => accept());
+ if (parentProcessUuid) {
+ prom = prom.then(() => {
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests/' + encodeURIComponent(parentProcessUuid));
+ });
+ prom = prom.then(xhr => {
+ const cr = xhr.response;
+ if (!cr.container_uuid)
+ return [];
+ const filters = [ [ 'requesting_container_uuid', '=', cr.container_uuid ] ];
+ return getAll(ofs =>
+ makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests?filters=' +
+ encodeURIComponent(JSON.stringify(filters)) +
+ '&order=' + encodeURIComponent(JSON.stringify(['uuid asc'])) +
+ '&offset=' + ofs));
+ });
+ } else {
+ prom = prom.then(() => {
+ return getAll(ofs => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests?order=' +
+ encodeURIComponent(JSON.stringify(['uuid asc'])) + '&offset=' + ofs));
+ });
+ }
+ let crlist;
+ prom = prom.then(crl => {
+ crlist = crl;
+ const uuids = crlist.map(a => a.container_uuid);
+ // uuids = uuids.slice(0, 2);
+ // crlist.map(a => ( crdict[a.uuid] = a));
+ const filters = [ [ 'uuid', 'in', uuids ] ];
+ return getAll(ofs => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/containers?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&order=' + encodeURIComponent(JSON.stringify([ 'uuid asc' ])) +
+ '&offset=' + ofs));
+ });
+ prom = prom.then(cl => {
+ cl.map(a => (crlist.find(b => (b.container_uuid === a.uuid)).container = a));
+ crlist.map(a => (a.wb_state = wbProcessStateName(a, a.container)));
+ const stats = {};
+ for (let state in { 'Pending': 1, 'Running': 1, 'Complete': 1, 'Failed': 1, 'Cancelled': 1 }) {
+ const f = crlist.filter(a => (a.wb_state === state));
+ stats[state] = { 'Count': f.length };
+ if (state === 'Pending')
+ f.map(a => (a.wb_wait_time = (new Date() - new Date(a.created_at)) / 3.6e6));
+ else
+ f.map(a => (a.wb_wait_time = Math.max(0, (a.container ? new Date(a.container.started_at) : new Date(0)) - new Date(a.created_at)) / 3.6e6));
+ f.sort((a, b) => (a.wb_wait_time - b.wb_wait_time));
+ stats[state]['Shortest Wait Time'] = f.length ? (f[0].wb_wait_time.toFixed(2) + ' hours') : '-';
+ stats[state]['Longest Wait Time'] = f.length ? (f[f.length - 1].wb_wait_time.toFixed(2) + ' hours') : '-';
+ if (state === 'Pending')
+ f.map(a => (a.wb_run_time = 0));
+ else if (state === 'Running')
+ f.map(a => (a.wb_run_time = (new Date() - new Date(a.container.started_at)) / 3.6e6));
+ else
+ f.map(a => (a.wb_run_time = Math.max(0, a.container ? new Date(a.container.finished_at) - new Date(a.container.started_at) : 0) / 3.6e6));
+ f.sort((a, b) => (a.wb_run_time - b.wb_run_time));
+ stats[state]['Shortest Run Time'] = f.length ? (f[0].wb_run_time.toFixed(2) + ' hours') : '-';
+ stats[state]['Longest Run Time'] = f.length ? (f[f.length - 1].wb_run_time.toFixed(2) + ' hours') : '-';
+ }
+ const rows = [];
+ for (let st in { 'Count': 1, 'Shortest Wait Time': 1, 'Longest Wait Time': 1,
+ 'Shortest Run Time': 1, 'Longest Run Time': 1}) {
+ rows.push([st].concat(['Pending', 'Running', 'Complete', 'Failed', 'Cancelled'].map(a => stats[a][st])));
+ }
+ this.setState({ rows });
+ });
+ }
+
+ componentDidMount() {
+ if (!this.props.lazy)
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (nextProps.parentProcessUuid === this.props.parentProcessUuid)
+ return;
+
+ if (this.props.lazy) {
+ this.setState({ rows: Array(5).fill(Array(6).fill('-')) });
+ return;
+ }
+
+ this.props = nextProps;
+ this.fetchData();
+ }
+
+ render({ lazy }, { rows }) {
+ return (
+
+ );
+ }
+}
+
+export default WBProcessDashboard;
diff --git a/frontend/src/js/component/wb-process-listing.js b/frontend/src/js/component/wb-process-listing.js
new file mode 100644
index 0000000..355fc90
--- /dev/null
+++ b/frontend/src/js/component/wb-process-listing.js
@@ -0,0 +1,155 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+import makeArvadosRequest from 'make-arvados-request';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import WBCheckboxes from 'wb-checkboxes';
+import wbFormatDate from 'wb-format-date';
+import wbFetchObjects from 'wb-fetch-objects';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import WBProcessState from 'wb-process-state';
+
+function maskRows(rows) {
+ return rows.map(r => r.map(c => '-'));
+}
+
+class WBProcessListing extends Component {
+
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.numPages = 0;
+ this.state.requestStates = [ 'Uncommitted', 'Committed', 'Final' ];
+ this.state.reqStateMask = [ true, true, true ];
+ }
+
+ componentDidMount() {
+ this.fetchItems();
+ }
+
+ cancelProcess(uuid) {
+ const { app } = this.props;
+ const { arvHost, arvToken } = app.state;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests/' + encodeURIComponent(uuid),
+ { method: 'PUT', data: JSON.stringify({ priority: 0 }) });
+ prom = prom.then(() => {
+ this.setState({ rows: maskRows(this.state.rows) });
+ this.fetchItems();
+ });
+ }
+
+ prepareRows(requests, containerLookup, ownerLookup, outputLookup) {
+ const { app, renderRenameLink, renderDeleteButton,
+ renderSelectionCell, renderSharingButton,
+ renderEditDescription } = this.props;
+
+ return requests.map(item => {
+ return ( [
+ renderSelectionCell(item),
+ (
+
+
{ item['uuid'] }
+
+ { item.description } { renderEditDescription(item, () => this.fetchItems()) }
+
+
),
+ ( ),
+ ( ),
+ wbFormatDate(item['created_at']),
+ ( ),
+ (
+
+ { renderDeleteButton(item, () => this.fetchItems()) }
+ { renderSharingButton(item) }
+
)
+ ] );
+ });
+ }
+
+ fetchItems() {
+ const { arvHost, arvToken } = this.props.appState;
+ const { requestStates, reqStateMask } = this.state;
+ const { activePage, itemsPerPage, ownerUuid,
+ requestingContainerUuid, waitForNextProps,
+ textSearch } = this.props;
+
+ if (waitForNextProps)
+ return;
+
+ const filters = [
+ [ 'requesting_container_uuid', '=', requestingContainerUuid ]
+ ];
+ if (!reqStateMask.reduce((a, b) => a & b))
+ filters.push([ 'state', 'in', requestStates.filter((_, idx) => reqStateMask[idx]) ]);
+ if (ownerUuid)
+ filters.push([ 'owner_uuid', '=', ownerUuid ]);
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ])
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (itemsPerPage * activePage));
+
+ prom = prom.then(xhr =>
+ this.setState({
+ 'numPages': Math.ceil(xhr.response['items_available'] / xhr.response['limit']),
+ 'rows': this.prepareRows(xhr.response.items)
+ }));
+ }
+
+ componentWillReceiveProps(nextProps, nextState) {
+ this.props = nextProps;
+ this.setState({ 'rows': maskRows(this.state.rows) });
+ this.fetchItems();
+ }
+
+ render({ appState, ownerUuid, activePage, onPageChanged, getPageUrl },
+ { rows, numPages, requestStates, containerStates,
+ reqStateMask, contStateMask }) {
+
+ return (
+
+ this.fetchItems() } />
+
+
+
+
+
+ );
+ }
+}
+
+WBProcessListing.defaultProps = {
+ itemsPerPage: 100,
+ ownerUuid: null,
+ requestingContainerUuid: null,
+ renderRenameLink: () => null,
+ renderDeleteButton: () => null,
+ renderSelectionCell: () => null,
+ renderSharingButton: () => null,
+ renderEditDescription: () => null
+};
+
+export default WBProcessListing;
diff --git a/frontend/src/js/component/wb-process-state.js b/frontend/src/js/component/wb-process-state.js
new file mode 100644
index 0000000..a1446d6
--- /dev/null
+++ b/frontend/src/js/component/wb-process-state.js
@@ -0,0 +1,64 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import wbProcessStateName from 'wb-process-state-name';
+
+class WBProcessState extends Component {
+ componentDidMount() {
+ if (!this.props.lazy)
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (this.props.lazy) {
+ this.setState({ container: null, apiError: null });
+
+ } else {
+ this.props = nextProps;
+ this.fetchData();
+ }
+ }
+
+ fetchData() {
+ const { app, process } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ if (!process.container_uuid)
+ return;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/containers/' + process.container_uuid);
+ prom = prom.then(xhr => this.setState({ 'container': xhr.response }));
+ prom = prom.catch(() => this.setState({ 'apiError': 'Failed to fetch container' }));
+ }
+
+ render({ process, lazy }, { container, apiError }) {
+ const runtimeStatus = container ? container.runtime_status : null;
+ const error = runtimeStatus ? runtimeStatus.error : null;
+ const warning = runtimeStatus ? runtimeStatus.warning : null;
+
+ return (
+
+ { wbProcessStateName(process, container) }
+ { apiError ?
{ [ ' / ', apiError ] } : null }
+ { error ? [" / ",
E ] : null }
+ { warning ? [ " / ",
W ] : null } {}
+ { lazy && !container && !apiError ? (
+
{ e.preventDefault(); this.fetchData(); } }>
+
+
+ ) : null }
+
+ );
+ }
+}
+
+export default WBProcessState;
diff --git a/frontend/src/js/component/wb-project-fields.js b/frontend/src/js/component/wb-project-fields.js
new file mode 100644
index 0000000..f821c9c
--- /dev/null
+++ b/frontend/src/js/component/wb-project-fields.js
@@ -0,0 +1,77 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import WBAccordion from 'wb-accordion';
+import WBJsonViewer from 'wb-json-viewer';
+import wbFormatSpecialValue from 'wb-format-special-value';
+import WBLazyInlineName from 'wb-lazy-inline-name';
+import wbFormatDate from 'wb-format-date';
+import wbUpdateField from 'wb-update-field';
+import WBJsonEditor from 'wb-json-editor';
+
+class WBProjectFields extends Component {
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchData();
+ }
+
+ prepareRows(item) {
+ const { app } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ const rows = [
+ [ 'Name', wbFormatSpecialValue(item.name) ],
+ [ 'Description', wbFormatSpecialValue(item.description) ],
+ [ 'Properties', (
+ wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value)
+ .then(() => { item.properties = value; this.prepareRows(item); }) } />
+ ) ],
+ [ 'Writable by', item.writable_by
+ .map(a => ())
+ .reduce((a, b) => [].concat(a).concat(', ').concat(b))
+ ],
+ [ 'Trash At', wbFormatDate(item.trash_at) ],
+ [ 'Delete At', wbFormatDate(item.delete_at) ],
+ [ 'Is Trashed', wbFormatSpecialValue(item.is_trashed) ]
+ ];
+
+ this.setState({ rows });
+ }
+
+ fetchData() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups/' + uuid);
+
+ prom = prom.then(xhr => this.prepareRows(xhr.response));
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBProjectFields;
diff --git a/frontend/src/js/component/wb-project-listing.js b/frontend/src/js/component/wb-project-listing.js
new file mode 100644
index 0000000..b3b6557
--- /dev/null
+++ b/frontend/src/js/component/wb-project-listing.js
@@ -0,0 +1,110 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+import makeArvadosRequest from 'make-arvados-request';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import urlForObject from 'url-for-object';
+
+class WBProjectListing extends Component {
+
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.numPages = 0;
+ }
+
+ componentDidMount() {
+ this.fetchItems();
+ }
+
+ prepareRows(items) {
+ const { app, renderRenameLink, renderDeleteButton,
+ renderSelectionCell, renderSharingButton,
+ renderEditDescription } = this.props;
+
+ return items.map(item => [
+ renderSelectionCell(item),
+ (),
+ (
+ { item['description'] } { renderEditDescription(item, () => this.fetchItems()) }
+
),
+ ( ),
+ (
+
+
+
+ { renderDeleteButton(item, () => this.fetchItems()) }
+ { renderSharingButton(item) }
+
)
+ ]);
+ }
+
+ fetchItems() {
+ let { activePage, mode, itemsPerPage, ownerUuid, app, textSearch } = this.props;
+ let { arvHost, arvToken } = app.state;
+ let filters = [
+ [ 'group_class', '=', 'project' ]
+ ];
+ if (ownerUuid)
+ filters.push([ 'owner_uuid', '=', ownerUuid ]);
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]);
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups' + (mode === 'shared-with-me' ? '/shared' : '') +
+ '?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (itemsPerPage * activePage));
+ prom = prom.then(xhr =>
+ this.setState({
+ 'numPages': Math.ceil(xhr.response['items_available'] / xhr.response['limit']),
+ 'rows': this.prepareRows(xhr.response['items'])
+ }));
+ }
+
+ componentWillReceiveProps(nextProps, nextState) {
+ // this.setState({ 'rows': [] }); // .rows = [];
+ this.props = nextProps;
+ this.fetchItems();
+ }
+
+ render({ arvHost, arvToken, ownerUuid, activePage, getPageUrl }, { rows, numPages }) {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+WBProjectListing.defaultProps = {
+ 'itemsPerPage': 100,
+ 'ownerUuid': null,
+ 'renderRenameLink': () => null,
+ 'renderDeleteButton': () => null,
+ 'renderSelectionCell': () => null,
+ 'renderEditDescription': () => null
+};
+
+export default WBProjectListing;
diff --git a/frontend/src/js/component/wb-user-listing.js b/frontend/src/js/component/wb-user-listing.js
new file mode 100644
index 0000000..acf5257
--- /dev/null
+++ b/frontend/src/js/component/wb-user-listing.js
@@ -0,0 +1,75 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBPagination from 'wb-pagination';
+import makeArvadosRequest from 'make-arvados-request';
+import urlForObject from 'url-for-object';
+
+class WBUserListing extends Component {
+ componentDidMount() {
+ this.preparePage();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.preparePage();
+ }
+
+ preparePage() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { itemsPerPage, page, textSearch } = this.props;
+
+ const order = ['last_name asc'];
+
+ const filters = [];
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]);
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/users?order=' + encodeURIComponent(JSON.stringify(order)) +
+ '&filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage + '&offset=' + (itemsPerPage * page));
+ prom = prom.then(xhr => {
+ this.setState({
+ 'items': xhr.response['items'],
+ 'numPages': Math.ceil(xhr.response['items_available'] / itemsPerPage)
+ });
+ });
+ }
+
+ render({ app, page, getPageUrl }, { items, numPages }) {
+ return (
+
+
Users
+
+ { items ? items.map(it => (
+
+ )) : 'Loading...' }
+
+
+
+
+ );
+ }
+}
+
+WBUserListing.defaultProps = {
+ 'itemsPerPage': 20,
+ 'page': 0
+};
+
+export default WBUserListing;
diff --git a/frontend/src/js/component/wb-workflow-fields.js b/frontend/src/js/component/wb-workflow-fields.js
new file mode 100644
index 0000000..92a53f0
--- /dev/null
+++ b/frontend/src/js/component/wb-workflow-fields.js
@@ -0,0 +1,112 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import WBAccordion from 'wb-accordion';
+import WBJsonViewer from 'wb-json-viewer';
+
+class WBWorkflowFields extends Component {
+ componentDidMount() {
+ this.prepareRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.prepareRows();
+ }
+
+ prepareRows() {
+ let { uuid, app } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/workflows/' + uuid);
+
+ prom = prom.then(xhr => {
+ const item = xhr.response;
+ let definition;
+ try {
+ definition = JSON.parse(item.definition);
+ } catch (_) {
+ definition = jsyaml.load(item.definition);
+ }
+ const graph = definition['$graph'];
+
+ let rows = [
+ [ 'Name', item.name ],
+ [ 'Description', item.description || ({ String(item.description) }) ],
+ [ 'CWL Version', definition.cwlVersion ],
+ ];
+
+ let keys = graph.map(it => it.id);
+ keys.sort();
+ keys = keys.splice(keys.indexOf('#main'), 1).concat(keys);
+
+ keys.map(k => {
+ const it = graph.find(it => (it.id === k));
+ rows.push([
+ it.id, (
+
+
Class: { it['class'] }
+ { it.label ?
Label: { it.label }
: null }
+ { it.doc ?
Doc: { it.doc }
: null }
+
+
+
+
+
+
+ { (() => {
+ delete it['inputs'];
+ delete it['outputs'];
+ delete it['class'];
+ delete it['label'];
+ delete it['doc'];
+ delete it['id'];
+ return (
+
+ );
+ })() }
+
+
+
+ )]);
+ });
+
+ /* [ 'Graph', (
+ it.id) }
+ cardHeaderClass="card-header-sm">
+
+ { graph.map(it => (
+
+ )) }
+
+
+ ) ]
+ ];*/
+ this.setState({ 'rows': rows });
+ });
+ }
+
+ render({}, { rows }) {
+ return (
+ rows ? (
+
+ ) : (
+ Loading...
+ )
+ );
+ }
+}
+
+export default WBWorkflowFields;
diff --git a/frontend/src/js/component/wb-workflow-input.js b/frontend/src/js/component/wb-workflow-input.js
new file mode 100644
index 0000000..633c0a5
--- /dev/null
+++ b/frontend/src/js/component/wb-workflow-input.js
@@ -0,0 +1,78 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import wbInputSpecInfo from 'wb-input-spec-info';
+import WBPathDisplay from 'wb-path-display';
+import { parseKeepRef } from 'wb-process-misc';
+
+class WBWorkflowInput extends Component {
+ render({ app, inputSpec, inputsDict, browseDialogRef }) {
+ const { isFile, isDirectory, isArray } = wbInputSpecInfo(inputSpec);
+
+ if (!isFile && !isDirectory)
+ return (
+
+
(inputsDict[inputSpec.id] = e.target.value) }>
+
{ inputSpec.doc }
+
+ );
+
+ const button = (
+
+ );
+
+ let value = inputsDict[inputSpec.id];
+ if (value) {
+ try {
+ value = jsyaml.load(value);
+ } catch (_) {}
+ }
+
+ return (
+
+
+
{ inputSpec.doc }
+ { value ?
+ isArray ? (
+
+ { value.map(path => (
+ -
+
+
+ )) }
+
+ ) : (
+
+ ) : null }
+
+ );
+ }
+}
+
+export default WBWorkflowInput;
diff --git a/frontend/src/js/component/wb-workflow-listing.js b/frontend/src/js/component/wb-workflow-listing.js
new file mode 100644
index 0000000..6e5c806
--- /dev/null
+++ b/frontend/src/js/component/wb-workflow-listing.js
@@ -0,0 +1,116 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import wbFetchObjects from 'wb-fetch-objects';
+import wbFormatDate from 'wb-format-date';
+import urlForObject from 'url-for-object';
+import arvadosObjectName from 'arvados-object-name';
+
+class WBWorkflowListing extends Component {
+
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.numPages = 0;
+ }
+
+ componentDidMount() {
+ this.fetchItems();
+ }
+
+ prepareRows(items, ownerLookup) {
+ const { renderRenameLink, renderDeleteButton,
+ renderSelectionCell, renderSharingButton,
+ renderEditDescription } = this.props;
+
+ return items.map(item => [
+ renderSelectionCell(item),
+ (
+
+ ),
+ (
+ { item.description } { renderEditDescription(item, () => this.fetchItems()) }
+
),
+ ( ),
+ wbFormatDate(item.created_at),
+ (
+
+
+ { renderDeleteButton(item, () => this.fetchItems()) }
+ { renderSharingButton(item) }
+
)
+ ]);
+ }
+
+ fetchItems() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { page, itemsPerPage, ownerUuid, textSearch } = this.props;
+ const filters = [];
+ if (ownerUuid)
+ filters.push([ 'owner_uuid', '=', ownerUuid ]);
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]);
+ const select = ['uuid', 'name', 'description', 'owner_uuid', 'created_at'];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/workflows?filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&select=' + encodeURIComponent(JSON.stringify(select)) +
+ '&limit=' + encodeURIComponent(itemsPerPage) +
+ '&offset=' + encodeURIComponent(itemsPerPage * page));
+ let workflowResp;
+ prom = prom.then(xhr => (workflowResp = xhr.response));
+ prom = prom.then(() => wbFetchObjects(arvHost, arvToken,
+ workflowResp.items.map(it => it.owner_uuid)));
+ let ownerLookup;
+ prom = prom.then(lookup => (ownerLookup = lookup));
+ prom = prom.then(() =>
+ this.setState({
+ 'numPages': Math.ceil(workflowResp['items_available'] / workflowResp['limit']),
+ 'rows': this.prepareRows(workflowResp.items, ownerLookup)
+ }));
+ }
+
+ componentWillReceiveProps(nextProps, nextState) {
+ this.props = nextProps;
+ this.fetchItems();
+ }
+
+ render({ app, ownerUuid, page, getPageUrl }, { rows, numPages }) {
+ return (
+
+
+
+
+
+ );
+ }
+}
+
+WBWorkflowListing.defaultProps = {
+ 'itemsPerPage': 100,
+ 'ownerUuid': null,
+ 'renderSharingButton': () => null,
+ 'renderEditDescription': () => null
+};
+
+export default WBWorkflowListing;
diff --git a/frontend/src/js/deprecated/fetch-project-parents.js b/frontend/src/js/deprecated/fetch-project-parents.js
new file mode 100644
index 0000000..2106eb7
--- /dev/null
+++ b/frontend/src/js/deprecated/fetch-project-parents.js
@@ -0,0 +1,50 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+
+function fetchProjectParents(arvHost, arvToken, uuid) {
+ let parents = [];
+
+ let cb = xhr => {
+ let objectType = xhr.response['uuid'].split('-')[1];
+
+ if (objectType === 'tpzed') {
+ let name = xhr.response['first_name'] + ' ' + xhr.response['last_name'];
+ parents.push({ 'name': name, 'uuid': xhr.response['uuid'] });
+ } else {
+ parents.push({ 'name': xhr.response['name'],
+ 'uuid': xhr.response['uuid'] });
+ }
+
+ if (!xhr.response['owner_uuid'] ||
+ xhr.response['owner_uuid'].endsWith('-tpzed-000000000000000')) {
+
+ return parents.reverse();
+ }
+
+ objectType = xhr.response['owner_uuid'].split('-')[1];
+
+ if (objectType === 'tpzed') {
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/users/' + xhr.response['owner_uuid']).then(cb);
+
+ } else {
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups/' + xhr.response['owner_uuid']).then(cb);
+ }
+ };
+
+ let objectType = uuid.split('-')[1];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + (objectType === 'tpzed' ? 'users' : 'groups') + '/' + uuid);
+ prom = prom.then(cb);
+
+ return prom;
+}
+
+export default fetchProjectParents;
diff --git a/frontend/src/js/deprecated/wb-arvados-collection.js b/frontend/src/js/deprecated/wb-arvados-collection.js
new file mode 100644
index 0000000..e54b291
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-arvados-collection.js
@@ -0,0 +1,57 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBArvadosCollection {
+ constructor(arvHost, arvToken, uuid) {
+ this.arvHost = arvHost;
+ this.arvToken = arvToken;
+ this.uuid = uuid;
+ this.meta = null;
+ }
+
+ fetchMeta() {
+ let prom = makeArvadosRequest(this.arvHost, this.arvToken,
+ '/arvados/v1/collections/' + this.uuid);
+ prom = prom.then(xhr => {
+ this.meta = xhr.response;
+ });
+ return prom;
+ }
+
+ parseManifest() {
+ if (this.meta === null)
+ throw Error('You must call fetchMeta() first and wait for the returned Promise.');
+
+ let manifest = this.meta.manifest_text;
+ let streams = manifest.split('\n');
+ this.content = streams.map(s => {
+ let tokens = s.split(' ');
+ let streamName = tokens[0];
+ let rx = /^[a-f0-9]{32}\+[0-9]+/;
+ let n = tokens.map(t => rx.exec(t));
+ n = n.indexOf(null);
+ let locators = tokens.slice(1, n)
+ let fileTokens = tokens.slice(n);
+ let fileNames = fileTokens.map(t => t.split(':')[2]);
+ let fileSizes = {};
+ fileTokens.map(t => {
+ let [ start, end, name ] = t.split(':');
+ if (!(name in fileSizes))
+ fileSizes[name] = 0;
+ fileSizes[name] += Number(end) - Number(start);
+ });
+ fileSizes = fileNames.map(n => fileSizes[n]);
+ return [ streamName, fileNames, fileSizes ];
+ });
+
+ return this.content;
+ }
+}
+
+export WBArvadosCollection;
diff --git a/frontend/src/js/deprecated/wb-browse-dialog.js b/frontend/src/js/deprecated/wb-browse-dialog.js
new file mode 100644
index 0000000..4009914
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-browse-dialog.js
@@ -0,0 +1,42 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBBrowseDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ }
+
+ render({ id }) {
+ return (
+
+ );
+ }
+}
+
+export default WBBrowseDialog;
diff --git a/frontend/src/js/deprecated/wb-collection-manifest.js b/frontend/src/js/deprecated/wb-collection-manifest.js
new file mode 100644
index 0000000..17f892e
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-collection-manifest.js
@@ -0,0 +1,187 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+//
+// Directory: Hash[string, [Directory, File]]
+// File = [blockRefs, size]
+// blockRefs: Array[blockRef]
+// blockRef: [locator, position, size]
+// locator: String
+// position: Number
+// size: Number
+//
+
+class WBManifestReader {
+ constructor(manifest_text) {
+ this.rootDir = {};
+
+ if (!manifest_text)
+ return;
+
+ this.parse(manifest_text);
+ }
+
+ makeDir(parent, name) {
+ if (!(name in parent))
+ parent[name] = {};
+ if (parent[name] instanceof Array)
+ throw Error('Conflict trying to create a directory - a file with the same name already exists: ' + name);
+ return parent[name];
+ }
+
+ makePath(path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ let dir = this.rootDir;
+ for (let i = 1; i < path.length; i++)
+ dir = this.makeDir(dir, path[i]);
+ return dir;
+ }
+
+ appendFile(streamName, locators, position, size, fileName) {
+ let path = streamName + '/' + fileName;
+ path = path.split('/');
+ let dir = this.makePath(path.slice(0, path.length - 1));
+ if (!(fileName in dir))
+ dir[fileName] = [[], 0];
+ if (!(dir[fileName] instanceof Array))
+ throw Error('Conflict trying to create a file - a directory with the same name already exists: ' + fileName);
+ //this.appendReferences(dir[fileName], locators, position, size);
+ }
+
+ appendReferences(file, locators, position, size) {
+ if (size === 0)
+ return;
+
+ let cum = 0;
+ let locHashes = locators.map(loc => loc[0]);
+ let locSizes = locators.map(loc => loc[1]);
+ let locPositions = locators.map(loc => {
+ let res = cum;
+ cum += loc[1];
+ return res;
+ });
+
+ let used = locators.map((_, i) => (locPositions[i] + locSizes[i] > position &&
+ locPositions[i] < position + size));
+
+ let startBlock = used.indexOf(true);
+ let endBlock = used.lastIndexOf(true) + 1;
+
+ // console.log('startBlock: ' + startBlock + ', endBlock: ' + endBlock);
+
+ if (startBlock === -1)
+ return;
+
+ let blockRefs = [];
+ let runPos = position;
+ let runSize = size;
+
+ for (let i = startBlock; i < endBlock; i++) {
+ let blockPos = runPos - locPositions[i];
+ let blockSize = Math.min(runSize, locSizes[i] - blockPos);
+ blockRefs.push([ locHashes[i], blockPos, blockSize ]);
+ runPos += blockSize;
+ runSize -= blockSize;
+ }
+
+ file[0] = file[0].concat(blockRefs);
+ file[1] += size;
+ }
+
+ parse(manifest_text) {
+ let rx = /^[a-f0-9]{32}\+[0-9]+/;
+
+ let streams = manifest_text.split('\n');
+ if (!streams[streams.length - 1])
+ streams = streams.slice(0, streams.length - 1);
+
+ streams.map(s => {
+ let tokens = s.split(' ');
+ let streamName = this.unescapeName(tokens[0]);
+
+ let n = tokens.map(t => rx.exec(t));
+ n = n.indexOf(null, 1);
+
+ let locators = tokens.slice(1, n);
+ locators = locators.map(loc => [ loc, Number(loc.split('+')[1]) ]);
+
+ let fileTokens = tokens.slice(n);
+ fileTokens.map(t => {
+ let [ position, size, ...fileName ] = t.split(':');
+ fileName = fileName.join(':');
+ fileName = this.unescapeName(fileName);
+ this.appendFile(streamName, locators,
+ Number(position), Number(size), fileName);
+ });
+ });
+ }
+
+ findDir(path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ if (path[0] !== '.')
+ throw Error('Path must begin with a dot component');
+ let dir = this.rootDir;
+ for (let i = 1; i < path.length; i++) {
+ if (!(path[i] in dir))
+ throw Error('Directory not found');
+ if (dir[path[i]] instanceof Array)
+ throw Error('Path is a file not directory');
+ dir = dir[path[i]];
+ }
+ return dir;
+ }
+
+ listDirectory(path) {
+ let dir = this.findDir(path);
+ let keys = Object.keys(dir);
+ keys.sort();
+ let subdirs = keys.filter(k => !(dir[k] instanceof Array));
+ let files = keys.filter(k => (dir[k] instanceof Array));
+ let res = subdirs.map(k => [ 'd', k, null ]);
+ res = res.concat(files.map(k => [ 'f', k, dir[k][1] ]));
+ return res;
+ }
+
+ unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g, (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+ }
+
+ escapeName(name) {
+ return name.replace(/ /g, '\\040');
+ }
+ /* let ids = { '\\': 1, '0': 2, '4': 3 };
+ let transitions = [
+ [ [0, 0], [1, ''], [0, 0], [0, 0] ],
+ [ [0, 0], [0, '\\'], [2, ''], [0, 0] ],
+ ];
+ let mode = 0;
+ for (let i = 0; i < name.length; i++) {
+ let b = name[i];
+ let tokenId = Number(ids[b]);
+ [ mode, out ] = transitions[mode][tokenId];
+ if (out === 0)
+ out = b;
+ }
+ }*/
+ getFile(path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ if (path.length < 2)
+ throw Error('Invalid file path');
+ let name = path[path.length - 1];
+ let dir = this.findDir(path.slice(0, path.length - 1));
+ if (!(name in dir))
+ throw Error('File not found');
+ if (!(dir[name] instanceof Array))
+ throw Error('Path points to a directory not a file');
+ return dir[name];
+ }
+}
+
+export { WBManifestReader };
diff --git a/frontend/src/js/deprecated/wb-download-file.js b/frontend/src/js/deprecated/wb-download-file.js
new file mode 100644
index 0000000..37b0586
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-download-file.js
@@ -0,0 +1,72 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import makeArvadosRequest from 'make-arvados-request';
+
+function rdvHash(serviceId, locator) {
+ let blockHash = /^[0-9a-f]{32}/.exec(locator);
+ if (!blockHash)
+ throw Error('Invalid locator');
+ if (typeof(serviceId) !== 'string')
+ throw Error('Invalid service ID');
+ let res = CryptoJS.MD5(serviceId + blockHash).toString();
+ return res;
+}
+
+function wbDownloadFile(arvHost, arvToken,
+ manifestReader, path) {
+
+ const file = manifestReader.getFile(path);
+ const name = path.split('/').reverse()[0];
+ const blockRefs = file[0];
+ let services;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/keep_services/accessible');
+ prom = prom.then(xhr => (services = xhr.response['items']));
+
+ const blocks = [];
+ for (let i = 0; i < blockRefs.length; i++) {
+ prom = prom.then(() => {
+ const [ locator, position, size ] = blockRefs[i];
+ const weights = services.map(s => rdvHash(s['uuid'], locator));
+ const order = Object.keys(services).sort((a, b) => weights[b].localeCompare(weights[a]));
+ const orderedServices = order.map(i => services[i]);
+
+ let k = 0;
+ const cb = () => {
+ if (k >= orderedServices.length)
+ throw Error('Block not found');
+
+ const svc = orderedServices[k];
+ k++;
+
+ let prom_1 = makeArvadosRequest(svc.service_host +
+ ':' + svc.service_port, arvToken,
+ '/' + locator, { 'useSsl': svc.service_ssl_flag,
+ 'responseType': 'arraybuffer' });
+ //prom_1 = prom_1.then(xhr => xhr.response);
+ prom_1 = prom_1.catch(cb);
+
+ return prom_1;
+ };
+
+ return cb().then(xhr => (blocks.append(xhr.response.slice(position, size))));
+ });
+ }
+
+ prom = prom.then(() => {
+ const blob = new Blob(blocks);
+ const url = window.URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = name;
+
+ });
+}
+
+export default wbDownloadFile;
diff --git a/frontend/src/js/deprecated/wb-manifest-reader.js b/frontend/src/js/deprecated/wb-manifest-reader.js
new file mode 100644
index 0000000..c407002
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-manifest-reader.js
@@ -0,0 +1,188 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function mkdir(parent, name) {
+ if (name in parent && (parent[name] instanceof Array))
+ throw Error('File with the same name already exists');
+ if (name in parent)
+ return parent[name];
+ const dir = {};
+ parent[name] = dir;
+ return dir;
+}
+
+function mkpath(parent, path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ let dir = parent;
+ for (let i = 1; i < path.length; i++) {
+ dir = mkdir(dir, path[i]);
+ }
+ return dir;
+}
+
+function makeFile(dir, name) {
+ if (name in dir) {
+ if (!(dir[name] instanceof Array))
+ throw Error('Directory with the same name already exists');
+ return dir[name];
+ }
+ const f = [[], 0];
+ dir[name] = f;
+ return f;
+}
+
+function appendFile(f, sidx, seg) {
+ f[0].push([ sidx, seg[0], seg[1] ]);
+ //f[1] += seg[1];
+ return f;
+}
+
+function unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g,
+ (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+}
+
+function process(streams) {
+ const rootDir = {};
+
+ streams.map((s, sidx) => {
+ const [ streamName, locators, segments ] = s;
+ const streamDir = mkpath(rootDir, streamName);
+ segments.map((seg, segidx) => {
+ let name = seg[2].split('/');
+ const dir = (name.length === 1 ? streamDir :
+ mkpath(streamDir, ['.'].concat(name.slice(0, name.length - 1))));
+ name = name[name.length - 1];
+ appendFile(dir, name, sidx, seg);
+ });
+ });
+
+ return rootDir;
+}
+
+function parse(manifestText) {
+ const M_STREAM_NAME = 0;
+ const M_LOCATORS = 1;
+ const M_FILE_SEGMENTS = 2;
+
+ let mode = M_STREAM_NAME;
+
+ const streams = [];
+ let locators = [];
+
+ let streamName;
+ let accum = '';
+ let tokenStart = 0;
+
+ let lastFile = null;
+ let lastPath = null;
+ const rootDir = {};
+
+ for (let i = 0; i < manifestText.length; i++) {
+ const c = manifestText[i];
+
+ if (mode === M_STREAM_NAME) {
+ if (c === ' ') {
+ mode = M_LOCATORS;
+ streamName = unescapeName(accum);
+ accum = '';
+ tokenStart = i + 1;
+ } else {
+ accum += c;
+ }
+
+ } else if (mode === M_LOCATORS) {
+ if (c === ':') {
+ mode = M_FILE_SEGMENTS;
+ accum = '';
+ i = tokenStart - 1;
+ let pos = 0;
+ locators = locators.map(loc => {
+ const r = loc.concat([ pos, pos + loc[1] ]);
+ pos += loc[1];
+ return r;
+ });
+ } else if (c === ' ') {
+ const sz = Number(accum.split('+')[1]);
+ locators.push([accum, sz]);
+ accum = '';
+ tokenStart = i + 1;
+ } else {
+ accum += c;
+ }
+
+ } else if (mode === M_FILE_SEGMENTS) {
+ if (c === ' ' || c === '\n') {
+ let seg = accum.split(':');
+ seg = [Number(seg[0]), Number(seg[1]), seg.slice(2).join(':')];
+ const path = streamName + '/' + unescapeName(seg[2]);
+ let f;
+ if (path !== lastPath) {
+ let dirName = path.split('/');
+ const fileName = dirName[dirName.length - 1];
+ dirName = dirName.slice(0, dirName.length - 1);
+ const dir = mkpath(rootDir, dirName);
+ f = makeFile(dir, fileName);
+ lastPath = path;
+ lastFile = f;
+ } else {
+ f = lastFile;
+ }
+ appendFile(f, streams.length, seg);
+ accum = '';
+ tokenStart = i + 1;
+ if (c === '\n') {
+ streams.push([ streamName, locators ]);
+ locators = [];
+ mode = M_STREAM_NAME;
+ }
+ } else {
+ accum += c;
+ }
+
+ }
+ }
+
+ return { rootDir, streams };
+}
+
+function findDir(parent, path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ if (path[0] !== '.')
+ throw Error('Path must start with a dot (.)');
+ let dir = parent;
+ for (let i = 1; i < path.length; i++) {
+ if (!(path[i] in dir))
+ throw Error('Directory not found');
+ dir = dir[path[i]];
+ }
+ return dir;
+}
+
+class WBManifestReader {
+ constructor(manifestText) {
+ const {rootDir, streams} = parse(manifestText);
+ this.rootDir = rootDir;
+ this.streams = streams;
+ //this.rootDir = process(this.streams);
+ }
+
+ listDirectory(path) {
+ let dir = findDir(this.rootDir, path);
+ let keys = Object.keys(dir);
+ keys.sort();
+ let subdirs = keys.filter(k => !(dir[k] instanceof Array));
+ let files = keys.filter(k => (dir[k] instanceof Array));
+ let res = subdirs.map(k => [ 'd', k, null ]);
+ res = res.concat(files.map(k => [ 'f', k, dir[k][1] ]));
+ return res;
+ }
+}
+
+export default WBManifestReader;
diff --git a/frontend/src/js/deprecated/wb-project-crumbs.js b/frontend/src/js/deprecated/wb-project-crumbs.js
new file mode 100644
index 0000000..8ae625d
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-project-crumbs.js
@@ -0,0 +1,46 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBBreadcrumbs from 'wb-breadcrumbs';
+import fetchProjectParents from 'fetch-project-parents';
+
+class WBProjectCrumbs extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.items = [ { 'name': 'All Projects' } ];
+ }
+
+ fetchCrumbs() {
+ if (!this.props.uuid) {
+ this.setState({ 'items': [ { 'name': 'All Projects' } ] });
+ return;
+ }
+
+ let { arvHost, arvToken } = this.props.appState;
+ let prom = fetchProjectParents(arvHost, arvToken, this.props.uuid);
+ prom = prom.then(parents => this.setState({ 'items': parents }));
+ }
+
+ componentDidMount() {
+ this.fetchCrumbs();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchCrumbs();
+ }
+
+ render({ onItemClicked }, { items }) {
+ return (
+
+ );
+ }
+}
+
+export default WBProjectCrumbs;
diff --git a/frontend/src/js/deprecated/wb-rootdir-wrapper.js b/frontend/src/js/deprecated/wb-rootdir-wrapper.js
new file mode 100644
index 0000000..c527710
--- /dev/null
+++ b/frontend/src/js/deprecated/wb-rootdir-wrapper.js
@@ -0,0 +1,89 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+class WBRootDirWrapper {
+ constructor(rootDir, streams) {
+ this.rootDir = rootDir;
+ this.streams = streams;
+ }
+
+ findDir(path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ if (path[0] !== '.')
+ throw Error('Path must begin with a dot component');
+ let dir = this.rootDir;
+ for (let i = 1; i < path.length; i++) {
+ if (!(path[i] in dir))
+ throw Error('Directory not found');
+ if (dir[path[i]] instanceof Array)
+ throw Error('Path is a file not directory');
+ dir = dir[path[i]];
+ }
+ return dir;
+ }
+
+ listDirectory(path) {
+ let dir = this.findDir(path);
+ let keys = Object.keys(dir);
+ keys.sort();
+ let subdirs = keys.filter(k => !(dir[k] instanceof Array));
+ let files = keys.filter(k => (dir[k] instanceof Array));
+ let res = subdirs.map(k => [ 'd', k, null ]);
+ res = res.concat(files.map(k => [ 'f', k, dir[k][1] ]));
+ return res;
+ }
+
+ unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g, (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+ }
+
+ escapeName(name) {
+ return name.replace(/ /g, '\\040');
+ }
+
+ getFile(path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+
+ if (path.length < 2)
+ throw Error('Invalid file path');
+
+ const name = path[path.length - 1];
+
+ const dir = this.findDir(path.slice(0, path.length - 1));
+
+ if (!(name in dir))
+ throw Error('File not found');
+
+ if (!(dir[name] instanceof Array))
+ throw Error('Path points to a directory not a file');
+
+ const streams = this.streams;
+ let file = dir[name];
+ file = [ file[0].map(seg => {
+ const stm = streams[seg[0]];
+ const used = stm.map(loc => !( loc[2] <= seg[1] || loc[1] >= seg[1] + seg[2] ) );
+ const start = used.indexOf(true);
+ const end = used.lastIndexOf(true) + 1;
+ if (start === -1)
+ return [];
+ const res = [];
+ for (let i = start; i < end; i++) {
+ const loc = stm[i];
+ res.push([ loc[0], Math.max(0, seg[1] - loc[1]),
+ Math.min(loc[2] - loc[1], seg[1] + seg[2] - loc[1]) ]);
+ }
+ return res;
+ }), file[1] ];
+ file[0] = file[0].reduce((a, b) => a.concat(b));
+
+ return file;
+ }
+}
+
+export default WBRootDirWrapper;
diff --git a/frontend/src/js/dialog/wb-browse-dialog-collection-content.js b/frontend/src/js/dialog/wb-browse-dialog-collection-content.js
new file mode 100644
index 0000000..a8470bf
--- /dev/null
+++ b/frontend/src/js/dialog/wb-browse-dialog-collection-content.js
@@ -0,0 +1,125 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBManifestWorkerWrapper from 'wb-manifest-worker-wrapper';
+import makeArvadosRequest from 'make-arvados-request';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+
+function unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g,
+ (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+}
+
+class WBBrowseDialogCollectionContent extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.manifestWorker = new WBManifestWorkerWrapper();
+ this.state.mode = 'manifestDownload';
+ this.state.rows = [];
+ }
+
+ componentDidMount() {
+ const { app, collectionUuid } = this.props;
+ const { arvHost, arvToken } = app.state;
+ const { manifestWorker } = this.state;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections/' + collectionUuid);
+
+ let streams;
+
+ prom = prom.then(xhr => {
+ streams = xhr.response.manifest_text.split('\n');
+ const paths = streams.filter(s => s).map(s => {
+ const n = s.indexOf(' ');
+ return unescapeName(s.substr(0, n));
+ });
+
+ return manifestWorker.postMessage([ 'precreatePaths', paths ]);
+ });
+
+ prom = prom.then(() => {
+ this.setState({ 'mode': 'manifestParse' });
+ let prom_1 = new Promise(accept => accept());
+ for (let i = 0; i < streams.length; i++) {
+ prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'parseStream', streams[i] ]));
+ prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'listDirectory', '.' + this.props.collectionPath, true ]));
+ prom_1 = prom_1.then(e => this.prepareRows(e.data[1]));
+ }
+ return prom_1;
+ });
+
+ prom = prom.then(() => manifestWorker.postMessage([ 'listDirectory', '.' + this.props.collectionPath, true ]));
+ prom = prom.then(e => {
+ this.state.mode = 'browsingReady';
+ this.prepareRows(e.data[1])
+ });
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ if (this.state.mode !== 'browsingReady')
+ return;
+ let prom = this.state.manifestWorker.postMessage([
+ 'listDirectory', '.' + this.props.collectionPath, true
+ ]);
+ prom = prom.then(e => this.prepareRows(e.data[1]));
+ }
+
+ prepareRows(listing) {
+ const { makeSelectionCell, collectionPath, navigate,
+ page, itemsPerPage, collectionUuid, textSearch, selectWhat } = this.props;
+ const textLower = textSearch.toLowerCase();
+ listing = listing.filter(it => (it[1].toLowerCase().indexOf(textLower) !== -1));
+ const numPages = Math.ceil(listing.length / itemsPerPage);
+ const rows = listing.slice(page * itemsPerPage,
+ (page + 1) * itemsPerPage).map(it => [
+ ((it[0] === 'd' && [].concat(selectWhat).indexOf('directory') !== -1) ||
+ (it[0] === 'f' && [].concat(selectWhat).indexOf('file') !== -1)) ?
+ makeSelectionCell(collectionUuid + collectionPath + '/' + it[1] + (it[0] === 'd' ? '/' : '')) :
+ null,
+ it[0] === 'd' ? (
+ {
+ e.preventDefault();
+ navigate({ 'collectionPath': collectionPath + '/' + it[1],
+ 'bottomPage': 0 });
+ } }>{ it[1] }
+ ) : it[1],
+ it[0] === 'f' ? filesize(it[2]) : ''
+ ]);
+ this.setState({ rows, numPages });
+ }
+
+ render({ page, navigate }, { rows, mode, numPages }) {
+ return (
+
+ { mode === 'browsingReady' ? (
+ null
+ ) : [
+
{ mode === 'manifestParse' ? 'Parsing manifest...' : 'Downloading manifest...' }
,
+
+ ] }
+
+
navigate({ 'bottomPage': i }) } />
+
+ );
+ }
+}
+
+WBBrowseDialogCollectionContent.defaultProps = {
+ 'itemsPerPage': 20
+};
+
+export default WBBrowseDialogCollectionContent;
diff --git a/frontend/src/js/dialog/wb-browse-dialog-collection-list.js b/frontend/src/js/dialog/wb-browse-dialog-collection-list.js
new file mode 100644
index 0000000..190d4ff
--- /dev/null
+++ b/frontend/src/js/dialog/wb-browse-dialog-collection-list.js
@@ -0,0 +1,78 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBBrowseDialogCollectionList extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ }
+
+ componentDidMount() {
+ this.fetchRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchRows();
+ }
+
+ prepareRows(items) {
+ const { navigate, selectWhat, makeSelectionCell } = this.props;
+ return items.map(it => [
+ ([].concat(selectWhat).indexOf('directory') !== -1 ? makeSelectionCell(it.uuid + '/') : null),
+ (
+ { e.preventDefault();
+ navigate('/browse-dialog/content/' + it.uuid + '////'); } }>{ it.name }
+ ),
+ it.uuid
+ ]);
+ }
+
+ fetchRows() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { ownerUuid, textSearch, page, itemsPerPage } = this.props;
+ const filters = [];
+ if (ownerUuid)
+ filters.push(['owner_uuid', '=', ownerUuid]);
+ if (textSearch)
+ filters.push(['name', 'ilike', '%' + textSearch + '%']);
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/collections?filters=' +
+ encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (itemsPerPage * page));
+ prom = prom.then(xhr => this.setState({
+ 'rows': this.prepareRows(xhr.response.items),
+ 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage)
+ }));
+ return prom;
+ }
+
+ render({ selectWhat, page, navigate }, { rows, numPages }) {
+ return (
+
+
+
+ navigate({ 'bottomPage': i }) } />
+
+ );
+ }
+}
+
+WBBrowseDialogCollectionList.defaultProps = {
+ 'itemsPerPage': 20
+};
+
+export default WBBrowseDialogCollectionList;
diff --git a/frontend/src/js/dialog/wb-browse-dialog-project-list.js b/frontend/src/js/dialog/wb-browse-dialog-project-list.js
new file mode 100644
index 0000000..c6b3f74
--- /dev/null
+++ b/frontend/src/js/dialog/wb-browse-dialog-project-list.js
@@ -0,0 +1,113 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import WBPagination from 'wb-pagination';
+import WBTable from 'wb-table';
+
+class WBBrowseDialogProjectList extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.history = [];
+ }
+
+ componentDidMount() {
+ this.fetchRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchRows();
+ }
+
+ prepareRows(items) {
+ const { navigate, selectWhat, makeSelectionCell } = this.props;
+ return items.map(it => ([].concat(selectWhat).indexOf('owner') !== -1 ? [ makeSelectionCell(it.uuid, 'project') ] : []).concat([
+ (
+ {
+ e.preventDefault();
+ navigate('/browse-dialog/browse/' + it.uuid);
+ } }>{ it.name }
+ ),
+ it.uuid
+ ]));
+ }
+
+ fetchSharedWithMe() {
+ const { arvHost, arvToken, currentUser } = this.props.app.state;
+ const { textSearch, itemsPerPage, page } = this.props;
+ const filters = [
+ ['group_class', '=', 'project']
+ ];
+ if (textSearch)
+ filters.push([ 'name', 'ilike', '%' + textSearch + '%']);
+ const prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups/shared?filters=' +
+ encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (itemsPerPage * page));
+ return prom;
+ }
+
+ fetchOwned() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { ownerUuid, page, textSearch, itemsPerPage } = this.props;
+ const filters = [
+ ['group_class', '=', 'project']
+ ];
+ if (ownerUuid)
+ filters.push(['owner_uuid', '=', ownerUuid]);
+ if (textSearch)
+ filters.push(['name', 'ilike', '%' + textSearch + '%']);
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups?filters=' +
+ encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (page * itemsPerPage));
+ return prom;
+ }
+
+ fetchRows() {
+ const { mode, itemsPerPage } = this.props;
+
+ let prom = (mode === 'shared-with-me') ?
+ this.fetchSharedWithMe() :
+ this.fetchOwned();
+
+ prom = prom.then(xhr => {
+ this.setState({
+ 'rows': this.prepareRows(xhr.response.items),
+ 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage)
+ });
+ });
+
+ return prom;
+ }
+
+ render({ app, navigate, page, selectWhat }, { numPages, rows }) {
+ return (
+
+
+
+ navigate({ 'topPage': i }) }
+ chunkSize="3" />
+
+ );
+ }
+}
+
+WBBrowseDialogProjectList.defaultProps = {
+ 'itemsPerPage': 5,
+ 'resetSearch': () => {}
+};
+
+export default WBBrowseDialogProjectList;
diff --git a/frontend/src/js/dialog/wb-browse-dialog-user-list.js b/frontend/src/js/dialog/wb-browse-dialog-user-list.js
new file mode 100644
index 0000000..9ab6d56
--- /dev/null
+++ b/frontend/src/js/dialog/wb-browse-dialog-user-list.js
@@ -0,0 +1,78 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBBrowseDialogUserList extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ }
+
+ componentDidMount() {
+ this.fetchRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchRows();
+ }
+
+ prepareRows(items) {
+ const { navigate } = this.props;
+ return items.map(it => [
+ (
+ { e.preventDefault();
+ navigate('/browse-dialog/browse/' + it.uuid); } }>
+ { it.last_name + ', ' + it.first_name }
+
+ ),
+ it.uuid
+ ]);
+ }
+
+ fetchRows() {
+ const { arvHost, arvToken } = this.props.app.state;
+ const { itemsPerPage, page, textSearch } = this.props;
+ const order = ['last_name asc', 'first_name asc'];
+ const filters = [];
+ if (textSearch)
+ filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]);
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/users?order=' +
+ encodeURIComponent(JSON.stringify(order)) +
+ '&filters=' +
+ encodeURIComponent(JSON.stringify(filters)) +
+ '&limit=' + itemsPerPage +
+ '&offset=' + (itemsPerPage * page));
+ prom = prom.then(xhr => this.setState({
+ 'rows': this.prepareRows(xhr.response.items),
+ 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage)
+ }));
+ }
+
+ render({ page, navigate }, { rows, numPages }) {
+ return (
+
+
+
+ navigate({ 'topPage': i }) } />
+
+ );
+ }
+}
+
+WBBrowseDialogUserList.defaultProps = {
+ 'itemsPerPage': 20
+};
+
+export default WBBrowseDialogUserList;
diff --git a/frontend/src/js/dialog/wb-browse-dialog.js b/frontend/src/js/dialog/wb-browse-dialog.js
new file mode 100644
index 0000000..f804fa5
--- /dev/null
+++ b/frontend/src/js/dialog/wb-browse-dialog.js
@@ -0,0 +1,269 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBBrowseDialogProjectList from 'wb-browse-dialog-project-list';
+import WBBrowseDialogCollectionList from 'wb-browse-dialog-collection-list';
+import WBBrowseDialogCollectionContent from 'wb-browse-dialog-collection-content';
+import WBBrowseDialogUserList from 'wb-browse-dialog-user-list';
+import linkState from 'linkstate';
+import { Router } from 'preact-router';
+import { createHashHistory } from 'history';
+
+//
+// internal URLs look like this
+//
+// /browse-dialog/browse/( owner-uuid )/( project-page )/( text-search )
+// /browse-dialog/users//( users-page )/( text-search )
+// /browse-dialog/shared-with-me//( project-page )/( collection-page )/( text-search )
+// /browse-dialog/content/( collection-uuid )//( content-page )/( text-search )/( collection-path )
+//
+// general pattern therefore:
+// /browse-dialog/( mode )/( uuid )/( top-page )/( bottom-page )/( text-search )
+//
+// props:
+// selectMany: Boolean
+// selectWhat: [ 'file', 'directory', 'owner' ]
+//
+// state:
+// selected: Array of UUID
+// textSearch: string
+// textSearchInput: string
+//
+
+class WBBrowseDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.history = [];
+ this.state.selected = {};
+ this.state.selectedOrder = [];
+ const { currentUser } = this.props.app.state;
+ this.state.currentUrl = '/browse-dialog/browse/' + currentUser.uuid;
+ this.state.uuid = currentUser.uuid;
+ this.state.mode = 'browse';
+ this.state.topPage = 0;
+ this.state.bottomPage = 0;
+ this.state.collectionPath = '';
+ this.state.textSearch = '';
+ this.state.id = ('id' in this.props) ? this.props.id : uuid.v4();
+ this.state.accept = () => {};
+ this.modalRef = createRef();
+ }
+
+ navigateBack() {
+ if (this.state.history.length === 0)
+ return;
+ const url = this.state.history.pop();
+ this.navigate(url, false);
+ }
+
+ navigate(url, useHistory=true, stateUpdate={}) {
+ if (typeof(url) === 'object') {
+ url = ['', 'browse-dialog',
+ 'mode' in url ? url.mode : this.state.mode,
+ 'uuid' in url ? url.uuid : this.state.uuid,
+ 'topPage' in url ? url.topPage : this.state.topPage,
+ 'bottomPage' in url ? url.bottomPage : this.state.bottomPage,
+ 'textSearch' in url ? url.textSearch : this.state.textSearch,
+ encodeURIComponent('collectionPath' in url ? url.collectionPath : this.state.collectionPath)
+ ].join('/');
+ }
+
+ url = url.substr(url.indexOf('/browse-dialog/'));
+
+ if (useHistory)
+ this.state.history.push(this.state.currentUrl);
+ let [ _1, _2, mode, uuid, topPage, bottomPage, textSearch, collectionPath ] = url.split('/');
+ topPage = parseInt(topPage, 10) || 0;
+ bottomPage = parseInt(bottomPage, 10) || 0;
+ collectionPath = decodeURIComponent(collectionPath || '');
+ this.setState(Object.assign({
+ 'currentUrl': url,
+ mode, uuid, topPage, bottomPage, textSearch, collectionPath
+ }, stateUpdate));
+ }
+
+ select(uuid) {
+ let { selected, selectedOrder } = this.state;
+ if (uuid in selected) {
+ const n = selectedOrder.indexOf(uuid);
+ selectedOrder = selected.splice(n, n + 1);
+ }
+ selected[uuid] = true;
+ selectedOrder.push(uuid);
+ /* this.setState({
+ selected, selectedOrder
+ }); */
+ }
+
+ deselect(uuid) {
+ let { selected, selectedOrder } = this.state;
+ if (!(uuid in selected))
+ return;
+ const n = selectedOrder.indexOf(uuid);
+ selectedOrder = selected.splice(n, n + 1);
+ delete selected[uuid];
+ /* this.setState({
+ selected, selectedOrder
+ }); */
+ }
+
+ resetSelection() {
+ this.setState({
+ 'selected': {},
+ 'selectedOrder': []
+ });
+ }
+
+ makeSelectionCell(uuid) {
+ const { selected, accept, selectMany, id } = this.state;
+
+ return selectMany ? (
+
+ {
+ if (e.target.checked)
+ this.select(uuid);
+ else
+ this.deselect(uuid);
+ } } /> { '\u00A0' }
+
+ ) : (
+
+ );
+ }
+
+ show(selectWhat, selectMany, accept=(() => {})) {
+ const { app } = this.props;
+ const { currentUser } = app.state;
+ this.navigate('/browse-dialog/browse/' + currentUser.uuid, false,
+ { selectWhat, selectMany, accept, history: [],
+ selected: {}, selectedOrder: [] });
+ $('#' + this.state.id).modal();
+ }
+
+ componentWillUnmount() {
+ $(this.modalRef.current).modal('hide');
+ }
+
+ render({ app },
+ { history, currentUrl, mode, uuid,
+ topPage, bottomPage, textSearch,
+ collectionPath, id, accept, selectedOrder,
+ selectMany, selectWhat }) {
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ { (mode === 'browse' || mode === 'shared-with-me') ? (
+
+
Projects
+ this.navigate(url) }
+ mode={ mode } ownerUuid={ uuid }
+ page={ topPage } textSearch={ textSearch }
+ selectWhat={ selectWhat }
+ makeSelectionCell={ uuid => this.makeSelectionCell(uuid) } />
+
+ ) : null }
+
+ { (mode === 'users') ? (
+
this.navigate(url) }
+ page={ topPage } textSearch={ textSearch }/>
+ ) : null }
+
+ { (mode === 'content') ? (
+
+
Content
+ this.makeSelectionCell(uuid) }
+ navigate={ url => this.navigate(url) }
+ textSearch={ textSearch } />
+
+
+ ) : (selectWhat !== 'owner' && mode === 'browse') ? (
+
+
Collections
+ this.navigate(url) }
+ ownerUuid={ uuid } selectWhat={ selectWhat }
+ makeSelectionCell={ uuid => this.makeSelectionCell(uuid) } />
+
+ ) : null }
+
+
+
+
+
+
+ );
+ }
+}
+
+WBBrowseDialog.defaultProps = {
+ 'accept': () => {}
+};
+
+export default WBBrowseDialog;
diff --git a/frontend/src/js/dialog/wb-delete-dialog.js b/frontend/src/js/dialog/wb-delete-dialog.js
new file mode 100644
index 0000000..518f7b7
--- /dev/null
+++ b/frontend/src/js/dialog/wb-delete-dialog.js
@@ -0,0 +1,60 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBDialog from 'wb-dialog';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import linkState from 'linkstate';
+import wbDeleteObject from 'wb-delete-object';
+import arvadosTypeName from 'arvados-type-name';
+
+class WBDeleteDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ }
+
+ show(item, callback) {
+ this.setState({
+ 'item': item,
+ 'callback': callback || (() => {})
+ });
+ this.dialogRef.current.show();
+ }
+
+ hide() {
+ this.dialogRef.current.hide();
+ }
+
+ render({ app }, { item, callback }) {
+ const { arvHost, arvToken } = app.state;
+
+ return (
+
+
+
+ Are you sure you want to delete the following { item ? arvadosTypeName(item.uuid) : null }:
+
+ { item ?
: null }
+
???
+
+
+
+ { e.preventDefault(); this.hide();
+ wbDeleteObject(arvHost, arvToken, item.uuid).then(callback); } } />
+
+
+
+ );
+ }
+}
+
+export default WBDeleteDialog;
diff --git a/frontend/src/js/dialog/wb-edit-description-dialog.js b/frontend/src/js/dialog/wb-edit-description-dialog.js
new file mode 100644
index 0000000..9c047fd
--- /dev/null
+++ b/frontend/src/js/dialog/wb-edit-description-dialog.js
@@ -0,0 +1,59 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBDialog from 'wb-dialog';
+import linkState from 'linkstate';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosTypeName from 'arvados-type-name';
+
+class WBEditDescriptionDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ this.state.inputId = uuid.v4();
+ }
+
+ show(item, callback) {
+ const { inputId } = this.state;
+ this.setState({
+ 'item': item,
+ 'newDescription': null,
+ 'callback': callback || (() => {})
+ });
+ this.dialogRef.current.show();
+ $('#' + inputId).focus();
+ }
+
+ hide() {
+ this.dialogRef.current.hide();
+ }
+
+ render({ app }, { item, newDescription, callback, inputId }) {
+ const { arvHost, arvToken } = app.state;
+ return (
+
+ makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + arvadosTypeName(item.uuid) +
+ 's/' + item.uuid, {
+ method: 'PUT',
+ data: JSON.stringify({
+ description: newDescription || null
+ })
+ }).then(callback)
+ }>
+
+
+
+
+ );
+ }
+}
+
+export default WBEditDescriptionDialog;
diff --git a/frontend/src/js/dialog/wb-new-project-dialog.js b/frontend/src/js/dialog/wb-new-project-dialog.js
new file mode 100644
index 0000000..c4dafa6
--- /dev/null
+++ b/frontend/src/js/dialog/wb-new-project-dialog.js
@@ -0,0 +1,73 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBDialog from 'wb-dialog';
+import linkState from 'linkstate';
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBNewProjectDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ this.state.inputId = uuid.v4();
+ }
+
+ show(ownerUuid, callback) {
+ const { inputId } = this.state;
+ this.setState({
+ 'ownerUuid': ownerUuid,
+ 'newName': null,
+ 'placeholderName': 'New Project (' + (new Date()).toISOString() + ')',
+ 'callback': callback || (() => {})
+ });
+ this.dialogRef.current.show();
+ $('#' + inputId).focus();
+ }
+
+ hide() {
+ this.dialogRef.current.hide();
+ }
+
+ render({ app }, { ownerUuid, newName, placeholderName, callback, inputId,
+ projectDescription }) {
+
+ const { arvHost, arvToken } = app.state;
+ return (
+ {
+ const group = {
+ 'group_class': 'project',
+ 'name': newName || placeholderName,
+ 'description': projectDescription || null,
+ 'owner_uuid': ownerUuid
+ };
+ makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/groups', { 'method': 'POST',
+ 'data': JSON.stringify(group),
+ 'expectedStatus': [200, 202] }
+ ).then(callback);
+ } }>
+
+
+ );
+ }
+}
+
+export default WBNewProjectDialog;
diff --git a/frontend/src/js/dialog/wb-pick-object-dialog.js b/frontend/src/js/dialog/wb-pick-object-dialog.js
new file mode 100644
index 0000000..1222a96
--- /dev/null
+++ b/frontend/src/js/dialog/wb-pick-object-dialog.js
@@ -0,0 +1,118 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBDialog from 'wb-dialog';
+import WBTable from 'wb-table';
+import WBPagination from 'wb-pagination';
+import makeArvadosRequest from 'make-arvados-request';
+import arvadosObjectName from 'arvados-object-name';
+
+class WBPickObjectDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.title = 'WBPickObjectDialog';
+ this.state.rows = [];
+ this.state.textSearch = null;
+ this.dialogRef = createRef();
+ }
+
+ show(title, objectType, accept, filters=[]) {
+ this.setState({ title, objectType, page: 0, rows: [], accept, filters, textSearch: null });
+ this.dialogRef.current.show();
+ this.fetchData();
+ }
+
+ hide() {
+ this.dialogRef.current.hide();
+ }
+
+ fetchData() {
+ const { app, itemsPerPage } = this.props;
+ const { arvHost, arvToken } = app.state;
+ const { objectType, page, textSearch } = this.state;
+ let { filters } = this.state;
+ if (textSearch)
+ filters = filters.concat([['any', 'ilike', '%' + textSearch + '%']]);
+ const order = (objectType === 'user') ?
+ ['last_name asc', 'first_name asc'] :
+ ['name asc'];
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/' + objectType +
+ 's?offset=' + (page * itemsPerPage) +
+ '&limit=' + itemsPerPage +
+ '&filters=' + encodeURIComponent(JSON.stringify(filters)) +
+ '&order=' + encodeURIComponent(JSON.stringify(order))
+ );
+ prom = prom.then(xhr => this.setState({
+ numPages: Math.ceil(xhr.response.items_available / itemsPerPage),
+ rows: this.prepareRows(xhr.response.items)
+ }));
+ return prom;
+ }
+
+ prepareRows(items) {
+ const { accept } = this.state;
+ const { dialogRef } = this;
+
+ return items.map(it => [
+ ()
+ ]);
+ }
+
+ search(textSearch) {
+ this.setState({ textSearch, page: 0});
+ this.fetchData();
+ }
+
+ render({}, { title, rows, page, numPages, textSearch }) {
+ return (
+
+
+
+
+
+
+
{ this.setState({ page: i }); this.fetchData(); } } />
+
+
+
+
+
+
+ );
+ }
+}
+
+WBPickObjectDialog.defaultProps = {
+ itemsPerPage: 20
+};
+
+export default WBPickObjectDialog;
diff --git a/frontend/src/js/dialog/wb-rename-dialog.js b/frontend/src/js/dialog/wb-rename-dialog.js
new file mode 100644
index 0000000..8a3ef4f
--- /dev/null
+++ b/frontend/src/js/dialog/wb-rename-dialog.js
@@ -0,0 +1,52 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBDialog from 'wb-dialog';
+import linkState from 'linkstate';
+import wbRenameObject from 'wb-rename-object';
+
+class WBRenameDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.dialogRef = createRef();
+ this.state.inputId = uuid.v4();
+ }
+
+ show(item, callback) {
+ const { inputId } = this.state;
+ this.setState({
+ 'item': item,
+ 'newName': null,
+ 'callback': callback || (() => {})
+ });
+ this.dialogRef.current.show();
+ $('#' + inputId).focus();
+ }
+
+ hide() {
+ this.dialogRef.current.hide();
+ }
+
+ render({ app }, { item, newName, callback, inputId }) {
+ const { arvHost, arvToken } = app.state;
+ return (
+ {
+ if (newName)
+ wbRenameObject(arvHost, arvToken, item.uuid, newName).then(callback);
+ } }>
+
+
+
+
+ );
+ }
+}
+
+export default WBRenameDialog;
diff --git a/frontend/src/js/dialog/wb-toolbox-dialog.js b/frontend/src/js/dialog/wb-toolbox-dialog.js
new file mode 100644
index 0000000..896c799
--- /dev/null
+++ b/frontend/src/js/dialog/wb-toolbox-dialog.js
@@ -0,0 +1,154 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBTable from 'wb-table';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import wbFetchObjects from 'wb-fetch-objects';
+import wbFormatDate from 'wb-format-date';
+
+class WBToolboxDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.state.selectedValues = {};
+ }
+
+ componentDidMount() {
+ this.fetchRows();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.fetchRows();
+ }
+
+ fetchRows() {
+ const { items, id, selectMany, onAccepted } = this.props;
+ const { arvHost, arvToken } = this.props.app.state;
+ const { selectedValues } = this.state;
+ let prom = wbFetchObjects(arvHost, arvToken,
+ items);
+ let lookup;
+ prom = prom.then(lkup => (lookup = lkup));
+ prom = prom.then(() => wbFetchObjects(arvHost, arvToken,
+ items.map(uuid => lookup[uuid].owner_uuid)));
+ let ownerLookup;
+ prom = prom.then(lkup => (ownerLookup = lkup));
+ prom = prom.then(() => {
+ const rows = items.map((uuid, idx) => {
+ const it = lookup[uuid];
+ const ow = ownerLookup[it.owner_uuid];
+ let r = [];
+ if (selectMany)
+ r.push();
+ r = r.concat([
+ selectMany ? (
+
+ {
+ if (e.target.value === 'on')
+ selectedValues[uuid] = true;
+ else
+ delete selectedValues[uuid];
+ } } /> { '\u00A0' }
+
+ ) : (
+
+ ),
+ ( $('#' + id).modal('hide') } /> ),
+ it.kind,
+ wbFormatDate(it.created_at),
+ ( $('#' + id).modal('hide') } /> )
+ ]);
+ return r;
+ });
+ this.setState({ rows });
+ });
+ }
+
+ render({ id, selectMany, onAccepted, items, app }, { rows, selectedValues }) {
+ return (
+
+
+
+
+
+
+
+ { selectMany ? (
+
+ ) : null }
+ { selectMany ? (
+
+ ) : null }
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+WBToolboxDialog.defaultProps = {
+ 'onAccepted': () => {}
+};
+
+export default WBToolboxDialog;
diff --git a/frontend/src/js/index.js b/frontend/src/js/index.js
new file mode 100755
index 0000000..e6565bc
--- /dev/null
+++ b/frontend/src/js/index.js
@@ -0,0 +1,13 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, render } from 'preact';
+import WBApp from 'wb-app';
+
+render((
+
+), document.body);
diff --git a/frontend/src/js/misc/wb-apply-promise-ordering.js b/frontend/src/js/misc/wb-apply-promise-ordering.js
new file mode 100644
index 0000000..97c6cfc
--- /dev/null
+++ b/frontend/src/js/misc/wb-apply-promise-ordering.js
@@ -0,0 +1,120 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+const defaultOrderRegistry = {};
+
+/* function notify(orderRegistry) {
+ if (!('listeners' in orderRegistry))
+ return;
+ for (let k in orderRegistry.listeners) {
+ orderRegistry.listeners[k](orderRegistry);
+ }
+} */
+
+function cursorDecor() {
+ let d = $('#cursor-decor');
+ if (d.length === 1)
+ return $(d[0]);
+ d = $('');
+ $(document.body).append(d);
+ let pageX = 0, pageY = 0, scrollX = 0, scrollY = 0;
+ document.addEventListener('mousemove', e => {
+ pageX = e.pageX;
+ pageY = e.pageY;
+ d.css({ left: (e.pageX + 16) + 'px', top: (e.pageY + 16) + 'px' })
+ scrollX = window.scrollX;
+ scrollY = window.scrollY;
+ });
+ document.addEventListener('scroll', e => {
+ d.css({ left: (pageX + window.scrollX - scrollX + 16) + 'px',
+ top: (pageY + window.scrollY - scrollY + 16) + 'px' });
+ });
+ return d;
+}
+
+function updateCursorDecor(orderRegistry) {
+ const d = cursorDecor();
+ if (Object.keys(orderRegistry.pendingCompletion).length === 0)
+ d.hide();
+ else
+ d.show();
+}
+
+function wbApplyPromiseOrdering(prom, orderRegistry) {
+ let orderId;
+
+ if (!orderRegistry)
+ orderRegistry = defaultOrderRegistry;
+
+ //if (Object.keys(orderRegistry).length === 0) {
+ if (!('started' in orderRegistry)) {
+ orderRegistry.started = 0;
+ orderRegistry.pendingCompletion = {};
+ orderRegistry.completed = { 0: true };
+ }
+
+ orderRegistry.started += 1;
+ orderId = orderRegistry.started;
+ // console.log('New orderId: ' + orderId);
+ // notify(orderRegistry);
+ cursorDecor().show();
+
+ const orderCallback = ((isCatch, payload) => {
+ // console.log('orderId: ' + orderId +
+ // ', pendingCompletion: ' + Object.keys(orderRegistry.pendingCompletion) +
+ // ', completed: ' + Object.keys(orderRegistry.completed));
+
+ if ((orderId - 1) in orderRegistry.completed) {
+ // console.log('Running: ' + orderId);
+ orderRegistry.completed[orderId] = true;
+ delete orderRegistry.pendingCompletion[orderId];
+
+ const keys = Object.keys(orderRegistry.pendingCompletion);
+ keys.sort((a, b) => (a - b));
+ keys.map(k => {
+ if ((k - 1) in orderRegistry.completed) {
+ // console.log('Running: ' + k);
+ orderRegistry.pendingCompletion[k]();
+ orderRegistry.completed[k] = true;
+ delete orderRegistry.pendingCompletion[k];
+ }
+ });
+
+ if (orderRegistry.started in orderRegistry.completed) {
+ // console.log('Garbage collect');
+ orderRegistry.started = 0;
+ orderRegistry.completed = { 0: true };
+ cursorDecor().hide();
+ }
+
+ if (isCatch)
+ throw payload;
+ else
+ return payload;
+ }
+
+ const prom_1 = new Promise((accept, reject) => {
+ orderRegistry.pendingCompletion[orderId] = (() =>
+ (isCatch ? reject(payload) : accept(payload)));
+ });
+
+ return prom_1;
+ });
+
+ prom = prom.then(xhr => orderCallback(false, xhr));
+ prom = prom.catch(e => orderCallback(true, e));
+
+ return prom;
+}
+
+export default wbApplyPromiseOrdering;
diff --git a/frontend/src/js/misc/wb-disable-controls.js b/frontend/src/js/misc/wb-disable-controls.js
new file mode 100644
index 0000000..95d525b
--- /dev/null
+++ b/frontend/src/js/misc/wb-disable-controls.js
@@ -0,0 +1,19 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+function wbDisableControls() {
+ $('input, select, button').attr('disabled', 'disabled');
+ $('a').each(function() { $(this).data('old_href', $(this).attr('href')); });
+ $('a').attr('href', null);
+}
+
+function wbEnableControls() {
+ $('input, select, button').attr('disabled', null);
+ $('a').each(function() { $(this).attr('href', $(this).data('old_href')); });
+}
+
+export { wbEnableControls, wbDisableControls };
diff --git a/frontend/src/js/misc/wb-format-date.js b/frontend/src/js/misc/wb-format-date.js
new file mode 100644
index 0000000..ebf268a
--- /dev/null
+++ b/frontend/src/js/misc/wb-format-date.js
@@ -0,0 +1,19 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h } from 'preact';
+
+function wbFormatDate(dateStr) {
+ if (!dateStr)
+ return (
+ { String(dateStr) }
+ );
+ let date = new Date(dateStr);
+ return date.toLocaleString();
+}
+
+export default wbFormatDate;
diff --git a/frontend/src/js/misc/wb-format-special-value.js b/frontend/src/js/misc/wb-format-special-value.js
new file mode 100644
index 0000000..e7f4386
--- /dev/null
+++ b/frontend/src/js/misc/wb-format-special-value.js
@@ -0,0 +1,18 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h } from 'preact';
+
+function wbFormatSpecialValue(value) {
+ if (value === null) return (null);
+ if (value === undefined) return (undefined);
+ if (typeof(value) === 'boolean') return ({ String(value) });
+ if (value === '') return '-';
+ return String(value);
+}
+
+export default wbFormatSpecialValue;
diff --git a/frontend/src/js/page/wb-app.js b/frontend/src/js/page/wb-app.js
new file mode 100644
index 0000000..c4254ba
--- /dev/null
+++ b/frontend/src/js/page/wb-app.js
@@ -0,0 +1,120 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { Router, route } from 'preact-router';
+import WBBrowse from 'wb-browse';
+import WBSignIn from 'wb-sign-in';
+import WBSignOut from 'wb-sign-out';
+import WBLandingPage from 'wb-landing-page';
+import WBProcessView from 'wb-process-view';
+import WBContainerView from 'wb-container-view';
+import WBCollectionView from 'wb-collection-view';
+import WBCollectionBrowse from 'wb-collection-browse';
+import WBUsersPage from 'wb-users-page';
+import WBWorkflowView from 'wb-workflow-view';
+import WBLaunchWorkflowPage from 'wb-launch-workflow-page';
+import WBDownloadPage from 'wb-download-page';
+import WBImageViewerPage from 'wb-image-viewer-page';
+import WBSharingPage from 'wb-sharing-page';
+import WBProjectView from 'wb-project-view';
+import arvadosTypeName from 'arvados-type-name';
+
+class WBApp extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.arvHost = window.localStorage['arvHost'];
+ this.state.arvToken = window.localStorage['arvToken'];
+ if ('currentUser' in window.localStorage)
+ this.state.currentUser = JSON.parse(window.localStorage['currentUser']);
+ this.loadToolbox();
+ }
+
+ navbarItemUrl(item) {
+ if (item['id'] === 'sign-out') {
+ return ('/sign-out');
+ } else if (item['id'] === 'home') {
+ return ('/browse/' + this.state.currentUser.uuid);
+ } else if (item['id'] === 'all-projects') {
+ return ('/browse');
+ } else if (item['id'] === 'all-users') {
+ return ('/users');
+ } else if (item['id'] === 'shared-with-me') {
+ return ('/shared-with-me');
+ } else if (item['id'] === 'whatsnew') {
+ return ('https://adared.ch/wba');
+ }
+ }
+
+ breadcrumbClicked(item) {
+ let objectType = arvadosTypeName(item.uuid.split('-')[1]);
+ if (objectType === 'user')
+ route('/browse/' + item.uuid)
+ else if (objectType === 'group' && item.group_class === 'project')
+ route('/browse/' + item.uuid);
+ else if (objectType === 'container_request')
+ route('/process/' + item.uuid)
+ }
+
+ addToToolbox(uuid) {
+ this.state.toolboxItems.push(uuid);
+ window.localStorage['toolboxItems'] =
+ JSON.stringify(this.state.toolboxItems);
+ }
+
+ clearToolbox() {
+ this.state.toolboxItems = [];
+ delete window.localStorage['toolboxItems'];
+ }
+
+ loadToolbox() {
+ this.state.toolboxItems = ('toolboxItems' in window.localStorage) ?
+ JSON.parse(window.localStorage['toolboxItems']) : [];
+ }
+
+ render() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default WBApp;
diff --git a/frontend/src/js/page/wb-browse.js b/frontend/src/js/page/wb-browse.js
new file mode 100644
index 0000000..25a0098
--- /dev/null
+++ b/frontend/src/js/page/wb-browse.js
@@ -0,0 +1,255 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import { route } from 'preact-router';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBProjectListing from 'wb-project-listing';
+import WBInlineSearch from 'wb-inline-search';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBTabs from 'wb-tabs';
+import WBProcessListing from 'wb-process-listing';
+import WBCollectionListing from 'wb-collection-listing';
+import WBWorkflowListing from 'wb-workflow-listing';
+import WBRenameDialog from 'wb-rename-dialog';
+import WBDeleteDialog from 'wb-delete-dialog';
+import WBNewProjectDialog from 'wb-new-project-dialog';
+import WBEditDescriptionDialog from 'wb-edit-description-dialog';
+import wbMoveObject from 'wb-move-object';
+import wbCopyCollection from 'wb-copy-collection';
+import arvadosTypeName from 'arvados-type-name';
+
+class WBBrowseProjectTabs extends Component {
+ render({ ownerUuid, selected, newProjectDialogRef, projectListingRef,
+ moveHere, copyHere }) {
+
+ return (
+ New Project ),
+ 'onClick': () => newProjectDialogRef.current.show(ownerUuid,
+ () => projectListingRef.current.fetchItems() ) } : null,
+ ( ownerUuid && Object.keys(selected).length > 0 ) ?
+ { 'name': ( Move Here ),
+ 'onClick': moveHere } : null,
+ ( ownerUuid && (uuids => uuids.length > 0 && uuids.length ===
+ uuids.map(arvadosTypeName).filter(a => (a === 'collection')).length )(Object.keys(selected)) ) ?
+ { 'name': ( Copy Here ),
+ 'onClick': copyHere } : null
+ ] } />
+ );
+ }
+}
+
+class WBBrowse extends Component {
+ constructor(...args) {
+ super(...args);
+ this.renameDialogRef = createRef();
+ this.deleteDialogRef = createRef();
+ this.newProjectDialogRef = createRef();
+ this.projectListingRef = createRef();
+ this.projectTabsRef = createRef();
+ this.editDescriptionDialogRef = createRef();
+ this.state.selected = {};
+ }
+
+ getUrl(params) {
+ const mode = ('mode' in params ? params.mode : this.props.mode);
+
+ if (mode === 'shared-with-me')
+ return '/shared-with-me/' +
+ ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' +
+ ('textSearch' in params ? params.textSearch : (this.props.textSearch || ''));
+
+ let res = '/browse/' +
+ ('ownerUuid' in params ? params.ownerUuid : (this.props.ownerUuid || '')) + '/' +
+ ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' +
+ ('objTypeTab' in params ? params.objTypeTab : (this.props.objTypeTab || '')) + '/' +
+ ('collectionPage' in params ? params.collectionPage : (this.props.collectionPage || '')) + '/' +
+ ('processPage' in params ? params.processPage : (this.props.processPage || '')) + '/' +
+ ('workflowPage' in params ? params.workflowPage : (this.props.workflowPage || '')) + '/' +
+ encodeURIComponent('textSearch' in params ? params.textSearch : (this.props.textSearch || ''));
+
+ return res;
+ }
+
+ route(params) {
+ route(this.getUrl(params));
+ }
+
+ renameDialog(item, callback) {
+ // throw Error('Not implemented');
+ this.renameDialogRef.current.show(item, callback);
+ }
+
+ renderRenameLink(item, callback) {
+ return (
+ { e.preventDefault(); this.renameDialog(item, callback); } }>
+
+
+ );
+ }
+
+ renderEditDescription(item, callback) {
+ return (
+ { e.preventDefault();
+ this.editDescriptionDialogRef.current.show(item, callback); } }>
+
+
+
+ );
+ }
+
+ renderDeleteButton(item, callback) {
+ return (
+
+ );
+ }
+
+ renderSelectionCell(item) {
+ const { selected } = this.state;
+ const { uuid } = item;
+ return (
+
+ {
+ if (e.target.checked)
+ selected[uuid] = true;
+ else
+ delete selected[uuid];
+ this.projectTabsRef.current.setState({});
+ } } /> { '\u00A0' }
+
+ );
+ }
+
+ renderSharingButton(item) {
+ return (
+
+
+
+ );
+ }
+
+ moveOrCopyOp(op) {
+ const { ownerUuid, app } = this.props;
+ const { selected } = this.state;
+ const { arvHost, arvToken } = app.state;
+ let prom = new Promise(accept => accept());
+ const uuids = Object.keys(selected);
+ for (let i = 0; i < uuids.length; i++) {
+ prom = prom.then(() => op(arvHost, arvToken, uuids[i], ownerUuid));
+ prom = prom.then(() => ( delete selected[uuids[i]] ));
+ prom = prom.catch(() => {});
+ }
+ prom = prom.then(() => this.setState({}));
+ }
+
+ moveHere() {
+ this.moveOrCopyOp(wbMoveObject);
+ }
+
+ copyHere() {
+ this.moveOrCopyOp(wbCopyCollection);
+ }
+
+ render({ mode, ownerUuid, activePage, app,
+ objTypeTab, collectionPage, processPage, workflowPage,
+ textSearch }, { selected }) {
+
+ const commonProps = {
+ renderRenameLink: (it, cb) => this.renderRenameLink(it, cb),
+ renderEditDescription: (it, cb) => this.renderEditDescription(it, cb),
+ renderDeleteButton: (it, cb) => this.renderDeleteButton(it, cb),
+ renderSelectionCell: it => this.renderSelectionCell(it),
+ renderSharingButton: it => this.renderSharingButton(it),
+ textSearch,
+ app,
+ appState: app.state,
+ arvHost: app.state.arvHost,
+ arvToken: app.state.arvToken,
+ ownerUuid
+ };
+
+ const { currentUser } = app.state;
+ const noDefaultTab = (!ownerUuid || ownerUuid === currentUser.uuid);
+
+ return (
+
+
+
+
+
+
+
+
+
+ route(this.getUrl({ textSearch,
+ activePage: 0, collectionPage: 0, processPage: 0, workflowPage: 0 })) } />
+
+
+
+ this.moveHere() }
+ copyHere={ () => this.copyHere() } />
+
+ this.getUrl({ 'activePage': i }) }
+ { ...commonProps } />
+
+ { (mode !== 'browse') ? null : (
+ this.route({ 'objTypeTab': tab['id'] }) } />
+ ) }
+
+ {
+ (mode !== 'browse') ? null :
+ ((!objTypeTab && !noDefaultTab) || objTypeTab === 'collection') ? (
+ this.getUrl({ 'collectionPage': i }) }
+ { ...commonProps } />
+
+ ) : (objTypeTab === 'process') ? (
+ this.route({ 'processPage': i }) }
+ { ...commonProps } />
+
+ ) : (objTypeTab === 'workflow') ? (
+ this.getUrl({ 'workflowPage': i }) }
+ { ...commonProps } />
+
+ ) : null
+ }
+
+ );
+ }
+}
+
+export default WBBrowse;
diff --git a/frontend/src/js/page/wb-collection-browse.js b/frontend/src/js/page/wb-collection-browse.js
new file mode 100644
index 0000000..b4fd418
--- /dev/null
+++ b/frontend/src/js/page/wb-collection-browse.js
@@ -0,0 +1,32 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBCollectionContent from 'wb-collection-content';
+
+class WBCollectionBrowse extends Component {
+ render({ app, uuid, collectionPath, page }, {}) {
+ return (
+
+
+
+
+
+
+ This is the collection browser for { uuid }
+
+
+
+
+ );
+ }
+}
+
+export default WBCollectionBrowse;
diff --git a/frontend/src/js/page/wb-collection-view.js b/frontend/src/js/page/wb-collection-view.js
new file mode 100644
index 0000000..ebd1043
--- /dev/null
+++ b/frontend/src/js/page/wb-collection-view.js
@@ -0,0 +1,36 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBCommonFields from 'wb-common-fields';
+import WBCollectionFields from 'wb-collection-fields';
+
+class WBCollectionView extends Component {
+ render({ app, uuid }, {}) {
+ return (
+
+
+
+
+
+
+ This is the collection view for { uuid }
+
+
+
Common Fields
+
+
+
Collection Fields
+
+
+ );
+ }
+}
+
+export default WBCollectionView;
diff --git a/frontend/src/js/page/wb-container-view.js b/frontend/src/js/page/wb-container-view.js
new file mode 100644
index 0000000..446e2a7
--- /dev/null
+++ b/frontend/src/js/page/wb-container-view.js
@@ -0,0 +1,40 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBCommonFields from 'wb-common-fields';
+import WBContainerFields from 'wb-container-fields';
+import WBLiveLogs from 'wb-live-logs';
+
+class WBContainerView extends Component {
+ render({ app, uuid }) {
+ return (
+
+
+
+
+
+
+ This is the container view for { uuid }
+
+
+
Common Fields
+
+
+
Container Fields
+
+
+
Live Logs
+
+
+ );
+ }
+}
+
+export default WBContainerView;
diff --git a/frontend/src/js/page/wb-download-page.js b/frontend/src/js/page/wb-download-page.js
new file mode 100644
index 0000000..1dc4907
--- /dev/null
+++ b/frontend/src/js/page/wb-download-page.js
@@ -0,0 +1,133 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+
+function contentTypeFromFilename(name) {
+ let ext = name.split('.');
+ ext = ext[ext.length - 1].toUpperCase();
+ if (ext === 'TXT')
+ return 'text/plain; charset=utf-8';
+ if (ext === 'JPG' || ext === 'JPEG')
+ return 'image/jpeg';
+ if (ext === 'PNG')
+ return 'image/png';
+ return 'application/octet-stream; charset=utf-8';
+}
+
+class WBDownloadPage extends Component {
+ componentDidMount() {
+ const { app, blocksBlobUrl, inline } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ let prom = new Promise((accept, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', blocksBlobUrl);
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState !== 4)
+ return;
+ if (xhr.status !== 200)
+ reject(xhr);
+ else
+ accept(xhr);
+ };
+ xhr.responseType = 'blob';
+ xhr.send();
+ });
+ prom = prom.then(xhr => xhr.response.text());
+
+ let name, file;
+
+ const { streamSaver, location } = window;
+ streamSaver.mitm = location.protocol + '//' +
+ location.hostname + (location.port ?
+ ':' + location.port : '') + '/mitm.html';
+
+ let fileStream;
+ let writer;
+ let done = false;
+
+ prom = prom.then(text => {
+ let _;
+
+ [ _, _, name, file ] = JSON.parse(text);
+
+ fileStream = streamSaver.createWriteStream(name, {
+ size: file[1],
+ inline: inline,
+ contentType: contentTypeFromFilename(name)
+ });
+ writer = fileStream.getWriter();
+
+ window.onunload = () => {
+ writer.abort()
+ };
+
+ window.onbeforeunload = evt => {
+ if (!done) {
+ evt.returnValue = `Are you sure you want to leave?`;
+ }
+ };
+
+ const filters = [
+ ['service_type', '=', 'proxy']
+ ];
+
+ return makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/keep_services?filters=' +
+ encodeURIComponent(JSON.stringify(filters)));
+ });
+
+ prom = prom.then(xhr => {
+ const services = xhr.response.items;
+ const i = Math.floor(Math.random() * services.length);
+ const proxy = services[i];
+
+ let prom_1 = new Promise(accept => accept());
+ for (let k = 0; k < file[0].length; k++) {
+ const loc = file[0][k];
+ prom_1 = prom_1.then(() => makeArvadosRequest(
+ proxy.service_host + ':' + proxy.service_port,
+ arvToken,
+ '/' + loc[0],
+ { 'useSsl': proxy.service_ssl_flag,
+ 'responseType': 'arraybuffer' }
+ ));
+ prom_1 = prom_1.then(xhr_1 => {
+ const blk = xhr_1.response.slice(loc[1], loc[2]);
+ // const r = new Response(blk);
+ // r.body.pipeTo(fileStream);
+ return writer.write(new Uint8Array(blk));
+ });
+ }
+ return prom_1;
+ });
+
+ prom = prom.then(() => {
+ writer.close();
+ done = true;
+ });
+ }
+
+ render() {
+ return (
+
+
+
+ Downloading, please wait...
+
+
+
+ Do not close this window until the download is finished.
+
+
+ );
+ }
+}
+
+export default WBDownloadPage;
diff --git a/frontend/src/js/page/wb-image-viewer-page.js b/frontend/src/js/page/wb-image-viewer-page.js
new file mode 100644
index 0000000..cc67217
--- /dev/null
+++ b/frontend/src/js/page/wb-image-viewer-page.js
@@ -0,0 +1,110 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+
+function downloadFile(arvHost, arvToken, file) {
+ const blockRefs = file[0];
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/keep_services');
+
+ let proxy;
+ prom = prom.then(xhr => {
+ const services = xhr.response['items'];
+ const proxies = services.filter(svc => (svc.service_type === 'proxy'));
+ const n = Math.floor(Math.random() * proxies.length);
+ proxy = proxies[n];
+ });
+
+ const blocks = [];
+
+ for (let i = 0; i < blockRefs.length; i++) {
+ let locator, start, end;
+
+ prom = prom.then(() => {
+ [ locator, start, end ] = blockRefs[i];
+ return makeArvadosRequest(
+ proxy.service_host + ':' + proxy.service_port,
+ arvToken, '/' + locator,
+ { 'useSsl': proxy.service_ssl_flag,
+ 'responseType': 'arraybuffer' }
+ );
+ });
+
+ prom = prom.then(xhr => blocks.push(xhr.response.slice(start, end)));
+ }
+
+ prom = prom.then(() => {
+ const url = URL.createObjectURL(new Blob(blocks));
+ const totalSize = blocks.reduce((a, b) => a.length + b.length);
+ const big = new Uint8Array(totalSize);
+ for (let i = 0, pos = 0; i < blocks.length; i++) {
+ big.set(blocks[i], pos);
+ pos += blocks[i].length;
+ }
+ // papayaContainers[0].startPapaya();
+ const poll = () => {
+ setTimeout(() => {
+ console.log('Polling Papaya startup...')
+ if (window.papaya && window.papaya.Container) { // window.papayaContainers && window.papayaContainers[0]) {
+ console.log('Great, Papaya started!');
+ papaya.Container.startPapaya();
+ //papaya.Container.addImage(0, big.buffer);
+ document.body.id = "bod";
+ document.body.style.background = "#555";
+ papaya.Container.addViewer("bod", {
+ 'binaryImages': [ big.buffer ],
+ 'noNewFiles': true,
+ });
+ } else
+ poll();
+ }, 1000);
+ };
+ //setTimeout(poll, 10000);
+ poll();
+ });
+}
+
+class WBImageViewerPage extends Component {
+ componentDidMount() {
+ const { blobUrl, app } = this.props;
+ const { arvHost, arvToken } = app.state;
+
+ let prom = new Promise((accept, reject) => {
+ const xhr = new XMLHttpRequest();
+ xhr.open('GET', blobUrl);
+ xhr.onreadystatechange = () => {
+ if (xhr.readyState !== 4) return;
+ if (xhr.status !== 200) reject(xhr);
+ else accept(xhr);
+ };
+ xhr.responseType = 'blob';
+ xhr.send();
+ });
+
+ prom = prom.then(xhr => xhr.response.text());
+
+ prom = prom.then(data => {
+ data = JSON.parse(data);
+ downloadFile(arvHost, arvToken, data.file);
+ });
+ }
+
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default WBImageViewerPage;
diff --git a/frontend/src/js/page/wb-landing-page.js b/frontend/src/js/page/wb-landing-page.js
new file mode 100644
index 0000000..f261d24
--- /dev/null
+++ b/frontend/src/js/page/wb-landing-page.js
@@ -0,0 +1,27 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import makeArvadosRequest from 'make-arvados-request';
+import { route } from 'preact-router';
+
+class WBLandingPage extends Component {
+ componentDidMount() {
+ let { arvHost, arvToken } = window.localStorage;
+ let prom = makeArvadosRequest(arvHost, arvToken, '/arvados/v1/users/current');
+ prom = prom.then(xhr => route('/browse/' + xhr.response['uuid']));
+ prom = prom.catch(() => route('/sign-in'));
+ }
+
+ render() {
+ return (
+ Please wait...
+ );
+ }
+}
+
+export default WBLandingPage;
diff --git a/frontend/src/js/page/wb-launch-workflow-page.js b/frontend/src/js/page/wb-launch-workflow-page.js
new file mode 100644
index 0000000..492227f
--- /dev/null
+++ b/frontend/src/js/page/wb-launch-workflow-page.js
@@ -0,0 +1,185 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import { route } from 'preact-router';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBBrowseDialog from 'wb-browse-dialog';
+import WBTable from 'wb-table';
+import makeArvadosRequest from 'make-arvados-request';
+import { wbDisableControls, wbEnableControls } from 'wb-disable-controls';
+import linkState from 'linkstate';
+import wbParseWorkflowDef from 'wb-parse-workflow-def';
+import { wbParseWorkflowInputs, wbSubmitContainerRequest } from 'wb-submit-container-request';
+import WBWorkflowInput from 'wb-workflow-input';
+import { parseKeepRef } from 'wb-process-misc';
+
+class WBLaunchWorkflowPage extends Component {
+ constructor(...args) {
+ super(...args);
+ this.browseDialogRef = createRef();
+ this.state.inputs = {};
+ this.state.errors = [];
+ this.state.placeInSubProject = true;
+ }
+
+ componentDidMount() {
+ let { app, workflowUuid } = this.props;
+ let { arvHost, arvToken } = app.state;
+
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/workflows/' + workflowUuid);
+ prom = prom.then(xhr => {
+ const def = wbParseWorkflowDef(xhr.response.definition);
+ const inputs = {};
+ const main = def['$graph'].find(a => (a.id === '#main'));
+ main.inputs.map(a => (inputs[a.id] = JSON.stringify(parseKeepRef(a.default))));
+ this.setState({
+ 'workflow': xhr.response,
+ 'workflowDefinition': def,
+ 'defaultProcessName': xhr.response.name + ' ' + (new Date().toISOString()),
+ 'defaultProcessDescription': xhr.response.description,
+ inputs
+ });
+ });
+ }
+
+ submit() {
+ // first see if all inputs are parseable
+ const { app, workflowUuid } = this.props;
+ const { arvHost, arvToken, currentUser } = app.state;
+ const { workflowDefinition, projectUuid,
+ processName, processDescription,
+ defaultProcessName, defaultProcessDescription,
+ placeInSubProject } = this.state;
+
+ const errors = [];
+ const inputs = wbParseWorkflowInputs(workflowDefinition,
+ this.state.inputs, errors);
+ if (errors.length > 0) {
+ this.setState({ errors });
+ return;
+ }
+
+ const params = {
+ arvHost, arvToken, inputs,
+ processName: processName || defaultProcessName,
+ processDescription: processDescription || defaultProcessDescription,
+ projectUuid: projectUuid || currentUser.uuid,
+ workflowUuid, workflowDefinition, placeInSubProject
+ }
+
+ wbDisableControls();
+ let prom = wbSubmitContainerRequest(params);
+ prom = prom.then(xhr => {
+ wbEnableControls();
+ route('/process/' + xhr.response.uuid);
+ });
+ prom = prom.catch(exc => {
+ wbEnableControls();
+ this.setState({ errors: [ exc.message ] });
+ });
+ }
+
+ render({ app, workflowUuid },
+ { workflow, workflowDefinition, projectUuid, processName, processDescription,
+ defaultProcessName, defaultProcessDescription, errors, placeInSubProject }) {
+
+ return (
+
+
+
+
+
+ { workflow ?
+ (
) :
Loading...
}
+
+ );
+ }
+}
+
+export default WBLaunchWorkflowPage;
diff --git a/frontend/src/js/page/wb-process-view.js b/frontend/src/js/page/wb-process-view.js
new file mode 100644
index 0000000..ea426a5
--- /dev/null
+++ b/frontend/src/js/page/wb-process-view.js
@@ -0,0 +1,95 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import makeArvadosRequest from 'make-arvados-request';
+import WBCommonFields from 'wb-common-fields';
+import WBContainerRequestFields from 'wb-container-request-fields';
+import WBProcessListing from 'wb-process-listing';
+import WBProcessDashboard from 'wb-process-dashboard';
+
+class WBProcessView extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.objectUrls = [];
+ }
+
+ getUrl(props) {
+ const page = ('page' in props ? props.page : this.props.page);
+ return ('/process/' + this.props.uuid +
+ (page ? '/' + page : ''));
+ }
+
+ fetchData() {
+ let { arvHost, arvToken } = this.props.app.state;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/container_requests/' + this.props.uuid);
+ let req;
+ let cont;
+ prom = prom.then(xhr => {
+ req = xhr.response;
+ if (req.container_uuid) {
+ let prom_1 = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/containers/' + req.container_uuid);
+ prom_1 = prom_1.then(xhr => (cont = xhr.response));
+ return prom_1;
+ }
+ });
+ prom = prom.then(() => {
+ this.setState({
+ 'request': req,
+ 'container': cont
+ });
+ });
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.props = nextProps;
+ this.setState({ 'objectUrls': [], 'request': null, 'container': null });
+ this.fetchData();
+ }
+
+ render({ app, uuid, page }, { container }) {
+ return (
+
+
+
+
+
+
+ This is the process view for { uuid }
+
+
+
Children Dashboard
+
+
+
Common Fields
+
+
+
Container Request Fields
+
+
+
Children
+
this.getUrl({ page }) } />
+
+ );
+ }
+}
+
+export default WBProcessView;
diff --git a/frontend/src/js/page/wb-project-view.js b/frontend/src/js/page/wb-project-view.js
new file mode 100644
index 0000000..19b0221
--- /dev/null
+++ b/frontend/src/js/page/wb-project-view.js
@@ -0,0 +1,36 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBCommonFields from 'wb-common-fields';
+import WBProjectFields from 'wb-project-fields';
+
+class WBProjectView extends Component {
+ render({ app, uuid }, {}) {
+ return (
+
+
+
+
+
+
+ This is the project view for { uuid }
+
+
+
Common Fields
+
+
+
Project Fields
+
+
+ );
+ }
+}
+
+export default WBProjectView;
diff --git a/frontend/src/js/page/wb-sharing-page.js b/frontend/src/js/page/wb-sharing-page.js
new file mode 100644
index 0000000..c05d511
--- /dev/null
+++ b/frontend/src/js/page/wb-sharing-page.js
@@ -0,0 +1,177 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBNameAndUuid from 'wb-name-and-uuid';
+import WBSelect from 'wb-select';
+import WBTable from 'wb-table';
+import WBPickObjectDialog from 'wb-pick-object-dialog';
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBSharingPage extends Component {
+ constructor(...args) {
+ super(...args);
+ this.state.rows = [];
+ this.dialogRef = createRef();
+ }
+
+ componentDidMount() {
+ this.fetchData();
+ }
+
+ fetchData() {
+ const { app, uuid } = this.props;
+ const { arvHost, arvToken } = app.state;
+ let prom = makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/permissions/' + encodeURIComponent(uuid) +
+ '?limit=100000');
+ prom = prom.then(xhr => this.setState({
+ 'entries': xhr.response.items,
+ 'rows': this.prepareRows(xhr.response.items)
+ }));
+ }
+
+ deleteEntry(it) {
+ it._delete = true;
+ this.setState({ rows: this.prepareRows(this.state.entries) });
+ }
+
+ prepareRows(items) {
+ const { app } = this.props;
+ return items.filter(it => (!it._delete)).map(it => [
+ ( ),
+ ( this.modifyEntry(it, e.target.value) } /> ),
+ ( )
+ ]);
+ }
+
+ modifyEntry(it, newPermissionName) {
+ it.name = newPermissionName;
+ it._dirty = true;
+ // this.setState({ rows: this.prepareRows(this.state.entries) });
+ }
+
+ addEntry(it, permissionName='can_read') {
+ // throw Error('Not implemented');
+ const { uuid } = this.props;
+ let { entries } = this.state;
+ if (entries.filter(e => (e.tail_uuid === it.uuid)).length > 0)
+ return; // already in the list
+ const e = {
+ //_dirty: true,
+ link_class: 'permission',
+ head_uuid: uuid,
+ tail_uuid: it.uuid,
+ name: permissionName
+ };
+ entries = entries.concat([e]);
+ this.setState({
+ entries,
+ rows: this.prepareRows(entries)
+ });
+ }
+
+ disableControls() {
+ $('input, select, button').attr('disabled', 'disabled');
+ $('a').each(function() { $(this).data('old_href', $(this).attr('href')); });
+ $('a').attr('href', null);
+ }
+
+ enableControls() {
+ $('input, select, button').attr('disabled', null);
+ $('a').each(function() { $(this).attr('href', $(this).data('old_href')); });
+ }
+
+ save() {
+ const { entries } = this.state;
+ const { arvHost, arvToken } = this.props.app.state;
+ let prom = new Promise(accept => accept());
+ this.disableControls();
+ this.setState({ working: true });
+ for (let i = 0; i < entries.length; i++) {
+ const e = entries[i];
+
+ //if (!e._dirty && !e._delete)
+ //continue;
+
+ if (!e.uuid) {
+ prom = prom.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/links',
+ { 'method': 'POST',
+ 'data': JSON.stringify({
+ 'link_class': 'permission',
+ 'head_uuid': e.head_uuid,
+ 'tail_uuid': e.tail_uuid,
+ 'name': e.name
+ }) }));
+
+ } else if (e._delete) {
+ prom = prom.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/links/' + e.uuid,
+ { 'method': 'DELETE' }));
+
+ } else if (e._dirty) {
+ prom = prom.then(() => makeArvadosRequest(arvHost, arvToken,
+ '/arvados/v1/links/' + e.uuid,
+ { 'method': 'PUT',
+ 'data': JSON.stringify({
+ 'name': e.name
+ }) }));
+ }
+ prom = prom.catch(() => {});
+ }
+ prom = prom.then(() => {
+ this.enableControls();
+ this.fetchData();
+ this.setState({ working: false });
+ });
+ }
+
+ render({ app, uuid }, { rows, working }) {
+ return (
+
+
+
+
+
+
+
+ This is the sharing management page for { uuid }
+
+
+
+
+
+
+ { working ? (
) : null }
+
+
+
+
+
+
+ );
+ }
+}
+
+export default WBSharingPage;
diff --git a/frontend/src/js/page/wb-sign-in.js b/frontend/src/js/page/wb-sign-in.js
new file mode 100644
index 0000000..cd55303
--- /dev/null
+++ b/frontend/src/js/page/wb-sign-in.js
@@ -0,0 +1,109 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+import WBNavbar from 'wb-navbar';
+import WBTabs from 'wb-tabs';
+import linkState from 'linkstate';
+import makeArvadosRequest from 'make-arvados-request';
+
+class WBSignIn extends Component {
+ constructor(...args) {
+ super(...args);
+ const search = new URLSearchParams(window.location.search);
+ this.state.arvHost = window.localStorage.arvHost;
+ this.state.arvToken = search.get('api_token');
+ }
+
+ componentDidMount() {
+ // const arvHost = window.localStorage.arvHost;
+ // const arvToken = search.get('api_token');
+ const { arvHost, arvToken } = this.state;
+ if (arvHost && arvToken) {
+ this.state.arvHost = arvHost;
+ this.state.arvToken = arvToken;
+ this.submitToken();
+ }
+ }
+
+ submit() {
+ const { mode } = this.props;
+ if (mode === 'token')
+ this.submitToken();
+ else if (!mode || mode === 'sso')
+ this.submitSingleSignOn();
+ else
+ throw Error('Unsupported mode');
+ }
+
+ submitSingleSignOn() {
+ const { arvHost } = this.state;
+ window.localStorage.arvHost = arvHost;
+ window.location = 'https://' + arvHost + '/login?return_to='
+ + encodeURIComponent(window.location.protocol + '//' + window.location.host + '/sign-in/token');
+ }
+
+ submitToken() {
+ let { appState } = this.props;
+ let { arvHost, arvToken } = this.state;
+ let prom = makeArvadosRequest(arvHost, arvToken, '/arvados/v1/users/current');
+ prom = prom.then(xhr => {
+ window.localStorage['arvHost'] = arvHost;
+ window.localStorage['arvToken'] = arvToken;
+ window.localStorage['currentUser'] = JSON.stringify(xhr.response);
+ appState.arvHost = arvHost;
+ appState.arvToken = arvToken;
+ appState.currentUser = xhr.response;
+ route('/browse/' + xhr.response['uuid']);
+ });
+ prom = prom.catch(() => {
+ alert('Sign in unsuccessful. Verify your input and try again.')
+ });
+ }
+
+ render({ mode }, { arvHost, arvToken }) {
+ return (
+
+
+
+
+
+
+
Sign In
+
route(t.name === 'Token' ? '/sign-in/token' : '/sign-in/sso') } />
+
+
+
+
+
+ );
+ }
+}
+
+export default WBSignIn;
diff --git a/frontend/src/js/page/wb-sign-out.js b/frontend/src/js/page/wb-sign-out.js
new file mode 100644
index 0000000..5b781be
--- /dev/null
+++ b/frontend/src/js/page/wb-sign-out.js
@@ -0,0 +1,29 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+
+class WBSignOut extends Component {
+ componentDidMount() {
+ const { app } = this.props;
+ const keys = ['arvHost', 'arvToken', 'currentUser'];
+ for (let i = 0; i < keys.length; i++) {
+ delete app.state[keys[i]];
+ delete window.localStorage[keys[i]];
+ }
+ route('/sign-in');
+ }
+
+ render() {
+ return (
+ Signing out...
+ );
+ }
+}
+
+export default WBSignOut;
diff --git a/frontend/src/js/page/wb-users-page.js b/frontend/src/js/page/wb-users-page.js
new file mode 100644
index 0000000..dce190b
--- /dev/null
+++ b/frontend/src/js/page/wb-users-page.js
@@ -0,0 +1,37 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import { route } from 'preact-router';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBUserListing from 'wb-user-listing';
+
+class WBUsersPage extends Component {
+ getUrl(params) {
+ const url = '/users/' +
+ Number('page' in params ? params.page : (this.props.page || 0)) + '/' +
+ encodeURIComponent('textSearch' in params ? params.textSearch : (this.props.textSearch || ''));
+ return url;
+ // return ('/users/' + page);
+ }
+
+ render({ app, page, textSearch }) {
+ return (
+
+ route(this.getUrl({ textSearch, page: 0 })) } />
+
+ this.getUrl({ page }) } />
+
+ );
+ }
+};
+
+export default WBUsersPage;
diff --git a/frontend/src/js/page/wb-workflow-view.js b/frontend/src/js/page/wb-workflow-view.js
new file mode 100644
index 0000000..5a0f986
--- /dev/null
+++ b/frontend/src/js/page/wb-workflow-view.js
@@ -0,0 +1,36 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+import WBNavbarCommon from 'wb-navbar-common';
+import WBArvadosCrumbs from 'wb-arvados-crumbs';
+import WBCommonFields from 'wb-common-fields';
+import WBWorkflowFields from 'wb-workflow-fields';
+
+class WBWorkflowView extends Component {
+ render({ app, uuid }, {}) {
+ return (
+
+
+
+
+
+
+ This is the workflow view for { uuid }
+
+
+
Common Fields
+
+
+
Workflow Fields
+
+
+ );
+ }
+}
+
+export default WBWorkflowView;
diff --git a/frontend/src/js/thirdparty/StreamSaver.js b/frontend/src/js/thirdparty/StreamSaver.js
new file mode 100644
index 0000000..0a596be
--- /dev/null
+++ b/frontend/src/js/thirdparty/StreamSaver.js
@@ -0,0 +1,296 @@
+/* global chrome location ReadableStream define MessageChannel TransformStream */
+
+;((name, definition) => {
+ typeof module !== 'undefined'
+ ? module.exports = definition()
+ : typeof define === 'function' && typeof define.amd === 'object'
+ ? define(definition)
+ : this[name] = definition()
+})('streamSaver', () => {
+ 'use strict'
+
+ let mitmTransporter = null
+ let supportsTransferable = false
+ const test = fn => { try { fn() } catch (e) {} }
+ const ponyfill = window.WebStreamsPolyfill || {}
+ const isSecureContext = window.isSecureContext
+ let useBlobFallback = /constructor/i.test(window.HTMLElement) || !!window.safari
+ const downloadStrategy = isSecureContext || 'MozAppearance' in document.documentElement.style
+ ? 'iframe'
+ : 'navigate'
+
+ const streamSaver = {
+ createWriteStream,
+ WritableStream: window.WritableStream || ponyfill.WritableStream,
+ supported: true,
+ version: { full: '2.0.0', major: 2, minor: 0, dot: 0 },
+ mitm: 'https://jimmywarting.github.io/StreamSaver.js/mitm.html?version=2.0.0'
+ }
+
+ /**
+ * create a hidden iframe and append it to the DOM (body)
+ *
+ * @param {string} src page to load
+ * @return {HTMLIFrameElement} page to load
+ */
+ function makeIframe (src) {
+ if (!src) throw new Error('meh')
+ const iframe = document.createElement('iframe')
+ iframe.hidden = true
+ iframe.src = src
+ iframe.loaded = false
+ iframe.name = 'iframe'
+ iframe.isIframe = true
+ iframe.postMessage = (...args) => iframe.contentWindow.postMessage(...args)
+ iframe.addEventListener('load', () => {
+ iframe.loaded = true
+ }, { once: true })
+ document.body.appendChild(iframe)
+ return iframe
+ }
+
+ /**
+ * create a popup that simulates the basic things
+ * of what a iframe can do
+ *
+ * @param {string} src page to load
+ * @return {object} iframe like object
+ */
+ function makePopup (src) {
+ const options = 'width=200,height=100'
+ const delegate = document.createDocumentFragment()
+ const popup = {
+ frame: window.open(src, 'popup', options),
+ loaded: false,
+ isIframe: false,
+ isPopup: true,
+ remove () { popup.frame.close() },
+ addEventListener (...args) { delegate.addEventListener(...args) },
+ dispatchEvent (...args) { delegate.dispatchEvent(...args) },
+ removeEventListener (...args) { delegate.removeEventListener(...args) },
+ postMessage (...args) { popup.frame.postMessage(...args) }
+ }
+
+ const onReady = evt => {
+ if (evt.source === popup.frame) {
+ popup.loaded = true
+ window.removeEventListener('message', onReady)
+ popup.dispatchEvent(new Event('load'))
+ }
+ }
+
+ window.addEventListener('message', onReady)
+
+ return popup
+ }
+
+ try {
+ // We can't look for service worker since it may still work on http
+ new Response(new ReadableStream())
+ if (isSecureContext && !('serviceWorker' in navigator)) {
+ useBlobFallback = true
+ }
+ } catch (err) {
+ useBlobFallback = true
+ }
+
+ test(() => {
+ // Transfariable stream was first enabled in chrome v73 behind a flag
+ const { readable } = new TransformStream()
+ const mc = new MessageChannel()
+ mc.port1.postMessage(readable, [readable])
+ mc.port1.close()
+ mc.port2.close()
+ supportsTransferable = true
+ // Freeze TransformStream object (can only work with native)
+ Object.defineProperty(streamSaver, 'TransformStream', {
+ configurable: false,
+ writable: false,
+ value: TransformStream
+ })
+ })
+
+ function loadTransporter () {
+ if (!mitmTransporter) {
+ mitmTransporter = isSecureContext
+ ? makeIframe(streamSaver.mitm)
+ : makePopup(streamSaver.mitm)
+ }
+ }
+
+ /**
+ * @param {string} filename filename that should be used
+ * @param {object} options [description]
+ * @param {number} size depricated
+ * @return {WritableStream}
+ */
+ function createWriteStream (filename, options, size) {
+ let opts = {
+ size: null,
+ pathname: null,
+ writableStrategy: undefined,
+ readableStrategy: undefined
+ }
+
+ // normalize arguments
+ if (Number.isFinite(options)) {
+ [ size, options ] = [ options, size ]
+ console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
+ opts.size = size
+ opts.writableStrategy = options
+ } else if (options && options.highWaterMark) {
+ console.warn('[StreamSaver] Depricated pass an object as 2nd argument when creating a write stream')
+ opts.size = size
+ opts.writableStrategy = options
+ } else {
+ opts = options || {}
+ }
+ if (!useBlobFallback) {
+ loadTransporter()
+
+ var bytesWritten = 0 // by StreamSaver.js (not the service worker)
+ var downloadUrl = null
+ var channel = new MessageChannel()
+
+ // Make filename RFC5987 compatible
+ filename = encodeURIComponent(filename.replace(/\//g, ':'))
+ .replace(/['()]/g, escape)
+ .replace(/\*/g, '%2A')
+
+ const response = {
+ transferringReadable: supportsTransferable,
+ pathname: opts.pathname || Math.random().toString().slice(-6) + '/' + filename,
+ headers: {
+ 'Content-Type': (options.contentType ? options.contentType :
+ 'application/octet-stream; charset=utf-8'),
+ 'Content-Disposition': (options.inline ? 'inline' :
+ ("attachment; filename*=UTF-8''" + filename))
+ }
+ }
+
+ if (opts.size) {
+ response.headers['Content-Length'] = opts.size
+ }
+
+ const args = [ response, '*', [ channel.port2 ] ]
+
+ if (supportsTransferable) {
+ const transformer = downloadStrategy === 'iframe' ? undefined : {
+ // This transformer & flush method is only used by insecure context.
+ transform (chunk, controller) {
+ bytesWritten += chunk.length
+ controller.enqueue(chunk)
+
+ if (downloadUrl) {
+ location.href = downloadUrl
+ downloadUrl = null
+ }
+ },
+ flush () {
+ if (downloadUrl) {
+ location.href = downloadUrl
+ }
+ }
+ }
+ var ts = new streamSaver.TransformStream(
+ transformer,
+ opts.writableStrategy,
+ opts.readableStrategy
+ )
+ const readableStream = ts.readable
+
+ channel.port1.postMessage({ readableStream }, [ readableStream ])
+ }
+
+ channel.port1.onmessage = evt => {
+ // Service worker sent us a link that we should open.
+ if (evt.data.download) {
+ // Special treatment for popup...
+ if (downloadStrategy === 'navigate') {
+ mitmTransporter.remove()
+ mitmTransporter = null
+ if (bytesWritten) {
+ location.href = evt.data.download
+ } else {
+ downloadUrl = evt.data.download
+ }
+ } else {
+ if (mitmTransporter.isPopup) {
+ mitmTransporter.remove()
+ // Special case for firefox, they can keep sw alive with fetch
+ if (downloadStrategy === 'iframe') {
+ makeIframe(streamSaver.mitm)
+ }
+ }
+
+ // We never remove this iframes b/c it can interrupt saving
+ makeIframe(evt.data.download)
+ }
+ }
+ }
+
+ if (mitmTransporter.loaded) {
+ mitmTransporter.postMessage(...args)
+ } else {
+ mitmTransporter.addEventListener('load', () => {
+ mitmTransporter.postMessage(...args)
+ }, { once: true })
+ }
+ }
+
+ let chunks = []
+
+ return (!useBlobFallback && ts && ts.writable) || new streamSaver.WritableStream({
+ write (chunk) {
+ if (useBlobFallback) {
+ // Safari... The new IE6
+ // https://github.com/jimmywarting/StreamSaver.js/issues/69
+ //
+ // even doe it has everything it fails to download anything
+ // that comes from the service worker..!
+ chunks.push(chunk)
+ return
+ }
+
+ // is called when a new chunk of data is ready to be written
+ // to the underlying sink. It can return a promise to signal
+ // success or failure of the write operation. The stream
+ // implementation guarantees that this method will be called
+ // only after previous writes have succeeded, and never after
+ // close or abort is called.
+
+ // TODO: Kind of important that service worker respond back when
+ // it has been written. Otherwise we can't handle backpressure
+ // EDIT: Transfarable streams solvs this...
+ channel.port1.postMessage(chunk)
+ bytesWritten += chunk.length
+
+ if (downloadUrl) {
+ location.href = downloadUrl
+ downloadUrl = null
+ }
+ },
+ close () {
+ if (useBlobFallback) {
+ const blob = new Blob(chunks, { type: 'application/octet-stream; charset=utf-8' })
+ const link = document.createElement('a')
+ link.href = URL.createObjectURL(blob)
+ link.download = filename
+ link.click()
+ } else {
+ channel.port1.postMessage('end')
+ }
+ },
+ abort () {
+ chunks = []
+ channel.port1.postMessage('abort')
+ channel.port1.onmessage = null
+ channel.port1.close()
+ channel.port2.close()
+ channel = null
+ }
+ }, opts.writableStrategy)
+ }
+
+ return streamSaver
+})
diff --git a/frontend/src/js/widget/wb-accordion.js b/frontend/src/js/widget/wb-accordion.js
new file mode 100644
index 0000000..69659e6
--- /dev/null
+++ b/frontend/src/js/widget/wb-accordion.js
@@ -0,0 +1,87 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBAccordion extends Component {
+ constructor(...args) {
+ super(...args);
+ this.setupIds();
+ }
+
+ setupIds() {
+ this.state.domId = 'accordion-' + uuid.v4();
+ const { names } = this.props;
+ const { state } = this;
+ state.headerDomIds = names.map(() => ('accordion-' + uuid.v4()));
+ state.collapseDomIds = names.map(() => ('accordion-' + uuid.v4()));
+ state.collapseClass = Array(names.length).fill('collapse');
+ state.ariaExpanded = Array(names.length).fill(false);
+ state.buttonClass = Array(names.length).fill('btn btn-link collapsed');
+ state.collapseElements = null;
+ }
+
+ componentWillReceiveProps(nextProps) {
+ if (JSON.stringify(nextProps.names) === JSON.stringify(this.props.names))
+ return;
+ this.props = nextProps;
+ this.setupIds();
+ }
+
+ toggle(idx) {
+ const { ariaExpanded, collapseClass, buttonClass } = this.state;
+ const isOpen = ariaExpanded[idx];
+ for (let i = 0; i < ariaExpanded.length; i++) {
+ ariaExpanded[i] = false;
+ collapseClass[i] = 'collapse';
+ buttonClass[i] = 'btn btn-link collapsed';
+ }
+ if (!isOpen) {
+ ariaExpanded[idx] = true;
+ collapseClass[idx] = 'collapse show';
+ buttonClass[idx] = 'btn btn-link';
+ }
+ this.setState({ ariaExpanded, collapseClass, buttonClass });
+ }
+
+ render({ children, names, extraHeaderUi, cardHeaderClass }, { domId, headerDomIds,
+ collapseDomIds, collapseClass, ariaExpanded, buttonClass }) {
+
+ return (
+
+ { children.map((_, i) => (
+
+
+
+
+ { extraHeaderUi[i] }
+
+
+
+
+
+ )) }
+
+ );
+ }
+};
+
+WBAccordion.defaultProps = {
+ cardHeaderClass: 'card-header',
+ extraHeaderUi: []
+};
+
+export default WBAccordion;
diff --git a/frontend/src/js/widget/wb-breadcrumbs.js b/frontend/src/js/widget/wb-breadcrumbs.js
new file mode 100644
index 0000000..ce1037e
--- /dev/null
+++ b/frontend/src/js/widget/wb-breadcrumbs.js
@@ -0,0 +1,42 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBBreadcrumbs extends Component {
+ render({ items, onItemClicked, getItemUrl }) {
+ return (
+
+ );
+ }
+}
+
+WBBreadcrumbs.defaultProps = {
+ onItemClicked: null,
+ getItemUrl: null
+}
+
+export default WBBreadcrumbs;
diff --git a/frontend/src/js/widget/wb-checkboxes.js b/frontend/src/js/widget/wb-checkboxes.js
new file mode 100644
index 0000000..36db0ab
--- /dev/null
+++ b/frontend/src/js/widget/wb-checkboxes.js
@@ -0,0 +1,37 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBCheckboxes extends Component {
+ render({ items, checked, onChange, cssClass, title }) {
+ return (
+
+ { title }
+ {
+ items.map((name, idx) => (
+
+ ))
+ }
+
+ );
+ }
+}
+
+WBCheckboxes.defaultProps = {
+ 'checked': [],
+ 'onChange': () => {}
+}
+
+export default WBCheckboxes;
diff --git a/frontend/src/js/widget/wb-dialog.js b/frontend/src/js/widget/wb-dialog.js
new file mode 100644
index 0000000..71abeae
--- /dev/null
+++ b/frontend/src/js/widget/wb-dialog.js
@@ -0,0 +1,69 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, createRef } from 'preact';
+
+class WBDialog extends Component {
+ constructor(...args) {
+ super(...args);
+ this.modalRef = createRef();
+ }
+
+ show() {
+ $(this.modalRef.current).modal();
+ }
+
+ hide() {
+ $(this.modalRef.current).modal('hide');
+ }
+
+ componentWillUnmount() {
+ $(this.modalRef.current).modal('hide');
+ }
+
+ render({ title, children, canAccept, accept, reject }) {
+ return (
+
+ );
+ }
+}
+
+WBDialog.defaultProps = {
+ title: 'Dialog',
+ accept: () => {},
+ reject: () => {},
+ canAccept: () => true
+};
+
+export default WBDialog;
diff --git a/frontend/src/js/widget/wb-navbar.js b/frontend/src/js/widget/wb-navbar.js
new file mode 100644
index 0000000..ccea039
--- /dev/null
+++ b/frontend/src/js/widget/wb-navbar.js
@@ -0,0 +1,122 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBNavbar extends Component {
+ render({ title, items, rhs, onItemClicked, onTitleClicked,
+ activeItem, titleUrl, getItemUrl }) {
+
+ return (
+
+ );
+ }
+
+ titleClicked(e) {
+ let { onTitleClicked } = this.props;
+ if (!onTitleClicked)
+ return;
+ e.preventDefault();
+ onTitleClicked();
+ }
+
+ itemClicked(e, i) {
+ let { onItemClicked } = this.props;
+ if (!onItemClicked)
+ return;
+ e.preventDefault();
+ onItemClicked(i);
+ }
+}
+
+WBNavbar.defaultProps = {
+ 'title': 'Workbench Advanced',
+ 'items': [],
+ 'form': null,
+ 'activeItem': null,
+ 'titleUrl': '#',
+ 'getItemUrl': () => ('#')
+}
+
+export default WBNavbar;
diff --git a/frontend/src/js/widget/wb-pagination.js b/frontend/src/js/widget/wb-pagination.js
new file mode 100644
index 0000000..68692a4
--- /dev/null
+++ b/frontend/src/js/widget/wb-pagination.js
@@ -0,0 +1,91 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBPagination extends Component {
+ renderVisiblePages(numPages, activePage, chunkSize, onPageChanged, getPageUrl) {
+ let visible = {};
+
+ let begActChnk = activePage - Math.floor(chunkSize / 2);
+ let endActChnk = activePage + Math.floor(chunkSize / 2) + 1;
+ for (let i = Math.max(0, begActChnk); i < Math.min(numPages, endActChnk); i++)
+ visible[i] = true;
+
+ for (let i = 0; i < Math.min(numPages, chunkSize); i++)
+ visible[i] = true;
+
+ for (let i = Math.max(numPages - chunkSize, 0); i < numPages; i++)
+ visible[i] = true;
+
+ visible = Object.keys(visible).map(n => Number(n));
+ visible.sort((a, b) => (a - b));
+
+ let res = [];
+ let prev = 0;
+
+ res.push((
+
+ this.changePage(e, activePage - 1) }>Previous
+
+ ));
+
+ for (let idx = 0; idx < visible.length; idx++) {
+ let i = visible[idx];
+ let capturePrev = prev;
+ if (i > prev + 1)
+ res.push((
+
+ this.changePage(e, Math.round((i + capturePrev) / 2)) }>...
+
+ ));
+ prev = i;
+
+ res.push((
+
+ this.changePage(e, i) }>{ i + 1 }
+
+ ));
+ }
+
+ res.push((
+ = numPages - 1 ? "page-item disabled" : "page-item" }>
+ this.changePage(e, activePage + 1) }>Next
+
+ ));
+
+ return res;
+ }
+
+ changePage(e, pageIdx) {
+ if (this.props.onPageChanged) {
+ e.preventDefault();
+ this.props.onPageChanged(pageIdx);
+ }
+ }
+
+ render({ numPages, activePage, chunkSize, onPageChanged, getPageUrl }) {
+ return (
+
+ );
+ }
+}
+
+WBPagination.defaultProps = {
+ 'chunkSize': 5,
+ 'getPageUrl': () => ('#')
+};
+
+export default WBPagination;
diff --git a/frontend/src/js/widget/wb-select.js b/frontend/src/js/widget/wb-select.js
new file mode 100644
index 0000000..9ba0c33
--- /dev/null
+++ b/frontend/src/js/widget/wb-select.js
@@ -0,0 +1,26 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBSelect extends Component {
+ render({ value, options, onChange }) {
+ return (
+
+ );
+ }
+}
+
+export default WBSelect;
diff --git a/frontend/src/js/widget/wb-table.js b/frontend/src/js/widget/wb-table.js
new file mode 100644
index 0000000..bb64ed3
--- /dev/null
+++ b/frontend/src/js/widget/wb-table.js
@@ -0,0 +1,41 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component } from 'preact';
+
+class WBTable extends Component {
+ render({ columns, rows, headerClasses, verticalHeader }) {
+ return (
+
+
+
+ { columns.map((c, i) => { c } | ) }
+
+
+
+ { rows.map(r => (
+
+ { columns.map((_, idx) => (
+ (idx == 0 && verticalHeader) ? (
+ { r[idx] } |
+ ) : (
+ { r[idx] } |
+ )
+ )) }
+
+ )) }
+
+
+ );
+ }
+}
+
+WBTable.defaultProps = {
+ 'headerClasses': []
+};
+
+export default WBTable;
diff --git a/frontend/src/js/widget/wb-tabs.js b/frontend/src/js/widget/wb-tabs.js
new file mode 100644
index 0000000..ea9a5dd
--- /dev/null
+++ b/frontend/src/js/widget/wb-tabs.js
@@ -0,0 +1,55 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+import { h, Component, VNode } from 'preact';
+
+class WBTabs extends Component {
+ render({ tabs, onTabChanged }) {
+ return (
+
+ { tabs.map((t, idx) => {
+ let name, isActive, isDisabled, onClick;
+
+ if (!t)
+ return null;
+
+ if (typeof(t) === 'object') {
+ name = t.name;
+ isActive = t.isActive;
+ isDisabled = t.isDisabled;
+ onClick = t.onClick;
+
+ } else if (typeof(t) === 'string') {
+ name = t;
+ }
+
+ let cls = ['nav-link'];
+ if (isActive)
+ cls.push('active');
+ if (isDisabled)
+ cls.push('disabled');
+ cls = cls.join(' ');
+ return (
+ -
+ { e.preventDefault();
+ onClick ? onClick() : onTabChanged(t); } }>
+ { name }
+
+
+ );
+ }) }
+
+ );
+ }
+}
+
+WBTabs.defaultProps = {
+ 'onTabChanged': () => {}
+};
+
+export default WBTabs;
diff --git a/frontend/src/js/worker/wb-manifest-worker.js b/frontend/src/js/worker/wb-manifest-worker.js
new file mode 100644
index 0000000..cd99733
--- /dev/null
+++ b/frontend/src/js/worker/wb-manifest-worker.js
@@ -0,0 +1,193 @@
+//
+// Copyright (C) Stanislaw Adaszewski, 2020
+// Contact: s.adaszewski@gmail.com
+// Website: https://adared.ch/wba
+// License: GPLv3
+//
+
+const rx = /^[a-f0-9]{32}\+[0-9]+/;
+const rootDir = {};
+const streams = [];
+
+onmessage = function(e) {
+ switch (e.data[0]) {
+ case 'precreatePaths':
+ precreatePaths(e.data[1]);
+ postMessage([ 'precreatePathsResult' ]);
+ break;
+ case 'parseStream':
+ parseStream(e.data[1]);
+ postMessage([ 'parseStreamResult' ]);
+ break;
+ case 'listDirectory': {
+ const lst = listDirectory(rootDir, e.data[1], e.data[2]);
+ postMessage([ 'listDirectoryResult', lst ])
+ break; }
+ case 'getData':
+ postMessage([ 'getDataResult', rootDir, streams ]);
+ break;
+ case 'getFile':
+ postMessage([ 'getFileResult', getFile(rootDir, streams, e.data[1]) ]);
+ break;
+ default: {
+ const err = Error('Unknown verb: ' + e.data[0]);
+ postMessage([ 'error', err.message ]);
+ throw err; }
+ }
+}
+
+function precreatePaths(paths) {
+ for (let i = 0; i < paths.length; i++) {
+ mkpath(rootDir, paths[i]);
+ }
+}
+
+function parseStream(s) {
+ if (!s) return;
+
+ const tokens = s.split(' ');
+ const streamName = unescapeName(tokens[0]);
+
+ let n = tokens.map(t => rx.exec(t));
+ n = n.indexOf(null, 1);
+
+ let locators = tokens.slice(1, n);
+ let pos = 0;
+ locators = locators.map(loc => {
+ const sz = parseInt(loc.split('+')[1], 10);
+ return [ loc, pos, pos += sz ];
+ });
+
+ let fileTokens = tokens.slice(n);
+ let lastFile = null;
+ let lastPath = null;
+ fileTokens.map(t => {
+ let seg = t.split(':');
+ seg = [ parseInt(seg[0], 10), parseInt(seg[1], 10),
+ unescapeName(seg.slice(2).join(':')) ]
+ const path = streamName + '/' + seg[2];
+ let f;
+ if (path === lastPath) {
+ f = lastFile;
+ } else {
+ let dirName = path.split('/');
+ const name = dirName[dirName.length - 1];
+ dirName = dirName.slice(0, dirName.length - 1);
+ const d = mkpath(rootDir, dirName);
+ lastFile = f = makeFile(d, name);
+ lastPath = path;
+ }
+ appendFile(f, streams.length, seg);
+ });
+
+ streams.push(locators);
+}
+
+function mkdir(parent, name) {
+ if (name in parent && (parent[name] instanceof Array))
+ throw Error('File with the same name already exists');
+ if (name in parent)
+ return parent[name];
+ const dir = {};
+ parent[name] = dir;
+ return dir;
+}
+
+function mkpath(parent, path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ let dir = parent;
+ for (let i = 1; i < path.length; i++) {
+ dir = mkdir(dir, path[i]);
+ }
+ return dir;
+}
+
+function makeFile(dir, name) {
+ if (name in dir) {
+ if (!(dir[name] instanceof Array))
+ throw Error('Directory with the same name already exists');
+ return dir[name];
+ }
+ const f = [[], 0];
+ dir[name] = f;
+ return f;
+}
+
+function appendFile(f, sidx, seg) {
+ f[0].push([ sidx, seg[0], seg[1] ]);
+ f[1] += seg[1];
+ return f;
+}
+
+function unescapeName(name) {
+ return name.replace(/(\\\\|\\[0-9]{3})/g,
+ (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8))));
+}
+
+function findDir(parent, path, lenient=false) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+ if (path[0] !== '.')
+ throw Error('Path must start with a dot (.)');
+ let dir = parent;
+ for (let i = 1; i < path.length; i++) {
+ if (!(path[i] in dir)) {
+ if (lenient)
+ return {};
+ else
+ throw Error('Directory not found');
+ }
+ dir = dir[path[i]];
+ }
+ return dir;
+}
+
+function listDirectory(rootDir, path, lenient=false) {
+ let dir = findDir(rootDir, path, lenient);
+ let keys = Object.keys(dir);
+ keys.sort();
+ let subdirs = keys.filter(k => !(dir[k] instanceof Array));
+ let files = keys.filter(k => (dir[k] instanceof Array));
+ let res = subdirs.map(k => [ 'd', k, null ]);
+ res = res.concat(files.map(k => [ 'f', k, dir[k][1] ]));
+ return res;
+}
+
+function getFile(rootDir, streams, path) {
+ if (typeof(path) === 'string')
+ path = path.split('/');
+
+ if (path.length < 2)
+ throw Error('Invalid file path');
+
+ const name = path[path.length - 1];
+
+ const dir = findDir(rootDir, path.slice(0, path.length - 1));
+
+ if (!(name in dir))
+ throw Error('File not found');
+
+ if (!(dir[name] instanceof Array))
+ throw Error('Path points to a directory not a file');
+
+ let file = dir[name];
+ file = [ file[0].map(seg => {
+ const stm = streams[seg[0]];
+ const used = stm.map(loc => !( loc[2] <= seg[1] || loc[1] >= seg[1] + seg[2] ) );
+ const start = used.indexOf(true);
+ const end = used.lastIndexOf(true) + 1;
+ if (start === -1)
+ return [];
+ const res = [];
+ for (let i = start; i < end; i++) {
+ const loc = stm[i];
+ res.push([ loc[0], Math.max(0, seg[1] - loc[1]),
+ Math.min(loc[2] - loc[1], seg[1] + seg[2] - loc[1]) ]);
+ }
+ return res;
+ }), file[1] ];
+ file[0] = file[0].reduce((a, b) => a.concat(b));
+
+ return file;
+}
diff --git a/frontend/tmux.sh b/frontend/tmux.sh
new file mode 100644
index 0000000..25855bf
--- /dev/null
+++ b/frontend/tmux.sh
@@ -0,0 +1,23 @@
+#!/bin/sh
+
+set -e
+
+function panetitle () {
+ printf "\033]2;%s\033\\" "$1";
+}
+
+export -f panetitle
+
+export TERM=putty
+
+source ${HOME}/workspace/ml/ml tmux-2.7.0
+
+tmux new-session -x 2000 -y 1200 -d "ml python2; cd ${HOME}/workspace/arvados-workbench-advanced/frontend/dist; panetitle https_server; /bin/bash --init-file <(echo \"python2 ../../backend/srv.py\")"
+
+tmux split-window -v "source ${HOME}/workspace/ml/ml nodejs-8.11.3; cd ${HOME}/workspace/arvados-workbench-advanced/frontend; panetitle npm_run_watch; /bin/bash --init-file <(echo \"npm run watch\")"
+
+tmux split-window -v "cd ${HOME}/workspace/arvados-workbench-advanced; panetitle project; /bin/bash"
+
+tmux select-layout even-vertical
+
+tmux attach
diff --git a/wbadvanced/__main__.py b/wbadvanced/__main__.py
new file mode 100755
index 0000000..3e4f2cc
--- /dev/null
+++ b/wbadvanced/__main__.py
@@ -0,0 +1,55 @@
+import dash
+import dash_bootstrap_components as dbc
+import dash_html_components as html
+import dash_core_components as dcc
+import socket
+import arvados
+import functools
+from .projects import list_projects_html
+from .collections import list_collections_html
+from .processes import list_processes_html
+
+
+# list_projects_html = functools.lru_cache(maxsize=128)(list_projects_html)
+# list_collections_html = functools.lru_cache(maxsize=128)(list_collections_html)
+# list_processes_html = functools.lru_cache(maxsize=128)(list_processes_html)
+
+
+def main():
+ app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])
+ arv = arvados.api('v1')
+
+ user = arv.users().current().execute()
+ # nav_btns = [ dbc.Button(a['name']) for a in projects ]
+
+ app.layout = html.Div([
+ dcc.Location(id='url', refresh=False),
+ html.Div(id='page-content')
+ ], style={ 'margin': '10px' })
+
+ projects_table = list_projects_html(arv, user['uuid'])
+ collections_table = list_collections_html(arv, user['uuid'])
+ processes_table = list_processes_html(arv, user['uuid'])
+
+
+ @app.callback(dash.dependencies.Output('page-content', 'children'),
+ [ dash.dependencies.Input('url', 'pathname') ])
+ def display_page(pathname):
+ print('display_page(), pathname:', pathname)
+ return html.Div([
+ dbc.Tabs([
+ dbc.Tab(projects_table, label="Projects",
+ label_style={ 'cursor': 'pointer' })
+ ], style={ 'borderBottom': 'none' }),
+ dbc.Tabs([
+ dbc.Tab(collections_table, label="Collections",
+ label_style={ 'cursor': 'pointer' }),
+ dbc.Tab(processes_table, label="Processes",
+ label_style={ 'cursor': 'pointer' })
+ ], style={ 'borderBottom': 'none' })
+ ])
+
+ app.run_server(host=socket.getfqdn(), debug=False)
+
+
+main()
diff --git a/wbadvanced/collections.py b/wbadvanced/collections.py
new file mode 100755
index 0000000..5ba14f1
--- /dev/null
+++ b/wbadvanced/collections.py
@@ -0,0 +1,56 @@
+import dash_html_components as html
+import dash_bootstrap_components as dbc
+import humanize
+from .users import get_user
+
+
+def list_collections(arv, owner_uuid, limit=100):
+ res = arv.collections().list(where = {
+ 'owner_uuid': owner_uuid
+ }, limit=limit).execute()
+ res = res['items']
+ #while res['items_available'] > len(res['items']):
+ #res = arv.collections().list(where = {})
+ return res
+
+
+def list_collections_html(arv, parent, limit=100):
+ projects = list_collections(arv, parent, limit=limit)
+
+ header = html.Tr([
+ html.Th('Name'),
+ html.Th('Description'),
+ html.Th('Owner'),
+ html.Th('Size')
+ ])
+ header = [ html.Thead(header) ]
+
+ rows = []
+ for a in projects:
+ owner = get_user(arv, a['owner_uuid'])
+ rows.append(
+ html.Tr([
+ html.Td([
+ html.Div(html.A(href='/projects/' + a['uuid'],
+ children=a['name'])),
+ html.Div(a['uuid'])
+ ]),
+
+ html.Td(a['description'] or 'Modified at ' + a['modified_at']),
+
+ html.Td([
+ html.Div(html.A(href='mailto:' + owner['email'],
+ children=owner['first_name'] + ' ' + owner['last_name'])),
+ html.Div(a['owner_uuid'])
+ ]),
+
+ html.Td(humanize.naturalsize(a['file_size_total']))
+ ]))
+ rows = [ html.Tbody(rows) ]
+
+ res = dbc.Table(header + rows,
+ striped=True,
+ hover=True,
+ responsive=True)
+
+ return res
diff --git a/wbadvanced/processes.py b/wbadvanced/processes.py
new file mode 100755
index 0000000..7c1b71b
--- /dev/null
+++ b/wbadvanced/processes.py
@@ -0,0 +1,49 @@
+import dash_bootstrap_components as dbc
+import dash_html_components as html
+from .users import get_user
+
+
+def list_processes(arv, parent, limit=100):
+ res = arv.container_requests().list(where={
+ 'owner_uuid': parent
+ }, limit=100).execute()['items']
+ return res
+
+
+def list_processes_html(arv, parent, limit=100):
+ processes = list_processes(arv, parent, limit=limit)
+
+ header = html.Tr([
+ html.Th('Name'),
+ html.Th('Description'),
+ html.Th('Owner')
+ ])
+ header = [ html.Thead(header) ]
+
+ rows = []
+ for a in processes:
+ owner = get_user(arv, a['owner_uuid'])
+ rows.append(
+ html.Tr([
+ html.Td([
+ html.Div(html.A(href='/processes/' + a['uuid'],
+ children=a['name'])),
+ html.Div(a['uuid'])
+ ]),
+
+ html.Td(a['description'] or 'Process'),
+
+ html.Td([
+ html.Div(html.A(href='mailto:' + owner['email'],
+ children=owner['first_name'] + ' ' + owner['last_name'])),
+ html.Div(a['owner_uuid'])
+ ])
+ ]))
+ rows = [ html.Tbody(rows) ]
+
+ res = dbc.Table(header + rows,
+ striped=True,
+ hover=True,
+ responsive=True)
+
+ return res
diff --git a/wbadvanced/projects.py b/wbadvanced/projects.py
new file mode 100755
index 0000000..6adee71
--- /dev/null
+++ b/wbadvanced/projects.py
@@ -0,0 +1,51 @@
+import dash_bootstrap_components as dbc
+import dash_html_components as html
+import dash_core_components as dcc
+from .users import get_user
+
+
+def list_projects(arv, parent):
+ res = arv.groups().list(where={
+ 'owner_uuid': parent,
+ 'group_class': 'project'
+ }).execute()['items']
+ return res
+
+
+def list_projects_html(arv, parent):
+ projects = list_projects(arv, parent)
+
+ header = html.Tr([
+ html.Th('Name'),
+ html.Th('Description'),
+ html.Th('Owner')
+ ])
+ header = [ html.Thead(header) ]
+
+ rows = []
+ for a in projects:
+ owner = get_user(arv, a['owner_uuid'])
+ rows.append(
+ html.Tr([
+ html.Td([
+ html.Div(dcc.Link(href='/projects/' + a['uuid'],
+ children=a['name'])),
+ html.Div(a['uuid'])
+ ]),
+
+ html.Td(a['description'] or 'Project'),
+
+ html.Td([
+ html.Div(html.A(href='mailto:' + owner['email'],
+ children=owner['first_name'] + ' ' + owner['last_name'])),
+ html.Div(a['owner_uuid'])
+ ])
+ ]))
+ rows = [ html.Tbody(rows) ]
+
+ res = dbc.Table(header + rows,
+ striped=True,
+ hover=True,
+ responsive=True)
+
+ return res
diff --git a/wbadvanced/users.py b/wbadvanced/users.py
new file mode 100755
index 0000000..e3679c1
--- /dev/null
+++ b/wbadvanced/users.py
@@ -0,0 +1,6 @@
+import functools
+
+@functools.lru_cache(maxsize=128)
+def get_user(arv, uuid):
+ res = arv.users().get(uuid=uuid).execute()
+ return res