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['name'] } + { renderRenameLink(item, () => this.fetchItems()) } +
+
{ item['uuid'] }
+
), + (
+ { item['description'] } { renderEditDescription(item, () => this.fetchItems()) } +
), + (
+
+ { ownerLookup[item.owner_uuid] ? ( + + { arvadosObjectName(ownerLookup[item.owner_uuid]) } + + ) : 'Not Found' } +
+
{ 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 ? () : 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; + } }> +
+