| @@ -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 | |||
| @@ -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() | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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() | |||
| @@ -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 | |||
| @@ -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" | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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 | |||
| @@ -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" | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -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" | |||
| } | |||
| } | |||
| @@ -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() | |||
| ] | |||
| } | |||
| @@ -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; | |||
| } | |||
| @@ -0,0 +1,25 @@ | |||
| <html> | |||
| <head> | |||
| <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> | |||
| <link rel="stylesheet" type="text/css" href="/css/bootstrap.min.css" /> | |||
| <link rel="stylesheet" type="text/css" href="/css/all.min.css" /> | |||
| <link rel="stylesheet" type="text/css" href="/css/index.css" /> | |||
| <link rel="stylesheet" type="text/css" href="/css/papaya.css" /> | |||
| <script language="javascript" src="/js/web-streams-polyfill/ponyfill.js"></script> | |||
| <script language="javascript"> | |||
| window.process = { 'env': { 'NODE_ENV': 'production' } }; | |||
| </script> | |||
| <script language="javascript" src="/js/jquery.min.js"></script> | |||
| <script language="javascript" src="/js/bootstrap.min.js"></script> | |||
| <script language="javascript" src="/js/fontawesome.min.js"></script> | |||
| <script language="javascript" src="/js/js-uuid.js"></script> | |||
| <script language="javascript" src="/js/filesize.js"></script> | |||
| <script language="javascript" src="/js/crypto-js/core.js"></script> | |||
| <script language="javascript" src="/js/crypto-js/md5.js"></script> | |||
| <script language="javascript" src="/js/js-yaml.min.js"></script> | |||
| <script language="javascript" src="/js/StreamSaver.js"></script> | |||
| </head> | |||
| <body> | |||
| <script language="javascript" src="/js/app.min.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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; | |||
| @@ -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 } | |||
| @@ -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; | |||
| @@ -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 }; | |||
| @@ -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; | |||
| @@ -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 ( | |||
| <WBBreadcrumbs items={ items } | |||
| onItemClicked={ item => app.breadcrumbClicked(item) } /> | |||
| ); | |||
| } | |||
| } | |||
| export default WBArvadosCrumbs; | |||
| @@ -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') ? [ | |||
| (<a href={ this.getUrl({ 'collectionPath': collectionPath + '/' + item[1], 'page': 0 }) }>{ item[1] }/</a>), | |||
| 'Directory', | |||
| null, | |||
| (<div></div>) | |||
| ] : [ | |||
| item[1], | |||
| 'File', | |||
| filesize(item[2]), | |||
| ( (mode === 'browsingReady') ? ( | |||
| <div> | |||
| <button class="btn btn-outline-primary mx-1" title="Download" | |||
| onclick={ () => manifestWorker.postMessage([ 'getFile', | |||
| '.' + collectionPath + '/' + item[1] ]).then(e => { | |||
| const file = e.data[1]; | |||
| const blob = new Blob([ | |||
| JSON.stringify([ arvHost, arvToken, item[1], file ]) | |||
| ]); | |||
| const blocksBlobUrl = URL.createObjectURL(blob); | |||
| window.open('/download/' + encodeURIComponent(blocksBlobUrl), '_blank'); | |||
| }) }><i class="fas fa-download"></i></button> | |||
| <button class="btn btn-outline-primary mx-1" title="View" | |||
| onclick={ () => { | |||
| alert('Not implemented.') | |||
| } }><i class="far fa-eye"></i></button> | |||
| { endsWith(item[1].toLowerCase(), ['.nii', '.nii.gz']) ? ( | |||
| <button class="btn btn-outline-primary mx-1" title="View Image" | |||
| onclick={ () => manifestWorker.postMessage([ 'getFile', | |||
| '.' + collectionPath + '/' + item[1] ]).then(e => { | |||
| const file = e.data[1]; | |||
| const blob = new Blob([ | |||
| JSON.stringify({ 'name': item[1], 'file': file }) | |||
| ]); | |||
| const blocksBlobUrl = URL.createObjectURL(blob); | |||
| window.open('/image-viewer/' + encodeURIComponent(blocksBlobUrl), '_blank'); | |||
| }) }><i class="fas fa-image"></i></button> | |||
| ) : null } | |||
| </div> | |||
| ) : null) | |||
| ] | |||
| )) | |||
| }); | |||
| } | |||
| render({ collectionPath, page }, { manifestReader, rows, | |||
| numPages, loaded, total, mode, parsedStreams, totalStreams }) { | |||
| return ( | |||
| <div> | |||
| <WBBreadcrumbs items={ ('.' + collectionPath).split('/').map((name, index) => ({ name, index })) } | |||
| getItemUrl={ it => this.getUrl({ | |||
| collectionPath: ('.' + collectionPath).split('/').slice(0, it.index + 1).join('/').substr(1), | |||
| page: 0 | |||
| }) } /> | |||
| { (mode === 'manifestDownload') ? | |||
| ( | |||
| <div class="container-fluid"> | |||
| <div>Downloading manifest: { filesize(loaded) }</div> | |||
| <div class="progress"> | |||
| <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" | |||
| aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div> | |||
| </div> | |||
| </div> | |||
| ) : ( | |||
| <div> | |||
| { mode === 'manifestParse' ? ( | |||
| <div class="container-fluid mb-2"> | |||
| <div>Parsing manifest: { parsedStreams }/{ totalStreams }</div> | |||
| <div class="progress"> | |||
| <div class="progress-bar progress-bar-striped progress-bar-animated bg-success" role="progressbar" | |||
| aria-valuenow={ totalStreams } aria-valuemin="0" aria-valuemax={ parsedStreams } style={ 'width: ' + Math.round(parsedStreams * 100 / totalStreams) + '%' }></div> | |||
| </div> | |||
| </div> | |||
| ) : null } | |||
| <WBTable columns={ [ 'Name', 'Type', 'Size', 'Actions' ] } | |||
| rows={ rows } /> | |||
| <WBPagination activePage={ page } numPages={ numPages } | |||
| getPageUrl={ page => this.getUrl({ 'page': page }) } /> | |||
| </div> | |||
| ) } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBCollectionContent.defaultProps = { | |||
| 'collectionPath': '', | |||
| 'page': 0, | |||
| 'itemsPerPage': 20 | |||
| }; | |||
| export default WBCollectionContent; | |||
| @@ -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', ( | |||
| <WBJsonEditor name="Properties" app={ app } value={ item.properties } | |||
| onChange={ value => 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 : ( | |||
| <i>{ String(item.replication_desired) }</i> | |||
| ) ], | |||
| [ 'Replication Confirmed', item.replication_confirmed ? item.replication_confirmed : ( | |||
| <i>{ String(item.replication_confirmed) }</i> | |||
| ) ], | |||
| [ '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', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.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 ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| rows={ rows } | |||
| verticalHeader={ true } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBCollectionFields; | |||
| @@ -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), | |||
| (<div> | |||
| <div> | |||
| <a href={ urlForObject(item) }> | |||
| { item['name'] } | |||
| </a> { renderRenameLink(item, () => this.fetchItems()) } | |||
| </div> | |||
| <div>{ item['uuid'] }</div> | |||
| </div>), | |||
| (<div> | |||
| { item['description'] } { renderEditDescription(item, () => this.fetchItems()) } | |||
| </div>), | |||
| (<div> | |||
| <div> | |||
| { ownerLookup[item.owner_uuid] ? ( | |||
| <a href={ urlForObject(ownerLookup[item.owner_uuid]) }> | |||
| { arvadosObjectName(ownerLookup[item.owner_uuid]) } | |||
| </a> | |||
| ) : 'Not Found' } | |||
| </div> | |||
| <div>{ item.owner_uuid }</div> | |||
| </div>), | |||
| item['file_count'], | |||
| filesize(item['file_size_total']), | |||
| (<div> | |||
| <a class="btn btn-outline-primary m-1" title="Properties" | |||
| href={ urlForObject(item, 'properties') }> | |||
| <i class="fas fa-list-ul"></i> | |||
| </a> | |||
| { renderDeleteButton(item, () => this.fetchItems()) } | |||
| { renderSharingButton(item) } | |||
| </div>) | |||
| ]); | |||
| } | |||
| 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 ( | |||
| <div> | |||
| { error ? (<div class="alert alert-danger" role="alert"> | |||
| { error } | |||
| </div>) : null } | |||
| <WBCheckboxes items={ collectionTypes } checked={ collectionTypeMask } | |||
| cssClass="float-left mx-2 my-2" title="Collection Type: " | |||
| onChange={ () => route(getPageUrl(0)) } /> | |||
| <WBTable columns={ [ '', 'Name', 'Description', 'Owner', 'File Count', 'Total Size', 'Actions' ] } | |||
| headerClasses={ [ 'w-1'] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } | |||
| activePage={ activePage } | |||
| getPageUrl={ getPageUrl } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBCollectionListing.defaultProps = { | |||
| 'itemsPerPage': 100, | |||
| 'ownerUuid': null, | |||
| 'renderSharingButton': () => null, | |||
| 'renderEditDescription': () => null | |||
| }; | |||
| export default WBCollectionListing; | |||
| @@ -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', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.owner_uuid } /> | |||
| ) ], | |||
| [ 'Created at', wbFormatDate(item.created_at) ], | |||
| [ 'Modified at', wbFormatDate(item.modified_at) ], | |||
| [ 'Modified by User', ( | |||
| item.modified_by_user_uuid ? (<WBNameAndUuid app={ app } uuid={ item.modified_by_user_uuid } />) : '-' | |||
| ) ], | |||
| [ 'Modified by Client', ( | |||
| item.modified_by_client_uuid ? (<WBNameAndUuid app={ app } uuid={ item.modified_by_client_uuid } />) : '-' | |||
| ) ], | |||
| [ 'API Url', ( | |||
| <a href={ 'https://' + app.state.arvHost + '/arvados/v1/' + typeName + 's/' + uuid }> | |||
| { 'https://' + app.state.arvHost + '/arvados/v1/' + typeName + 's/' + uuid } | |||
| </a> | |||
| ) ], | |||
| [ 'ETag', item.etag ] | |||
| ]; | |||
| this.setState({ 'rows': rows }); | |||
| }); | |||
| } | |||
| render({}, { rows }) { | |||
| return ( | |||
| rows ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| verticalHeader={ true } | |||
| rows={ rows } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBCommonFields; | |||
| @@ -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 ? ( | |||
| <WBNameAndUuid app={ app } uuid={ item.log } /> | |||
| ) : ( <i>{ String(item.log) }</i> ) ], | |||
| [ 'Environment', ( | |||
| <WBAccordion names={ ['Environment'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.environment } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Working Directory', item.cwd ], | |||
| [ 'Command', ( | |||
| <WBJsonViewer app={ app } value={ item.command } pretty={ false } /> | |||
| ) ], | |||
| [ 'Output Path', item.output_path ], | |||
| [ 'Mounts', ( | |||
| <WBAccordion names={ Object.keys(item.mounts) } | |||
| cardHeaderClass="card-header-sm"> | |||
| { Object.keys(item.mounts).map(k => ( | |||
| <WBJsonViewer app={ app } value={ item.mounts[k] } /> | |||
| )) } | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Runtime Constraints', ( | |||
| <WBAccordion names={ ['Runtime Constraints'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.runtime_constraints } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Runtime Status', ( | |||
| <WBAccordion names={ ['Runtime Status'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.runtime_status } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Scheduling Parameters', ( | |||
| <WBAccordion names={ ['Scheduling Parameters'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.scheduling_parameters } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Output', item.output ? ( | |||
| <WBNameAndUuid app={ app } uuid={ item.output } /> | |||
| ) : ( <i>{ String(item.output) }</i> )], | |||
| [ 'Container Image', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.container_image } /> | |||
| ) ], | |||
| [ 'Progress', item.progress ], | |||
| [ 'Priority', item.priority ], | |||
| [ 'Exit Code', item.exit_code === null ? ( <i>null</i> ) : item.exit_code ], | |||
| [ 'Auth UUID', item.auth_uuid === null ? ( <i>null</i> ) : item.auth_uuid ], | |||
| [ 'Locked by UUID', item.locked_by_uuid === null ? ( <i>null</i> ) : item.locked_by_uuid ] | |||
| ]; | |||
| rows = rows.map(r => [r[0], r[1] ? r[1] : (<i>{ String(r[1]) }</i>)]); | |||
| this.setState({ 'rows': rows }); | |||
| }); | |||
| } | |||
| render({}, { rows }) { | |||
| return ( | |||
| rows ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| rows={ rows } | |||
| verticalHeader={ true } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBContainerFields; | |||
| @@ -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 || (<i>{ String(item.description) }</i>) ], | |||
| [ 'Properties', ( | |||
| <WBJsonEditor name="Properties" app={ app } value={ item.properties } | |||
| onChange={ value => wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value) | |||
| .then(() => { item.properties = value; this.prepareRows(item); }) } /> | |||
| ) ], | |||
| [ 'State', item.state ], | |||
| [ 'Requesting Container', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.requesting_container_uuid } /> | |||
| ) ], | |||
| [ 'Container', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.container_uuid } /> | |||
| ) ], | |||
| [ 'Container Count Max', item.container_count_max ], | |||
| [ 'Mounts', ( | |||
| <WBAccordion names={ Object.keys(item.mounts) } | |||
| cardHeaderClass="card-header-sm"> | |||
| { Object.keys(item.mounts).map(k => ( | |||
| <WBJsonViewer app={ app } value={ item.mounts[k] } /> | |||
| )) } | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Runtime Constraints', ( | |||
| <WBAccordion names={ ['Runtime Constraints'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.runtime_constraints } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Scheduling Parameters', ( | |||
| <WBAccordion names={ ['Scheduling Parameters'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.scheduling_parameters } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Container Image', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.container_image } /> | |||
| ) ], | |||
| [ 'Environment', ( | |||
| <WBAccordion names={ ['Environment'] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ item.environment } /> | |||
| </WBAccordion> | |||
| ) ], | |||
| [ 'Working Directory', item.cwd ], | |||
| [ 'Command', ( | |||
| <WBJsonViewer app={ app } value={ item.command } pretty={ false } /> | |||
| ) ], | |||
| [ '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', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.log_uuid } /> | |||
| ) ], | |||
| [ 'Output', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.output_uuid } /> | |||
| ) ], | |||
| [ 'Filters', ( | |||
| item.filters ? (<WBJsonViewer app={ app } value={ item.filters } />) : (<i>{ String(item.filters) }</i>) | |||
| ) ], | |||
| [ 'Runtime Token', item.runtime_token || (<i>{ String(item.runtime_token) }</i>) ], | |||
| [ 'Runtime User', ( | |||
| <WBNameAndUuid app={ app } uuid={ item.runtime_user } /> | |||
| ) ], | |||
| [ 'Runtime Auth Scopes', ( | |||
| item.runtime_auth_scopes ? ( | |||
| <WBJsonViewer app={ app } value={ item.runtime_auth_scopes } /> | |||
| ) : ( | |||
| <i>{ String(item.runtime_auth_scopes) }</i> | |||
| ) | |||
| ) ] | |||
| ]; | |||
| rows = rows.map(r => [r[0], r[1] ? r[1] : (<i>{ String(r[1]) }</i>)]); | |||
| 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 ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| rows={ rows } | |||
| verticalHeader={ true } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBContainerRequestFields; | |||
| @@ -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 ( | |||
| <div class="form-inline my-2 my-lg-0"> | |||
| <input class="form-control mr-sm-2" type="search" placeholder="Search" aria-label="Search" | |||
| value={ textSearch } onchange={ navigate ? (e => navigate(e.target.value)) : null } /> | |||
| <button class="btn btn-outline-success my-2 my-sm-0" type="submit">Search</button> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBInlineSearch; | |||
| @@ -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 ( | |||
| <WBDialog title={ 'Edit ' + name } ref={ this.dialogRef } | |||
| accept={ () => { | |||
| onChange(JSON.parse(editValue)); | |||
| } } | |||
| canAccept={ () => { | |||
| try { JSON.parse(editValue) } | |||
| catch (exc) { this.setState({ parseError: exc.message }); return false; } | |||
| return true; | |||
| } }> | |||
| <div> | |||
| <textarea class="form-control wb-json-editor" value={ editValue } rows="10" | |||
| onChange={ e => this.setState({ editValue: e.target.value }) } /> | |||
| { parseError ? ( | |||
| <div class="alert alert-danger mt-2" role="alert"> | |||
| { parseError } | |||
| </div> | |||
| ) : null } | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| show() { | |||
| this.dialogRef.current.show(); | |||
| } | |||
| } | |||
| class WBJsonEditor extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.dialogRef = createRef(); | |||
| } | |||
| render({ app, name, value, stringify, pretty, onChange }, { editValue, parseError }) { | |||
| return ( | |||
| <div> | |||
| <WbJsonEditorDialog name={ name } onChange={ onChange } ref={ this.dialogRef } /> | |||
| <WBAccordion names={ [ name ] } extraHeaderUi={ [ ( | |||
| <button class="btn btn-link px-0" title="Edit" | |||
| onclick={ () => { | |||
| const dlg = this.dialogRef.current; | |||
| dlg.setState({ parseError: null, | |||
| editValue: stringify ? | |||
| pretty ? JSON.stringify(value, null, 2) | |||
| : JSON.stringify(value) : value }); | |||
| dlg.show(); | |||
| } }> | |||
| <i class="fas fa-edit text-secondary" /> | |||
| </button> | |||
| ) ] } cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ value } stringify={ stringify } | |||
| pretty={ pretty } /> | |||
| </WBAccordion> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBJsonEditor.defaultProps = { | |||
| stringify: true, | |||
| pretty: true, | |||
| onChange: () => {} | |||
| }; | |||
| export default WBJsonEditor; | |||
| @@ -0,0 +1,50 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBIdTools from 'wb-id-tools'; | |||
| import urlForObject from 'url-for-object'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import arvadosObjectName from 'arvados-object-name'; | |||
| import WBLazyInlineName from 'wb-lazy-inline-name'; | |||
| function detectIds(value, app) { | |||
| const matches = WBIdTools.detectIdentifiers(value); | |||
| matches.sort((a, b) => (a.index - b.index)); | |||
| const res = []; | |||
| let ofs = 0; | |||
| for (let i = 0; i < matches.length; i++) { | |||
| const { index } = matches[i]; | |||
| const id = matches[i][0]; | |||
| const typeName = WBIdTools.typeName(id); | |||
| const url = (typeName === 'group' ? '/browse/' + id : | |||
| typeName === 'collection' ? '/collection-browse/' + id : | |||
| urlForObject({ uuid: id })); | |||
| res.push(value.substring(ofs, index)); | |||
| res.push(h(WBLazyInlineName, { identifier: id, app }, id)); | |||
| ofs = index + id.length; | |||
| } | |||
| res.push(value.substring(ofs)); | |||
| return res; | |||
| } | |||
| class WBJsonViewer extends Component { | |||
| render({ value, stringify, app, pretty }) { | |||
| if (stringify) | |||
| value = pretty ? JSON.stringify(value, null, 2) : JSON.stringify(value); | |||
| return ( | |||
| <div class="wb-json-viewer">{ detectIds(value, app) }</div> | |||
| ); | |||
| } | |||
| } | |||
| WBJsonViewer.defaultProps = { | |||
| stringify: true, | |||
| pretty: true | |||
| }; | |||
| export default WBJsonViewer; | |||
| @@ -0,0 +1,66 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBIdTools from 'wb-id-tools'; | |||
| import urlForObject from 'url-for-object'; | |||
| import arvadosObjectName from 'arvados-object-name'; | |||
| class WBLazyInlineName extends Component { | |||
| componentWillReceiveProps(nextProps) { | |||
| if (nextProps.identifier === this.props.identifier) | |||
| return; | |||
| this.setState({ item: null }); | |||
| } | |||
| fetchData() { | |||
| const { app, identifier } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| const typeName = WBIdTools.typeName(identifier); | |||
| if (WBIdTools.isPDH(identifier)) { | |||
| const filters = [ | |||
| [ 'portable_data_hash', '=', identifier ] | |||
| ]; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/collections?filters=' + encodeURIComponent(JSON.stringify(filters))); | |||
| prom = prom.then(xhr => this.setState({ item: { | |||
| uuid: xhr.response.items.length > 0 ? xhr.response.items[0].uuid : '', | |||
| name: xhr.response.items.length > 0 ? xhr.response.items[0].name : 'Not Found' + | |||
| ( xhr.response.items_available > 1 ? ' (+' + (xhr.response.items_available - 1) + ' others)' : '' ) | |||
| }})); | |||
| return; | |||
| } | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/' + typeName + 's/' + identifier); | |||
| prom = prom.then(xhr => this.setState({ item: xhr.response })); | |||
| prom = prom.catch(() => this.setState({ item: { name: 'Not Found' }})); | |||
| } | |||
| render({ identifier }, { item }) { | |||
| if (item) { | |||
| return ( | |||
| <a href={ urlForObject(item) }>{ arvadosObjectName(item) }</a> | |||
| ); | |||
| } | |||
| const typeName = WBIdTools.typeName(identifier); | |||
| const url = (typeName === 'group' ? '/browse/' + identifier : | |||
| typeName === 'collection' ? '/collection-browse/' + identifier : | |||
| urlForObject({ uuid: identifier })); | |||
| return ( | |||
| <span> | |||
| <a href={ url }>{ identifier }</a> <a href="#" title="Look up" | |||
| onclick={ e => { e.preventDefault(); this.fetchData(); } }> | |||
| <i class="fas fa-search"></i> | |||
| </a> | |||
| </span> | |||
| ); | |||
| } | |||
| } | |||
| export default WBLazyInlineName; | |||
| @@ -0,0 +1,82 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBPagination from 'wb-pagination'; | |||
| class WBLiveLogs extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.page = 0; | |||
| this.state.moreItemsPerPage = false; | |||
| this.terminalRef = createRef(); | |||
| } | |||
| componentDidMount() { | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| if (nextProps.uuid === this.props.uuid); | |||
| return; | |||
| this.props = nextProps; | |||
| this.state.page = 0; | |||
| this.fetchData(); | |||
| } | |||
| fetchData() { | |||
| const { uuid, app } = this.props; | |||
| let { itemsPerPage } = this.props; | |||
| const { page, moreItemsPerPage } = this.state; | |||
| if (moreItemsPerPage) | |||
| itemsPerPage *= 10; | |||
| const { arvHost, arvToken } = app.state; | |||
| const filters = [ | |||
| [ 'object_uuid', '=', uuid ] | |||
| ]; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/logs?filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&offset=' + (itemsPerPage * page) + | |||
| '&limit=' + itemsPerPage); | |||
| prom = prom.then(xhr => { | |||
| const { items } = xhr.response; | |||
| this.setState({ | |||
| content: items | |||
| .filter(a => ('text' in a.properties)) | |||
| .map(a => a.properties.text.trim()).join('\n'), | |||
| numPages: Math.ceil(xhr.response.items_available / itemsPerPage) | |||
| }); | |||
| this.terminalRef.current.scrollTo(0, 0); | |||
| }); | |||
| } | |||
| render({}, { content, page, numPages, moreItemsPerPage }) { | |||
| return ( | |||
| <div> | |||
| <div class="custom-control custom-switch"> | |||
| <input type="checkbox" class="custom-control-input" id="morePerPageSwitch" | |||
| checked = { moreItemsPerPage ? 'checked' : null } | |||
| onchange={ e => { this.state.moreItemsPerPage = e.target.checked; | |||
| this.state.page = 0; this.fetchData(); } } /> | |||
| <label class="custom-control-label" for="morePerPageSwitch">More log entries per page</label> | |||
| </div> | |||
| <WBPagination activePage={ page } numPages={ numPages } | |||
| onPageChanged={ page => { this.state.page = page; this.fetchData(); } } /> | |||
| <pre class="word-wrap terminal" ref={ this.terminalRef }> | |||
| { content } | |||
| </pre> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBLiveLogs.defaultProps = { | |||
| itemsPerPage: 100 | |||
| }; | |||
| export default WBLiveLogs; | |||
| @@ -0,0 +1,130 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import urlForObject from 'url-for-object'; | |||
| import arvadosObjectName from 'arvados-object-name'; | |||
| import arvadosTypeName from 'arvados-type-name'; | |||
| class WBNameAndUuid extends Component { | |||
| fetchData() { | |||
| const { uuid, app, lookup } = this.props; | |||
| if (!uuid) | |||
| return; | |||
| if (lookup && (uuid in lookup)) { | |||
| this.setState({ 'item': lookup[uuid]}); | |||
| return; | |||
| } | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = new Promise(accept => accept()); | |||
| if (/[0-9a-f]{32}\+[0-9]+/g.exec(uuid)) { | |||
| let filters = [ | |||
| ['portable_data_hash', '=', uuid] | |||
| ]; | |||
| prom = prom.then(() => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/collections?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)))); | |||
| prom = prom.then(xhr => { | |||
| if (xhr.response.items.length === 0) { | |||
| this.setState({ | |||
| 'item': { | |||
| 'uuid': uuid, | |||
| 'name': 'Collection with portable data hash ' + uuid | |||
| } | |||
| }); | |||
| return; | |||
| } | |||
| let item = xhr.response.items[0]; | |||
| if (xhr.response.items.length > 1) | |||
| item.name += ' +' + (xhr.response.items.length - 1) + ' others'; | |||
| this.setState({ item }); | |||
| }); | |||
| } else if (/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(uuid)) { | |||
| let typeName = arvadosTypeName(uuid); | |||
| const filters = [ | |||
| ['uuid', '=', uuid] | |||
| ]; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/' + typeName + | |||
| 's?filters=' + encodeURIComponent(JSON.stringify(filters))); | |||
| prom = prom.then(xhr => { | |||
| const item = xhr.response.items[0]; | |||
| if (!item) | |||
| this.setState({ 'error': 'Item not found' }); | |||
| else | |||
| this.setState({ | |||
| 'item': item | |||
| }); | |||
| }); | |||
| prom = prom.catch(xhr => { | |||
| this.setState({ | |||
| 'error': 'Unable to retrieve: ' + xhr.status + ' (' + xhr.statusText + ')' | |||
| }); | |||
| }); | |||
| } else { | |||
| this.setState({ | |||
| 'item': { | |||
| 'uuid': uuid | |||
| } | |||
| }); | |||
| } | |||
| } | |||
| componentDidMount() { | |||
| if (this.props.lazy) | |||
| ;//this.setState({ item: { uuid: this.props.uuid }}); | |||
| else | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| if (this.props.uuid === nextProps.uuid) | |||
| return; | |||
| if (nextProps.lazy) { | |||
| this.setState({ item: null }); | |||
| } else { | |||
| this.props = nextProps; | |||
| this.fetchData(); | |||
| } | |||
| } | |||
| render({ uuid, onLinkClicked, lazy }, { error, item }) { | |||
| if (!uuid) | |||
| return ( | |||
| <div><i>{ String(uuid) }</i></div> | |||
| ); | |||
| return ( | |||
| <div> | |||
| <div> | |||
| { error ? error : (item ? ( | |||
| <a href={ urlForObject(item) } onclick={ onLinkClicked }>{ arvadosObjectName(item) }</a> | |||
| ) : (lazy ? null : 'Loading...')) } | |||
| </div> | |||
| <div> | |||
| { uuid } { (lazy && !item) ? ( | |||
| <a href="#" title="Look up" onclick={ e => { e.preventDefault(); this.fetchData(); } }> | |||
| <i class="fas fa-search"></i> | |||
| </a> | |||
| ) : null } | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBNameAndUuid; | |||
| @@ -0,0 +1,38 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbar from 'wb-navbar'; | |||
| import WBInlineSearch from 'wb-inline-search'; | |||
| class WBNavbarCommon extends Component { | |||
| render({ app, items, activeItem, textSearch, textSearchNavigate }) { | |||
| return ( | |||
| <WBNavbar | |||
| items={ [ | |||
| { 'name': 'Home', 'id': 'home' }, | |||
| { 'name': 'All Projects', 'id': 'all-projects' }, | |||
| { 'name': 'All Users', 'id': 'all-users' }, | |||
| { 'name': 'Shared with Me', 'id': 'shared-with-me' }, | |||
| { 'name': 'Current User', 'dropdown': [ { 'id': 'sign-out', 'name': 'Sign Out' } ]}, | |||
| { name: (<span>What's New <sup><span class="badge badge-info">Info</span></sup></span>), id: 'whatsnew' } | |||
| ].concat(items) } | |||
| rhs={ textSearchNavigate ? ( | |||
| <WBInlineSearch textSearch={ textSearch } navigate={ textSearchNavigate } /> | |||
| ) : null } | |||
| titleUrl = { '/browse/' + app.state.currentUser.uuid } | |||
| getItemUrl={ item => app.navbarItemUrl(item) } | |||
| activeItem={ activeItem } /> | |||
| ); | |||
| } | |||
| } | |||
| WBNavbarCommon.defaultProps = { | |||
| 'items': [] | |||
| }; | |||
| export default WBNavbarCommon; | |||
| @@ -0,0 +1,60 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import { encodeURIComponentIncludingDots } from 'wb-process-misc'; | |||
| class WBPathDisplay extends Component { | |||
| fetchData() { | |||
| const { app } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let { path } = this.props; | |||
| if (path.endsWith('/')) | |||
| path = path.substr(0, path.length - 1); | |||
| let m; | |||
| if (m = /^[0-9a-f]{32}\+[0-9]+/.exec(path)); | |||
| else if (m = /^[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/.exec(path)); | |||
| else return; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/collections/' + m[0]); | |||
| prom = prom.then(xhr => this.setState({ | |||
| item: xhr.response, | |||
| tail: path.substr(m[0].length) | |||
| })); | |||
| prom = prom.catch(() => this.setState({ 'error': 'Cannot load' })); | |||
| } | |||
| componentDidMount() { | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchData(); | |||
| } | |||
| render({}, { item, tail, error }) { | |||
| if (error) | |||
| return error; | |||
| if (!item) | |||
| return 'Loading...'; | |||
| return ( | |||
| <span> | |||
| <a href={ '/collection-browse/' + item.uuid }> | |||
| { item.name || item.uuid } | |||
| </a><a href={ '/collection-browse/' + item.uuid + '/' + encodeURIComponentIncludingDots(tail) }> | |||
| { tail } | |||
| </a> | |||
| </span> | |||
| ); | |||
| } | |||
| } | |||
| export default WBPathDisplay; | |||
| @@ -0,0 +1,141 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import wbProcessStateName from 'wb-process-state-name'; | |||
| function getAll(makeRequest) { | |||
| let prom = makeRequest(0); | |||
| prom = prom.then(xhr => { | |||
| const { items, limit, items_available } = xhr.response; | |||
| let res = [].concat(items); | |||
| let prom_1 = new Promise(accept => accept()); | |||
| for (let ofs = limit; ofs < items_available; ofs += limit) { | |||
| prom_1 = prom_1.then(() => makeRequest(ofs)); | |||
| prom_1 = prom_1.then(xhr_1 => { | |||
| res = res.concat(xhr_1.response.items); | |||
| }); | |||
| } | |||
| prom_1 = prom_1.then(() => res); | |||
| return prom_1; | |||
| }); | |||
| return prom; | |||
| } | |||
| class WBProcessDashboard extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = Array(5).fill(Array(6).fill('-')); | |||
| } | |||
| fetchData() { | |||
| const { app, parentProcessUuid } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = new Promise(accept => accept()); | |||
| if (parentProcessUuid) { | |||
| prom = prom.then(() => { | |||
| return makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests/' + encodeURIComponent(parentProcessUuid)); | |||
| }); | |||
| prom = prom.then(xhr => { | |||
| const cr = xhr.response; | |||
| if (!cr.container_uuid) | |||
| return []; | |||
| const filters = [ [ 'requesting_container_uuid', '=', cr.container_uuid ] ]; | |||
| return getAll(ofs => | |||
| makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)) + | |||
| '&order=' + encodeURIComponent(JSON.stringify(['uuid asc'])) + | |||
| '&offset=' + ofs)); | |||
| }); | |||
| } else { | |||
| prom = prom.then(() => { | |||
| return getAll(ofs => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests?order=' + | |||
| encodeURIComponent(JSON.stringify(['uuid asc'])) + '&offset=' + ofs)); | |||
| }); | |||
| } | |||
| let crlist; | |||
| prom = prom.then(crl => { | |||
| crlist = crl; | |||
| const uuids = crlist.map(a => a.container_uuid); | |||
| // uuids = uuids.slice(0, 2); | |||
| // crlist.map(a => ( crdict[a.uuid] = a)); | |||
| const filters = [ [ 'uuid', 'in', uuids ] ]; | |||
| return getAll(ofs => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/containers?filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&order=' + encodeURIComponent(JSON.stringify([ 'uuid asc' ])) + | |||
| '&offset=' + ofs)); | |||
| }); | |||
| prom = prom.then(cl => { | |||
| cl.map(a => (crlist.find(b => (b.container_uuid === a.uuid)).container = a)); | |||
| crlist.map(a => (a.wb_state = wbProcessStateName(a, a.container))); | |||
| const stats = {}; | |||
| for (let state in { 'Pending': 1, 'Running': 1, 'Complete': 1, 'Failed': 1, 'Cancelled': 1 }) { | |||
| const f = crlist.filter(a => (a.wb_state === state)); | |||
| stats[state] = { 'Count': f.length }; | |||
| if (state === 'Pending') | |||
| f.map(a => (a.wb_wait_time = (new Date() - new Date(a.created_at)) / 3.6e6)); | |||
| else | |||
| f.map(a => (a.wb_wait_time = Math.max(0, (a.container ? new Date(a.container.started_at) : new Date(0)) - new Date(a.created_at)) / 3.6e6)); | |||
| f.sort((a, b) => (a.wb_wait_time - b.wb_wait_time)); | |||
| stats[state]['Shortest Wait Time'] = f.length ? (f[0].wb_wait_time.toFixed(2) + ' hours') : '-'; | |||
| stats[state]['Longest Wait Time'] = f.length ? (f[f.length - 1].wb_wait_time.toFixed(2) + ' hours') : '-'; | |||
| if (state === 'Pending') | |||
| f.map(a => (a.wb_run_time = 0)); | |||
| else if (state === 'Running') | |||
| f.map(a => (a.wb_run_time = (new Date() - new Date(a.container.started_at)) / 3.6e6)); | |||
| else | |||
| f.map(a => (a.wb_run_time = Math.max(0, a.container ? new Date(a.container.finished_at) - new Date(a.container.started_at) : 0) / 3.6e6)); | |||
| f.sort((a, b) => (a.wb_run_time - b.wb_run_time)); | |||
| stats[state]['Shortest Run Time'] = f.length ? (f[0].wb_run_time.toFixed(2) + ' hours') : '-'; | |||
| stats[state]['Longest Run Time'] = f.length ? (f[f.length - 1].wb_run_time.toFixed(2) + ' hours') : '-'; | |||
| } | |||
| const rows = []; | |||
| for (let st in { 'Count': 1, 'Shortest Wait Time': 1, 'Longest Wait Time': 1, | |||
| 'Shortest Run Time': 1, 'Longest Run Time': 1}) { | |||
| rows.push([st].concat(['Pending', 'Running', 'Complete', 'Failed', 'Cancelled'].map(a => stats[a][st]))); | |||
| } | |||
| this.setState({ rows }); | |||
| }); | |||
| } | |||
| componentDidMount() { | |||
| if (!this.props.lazy) | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| if (nextProps.parentProcessUuid === this.props.parentProcessUuid) | |||
| return; | |||
| if (this.props.lazy) { | |||
| this.setState({ rows: Array(5).fill(Array(6).fill('-')) }); | |||
| return; | |||
| } | |||
| this.props = nextProps; | |||
| this.fetchData(); | |||
| } | |||
| render({ lazy }, { rows }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ [ 'State', 'Pending', 'Running', 'Complete', 'Failed', 'Cancelled' ] } | |||
| rows={ rows } verticalHeader={ true } /> | |||
| { lazy ? ( | |||
| <a href="#" onclick={ e => { e.preventDefault(); this.fetchData(); } }>Refresh</a> | |||
| ) : null } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBProcessDashboard; | |||
| @@ -0,0 +1,155 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import { route } from 'preact-router'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import WBCheckboxes from 'wb-checkboxes'; | |||
| import wbFormatDate from 'wb-format-date'; | |||
| import wbFetchObjects from 'wb-fetch-objects'; | |||
| import WBNameAndUuid from 'wb-name-and-uuid'; | |||
| import WBProcessState from 'wb-process-state'; | |||
| function maskRows(rows) { | |||
| return rows.map(r => r.map(c => '-')); | |||
| } | |||
| class WBProcessListing extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.state.numPages = 0; | |||
| this.state.requestStates = [ 'Uncommitted', 'Committed', 'Final' ]; | |||
| this.state.reqStateMask = [ true, true, true ]; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchItems(); | |||
| } | |||
| cancelProcess(uuid) { | |||
| const { app } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests/' + encodeURIComponent(uuid), | |||
| { method: 'PUT', data: JSON.stringify({ priority: 0 }) }); | |||
| prom = prom.then(() => { | |||
| this.setState({ rows: maskRows(this.state.rows) }); | |||
| this.fetchItems(); | |||
| }); | |||
| } | |||
| prepareRows(requests, containerLookup, ownerLookup, outputLookup) { | |||
| const { app, renderRenameLink, renderDeleteButton, | |||
| renderSelectionCell, renderSharingButton, | |||
| renderEditDescription } = this.props; | |||
| return requests.map(item => { | |||
| return ( [ | |||
| renderSelectionCell(item), | |||
| (<div> | |||
| <div> | |||
| <a href={ '/process/' + item['uuid'] }> | |||
| { item['name'] } | |||
| </a> { renderRenameLink(item, () => this.fetchItems()) } | |||
| </div> | |||
| <div>{ item['uuid'] }</div> | |||
| <div class="mt-2"> | |||
| { item.description } { renderEditDescription(item, () => this.fetchItems()) } | |||
| </div> | |||
| </div>), | |||
| ( <WBProcessState app={ app } process={ item } lazy={ true } /> ), | |||
| ( <WBNameAndUuid app={ app } uuid={ item['owner_uuid'] } lazy={ true } /> ), | |||
| wbFormatDate(item['created_at']), | |||
| ( <WBNameAndUuid app={ app } uuid={ item['output_uuid'] } lazy={ true } /> ), | |||
| (<div> | |||
| <button class="btn btn-outline-warning m-1" onclick={ () => this.cancelProcess(item.uuid) }> | |||
| <i class="fas fa-stop-circle"></i> | |||
| </button> | |||
| { renderDeleteButton(item, () => this.fetchItems()) } | |||
| { renderSharingButton(item) } | |||
| </div>) | |||
| ] ); | |||
| }); | |||
| } | |||
| fetchItems() { | |||
| const { arvHost, arvToken } = this.props.appState; | |||
| const { requestStates, reqStateMask } = this.state; | |||
| const { activePage, itemsPerPage, ownerUuid, | |||
| requestingContainerUuid, waitForNextProps, | |||
| textSearch } = this.props; | |||
| if (waitForNextProps) | |||
| return; | |||
| const filters = [ | |||
| [ 'requesting_container_uuid', '=', requestingContainerUuid ] | |||
| ]; | |||
| if (!reqStateMask.reduce((a, b) => a & b)) | |||
| filters.push([ 'state', 'in', requestStates.filter((_, idx) => reqStateMask[idx]) ]); | |||
| if (ownerUuid) | |||
| filters.push([ 'owner_uuid', '=', ownerUuid ]); | |||
| if (textSearch) | |||
| filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]) | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests?filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (itemsPerPage * activePage)); | |||
| prom = prom.then(xhr => | |||
| this.setState({ | |||
| 'numPages': Math.ceil(xhr.response['items_available'] / xhr.response['limit']), | |||
| 'rows': this.prepareRows(xhr.response.items) | |||
| })); | |||
| } | |||
| componentWillReceiveProps(nextProps, nextState) { | |||
| this.props = nextProps; | |||
| this.setState({ 'rows': maskRows(this.state.rows) }); | |||
| this.fetchItems(); | |||
| } | |||
| render({ appState, ownerUuid, activePage, onPageChanged, getPageUrl }, | |||
| { rows, numPages, requestStates, containerStates, | |||
| reqStateMask, contStateMask }) { | |||
| return ( | |||
| <div> | |||
| <WBCheckboxes items={ requestStates } checked={ reqStateMask } | |||
| cssClass="float-left mx-2 my-2" title="Request State: " | |||
| onChange={ () => this.fetchItems() } /> | |||
| <WBTable columns={ [ '', 'Name', 'Status', 'Owner', 'Created At', 'Output', 'Actions' ] } | |||
| headerClasses={ [ 'w-1' ] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } | |||
| activePage={ activePage } | |||
| getPageUrl={ getPageUrl } | |||
| onPageChanged={ onPageChanged } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBProcessListing.defaultProps = { | |||
| itemsPerPage: 100, | |||
| ownerUuid: null, | |||
| requestingContainerUuid: null, | |||
| renderRenameLink: () => null, | |||
| renderDeleteButton: () => null, | |||
| renderSelectionCell: () => null, | |||
| renderSharingButton: () => null, | |||
| renderEditDescription: () => null | |||
| }; | |||
| export default WBProcessListing; | |||
| @@ -0,0 +1,64 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import wbProcessStateName from 'wb-process-state-name'; | |||
| class WBProcessState extends Component { | |||
| componentDidMount() { | |||
| if (!this.props.lazy) | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| if (this.props.lazy) { | |||
| this.setState({ container: null, apiError: null }); | |||
| } else { | |||
| this.props = nextProps; | |||
| this.fetchData(); | |||
| } | |||
| } | |||
| fetchData() { | |||
| const { app, process } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| if (!process.container_uuid) | |||
| return; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/containers/' + process.container_uuid); | |||
| prom = prom.then(xhr => this.setState({ 'container': xhr.response })); | |||
| prom = prom.catch(() => this.setState({ 'apiError': 'Failed to fetch container' })); | |||
| } | |||
| render({ process, lazy }, { container, apiError }) { | |||
| const runtimeStatus = container ? container.runtime_status : null; | |||
| const error = runtimeStatus ? runtimeStatus.error : null; | |||
| const warning = runtimeStatus ? runtimeStatus.warning : null; | |||
| return ( | |||
| <div> | |||
| { wbProcessStateName(process, container) } | |||
| { apiError ? <i>{ [ ' / ', apiError ] }</i> : null } | |||
| { error ? [" / ", <a href={ '/container/' + container.uuid } | |||
| title={ error }>E</a> ] : null } | |||
| { warning ? [ " / ", <a href={ '/container/' + container.uuid } | |||
| title={ warning }>W</a> ] : null } {} | |||
| { lazy && !container && !apiError ? ( | |||
| <a href="#" title="Look up" onclick={ e => { e.preventDefault(); this.fetchData(); } }> | |||
| <i class="fas fa-search"></i> | |||
| </a> | |||
| ) : null } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBProcessState; | |||
| @@ -0,0 +1,77 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBAccordion from 'wb-accordion'; | |||
| import WBJsonViewer from 'wb-json-viewer'; | |||
| import wbFormatSpecialValue from 'wb-format-special-value'; | |||
| import WBLazyInlineName from 'wb-lazy-inline-name'; | |||
| import wbFormatDate from 'wb-format-date'; | |||
| import wbUpdateField from 'wb-update-field'; | |||
| import WBJsonEditor from 'wb-json-editor'; | |||
| class WBProjectFields extends Component { | |||
| componentDidMount() { | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchData(); | |||
| } | |||
| prepareRows(item) { | |||
| const { app } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| const rows = [ | |||
| [ 'Name', wbFormatSpecialValue(item.name) ], | |||
| [ 'Description', wbFormatSpecialValue(item.description) ], | |||
| [ 'Properties', ( | |||
| <WBJsonEditor name="Properties" app={ app } value={ item.properties } | |||
| onChange={ value => wbUpdateField(arvHost, arvToken, item.uuid, 'properties', value) | |||
| .then(() => { item.properties = value; this.prepareRows(item); }) } /> | |||
| ) ], | |||
| [ 'Writable by', item.writable_by | |||
| .map(a => (<WBLazyInlineName app={ app } identifier={ a } />)) | |||
| .reduce((a, b) => [].concat(a).concat(', ').concat(b)) | |||
| ], | |||
| [ 'Trash At', wbFormatDate(item.trash_at) ], | |||
| [ 'Delete At', wbFormatDate(item.delete_at) ], | |||
| [ 'Is Trashed', wbFormatSpecialValue(item.is_trashed) ] | |||
| ]; | |||
| this.setState({ rows }); | |||
| } | |||
| fetchData() { | |||
| let { uuid, app } = this.props; | |||
| let { arvHost, arvToken } = app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups/' + uuid); | |||
| prom = prom.then(xhr => this.prepareRows(xhr.response)); | |||
| } | |||
| render({}, { rows }) { | |||
| return ( | |||
| rows ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| rows={ rows } | |||
| verticalHeader={ true } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBProjectFields; | |||
| @@ -0,0 +1,110 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import { route } from 'preact-router'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import WBNameAndUuid from 'wb-name-and-uuid'; | |||
| import urlForObject from 'url-for-object'; | |||
| class WBProjectListing extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.state.numPages = 0; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchItems(); | |||
| } | |||
| prepareRows(items) { | |||
| const { app, renderRenameLink, renderDeleteButton, | |||
| renderSelectionCell, renderSharingButton, | |||
| renderEditDescription } = this.props; | |||
| return items.map(item => [ | |||
| renderSelectionCell(item), | |||
| (<div> | |||
| <div> | |||
| <a href={ '/browse/' + item.uuid }> | |||
| { item['name'] } | |||
| </a> { renderRenameLink(item, () => this.fetchItems()) } | |||
| </div> | |||
| <div>{ item['uuid'] }</div> | |||
| </div>), | |||
| (<div> | |||
| { item['description'] } { renderEditDescription(item, () => this.fetchItems()) } | |||
| </div>), | |||
| ( <WBNameAndUuid app={ app } uuid={ item['owner_uuid'] } lazy={ true } /> ), | |||
| (<div> | |||
| <a class="btn btn-outline-primary m-1" title="Properties" | |||
| href={ urlForObject(item, 'properties') }> | |||
| <i class="fas fa-list-ul"></i> | |||
| </a> | |||
| { renderDeleteButton(item, () => this.fetchItems()) } | |||
| { renderSharingButton(item) } | |||
| </div>) | |||
| ]); | |||
| } | |||
| fetchItems() { | |||
| let { activePage, mode, itemsPerPage, ownerUuid, app, textSearch } = this.props; | |||
| let { arvHost, arvToken } = app.state; | |||
| let filters = [ | |||
| [ 'group_class', '=', 'project' ] | |||
| ]; | |||
| if (ownerUuid) | |||
| filters.push([ 'owner_uuid', '=', ownerUuid ]); | |||
| if (textSearch) | |||
| filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]); | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups' + (mode === 'shared-with-me' ? '/shared' : '') + | |||
| '?filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (itemsPerPage * activePage)); | |||
| prom = prom.then(xhr => | |||
| this.setState({ | |||
| 'numPages': Math.ceil(xhr.response['items_available'] / xhr.response['limit']), | |||
| 'rows': this.prepareRows(xhr.response['items']) | |||
| })); | |||
| } | |||
| componentWillReceiveProps(nextProps, nextState) { | |||
| // this.setState({ 'rows': [] }); // .rows = []; | |||
| this.props = nextProps; | |||
| this.fetchItems(); | |||
| } | |||
| render({ arvHost, arvToken, ownerUuid, activePage, getPageUrl }, { rows, numPages }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ [ '', 'Name', 'Description', 'Owner', 'Actions' ] } | |||
| headerClasses={ [ 'w-1' ] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } | |||
| activePage={ activePage } | |||
| getPageUrl={ getPageUrl } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBProjectListing.defaultProps = { | |||
| 'itemsPerPage': 100, | |||
| 'ownerUuid': null, | |||
| 'renderRenameLink': () => null, | |||
| 'renderDeleteButton': () => null, | |||
| 'renderSelectionCell': () => null, | |||
| 'renderEditDescription': () => null | |||
| }; | |||
| export default WBProjectListing; | |||
| @@ -0,0 +1,75 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import urlForObject from 'url-for-object'; | |||
| class WBUserListing extends Component { | |||
| componentDidMount() { | |||
| this.preparePage(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.preparePage(); | |||
| } | |||
| preparePage() { | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { itemsPerPage, page, textSearch } = this.props; | |||
| const order = ['last_name asc']; | |||
| const filters = []; | |||
| if (textSearch) | |||
| filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]); | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/users?order=' + encodeURIComponent(JSON.stringify(order)) + | |||
| '&filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + '&offset=' + (itemsPerPage * page)); | |||
| prom = prom.then(xhr => { | |||
| this.setState({ | |||
| 'items': xhr.response['items'], | |||
| 'numPages': Math.ceil(xhr.response['items_available'] / itemsPerPage) | |||
| }); | |||
| }); | |||
| } | |||
| render({ app, page, getPageUrl }, { items, numPages }) { | |||
| return ( | |||
| <div class="container-fluid"> | |||
| <h1>Users</h1> | |||
| <div class="d-flex flex-wrap"> | |||
| { items ? items.map(it => ( | |||
| <div class="card mx-2 my-2"> | |||
| <h5 class="card-header"> | |||
| <a href={ urlForObject(it) }>{ it.last_name + ', ' + it.first_name }</a> | |||
| </h5> | |||
| <div class="card-body"> | |||
| <div><a href={ 'mailto:' + it.email }>{ it.email }</a></div> | |||
| <div>{ it.uuid }</div> | |||
| </div> | |||
| </div> | |||
| )) : 'Loading...' } | |||
| </div> | |||
| <WBPagination activePage={ page } numPages={ numPages } | |||
| getPageUrl={ getPageUrl } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBUserListing.defaultProps = { | |||
| 'itemsPerPage': 20, | |||
| 'page': 0 | |||
| }; | |||
| export default WBUserListing; | |||
| @@ -0,0 +1,112 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBAccordion from 'wb-accordion'; | |||
| import WBJsonViewer from 'wb-json-viewer'; | |||
| class WBWorkflowFields extends Component { | |||
| componentDidMount() { | |||
| this.prepareRows(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.prepareRows(); | |||
| } | |||
| prepareRows() { | |||
| let { uuid, app } = this.props; | |||
| let { arvHost, arvToken } = app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/workflows/' + uuid); | |||
| prom = prom.then(xhr => { | |||
| const item = xhr.response; | |||
| let definition; | |||
| try { | |||
| definition = JSON.parse(item.definition); | |||
| } catch (_) { | |||
| definition = jsyaml.load(item.definition); | |||
| } | |||
| const graph = definition['$graph']; | |||
| let rows = [ | |||
| [ 'Name', item.name ], | |||
| [ 'Description', item.description || (<i>{ String(item.description) }</i>) ], | |||
| [ 'CWL Version', definition.cwlVersion ], | |||
| ]; | |||
| let keys = graph.map(it => it.id); | |||
| keys.sort(); | |||
| keys = keys.splice(keys.indexOf('#main'), 1).concat(keys); | |||
| keys.map(k => { | |||
| const it = graph.find(it => (it.id === k)); | |||
| rows.push([ | |||
| it.id, ( | |||
| <div> | |||
| <div>Class: { it['class'] }</div> | |||
| { it.label ? <div>Label: { it.label }</div> : null } | |||
| { it.doc ? <div>Doc: { it.doc }</div> : null } | |||
| <WBAccordion names={ [ 'Inputs', 'Outputs', 'Rest' ] } | |||
| cardHeaderClass="card-header-sm"> | |||
| <WBJsonViewer app={ app } value={ it.inputs } /> | |||
| <WBJsonViewer app={ app } value={ it.outputs } /> | |||
| { (() => { | |||
| delete it['inputs']; | |||
| delete it['outputs']; | |||
| delete it['class']; | |||
| delete it['label']; | |||
| delete it['doc']; | |||
| delete it['id']; | |||
| return ( | |||
| <WBJsonViewer app={ app } value={ it } /> | |||
| ); | |||
| })() } | |||
| </WBAccordion> | |||
| </div> | |||
| )]); | |||
| }); | |||
| /* [ 'Graph', ( | |||
| <WBAccordion names={ graph.map(it => it.id) } | |||
| cardHeaderClass="card-header-sm"> | |||
| { graph.map(it => ( | |||
| <WBJsonViewer app={ app } value={ it } /> | |||
| )) } | |||
| </WBAccordion> | |||
| ) ] | |||
| ];*/ | |||
| this.setState({ 'rows': rows }); | |||
| }); | |||
| } | |||
| render({}, { rows }) { | |||
| return ( | |||
| rows ? ( | |||
| <WBTable columns={ [ "Name", "Value" ] } | |||
| headerClasses={ [ "col-sm-2", "col-sm-4" ] } | |||
| rows={ rows } | |||
| verticalHeader={ true } /> | |||
| ) : ( | |||
| <div>Loading...</div> | |||
| ) | |||
| ); | |||
| } | |||
| } | |||
| export default WBWorkflowFields; | |||
| @@ -0,0 +1,78 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import wbInputSpecInfo from 'wb-input-spec-info'; | |||
| import WBPathDisplay from 'wb-path-display'; | |||
| import { parseKeepRef } from 'wb-process-misc'; | |||
| class WBWorkflowInput extends Component { | |||
| render({ app, inputSpec, inputsDict, browseDialogRef }) { | |||
| const { isFile, isDirectory, isArray } = wbInputSpecInfo(inputSpec); | |||
| if (!isFile && !isDirectory) | |||
| return ( | |||
| <div> | |||
| <input class="form-control w-100" type="text" placeholder={ inputSpec.label } | |||
| value={ inputsDict[inputSpec.id] } | |||
| onchange={ e => (inputsDict[inputSpec.id] = e.target.value) }></input> | |||
| <div class="mt-2 text-muted">{ inputSpec.doc }</div> | |||
| </div> | |||
| ); | |||
| const button = ( | |||
| <button class="btn btn-outline-primary" | |||
| onclick={ e => { | |||
| e.preventDefault(); | |||
| browseDialogRef.current.show( | |||
| [].concat(isFile ? 'file' : []).concat(isDirectory ? 'directory' : []), | |||
| isArray, | |||
| v => { | |||
| inputsDict[inputSpec.id] = JSON.stringify(v); | |||
| this.setState({}); | |||
| }); | |||
| } }> | |||
| Browse... | |||
| </button> | |||
| ); | |||
| let value = inputsDict[inputSpec.id]; | |||
| if (value) { | |||
| try { | |||
| value = jsyaml.load(value); | |||
| } catch (_) {} | |||
| } | |||
| return ( | |||
| <div> | |||
| <div class="input-group"> | |||
| <input class="form-control w-100" type="text" placeholder={ inputSpec.label } | |||
| value={ inputsDict[inputSpec.id] } | |||
| onchange={ e => (inputsDict[inputSpec.id] = e.target.value) }></input> | |||
| <div class="input-group-append"> | |||
| { button } | |||
| </div> | |||
| </div> | |||
| <div class="mt-2 text-muted">{ inputSpec.doc }</div> | |||
| { value ? | |||
| isArray ? ( | |||
| <ul class="mb-0"> | |||
| { value.map(path => ( | |||
| <li> | |||
| <WBPathDisplay app={ app } path={ parseKeepRef(path) } /> | |||
| </li> | |||
| )) } | |||
| </ul> | |||
| ) : ( | |||
| <WBPathDisplay app={ app } path={ parseKeepRef(value) } /> | |||
| ) : null } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBWorkflowInput; | |||
| @@ -0,0 +1,116 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import WBNameAndUuid from 'wb-name-and-uuid'; | |||
| import wbFetchObjects from 'wb-fetch-objects'; | |||
| import wbFormatDate from 'wb-format-date'; | |||
| import urlForObject from 'url-for-object'; | |||
| import arvadosObjectName from 'arvados-object-name'; | |||
| class WBWorkflowListing extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.state.numPages = 0; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchItems(); | |||
| } | |||
| prepareRows(items, ownerLookup) { | |||
| const { renderRenameLink, renderDeleteButton, | |||
| renderSelectionCell, renderSharingButton, | |||
| renderEditDescription } = this.props; | |||
| return items.map(item => [ | |||
| renderSelectionCell(item), | |||
| ( | |||
| <div> | |||
| <div> | |||
| <a href={ urlForObject(item) }> | |||
| { arvadosObjectName(item) } | |||
| </a> { renderRenameLink(item, () => this.fetchItems()) } | |||
| </div> | |||
| <div>{ item.uuid }</div> | |||
| </div> | |||
| ), | |||
| (<div> | |||
| { item.description } { renderEditDescription(item, () => this.fetchItems()) } | |||
| </div>), | |||
| ( <WBNameAndUuid uuid={ item.owner_uuid } lookup={ ownerLookup } /> ), | |||
| wbFormatDate(item.created_at), | |||
| (<div> | |||
| <a class="btn btn-outline-success mx-1 my-1" title="Launch" | |||
| href={ urlForObject(item, 'launch') }><i class="fas fa-running"></i></a> | |||
| <button class="btn btn-outline-primary mx-1 my-1" title="View"><i class="far fa-eye"></i></button> | |||
| { renderDeleteButton(item, () => this.fetchItems()) } | |||
| { renderSharingButton(item) } | |||
| </div>) | |||
| ]); | |||
| } | |||
| fetchItems() { | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { page, itemsPerPage, ownerUuid, textSearch } = this.props; | |||
| const filters = []; | |||
| if (ownerUuid) | |||
| filters.push([ 'owner_uuid', '=', ownerUuid ]); | |||
| if (textSearch) | |||
| filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]); | |||
| const select = ['uuid', 'name', 'description', 'owner_uuid', 'created_at']; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/workflows?filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&select=' + encodeURIComponent(JSON.stringify(select)) + | |||
| '&limit=' + encodeURIComponent(itemsPerPage) + | |||
| '&offset=' + encodeURIComponent(itemsPerPage * page)); | |||
| let workflowResp; | |||
| prom = prom.then(xhr => (workflowResp = xhr.response)); | |||
| prom = prom.then(() => wbFetchObjects(arvHost, arvToken, | |||
| workflowResp.items.map(it => it.owner_uuid))); | |||
| let ownerLookup; | |||
| prom = prom.then(lookup => (ownerLookup = lookup)); | |||
| prom = prom.then(() => | |||
| this.setState({ | |||
| 'numPages': Math.ceil(workflowResp['items_available'] / workflowResp['limit']), | |||
| 'rows': this.prepareRows(workflowResp.items, ownerLookup) | |||
| })); | |||
| } | |||
| componentWillReceiveProps(nextProps, nextState) { | |||
| this.props = nextProps; | |||
| this.fetchItems(); | |||
| } | |||
| render({ app, ownerUuid, page, getPageUrl }, { rows, numPages }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ [ '', 'Name', 'Description', 'Owner', 'Created At', 'Actions' ] } | |||
| headerClasses={ [ 'w-1' ] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } | |||
| activePage={ page } | |||
| getPageUrl={ getPageUrl } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBWorkflowListing.defaultProps = { | |||
| 'itemsPerPage': 100, | |||
| 'ownerUuid': null, | |||
| 'renderSharingButton': () => null, | |||
| 'renderEditDescription': () => null | |||
| }; | |||
| export default WBWorkflowListing; | |||
| @@ -0,0 +1,50 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| function fetchProjectParents(arvHost, arvToken, uuid) { | |||
| let parents = []; | |||
| let cb = xhr => { | |||
| let objectType = xhr.response['uuid'].split('-')[1]; | |||
| if (objectType === 'tpzed') { | |||
| let name = xhr.response['first_name'] + ' ' + xhr.response['last_name']; | |||
| parents.push({ 'name': name, 'uuid': xhr.response['uuid'] }); | |||
| } else { | |||
| parents.push({ 'name': xhr.response['name'], | |||
| 'uuid': xhr.response['uuid'] }); | |||
| } | |||
| if (!xhr.response['owner_uuid'] || | |||
| xhr.response['owner_uuid'].endsWith('-tpzed-000000000000000')) { | |||
| return parents.reverse(); | |||
| } | |||
| objectType = xhr.response['owner_uuid'].split('-')[1]; | |||
| if (objectType === 'tpzed') { | |||
| return makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/users/' + xhr.response['owner_uuid']).then(cb); | |||
| } else { | |||
| return makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups/' + xhr.response['owner_uuid']).then(cb); | |||
| } | |||
| }; | |||
| let objectType = uuid.split('-')[1]; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/' + (objectType === 'tpzed' ? 'users' : 'groups') + '/' + uuid); | |||
| prom = prom.then(cb); | |||
| return prom; | |||
| } | |||
| export default fetchProjectParents; | |||
| @@ -0,0 +1,57 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBArvadosCollection { | |||
| constructor(arvHost, arvToken, uuid) { | |||
| this.arvHost = arvHost; | |||
| this.arvToken = arvToken; | |||
| this.uuid = uuid; | |||
| this.meta = null; | |||
| } | |||
| fetchMeta() { | |||
| let prom = makeArvadosRequest(this.arvHost, this.arvToken, | |||
| '/arvados/v1/collections/' + this.uuid); | |||
| prom = prom.then(xhr => { | |||
| this.meta = xhr.response; | |||
| }); | |||
| return prom; | |||
| } | |||
| parseManifest() { | |||
| if (this.meta === null) | |||
| throw Error('You must call fetchMeta() first and wait for the returned Promise.'); | |||
| let manifest = this.meta.manifest_text; | |||
| let streams = manifest.split('\n'); | |||
| this.content = streams.map(s => { | |||
| let tokens = s.split(' '); | |||
| let streamName = tokens[0]; | |||
| let rx = /^[a-f0-9]{32}\+[0-9]+/; | |||
| let n = tokens.map(t => rx.exec(t)); | |||
| n = n.indexOf(null); | |||
| let locators = tokens.slice(1, n) | |||
| let fileTokens = tokens.slice(n); | |||
| let fileNames = fileTokens.map(t => t.split(':')[2]); | |||
| let fileSizes = {}; | |||
| fileTokens.map(t => { | |||
| let [ start, end, name ] = t.split(':'); | |||
| if (!(name in fileSizes)) | |||
| fileSizes[name] = 0; | |||
| fileSizes[name] += Number(end) - Number(start); | |||
| }); | |||
| fileSizes = fileNames.map(n => fileSizes[n]); | |||
| return [ streamName, fileNames, fileSizes ]; | |||
| }); | |||
| return this.content; | |||
| } | |||
| } | |||
| export WBArvadosCollection; | |||
| @@ -0,0 +1,42 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| class WBBrowseDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| } | |||
| render({ id }) { | |||
| return ( | |||
| <div class="modal" id={ id } tabindex="-1" role="dialog"> | |||
| <div class="modal-dialog modal-lg" role="document"> | |||
| <div class="modal-content"> | |||
| <div class="modal-header"> | |||
| <h5 class="modal-title">Browse</h5> | |||
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |||
| <span aria-hidden="true">×</span> | |||
| </button> | |||
| </div> | |||
| <div class="modal-body m-0 p-0"> | |||
| <iframe style="width: 100%;" src="/browse" /> | |||
| </div> | |||
| <div class="modal-footer"> | |||
| <button type="button" class="btn btn-primary">Accept</button> | |||
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBBrowseDialog; | |||
| @@ -0,0 +1,187 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| // | |||
| // Directory: Hash[string, [Directory, File]] | |||
| // File = [blockRefs, size] | |||
| // blockRefs: Array[blockRef] | |||
| // blockRef: [locator, position, size] | |||
| // locator: String | |||
| // position: Number | |||
| // size: Number | |||
| // | |||
| class WBManifestReader { | |||
| constructor(manifest_text) { | |||
| this.rootDir = {}; | |||
| if (!manifest_text) | |||
| return; | |||
| this.parse(manifest_text); | |||
| } | |||
| makeDir(parent, name) { | |||
| if (!(name in parent)) | |||
| parent[name] = {}; | |||
| if (parent[name] instanceof Array) | |||
| throw Error('Conflict trying to create a directory - a file with the same name already exists: ' + name); | |||
| return parent[name]; | |||
| } | |||
| makePath(path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| let dir = this.rootDir; | |||
| for (let i = 1; i < path.length; i++) | |||
| dir = this.makeDir(dir, path[i]); | |||
| return dir; | |||
| } | |||
| appendFile(streamName, locators, position, size, fileName) { | |||
| let path = streamName + '/' + fileName; | |||
| path = path.split('/'); | |||
| let dir = this.makePath(path.slice(0, path.length - 1)); | |||
| if (!(fileName in dir)) | |||
| dir[fileName] = [[], 0]; | |||
| if (!(dir[fileName] instanceof Array)) | |||
| throw Error('Conflict trying to create a file - a directory with the same name already exists: ' + fileName); | |||
| //this.appendReferences(dir[fileName], locators, position, size); | |||
| } | |||
| appendReferences(file, locators, position, size) { | |||
| if (size === 0) | |||
| return; | |||
| let cum = 0; | |||
| let locHashes = locators.map(loc => loc[0]); | |||
| let locSizes = locators.map(loc => loc[1]); | |||
| let locPositions = locators.map(loc => { | |||
| let res = cum; | |||
| cum += loc[1]; | |||
| return res; | |||
| }); | |||
| let used = locators.map((_, i) => (locPositions[i] + locSizes[i] > position && | |||
| locPositions[i] < position + size)); | |||
| let startBlock = used.indexOf(true); | |||
| let endBlock = used.lastIndexOf(true) + 1; | |||
| // console.log('startBlock: ' + startBlock + ', endBlock: ' + endBlock); | |||
| if (startBlock === -1) | |||
| return; | |||
| let blockRefs = []; | |||
| let runPos = position; | |||
| let runSize = size; | |||
| for (let i = startBlock; i < endBlock; i++) { | |||
| let blockPos = runPos - locPositions[i]; | |||
| let blockSize = Math.min(runSize, locSizes[i] - blockPos); | |||
| blockRefs.push([ locHashes[i], blockPos, blockSize ]); | |||
| runPos += blockSize; | |||
| runSize -= blockSize; | |||
| } | |||
| file[0] = file[0].concat(blockRefs); | |||
| file[1] += size; | |||
| } | |||
| parse(manifest_text) { | |||
| let rx = /^[a-f0-9]{32}\+[0-9]+/; | |||
| let streams = manifest_text.split('\n'); | |||
| if (!streams[streams.length - 1]) | |||
| streams = streams.slice(0, streams.length - 1); | |||
| streams.map(s => { | |||
| let tokens = s.split(' '); | |||
| let streamName = this.unescapeName(tokens[0]); | |||
| let n = tokens.map(t => rx.exec(t)); | |||
| n = n.indexOf(null, 1); | |||
| let locators = tokens.slice(1, n); | |||
| locators = locators.map(loc => [ loc, Number(loc.split('+')[1]) ]); | |||
| let fileTokens = tokens.slice(n); | |||
| fileTokens.map(t => { | |||
| let [ position, size, ...fileName ] = t.split(':'); | |||
| fileName = fileName.join(':'); | |||
| fileName = this.unescapeName(fileName); | |||
| this.appendFile(streamName, locators, | |||
| Number(position), Number(size), fileName); | |||
| }); | |||
| }); | |||
| } | |||
| findDir(path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| if (path[0] !== '.') | |||
| throw Error('Path must begin with a dot component'); | |||
| let dir = this.rootDir; | |||
| for (let i = 1; i < path.length; i++) { | |||
| if (!(path[i] in dir)) | |||
| throw Error('Directory not found'); | |||
| if (dir[path[i]] instanceof Array) | |||
| throw Error('Path is a file not directory'); | |||
| dir = dir[path[i]]; | |||
| } | |||
| return dir; | |||
| } | |||
| listDirectory(path) { | |||
| let dir = this.findDir(path); | |||
| let keys = Object.keys(dir); | |||
| keys.sort(); | |||
| let subdirs = keys.filter(k => !(dir[k] instanceof Array)); | |||
| let files = keys.filter(k => (dir[k] instanceof Array)); | |||
| let res = subdirs.map(k => [ 'd', k, null ]); | |||
| res = res.concat(files.map(k => [ 'f', k, dir[k][1] ])); | |||
| return res; | |||
| } | |||
| unescapeName(name) { | |||
| return name.replace(/(\\\\|\\[0-9]{3})/g, (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8)))); | |||
| } | |||
| escapeName(name) { | |||
| return name.replace(/ /g, '\\040'); | |||
| } | |||
| /* let ids = { '\\': 1, '0': 2, '4': 3 }; | |||
| let transitions = [ | |||
| [ [0, 0], [1, ''], [0, 0], [0, 0] ], | |||
| [ [0, 0], [0, '\\'], [2, ''], [0, 0] ], | |||
| ]; | |||
| let mode = 0; | |||
| for (let i = 0; i < name.length; i++) { | |||
| let b = name[i]; | |||
| let tokenId = Number(ids[b]); | |||
| [ mode, out ] = transitions[mode][tokenId]; | |||
| if (out === 0) | |||
| out = b; | |||
| } | |||
| }*/ | |||
| getFile(path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| if (path.length < 2) | |||
| throw Error('Invalid file path'); | |||
| let name = path[path.length - 1]; | |||
| let dir = this.findDir(path.slice(0, path.length - 1)); | |||
| if (!(name in dir)) | |||
| throw Error('File not found'); | |||
| if (!(dir[name] instanceof Array)) | |||
| throw Error('Path points to a directory not a file'); | |||
| return dir[name]; | |||
| } | |||
| } | |||
| export { WBManifestReader }; | |||
| @@ -0,0 +1,72 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| function rdvHash(serviceId, locator) { | |||
| let blockHash = /^[0-9a-f]{32}/.exec(locator); | |||
| if (!blockHash) | |||
| throw Error('Invalid locator'); | |||
| if (typeof(serviceId) !== 'string') | |||
| throw Error('Invalid service ID'); | |||
| let res = CryptoJS.MD5(serviceId + blockHash).toString(); | |||
| return res; | |||
| } | |||
| function wbDownloadFile(arvHost, arvToken, | |||
| manifestReader, path) { | |||
| const file = manifestReader.getFile(path); | |||
| const name = path.split('/').reverse()[0]; | |||
| const blockRefs = file[0]; | |||
| let services; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/keep_services/accessible'); | |||
| prom = prom.then(xhr => (services = xhr.response['items'])); | |||
| const blocks = []; | |||
| for (let i = 0; i < blockRefs.length; i++) { | |||
| prom = prom.then(() => { | |||
| const [ locator, position, size ] = blockRefs[i]; | |||
| const weights = services.map(s => rdvHash(s['uuid'], locator)); | |||
| const order = Object.keys(services).sort((a, b) => weights[b].localeCompare(weights[a])); | |||
| const orderedServices = order.map(i => services[i]); | |||
| let k = 0; | |||
| const cb = () => { | |||
| if (k >= orderedServices.length) | |||
| throw Error('Block not found'); | |||
| const svc = orderedServices[k]; | |||
| k++; | |||
| let prom_1 = makeArvadosRequest(svc.service_host + | |||
| ':' + svc.service_port, arvToken, | |||
| '/' + locator, { 'useSsl': svc.service_ssl_flag, | |||
| 'responseType': 'arraybuffer' }); | |||
| //prom_1 = prom_1.then(xhr => xhr.response); | |||
| prom_1 = prom_1.catch(cb); | |||
| return prom_1; | |||
| }; | |||
| return cb().then(xhr => (blocks.append(xhr.response.slice(position, size)))); | |||
| }); | |||
| } | |||
| prom = prom.then(() => { | |||
| const blob = new Blob(blocks); | |||
| const url = window.URL.createObjectURL(blob); | |||
| const a = document.createElement('a'); | |||
| a.href = url; | |||
| a.download = name; | |||
| }); | |||
| } | |||
| export default wbDownloadFile; | |||
| @@ -0,0 +1,188 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| function mkdir(parent, name) { | |||
| if (name in parent && (parent[name] instanceof Array)) | |||
| throw Error('File with the same name already exists'); | |||
| if (name in parent) | |||
| return parent[name]; | |||
| const dir = {}; | |||
| parent[name] = dir; | |||
| return dir; | |||
| } | |||
| function mkpath(parent, path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| let dir = parent; | |||
| for (let i = 1; i < path.length; i++) { | |||
| dir = mkdir(dir, path[i]); | |||
| } | |||
| return dir; | |||
| } | |||
| function makeFile(dir, name) { | |||
| if (name in dir) { | |||
| if (!(dir[name] instanceof Array)) | |||
| throw Error('Directory with the same name already exists'); | |||
| return dir[name]; | |||
| } | |||
| const f = [[], 0]; | |||
| dir[name] = f; | |||
| return f; | |||
| } | |||
| function appendFile(f, sidx, seg) { | |||
| f[0].push([ sidx, seg[0], seg[1] ]); | |||
| //f[1] += seg[1]; | |||
| return f; | |||
| } | |||
| function unescapeName(name) { | |||
| return name.replace(/(\\\\|\\[0-9]{3})/g, | |||
| (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8)))); | |||
| } | |||
| function process(streams) { | |||
| const rootDir = {}; | |||
| streams.map((s, sidx) => { | |||
| const [ streamName, locators, segments ] = s; | |||
| const streamDir = mkpath(rootDir, streamName); | |||
| segments.map((seg, segidx) => { | |||
| let name = seg[2].split('/'); | |||
| const dir = (name.length === 1 ? streamDir : | |||
| mkpath(streamDir, ['.'].concat(name.slice(0, name.length - 1)))); | |||
| name = name[name.length - 1]; | |||
| appendFile(dir, name, sidx, seg); | |||
| }); | |||
| }); | |||
| return rootDir; | |||
| } | |||
| function parse(manifestText) { | |||
| const M_STREAM_NAME = 0; | |||
| const M_LOCATORS = 1; | |||
| const M_FILE_SEGMENTS = 2; | |||
| let mode = M_STREAM_NAME; | |||
| const streams = []; | |||
| let locators = []; | |||
| let streamName; | |||
| let accum = ''; | |||
| let tokenStart = 0; | |||
| let lastFile = null; | |||
| let lastPath = null; | |||
| const rootDir = {}; | |||
| for (let i = 0; i < manifestText.length; i++) { | |||
| const c = manifestText[i]; | |||
| if (mode === M_STREAM_NAME) { | |||
| if (c === ' ') { | |||
| mode = M_LOCATORS; | |||
| streamName = unescapeName(accum); | |||
| accum = ''; | |||
| tokenStart = i + 1; | |||
| } else { | |||
| accum += c; | |||
| } | |||
| } else if (mode === M_LOCATORS) { | |||
| if (c === ':') { | |||
| mode = M_FILE_SEGMENTS; | |||
| accum = ''; | |||
| i = tokenStart - 1; | |||
| let pos = 0; | |||
| locators = locators.map(loc => { | |||
| const r = loc.concat([ pos, pos + loc[1] ]); | |||
| pos += loc[1]; | |||
| return r; | |||
| }); | |||
| } else if (c === ' ') { | |||
| const sz = Number(accum.split('+')[1]); | |||
| locators.push([accum, sz]); | |||
| accum = ''; | |||
| tokenStart = i + 1; | |||
| } else { | |||
| accum += c; | |||
| } | |||
| } else if (mode === M_FILE_SEGMENTS) { | |||
| if (c === ' ' || c === '\n') { | |||
| let seg = accum.split(':'); | |||
| seg = [Number(seg[0]), Number(seg[1]), seg.slice(2).join(':')]; | |||
| const path = streamName + '/' + unescapeName(seg[2]); | |||
| let f; | |||
| if (path !== lastPath) { | |||
| let dirName = path.split('/'); | |||
| const fileName = dirName[dirName.length - 1]; | |||
| dirName = dirName.slice(0, dirName.length - 1); | |||
| const dir = mkpath(rootDir, dirName); | |||
| f = makeFile(dir, fileName); | |||
| lastPath = path; | |||
| lastFile = f; | |||
| } else { | |||
| f = lastFile; | |||
| } | |||
| appendFile(f, streams.length, seg); | |||
| accum = ''; | |||
| tokenStart = i + 1; | |||
| if (c === '\n') { | |||
| streams.push([ streamName, locators ]); | |||
| locators = []; | |||
| mode = M_STREAM_NAME; | |||
| } | |||
| } else { | |||
| accum += c; | |||
| } | |||
| } | |||
| } | |||
| return { rootDir, streams }; | |||
| } | |||
| function findDir(parent, path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| if (path[0] !== '.') | |||
| throw Error('Path must start with a dot (.)'); | |||
| let dir = parent; | |||
| for (let i = 1; i < path.length; i++) { | |||
| if (!(path[i] in dir)) | |||
| throw Error('Directory not found'); | |||
| dir = dir[path[i]]; | |||
| } | |||
| return dir; | |||
| } | |||
| class WBManifestReader { | |||
| constructor(manifestText) { | |||
| const {rootDir, streams} = parse(manifestText); | |||
| this.rootDir = rootDir; | |||
| this.streams = streams; | |||
| //this.rootDir = process(this.streams); | |||
| } | |||
| listDirectory(path) { | |||
| let dir = findDir(this.rootDir, path); | |||
| let keys = Object.keys(dir); | |||
| keys.sort(); | |||
| let subdirs = keys.filter(k => !(dir[k] instanceof Array)); | |||
| let files = keys.filter(k => (dir[k] instanceof Array)); | |||
| let res = subdirs.map(k => [ 'd', k, null ]); | |||
| res = res.concat(files.map(k => [ 'f', k, dir[k][1] ])); | |||
| return res; | |||
| } | |||
| } | |||
| export default WBManifestReader; | |||
| @@ -0,0 +1,46 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBBreadcrumbs from 'wb-breadcrumbs'; | |||
| import fetchProjectParents from 'fetch-project-parents'; | |||
| class WBProjectCrumbs extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.items = [ { 'name': 'All Projects' } ]; | |||
| } | |||
| fetchCrumbs() { | |||
| if (!this.props.uuid) { | |||
| this.setState({ 'items': [ { 'name': 'All Projects' } ] }); | |||
| return; | |||
| } | |||
| let { arvHost, arvToken } = this.props.appState; | |||
| let prom = fetchProjectParents(arvHost, arvToken, this.props.uuid); | |||
| prom = prom.then(parents => this.setState({ 'items': parents })); | |||
| } | |||
| componentDidMount() { | |||
| this.fetchCrumbs(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchCrumbs(); | |||
| } | |||
| render({ onItemClicked }, { items }) { | |||
| return ( | |||
| <WBBreadcrumbs items={ items } | |||
| onItemClicked={ onItemClicked } /> | |||
| ); | |||
| } | |||
| } | |||
| export default WBProjectCrumbs; | |||
| @@ -0,0 +1,89 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| class WBRootDirWrapper { | |||
| constructor(rootDir, streams) { | |||
| this.rootDir = rootDir; | |||
| this.streams = streams; | |||
| } | |||
| findDir(path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| if (path[0] !== '.') | |||
| throw Error('Path must begin with a dot component'); | |||
| let dir = this.rootDir; | |||
| for (let i = 1; i < path.length; i++) { | |||
| if (!(path[i] in dir)) | |||
| throw Error('Directory not found'); | |||
| if (dir[path[i]] instanceof Array) | |||
| throw Error('Path is a file not directory'); | |||
| dir = dir[path[i]]; | |||
| } | |||
| return dir; | |||
| } | |||
| listDirectory(path) { | |||
| let dir = this.findDir(path); | |||
| let keys = Object.keys(dir); | |||
| keys.sort(); | |||
| let subdirs = keys.filter(k => !(dir[k] instanceof Array)); | |||
| let files = keys.filter(k => (dir[k] instanceof Array)); | |||
| let res = subdirs.map(k => [ 'd', k, null ]); | |||
| res = res.concat(files.map(k => [ 'f', k, dir[k][1] ])); | |||
| return res; | |||
| } | |||
| unescapeName(name) { | |||
| return name.replace(/(\\\\|\\[0-9]{3})/g, (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8)))); | |||
| } | |||
| escapeName(name) { | |||
| return name.replace(/ /g, '\\040'); | |||
| } | |||
| getFile(path) { | |||
| if (typeof(path) === 'string') | |||
| path = path.split('/'); | |||
| if (path.length < 2) | |||
| throw Error('Invalid file path'); | |||
| const name = path[path.length - 1]; | |||
| const dir = this.findDir(path.slice(0, path.length - 1)); | |||
| if (!(name in dir)) | |||
| throw Error('File not found'); | |||
| if (!(dir[name] instanceof Array)) | |||
| throw Error('Path points to a directory not a file'); | |||
| const streams = this.streams; | |||
| let file = dir[name]; | |||
| file = [ file[0].map(seg => { | |||
| const stm = streams[seg[0]]; | |||
| const used = stm.map(loc => !( loc[2] <= seg[1] || loc[1] >= seg[1] + seg[2] ) ); | |||
| const start = used.indexOf(true); | |||
| const end = used.lastIndexOf(true) + 1; | |||
| if (start === -1) | |||
| return []; | |||
| const res = []; | |||
| for (let i = start; i < end; i++) { | |||
| const loc = stm[i]; | |||
| res.push([ loc[0], Math.max(0, seg[1] - loc[1]), | |||
| Math.min(loc[2] - loc[1], seg[1] + seg[2] - loc[1]) ]); | |||
| } | |||
| return res; | |||
| }), file[1] ]; | |||
| file[0] = file[0].reduce((a, b) => a.concat(b)); | |||
| return file; | |||
| } | |||
| } | |||
| export default WBRootDirWrapper; | |||
| @@ -0,0 +1,125 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBManifestWorkerWrapper from 'wb-manifest-worker-wrapper'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| function unescapeName(name) { | |||
| return name.replace(/(\\\\|\\[0-9]{3})/g, | |||
| (_, $1) => ($1 === '\\\\' ? '\\' : String.fromCharCode(parseInt($1.substr(1), 8)))); | |||
| } | |||
| class WBBrowseDialogCollectionContent extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.manifestWorker = new WBManifestWorkerWrapper(); | |||
| this.state.mode = 'manifestDownload'; | |||
| this.state.rows = []; | |||
| } | |||
| componentDidMount() { | |||
| const { app, collectionUuid } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| const { manifestWorker } = this.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/collections/' + collectionUuid); | |||
| let streams; | |||
| prom = prom.then(xhr => { | |||
| streams = xhr.response.manifest_text.split('\n'); | |||
| const paths = streams.filter(s => s).map(s => { | |||
| const n = s.indexOf(' '); | |||
| return unescapeName(s.substr(0, n)); | |||
| }); | |||
| return manifestWorker.postMessage([ 'precreatePaths', paths ]); | |||
| }); | |||
| prom = prom.then(() => { | |||
| this.setState({ 'mode': 'manifestParse' }); | |||
| let prom_1 = new Promise(accept => accept()); | |||
| for (let i = 0; i < streams.length; i++) { | |||
| prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'parseStream', streams[i] ])); | |||
| prom_1 = prom_1.then(() => manifestWorker.postMessage([ 'listDirectory', '.' + this.props.collectionPath, true ])); | |||
| prom_1 = prom_1.then(e => this.prepareRows(e.data[1])); | |||
| } | |||
| return prom_1; | |||
| }); | |||
| prom = prom.then(() => manifestWorker.postMessage([ 'listDirectory', '.' + this.props.collectionPath, true ])); | |||
| prom = prom.then(e => { | |||
| this.state.mode = 'browsingReady'; | |||
| this.prepareRows(e.data[1]) | |||
| }); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| if (this.state.mode !== 'browsingReady') | |||
| return; | |||
| let prom = this.state.manifestWorker.postMessage([ | |||
| 'listDirectory', '.' + this.props.collectionPath, true | |||
| ]); | |||
| prom = prom.then(e => this.prepareRows(e.data[1])); | |||
| } | |||
| prepareRows(listing) { | |||
| const { makeSelectionCell, collectionPath, navigate, | |||
| page, itemsPerPage, collectionUuid, textSearch, selectWhat } = this.props; | |||
| const textLower = textSearch.toLowerCase(); | |||
| listing = listing.filter(it => (it[1].toLowerCase().indexOf(textLower) !== -1)); | |||
| const numPages = Math.ceil(listing.length / itemsPerPage); | |||
| const rows = listing.slice(page * itemsPerPage, | |||
| (page + 1) * itemsPerPage).map(it => [ | |||
| ((it[0] === 'd' && [].concat(selectWhat).indexOf('directory') !== -1) || | |||
| (it[0] === 'f' && [].concat(selectWhat).indexOf('file') !== -1)) ? | |||
| makeSelectionCell(collectionUuid + collectionPath + '/' + it[1] + (it[0] === 'd' ? '/' : '')) : | |||
| null, | |||
| it[0] === 'd' ? ( | |||
| <a href="#" onclick={ e => { | |||
| e.preventDefault(); | |||
| navigate({ 'collectionPath': collectionPath + '/' + it[1], | |||
| 'bottomPage': 0 }); | |||
| } }>{ it[1] }</a> | |||
| ) : it[1], | |||
| it[0] === 'f' ? filesize(it[2]) : '' | |||
| ]); | |||
| this.setState({ rows, numPages }); | |||
| } | |||
| render({ page, navigate }, { rows, mode, numPages }) { | |||
| return ( | |||
| <div> | |||
| { mode === 'browsingReady' ? ( | |||
| null | |||
| ) : [ | |||
| <div>{ mode === 'manifestParse' ? 'Parsing manifest...' : 'Downloading manifest...' }</div>, | |||
| <div class="progress my-2"> | |||
| <div class={ 'progress-bar progress-bar-striped progress-bar-animated' + | |||
| (mode === 'manifestParse' ? ' bg-success': '') } role="progressbar" | |||
| aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"></div> | |||
| </div> | |||
| ] } | |||
| <WBTable headerClasses={ [ 'w-1' ] } | |||
| columns={ [ '', 'Name', 'Size' ] } rows={ rows } /> | |||
| <WBPagination numPages={ numPages } activePage={ page } | |||
| onPageChanged={ i => navigate({ 'bottomPage': i }) } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBBrowseDialogCollectionContent.defaultProps = { | |||
| 'itemsPerPage': 20 | |||
| }; | |||
| export default WBBrowseDialogCollectionContent; | |||
| @@ -0,0 +1,78 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBBrowseDialogCollectionList extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchRows(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchRows(); | |||
| } | |||
| prepareRows(items) { | |||
| const { navigate, selectWhat, makeSelectionCell } = this.props; | |||
| return items.map(it => [ | |||
| ([].concat(selectWhat).indexOf('directory') !== -1 ? makeSelectionCell(it.uuid + '/') : null), | |||
| ( | |||
| <a href="#" onclick={ e => { e.preventDefault(); | |||
| navigate('/browse-dialog/content/' + it.uuid + '////'); } }>{ it.name }</a> | |||
| ), | |||
| it.uuid | |||
| ]); | |||
| } | |||
| fetchRows() { | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { ownerUuid, textSearch, page, itemsPerPage } = this.props; | |||
| const filters = []; | |||
| if (ownerUuid) | |||
| filters.push(['owner_uuid', '=', ownerUuid]); | |||
| if (textSearch) | |||
| filters.push(['name', 'ilike', '%' + textSearch + '%']); | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/collections?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (itemsPerPage * page)); | |||
| prom = prom.then(xhr => this.setState({ | |||
| 'rows': this.prepareRows(xhr.response.items), | |||
| 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage) | |||
| })); | |||
| return prom; | |||
| } | |||
| render({ selectWhat, page, navigate }, { rows, numPages }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ ['', 'Name', 'UUID'] } | |||
| headerClasses={ ['w-1'] } | |||
| rows={ rows } /> | |||
| <WBPagination activePage={ page } numPages={ numPages } | |||
| onPageChanged={ i => navigate({ 'bottomPage': i }) } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBBrowseDialogCollectionList.defaultProps = { | |||
| 'itemsPerPage': 20 | |||
| }; | |||
| export default WBBrowseDialogCollectionList; | |||
| @@ -0,0 +1,113 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import WBTable from 'wb-table'; | |||
| class WBBrowseDialogProjectList extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.state.history = []; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchRows(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchRows(); | |||
| } | |||
| prepareRows(items) { | |||
| const { navigate, selectWhat, makeSelectionCell } = this.props; | |||
| return items.map(it => ([].concat(selectWhat).indexOf('owner') !== -1 ? [ makeSelectionCell(it.uuid, 'project') ] : []).concat([ | |||
| ( | |||
| <a href="#" onclick={ e => { | |||
| e.preventDefault(); | |||
| navigate('/browse-dialog/browse/' + it.uuid); | |||
| } }>{ it.name }</a> | |||
| ), | |||
| it.uuid | |||
| ])); | |||
| } | |||
| fetchSharedWithMe() { | |||
| const { arvHost, arvToken, currentUser } = this.props.app.state; | |||
| const { textSearch, itemsPerPage, page } = this.props; | |||
| const filters = [ | |||
| ['group_class', '=', 'project'] | |||
| ]; | |||
| if (textSearch) | |||
| filters.push([ 'name', 'ilike', '%' + textSearch + '%']); | |||
| const prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups/shared?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (itemsPerPage * page)); | |||
| return prom; | |||
| } | |||
| fetchOwned() { | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { ownerUuid, page, textSearch, itemsPerPage } = this.props; | |||
| const filters = [ | |||
| ['group_class', '=', 'project'] | |||
| ]; | |||
| if (ownerUuid) | |||
| filters.push(['owner_uuid', '=', ownerUuid]); | |||
| if (textSearch) | |||
| filters.push(['name', 'ilike', '%' + textSearch + '%']); | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (page * itemsPerPage)); | |||
| return prom; | |||
| } | |||
| fetchRows() { | |||
| const { mode, itemsPerPage } = this.props; | |||
| let prom = (mode === 'shared-with-me') ? | |||
| this.fetchSharedWithMe() : | |||
| this.fetchOwned(); | |||
| prom = prom.then(xhr => { | |||
| this.setState({ | |||
| 'rows': this.prepareRows(xhr.response.items), | |||
| 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage) | |||
| }); | |||
| }); | |||
| return prom; | |||
| } | |||
| render({ app, navigate, page, selectWhat }, { numPages, rows }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ ([].concat(selectWhat).indexOf('owner') !== -1 ? [''] : []).concat(['Name', 'UUID']) } | |||
| headerClasses={ [].concat(selectWhat).indexOf('owner') !== -1 ? ['col-sm-1', 'col-sm-4', 'col-sm-4'] : [] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } activePage={ page } | |||
| onPageChanged={ i => navigate({ 'topPage': i }) } | |||
| chunkSize="3" /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBBrowseDialogProjectList.defaultProps = { | |||
| 'itemsPerPage': 5, | |||
| 'resetSearch': () => {} | |||
| }; | |||
| export default WBBrowseDialogProjectList; | |||
| @@ -0,0 +1,78 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBBrowseDialogUserList extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchRows(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchRows(); | |||
| } | |||
| prepareRows(items) { | |||
| const { navigate } = this.props; | |||
| return items.map(it => [ | |||
| ( | |||
| <a href="#" onclick={ e => { e.preventDefault(); | |||
| navigate('/browse-dialog/browse/' + it.uuid); } }> | |||
| { it.last_name + ', ' + it.first_name } | |||
| </a> | |||
| ), | |||
| it.uuid | |||
| ]); | |||
| } | |||
| fetchRows() { | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { itemsPerPage, page, textSearch } = this.props; | |||
| const order = ['last_name asc', 'first_name asc']; | |||
| const filters = []; | |||
| if (textSearch) | |||
| filters.push([ 'any', 'ilike', '%' + textSearch + '%' ]); | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/users?order=' + | |||
| encodeURIComponent(JSON.stringify(order)) + | |||
| '&filters=' + | |||
| encodeURIComponent(JSON.stringify(filters)) + | |||
| '&limit=' + itemsPerPage + | |||
| '&offset=' + (itemsPerPage * page)); | |||
| prom = prom.then(xhr => this.setState({ | |||
| 'rows': this.prepareRows(xhr.response.items), | |||
| 'numPages': Math.ceil(xhr.response.items_available / itemsPerPage) | |||
| })); | |||
| } | |||
| render({ page, navigate }, { rows, numPages }) { | |||
| return ( | |||
| <div> | |||
| <WBTable columns={ [ 'Name', 'UUID' ] } | |||
| rows={ rows } /> | |||
| <WBPagination numPages={ numPages } activePage={ page } | |||
| onPageChanged={ i => navigate({ 'topPage': i }) } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBBrowseDialogUserList.defaultProps = { | |||
| 'itemsPerPage': 20 | |||
| }; | |||
| export default WBBrowseDialogUserList; | |||
| @@ -0,0 +1,269 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBBrowseDialogProjectList from 'wb-browse-dialog-project-list'; | |||
| import WBBrowseDialogCollectionList from 'wb-browse-dialog-collection-list'; | |||
| import WBBrowseDialogCollectionContent from 'wb-browse-dialog-collection-content'; | |||
| import WBBrowseDialogUserList from 'wb-browse-dialog-user-list'; | |||
| import linkState from 'linkstate'; | |||
| import { Router } from 'preact-router'; | |||
| import { createHashHistory } from 'history'; | |||
| // | |||
| // internal URLs look like this | |||
| // | |||
| // /browse-dialog/browse/( owner-uuid )/( project-page )/( text-search ) | |||
| // /browse-dialog/users//( users-page )/( text-search ) | |||
| // /browse-dialog/shared-with-me//( project-page )/( collection-page )/( text-search ) | |||
| // /browse-dialog/content/( collection-uuid )//( content-page )/( text-search )/( collection-path ) | |||
| // | |||
| // general pattern therefore: | |||
| // /browse-dialog/( mode )/( uuid )/( top-page )/( bottom-page )/( text-search ) | |||
| // | |||
| // props: | |||
| // selectMany: Boolean | |||
| // selectWhat: [ 'file', 'directory', 'owner' ] | |||
| // | |||
| // state: | |||
| // selected: Array of UUID | |||
| // textSearch: string | |||
| // textSearchInput: string | |||
| // | |||
| class WBBrowseDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.history = []; | |||
| this.state.selected = {}; | |||
| this.state.selectedOrder = []; | |||
| const { currentUser } = this.props.app.state; | |||
| this.state.currentUrl = '/browse-dialog/browse/' + currentUser.uuid; | |||
| this.state.uuid = currentUser.uuid; | |||
| this.state.mode = 'browse'; | |||
| this.state.topPage = 0; | |||
| this.state.bottomPage = 0; | |||
| this.state.collectionPath = ''; | |||
| this.state.textSearch = ''; | |||
| this.state.id = ('id' in this.props) ? this.props.id : uuid.v4(); | |||
| this.state.accept = () => {}; | |||
| this.modalRef = createRef(); | |||
| } | |||
| navigateBack() { | |||
| if (this.state.history.length === 0) | |||
| return; | |||
| const url = this.state.history.pop(); | |||
| this.navigate(url, false); | |||
| } | |||
| navigate(url, useHistory=true, stateUpdate={}) { | |||
| if (typeof(url) === 'object') { | |||
| url = ['', 'browse-dialog', | |||
| 'mode' in url ? url.mode : this.state.mode, | |||
| 'uuid' in url ? url.uuid : this.state.uuid, | |||
| 'topPage' in url ? url.topPage : this.state.topPage, | |||
| 'bottomPage' in url ? url.bottomPage : this.state.bottomPage, | |||
| 'textSearch' in url ? url.textSearch : this.state.textSearch, | |||
| encodeURIComponent('collectionPath' in url ? url.collectionPath : this.state.collectionPath) | |||
| ].join('/'); | |||
| } | |||
| url = url.substr(url.indexOf('/browse-dialog/')); | |||
| if (useHistory) | |||
| this.state.history.push(this.state.currentUrl); | |||
| let [ _1, _2, mode, uuid, topPage, bottomPage, textSearch, collectionPath ] = url.split('/'); | |||
| topPage = parseInt(topPage, 10) || 0; | |||
| bottomPage = parseInt(bottomPage, 10) || 0; | |||
| collectionPath = decodeURIComponent(collectionPath || ''); | |||
| this.setState(Object.assign({ | |||
| 'currentUrl': url, | |||
| mode, uuid, topPage, bottomPage, textSearch, collectionPath | |||
| }, stateUpdate)); | |||
| } | |||
| select(uuid) { | |||
| let { selected, selectedOrder } = this.state; | |||
| if (uuid in selected) { | |||
| const n = selectedOrder.indexOf(uuid); | |||
| selectedOrder = selected.splice(n, n + 1); | |||
| } | |||
| selected[uuid] = true; | |||
| selectedOrder.push(uuid); | |||
| /* this.setState({ | |||
| selected, selectedOrder | |||
| }); */ | |||
| } | |||
| deselect(uuid) { | |||
| let { selected, selectedOrder } = this.state; | |||
| if (!(uuid in selected)) | |||
| return; | |||
| const n = selectedOrder.indexOf(uuid); | |||
| selectedOrder = selected.splice(n, n + 1); | |||
| delete selected[uuid]; | |||
| /* this.setState({ | |||
| selected, selectedOrder | |||
| }); */ | |||
| } | |||
| resetSelection() { | |||
| this.setState({ | |||
| 'selected': {}, | |||
| 'selectedOrder': [] | |||
| }); | |||
| } | |||
| makeSelectionCell(uuid) { | |||
| const { selected, accept, selectMany, id } = this.state; | |||
| return selectMany ? ( | |||
| <div> | |||
| <input type="checkbox" checked={ (uuid in selected) } | |||
| onChange={ e => { | |||
| if (e.target.checked) | |||
| this.select(uuid); | |||
| else | |||
| this.deselect(uuid); | |||
| } } /> { '\u00A0' } | |||
| </div> | |||
| ) : ( | |||
| <button class="btn btn-outline-primary" title="Use" | |||
| onclick={ () => { | |||
| $('#' + id).modal('hide'); | |||
| accept(uuid); | |||
| } }> | |||
| <i class="fas fa-hand-pointer"></i> | |||
| </button> | |||
| ); | |||
| } | |||
| show(selectWhat, selectMany, accept=(() => {})) { | |||
| const { app } = this.props; | |||
| const { currentUser } = app.state; | |||
| this.navigate('/browse-dialog/browse/' + currentUser.uuid, false, | |||
| { selectWhat, selectMany, accept, history: [], | |||
| selected: {}, selectedOrder: [] }); | |||
| $('#' + this.state.id).modal(); | |||
| } | |||
| componentWillUnmount() { | |||
| $(this.modalRef.current).modal('hide'); | |||
| } | |||
| render({ app }, | |||
| { history, currentUrl, mode, uuid, | |||
| topPage, bottomPage, textSearch, | |||
| collectionPath, id, accept, selectedOrder, | |||
| selectMany, selectWhat }) { | |||
| return ( | |||
| <div class="modal" id={ id } tabindex="-1" role="dialog" ref={ this.modalRef }> | |||
| <div class="modal-dialog modal-lg" role="document"> | |||
| <div class="modal-content"> | |||
| <div class="modal-header"> | |||
| { false ? <h5 class="modal-title">Browse</h5> : null } | |||
| <div>{ currentUrl }</div> | |||
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |||
| <span aria-hidden="true">×</span> | |||
| </button> | |||
| </div> | |||
| <div class="modal-body"> | |||
| <div class="mb-3"> | |||
| <a href="#" class={ 'btn btn-outline-secondary mr-2' + | |||
| (history.length === 0 ? ' disabled': '') } | |||
| onclick={ e => { e.preventDefault(); | |||
| this.navigateBack(); } }>Back</a> | |||
| <a href="#" class="btn btn-outline-primary mr-2" | |||
| onclick={ e => { e.preventDefault(); | |||
| this.navigate('/browse-dialog/browse/' + app.state.currentUser.uuid); } }>Home</a> | |||
| <a href="#" class="btn btn-outline-primary mr-2" | |||
| onclick={ e => { e.preventDefault(); | |||
| this.navigate('/browse-dialog/browse'); } }>All Projects</a> | |||
| <a href="#" class="btn btn-outline-primary mr-2" | |||
| onclick={ e => { e.preventDefault(); | |||
| this.navigate('/browse-dialog/users'); } }>All Users</a> | |||
| <a href="#" class="btn btn-outline-primary mr-2" | |||
| onclick={ e => { e.preventDefault(); | |||
| this.navigate('/browse-dialog/shared-with-me'); } }>Shared with Me</a> | |||
| </div> | |||
| <div class="input-group mb-3"> | |||
| <input type="text" class="form-control" placeholder="Search" | |||
| aria-label="Search" value={ textSearch } | |||
| onChange={ e => this.navigate({ | |||
| 'textSearch': e.target.value, | |||
| 'topPage': 0, | |||
| 'bottomPage': 0}) } /> | |||
| <div class="input-group-append"> | |||
| <button class="btn btn-outline-primary" type="button">Search</button> | |||
| </div> | |||
| </div> | |||
| { (mode === 'browse' || mode === 'shared-with-me') ? ( | |||
| <div> | |||
| <h5>Projects</h5> | |||
| <WBBrowseDialogProjectList app={ app } | |||
| navigate={ url => this.navigate(url) } | |||
| mode={ mode } ownerUuid={ uuid } | |||
| page={ topPage } textSearch={ textSearch } | |||
| selectWhat={ selectWhat } | |||
| makeSelectionCell={ uuid => this.makeSelectionCell(uuid) } /> | |||
| </div> | |||
| ) : null } | |||
| { (mode === 'users') ? ( | |||
| <WBBrowseDialogUserList app={ app } | |||
| navigate={ url => this.navigate(url) } | |||
| page={ topPage } textSearch={ textSearch }/> | |||
| ) : null } | |||
| { (mode === 'content') ? ( | |||
| <div> | |||
| <h5>Content</h5> | |||
| <WBBrowseDialogCollectionContent app={ app } | |||
| collectionUuid={ uuid } collectionPath={ collectionPath } | |||
| page={ bottomPage } selectWhat={ selectWhat } | |||
| makeSelectionCell={ uuid => this.makeSelectionCell(uuid) } | |||
| navigate={ url => this.navigate(url) } | |||
| textSearch={ textSearch } /> | |||
| </div> | |||
| ) : (selectWhat !== 'owner' && mode === 'browse') ? ( | |||
| <div> | |||
| <h5>Collections</h5> | |||
| <WBBrowseDialogCollectionList app={ app } | |||
| page={ bottomPage } textSearch={ textSearch } | |||
| navigate={ url => this.navigate(url) } | |||
| ownerUuid={ uuid } selectWhat={ selectWhat } | |||
| makeSelectionCell={ uuid => this.makeSelectionCell(uuid) } /> | |||
| </div> | |||
| ) : null } | |||
| </div> | |||
| <div class="modal-footer"> | |||
| { selectMany ? ( | |||
| <button type="button" class="btn btn-primary" | |||
| onclick={ e => { e.preventDefault(); accept(selectedOrder); $('#' + id).modal('hide'); } }>Accept</button> | |||
| ) : null } | |||
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBBrowseDialog.defaultProps = { | |||
| 'accept': () => {} | |||
| }; | |||
| export default WBBrowseDialog; | |||
| @@ -0,0 +1,60 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBDialog from 'wb-dialog'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import linkState from 'linkstate'; | |||
| import wbDeleteObject from 'wb-delete-object'; | |||
| import arvadosTypeName from 'arvados-type-name'; | |||
| class WBDeleteDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.dialogRef = createRef(); | |||
| } | |||
| show(item, callback) { | |||
| this.setState({ | |||
| 'item': item, | |||
| 'callback': callback || (() => {}) | |||
| }); | |||
| this.dialogRef.current.show(); | |||
| } | |||
| hide() { | |||
| this.dialogRef.current.hide(); | |||
| } | |||
| render({ app }, { item, callback }) { | |||
| const { arvHost, arvToken } = app.state; | |||
| return ( | |||
| <WBDialog title="Delete" ref={ this.dialogRef }> | |||
| <div> | |||
| <div class="mb-3"> | |||
| Are you sure you want to delete the following { item ? arvadosTypeName(item.uuid) : null }: | |||
| </div> | |||
| { item ? <WBArvadosCrumbs app={ app } uuid={ item.uuid } /> : null } | |||
| <div>???</div> | |||
| </div> | |||
| <div> | |||
| <input type="submit" class="btn btn-danger mr-2" value="Delete" | |||
| onclick={ e => { e.preventDefault(); this.hide(); | |||
| wbDeleteObject(arvHost, arvToken, item.uuid).then(callback); } } /> | |||
| <button class="btn btn-secondary mr-2" onclick={ e => { e.preventDefault(); | |||
| this.hide(); } }> | |||
| Cancel | |||
| </button> | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| } | |||
| export default WBDeleteDialog; | |||
| @@ -0,0 +1,59 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBDialog from 'wb-dialog'; | |||
| import linkState from 'linkstate'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import arvadosTypeName from 'arvados-type-name'; | |||
| class WBEditDescriptionDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.dialogRef = createRef(); | |||
| this.state.inputId = uuid.v4(); | |||
| } | |||
| show(item, callback) { | |||
| const { inputId } = this.state; | |||
| this.setState({ | |||
| 'item': item, | |||
| 'newDescription': null, | |||
| 'callback': callback || (() => {}) | |||
| }); | |||
| this.dialogRef.current.show(); | |||
| $('#' + inputId).focus(); | |||
| } | |||
| hide() { | |||
| this.dialogRef.current.hide(); | |||
| } | |||
| render({ app }, { item, newDescription, callback, inputId }) { | |||
| const { arvHost, arvToken } = app.state; | |||
| return ( | |||
| <WBDialog title="Edit Description" ref={ this.dialogRef } accept={ () => | |||
| makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/' + arvadosTypeName(item.uuid) + | |||
| 's/' + item.uuid, { | |||
| method: 'PUT', | |||
| data: JSON.stringify({ | |||
| description: newDescription || null | |||
| }) | |||
| }).then(callback) | |||
| }> | |||
| <div> | |||
| <input type="text" class="form-control" id={ inputId } | |||
| placeholder={ (item && item.description) ? item.description : 'Type new description here' } | |||
| value={ newDescription } onChange={ linkState(this, 'newDescription') } /> | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| } | |||
| export default WBEditDescriptionDialog; | |||
| @@ -0,0 +1,73 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBDialog from 'wb-dialog'; | |||
| import linkState from 'linkstate'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBNewProjectDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.dialogRef = createRef(); | |||
| this.state.inputId = uuid.v4(); | |||
| } | |||
| show(ownerUuid, callback) { | |||
| const { inputId } = this.state; | |||
| this.setState({ | |||
| 'ownerUuid': ownerUuid, | |||
| 'newName': null, | |||
| 'placeholderName': 'New Project (' + (new Date()).toISOString() + ')', | |||
| 'callback': callback || (() => {}) | |||
| }); | |||
| this.dialogRef.current.show(); | |||
| $('#' + inputId).focus(); | |||
| } | |||
| hide() { | |||
| this.dialogRef.current.hide(); | |||
| } | |||
| render({ app }, { ownerUuid, newName, placeholderName, callback, inputId, | |||
| projectDescription }) { | |||
| const { arvHost, arvToken } = app.state; | |||
| return ( | |||
| <WBDialog title="New Project" ref={ this.dialogRef } accept={ () => { | |||
| const group = { | |||
| 'group_class': 'project', | |||
| 'name': newName || placeholderName, | |||
| 'description': projectDescription || null, | |||
| 'owner_uuid': ownerUuid | |||
| }; | |||
| makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/groups', { 'method': 'POST', | |||
| 'data': JSON.stringify(group), | |||
| 'expectedStatus': [200, 202] } | |||
| ).then(callback); | |||
| } }> | |||
| <div> | |||
| <div class="form-group"> | |||
| <label for={ inputId }>Project Name</label> | |||
| <input type="text" class="form-control" id={ inputId } | |||
| placeholder={ placeholderName } | |||
| value={ newName } onChange={ linkState(this, 'newName') } /> | |||
| </div> | |||
| <div class="form-group"> | |||
| <label for="projectDescription">Project Description (optional)</label> | |||
| <input type="text" class="form-control" id="projectDescription" | |||
| placeholder="Project Description (optional)" | |||
| value={ projectDescription } onChange={ linkState(this, 'projectDescription') } /> | |||
| </div> | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| } | |||
| export default WBNewProjectDialog; | |||
| @@ -0,0 +1,118 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBDialog from 'wb-dialog'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPagination from 'wb-pagination'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import arvadosObjectName from 'arvados-object-name'; | |||
| class WBPickObjectDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.title = 'WBPickObjectDialog'; | |||
| this.state.rows = []; | |||
| this.state.textSearch = null; | |||
| this.dialogRef = createRef(); | |||
| } | |||
| show(title, objectType, accept, filters=[]) { | |||
| this.setState({ title, objectType, page: 0, rows: [], accept, filters, textSearch: null }); | |||
| this.dialogRef.current.show(); | |||
| this.fetchData(); | |||
| } | |||
| hide() { | |||
| this.dialogRef.current.hide(); | |||
| } | |||
| fetchData() { | |||
| const { app, itemsPerPage } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| const { objectType, page, textSearch } = this.state; | |||
| let { filters } = this.state; | |||
| if (textSearch) | |||
| filters = filters.concat([['any', 'ilike', '%' + textSearch + '%']]); | |||
| const order = (objectType === 'user') ? | |||
| ['last_name asc', 'first_name asc'] : | |||
| ['name asc']; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/' + objectType + | |||
| 's?offset=' + (page * itemsPerPage) + | |||
| '&limit=' + itemsPerPage + | |||
| '&filters=' + encodeURIComponent(JSON.stringify(filters)) + | |||
| '&order=' + encodeURIComponent(JSON.stringify(order)) | |||
| ); | |||
| prom = prom.then(xhr => this.setState({ | |||
| numPages: Math.ceil(xhr.response.items_available / itemsPerPage), | |||
| rows: this.prepareRows(xhr.response.items) | |||
| })); | |||
| return prom; | |||
| } | |||
| prepareRows(items) { | |||
| const { accept } = this.state; | |||
| const { dialogRef } = this; | |||
| return items.map(it => [ | |||
| (<div> | |||
| <div> | |||
| <a href="#" onclick={ () => { dialogRef.current.hide(); accept(it); } }> | |||
| { arvadosObjectName(it) } | |||
| </a> | |||
| </div> | |||
| <div>{ it.uuid }</div> | |||
| </div>) | |||
| ]); | |||
| } | |||
| search(textSearch) { | |||
| this.setState({ textSearch, page: 0}); | |||
| this.fetchData(); | |||
| } | |||
| render({}, { title, rows, page, numPages, textSearch }) { | |||
| return ( | |||
| <WBDialog title={ title } ref={ this.dialogRef }> | |||
| <div> | |||
| <div class="input-group mb-3"> | |||
| <input type="text" class="form-control" placeholder="Search" | |||
| aria-label="Search" value={ textSearch } | |||
| onkeydown={ e => { if (e.keyCode === 13) { | |||
| e.preventDefault(); | |||
| this.search(e.target.value); | |||
| } } } | |||
| onchange={ e => this.search(e.target.value) } /> | |||
| <div class="input-group-append"> | |||
| <button class="btn btn-outline-primary" type="button" onclick={ e => e.preventDefault() }>Search</button> | |||
| </div> | |||
| </div> | |||
| <WBTable columns={ [ 'Name' ] } rows={ rows } /> | |||
| <WBPagination activePage={ page } numPages={ numPages } chunkSize={ 3 } | |||
| onPageChanged={ i => { this.setState({ page: i }); this.fetchData(); } } /> | |||
| </div> | |||
| <div> | |||
| <button class="btn btn-secondary" | |||
| onclick={ e => { e.preventDefault(); this.hide(); } }> | |||
| Cancel | |||
| </button> | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| } | |||
| WBPickObjectDialog.defaultProps = { | |||
| itemsPerPage: 20 | |||
| }; | |||
| export default WBPickObjectDialog; | |||
| @@ -0,0 +1,52 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBDialog from 'wb-dialog'; | |||
| import linkState from 'linkstate'; | |||
| import wbRenameObject from 'wb-rename-object'; | |||
| class WBRenameDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.dialogRef = createRef(); | |||
| this.state.inputId = uuid.v4(); | |||
| } | |||
| show(item, callback) { | |||
| const { inputId } = this.state; | |||
| this.setState({ | |||
| 'item': item, | |||
| 'newName': null, | |||
| 'callback': callback || (() => {}) | |||
| }); | |||
| this.dialogRef.current.show(); | |||
| $('#' + inputId).focus(); | |||
| } | |||
| hide() { | |||
| this.dialogRef.current.hide(); | |||
| } | |||
| render({ app }, { item, newName, callback, inputId }) { | |||
| const { arvHost, arvToken } = app.state; | |||
| return ( | |||
| <WBDialog title="Rename" ref={ this.dialogRef } accept={ () => { | |||
| if (newName) | |||
| wbRenameObject(arvHost, arvToken, item.uuid, newName).then(callback); | |||
| } }> | |||
| <div> | |||
| <input type="text" class="form-control" id={ inputId } | |||
| placeholder={ item ? item.name : 'Type new name here' } | |||
| value={ newName } onChange={ linkState(this, 'newName') } /> | |||
| </div> | |||
| </WBDialog> | |||
| ); | |||
| } | |||
| } | |||
| export default WBRenameDialog; | |||
| @@ -0,0 +1,154 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBTable from 'wb-table'; | |||
| import WBNameAndUuid from 'wb-name-and-uuid'; | |||
| import wbFetchObjects from 'wb-fetch-objects'; | |||
| import wbFormatDate from 'wb-format-date'; | |||
| class WBToolboxDialog extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.state.selectedValues = {}; | |||
| } | |||
| componentDidMount() { | |||
| this.fetchRows(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.fetchRows(); | |||
| } | |||
| fetchRows() { | |||
| const { items, id, selectMany, onAccepted } = this.props; | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| const { selectedValues } = this.state; | |||
| let prom = wbFetchObjects(arvHost, arvToken, | |||
| items); | |||
| let lookup; | |||
| prom = prom.then(lkup => (lookup = lkup)); | |||
| prom = prom.then(() => wbFetchObjects(arvHost, arvToken, | |||
| items.map(uuid => lookup[uuid].owner_uuid))); | |||
| let ownerLookup; | |||
| prom = prom.then(lkup => (ownerLookup = lkup)); | |||
| prom = prom.then(() => { | |||
| const rows = items.map((uuid, idx) => { | |||
| const it = lookup[uuid]; | |||
| const ow = ownerLookup[it.owner_uuid]; | |||
| let r = []; | |||
| if (selectMany) | |||
| r.push(); | |||
| r = r.concat([ | |||
| selectMany ? ( | |||
| <div> | |||
| <input type="checkbox" checked={ (uuid in selectedValues) } | |||
| onChange={ e => { | |||
| if (e.target.value === 'on') | |||
| selectedValues[uuid] = true; | |||
| else | |||
| delete selectedValues[uuid]; | |||
| } } /> { '\u00A0' } | |||
| </div> | |||
| ) : ( | |||
| <button class="btn btn-outline-primary" title="Use" | |||
| onclick={ () => { | |||
| $('#' + id).modal('hide'); | |||
| onAccepted(uuid); | |||
| } }> | |||
| <i class="fas fa-hand-pointer"></i> | |||
| </button> | |||
| ), | |||
| ( <WBNameAndUuid uuid={ uuid } lookup={ lookup } | |||
| onLinkClicked={ () => $('#' + id).modal('hide') } /> ), | |||
| it.kind, | |||
| wbFormatDate(it.created_at), | |||
| ( <WBNameAndUuid uuid={ it.owner_uuid } lookup={ ownerLookup } | |||
| onLinkClicked={ () => $('#' + id).modal('hide') } /> ) | |||
| ]); | |||
| return r; | |||
| }); | |||
| this.setState({ rows }); | |||
| }); | |||
| } | |||
| render({ id, selectMany, onAccepted, items, app }, { rows, selectedValues }) { | |||
| return ( | |||
| <div class="modal" id={ id } tabindex="-1" role="dialog"> | |||
| <div class="modal-dialog modal-lg" role="document"> | |||
| <div class="modal-content"> | |||
| <div class="modal-header"> | |||
| <h5 class="modal-title">Browse Toolbox</h5> | |||
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> | |||
| <span aria-hidden="true">×</span> | |||
| </button> | |||
| </div> | |||
| <div class="modal-body"> | |||
| <div class="mb-2"> | |||
| { selectMany ? ( | |||
| <button class="btn btn-outline-primary mr-2" onclick={ () => { | |||
| items.map(uuid => (selectedValues[uuid] = true)); | |||
| this.fetchRows(); | |||
| } }> | |||
| Select All | |||
| </button> | |||
| ) : null } | |||
| { selectMany ? ( | |||
| <button class="btn btn-outline-primary mr-2" onclick={ () => { | |||
| this.setState({ 'selectedValues' : {} }); | |||
| this.fetchRows(); | |||
| } }> | |||
| Select None | |||
| </button> | |||
| ) : null } | |||
| <button class="btn btn-outline-primary mr-2" onclick={ () => { | |||
| app.clearToolbox(); | |||
| this.props.items = []; | |||
| this.fetchRows(); | |||
| } } > | |||
| Clear Toolbox | |||
| </button> | |||
| <button class="btn btn-outline-primary mr-2" onclick={ () => { | |||
| app.loadToolbox(); | |||
| this.props.items = app.state.toolboxItems; | |||
| this.fetchRows(); | |||
| } } > | |||
| Refresh Toolbox | |||
| </button> | |||
| </div> | |||
| <WBTable columns={ [ '', 'Name', 'Kind', 'Created At', 'Owner' ] } | |||
| rows={ rows } /> | |||
| </div> | |||
| <div class="modal-footer"> | |||
| { selectMany ? ( | |||
| <button type="button" class="btn btn-primary" onclick={ | |||
| () => { | |||
| $('#' + id).modal('hide'); | |||
| onAccepted(items.filter(uuid => (uuid in selectedValues))); | |||
| } | |||
| }>Accept</button> | |||
| ) : null } | |||
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| WBToolboxDialog.defaultProps = { | |||
| 'onAccepted': () => {} | |||
| }; | |||
| export default WBToolboxDialog; | |||
| @@ -0,0 +1,13 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, render } from 'preact'; | |||
| import WBApp from 'wb-app'; | |||
| render(( | |||
| <WBApp /> | |||
| ), document.body); | |||
| @@ -0,0 +1,120 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| const defaultOrderRegistry = {}; | |||
| /* function notify(orderRegistry) { | |||
| if (!('listeners' in orderRegistry)) | |||
| return; | |||
| for (let k in orderRegistry.listeners) { | |||
| orderRegistry.listeners[k](orderRegistry); | |||
| } | |||
| } */ | |||
| function cursorDecor() { | |||
| let d = $('#cursor-decor'); | |||
| if (d.length === 1) | |||
| return $(d[0]); | |||
| d = $('<div id="cursor-decor" style="z-index: 10000; position: absolute; left: 10px; top: 10px;"> \ | |||
| <div class="progress" style="height: 8px;"> \ | |||
| <div class="progress-bar progress-bar-striped progress-bar-animated" \ | |||
| role="progressbar" aria-valuenow="100" aria-valuemin="0" \ | |||
| aria-valuemax="100" style="width: 32px;"></div> \ | |||
| </div> \ | |||
| </div>'); | |||
| $(document.body).append(d); | |||
| let pageX = 0, pageY = 0, scrollX = 0, scrollY = 0; | |||
| document.addEventListener('mousemove', e => { | |||
| pageX = e.pageX; | |||
| pageY = e.pageY; | |||
| d.css({ left: (e.pageX + 16) + 'px', top: (e.pageY + 16) + 'px' }) | |||
| scrollX = window.scrollX; | |||
| scrollY = window.scrollY; | |||
| }); | |||
| document.addEventListener('scroll', e => { | |||
| d.css({ left: (pageX + window.scrollX - scrollX + 16) + 'px', | |||
| top: (pageY + window.scrollY - scrollY + 16) + 'px' }); | |||
| }); | |||
| return d; | |||
| } | |||
| function updateCursorDecor(orderRegistry) { | |||
| const d = cursorDecor(); | |||
| if (Object.keys(orderRegistry.pendingCompletion).length === 0) | |||
| d.hide(); | |||
| else | |||
| d.show(); | |||
| } | |||
| function wbApplyPromiseOrdering(prom, orderRegistry) { | |||
| let orderId; | |||
| if (!orderRegistry) | |||
| orderRegistry = defaultOrderRegistry; | |||
| //if (Object.keys(orderRegistry).length === 0) { | |||
| if (!('started' in orderRegistry)) { | |||
| orderRegistry.started = 0; | |||
| orderRegistry.pendingCompletion = {}; | |||
| orderRegistry.completed = { 0: true }; | |||
| } | |||
| orderRegistry.started += 1; | |||
| orderId = orderRegistry.started; | |||
| // console.log('New orderId: ' + orderId); | |||
| // notify(orderRegistry); | |||
| cursorDecor().show(); | |||
| const orderCallback = ((isCatch, payload) => { | |||
| // console.log('orderId: ' + orderId + | |||
| // ', pendingCompletion: ' + Object.keys(orderRegistry.pendingCompletion) + | |||
| // ', completed: ' + Object.keys(orderRegistry.completed)); | |||
| if ((orderId - 1) in orderRegistry.completed) { | |||
| // console.log('Running: ' + orderId); | |||
| orderRegistry.completed[orderId] = true; | |||
| delete orderRegistry.pendingCompletion[orderId]; | |||
| const keys = Object.keys(orderRegistry.pendingCompletion); | |||
| keys.sort((a, b) => (a - b)); | |||
| keys.map(k => { | |||
| if ((k - 1) in orderRegistry.completed) { | |||
| // console.log('Running: ' + k); | |||
| orderRegistry.pendingCompletion[k](); | |||
| orderRegistry.completed[k] = true; | |||
| delete orderRegistry.pendingCompletion[k]; | |||
| } | |||
| }); | |||
| if (orderRegistry.started in orderRegistry.completed) { | |||
| // console.log('Garbage collect'); | |||
| orderRegistry.started = 0; | |||
| orderRegistry.completed = { 0: true }; | |||
| cursorDecor().hide(); | |||
| } | |||
| if (isCatch) | |||
| throw payload; | |||
| else | |||
| return payload; | |||
| } | |||
| const prom_1 = new Promise((accept, reject) => { | |||
| orderRegistry.pendingCompletion[orderId] = (() => | |||
| (isCatch ? reject(payload) : accept(payload))); | |||
| }); | |||
| return prom_1; | |||
| }); | |||
| prom = prom.then(xhr => orderCallback(false, xhr)); | |||
| prom = prom.catch(e => orderCallback(true, e)); | |||
| return prom; | |||
| } | |||
| export default wbApplyPromiseOrdering; | |||
| @@ -0,0 +1,19 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| function wbDisableControls() { | |||
| $('input, select, button').attr('disabled', 'disabled'); | |||
| $('a').each(function() { $(this).data('old_href', $(this).attr('href')); }); | |||
| $('a').attr('href', null); | |||
| } | |||
| function wbEnableControls() { | |||
| $('input, select, button').attr('disabled', null); | |||
| $('a').each(function() { $(this).attr('href', $(this).data('old_href')); }); | |||
| } | |||
| export { wbEnableControls, wbDisableControls }; | |||
| @@ -0,0 +1,19 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h } from 'preact'; | |||
| function wbFormatDate(dateStr) { | |||
| if (!dateStr) | |||
| return ( | |||
| <i>{ String(dateStr) }</i> | |||
| ); | |||
| let date = new Date(dateStr); | |||
| return date.toLocaleString(); | |||
| } | |||
| export default wbFormatDate; | |||
| @@ -0,0 +1,18 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h } from 'preact'; | |||
| function wbFormatSpecialValue(value) { | |||
| if (value === null) return (<i>null</i>); | |||
| if (value === undefined) return (<i>undefined</i>); | |||
| if (typeof(value) === 'boolean') return (<i>{ String(value) }</i>); | |||
| if (value === '') return '-'; | |||
| return String(value); | |||
| } | |||
| export default wbFormatSpecialValue; | |||
| @@ -0,0 +1,120 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import { Router, route } from 'preact-router'; | |||
| import WBBrowse from 'wb-browse'; | |||
| import WBSignIn from 'wb-sign-in'; | |||
| import WBSignOut from 'wb-sign-out'; | |||
| import WBLandingPage from 'wb-landing-page'; | |||
| import WBProcessView from 'wb-process-view'; | |||
| import WBContainerView from 'wb-container-view'; | |||
| import WBCollectionView from 'wb-collection-view'; | |||
| import WBCollectionBrowse from 'wb-collection-browse'; | |||
| import WBUsersPage from 'wb-users-page'; | |||
| import WBWorkflowView from 'wb-workflow-view'; | |||
| import WBLaunchWorkflowPage from 'wb-launch-workflow-page'; | |||
| import WBDownloadPage from 'wb-download-page'; | |||
| import WBImageViewerPage from 'wb-image-viewer-page'; | |||
| import WBSharingPage from 'wb-sharing-page'; | |||
| import WBProjectView from 'wb-project-view'; | |||
| import arvadosTypeName from 'arvados-type-name'; | |||
| class WBApp extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.arvHost = window.localStorage['arvHost']; | |||
| this.state.arvToken = window.localStorage['arvToken']; | |||
| if ('currentUser' in window.localStorage) | |||
| this.state.currentUser = JSON.parse(window.localStorage['currentUser']); | |||
| this.loadToolbox(); | |||
| } | |||
| navbarItemUrl(item) { | |||
| if (item['id'] === 'sign-out') { | |||
| return ('/sign-out'); | |||
| } else if (item['id'] === 'home') { | |||
| return ('/browse/' + this.state.currentUser.uuid); | |||
| } else if (item['id'] === 'all-projects') { | |||
| return ('/browse'); | |||
| } else if (item['id'] === 'all-users') { | |||
| return ('/users'); | |||
| } else if (item['id'] === 'shared-with-me') { | |||
| return ('/shared-with-me'); | |||
| } else if (item['id'] === 'whatsnew') { | |||
| return ('https://adared.ch/wba'); | |||
| } | |||
| } | |||
| breadcrumbClicked(item) { | |||
| let objectType = arvadosTypeName(item.uuid.split('-')[1]); | |||
| if (objectType === 'user') | |||
| route('/browse/' + item.uuid) | |||
| else if (objectType === 'group' && item.group_class === 'project') | |||
| route('/browse/' + item.uuid); | |||
| else if (objectType === 'container_request') | |||
| route('/process/' + item.uuid) | |||
| } | |||
| addToToolbox(uuid) { | |||
| this.state.toolboxItems.push(uuid); | |||
| window.localStorage['toolboxItems'] = | |||
| JSON.stringify(this.state.toolboxItems); | |||
| } | |||
| clearToolbox() { | |||
| this.state.toolboxItems = []; | |||
| delete window.localStorage['toolboxItems']; | |||
| } | |||
| loadToolbox() { | |||
| this.state.toolboxItems = ('toolboxItems' in window.localStorage) ? | |||
| JSON.parse(window.localStorage['toolboxItems']) : []; | |||
| } | |||
| render() { | |||
| return ( | |||
| <Router> | |||
| <WBLandingPage path="/" /> | |||
| <WBSignIn path="/sign-in/:mode?" appState={ this.state } /> | |||
| <WBSignOut path='/sign-out' app={ this } /> | |||
| <WBBrowse path="/browse/:ownerUuid?/:activePage?/:objTypeTab?/:collectionPage?/:processPage?/:workflowPage?/:textSearch?" | |||
| app={ this } mode="browse" /> | |||
| <WBBrowse path="/shared-with-me/:activePage?/:textSearch?" | |||
| app={ this } mode="shared-with-me" /> | |||
| <WBProcessView path="/process/:uuid/:page?" app={ this } /> | |||
| <WBContainerView path="/container/:uuid" app={ this } /> | |||
| <WBCollectionView path="/collection/:uuid" app={ this } /> | |||
| <WBCollectionBrowse path='/collection-browse/:uuid/:collectionPath?/:page?' app={ this } /> | |||
| <WBUsersPage path='/users/:page?/:textSearch?' app={ this } /> | |||
| <WBWorkflowView path="/workflow/:uuid" app={ this } /> | |||
| <WBLaunchWorkflowPage path="/workflow-launch/:workflowUuid" app={ this } /> | |||
| <WBDownloadPage path="/download/:blocksBlobUrl/:inline?" app={ this } /> | |||
| <WBImageViewerPage path="/image-viewer/:blobUrl" app={ this } /> | |||
| <WBSharingPage path="/sharing/:uuid" app={ this } /> | |||
| <WBProjectView path="/project/:uuid" app={ this } /> | |||
| </Router> | |||
| ); | |||
| } | |||
| } | |||
| export default WBApp; | |||
| @@ -0,0 +1,255 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import { route } from 'preact-router'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBProjectListing from 'wb-project-listing'; | |||
| import WBInlineSearch from 'wb-inline-search'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBTabs from 'wb-tabs'; | |||
| import WBProcessListing from 'wb-process-listing'; | |||
| import WBCollectionListing from 'wb-collection-listing'; | |||
| import WBWorkflowListing from 'wb-workflow-listing'; | |||
| import WBRenameDialog from 'wb-rename-dialog'; | |||
| import WBDeleteDialog from 'wb-delete-dialog'; | |||
| import WBNewProjectDialog from 'wb-new-project-dialog'; | |||
| import WBEditDescriptionDialog from 'wb-edit-description-dialog'; | |||
| import wbMoveObject from 'wb-move-object'; | |||
| import wbCopyCollection from 'wb-copy-collection'; | |||
| import arvadosTypeName from 'arvados-type-name'; | |||
| class WBBrowseProjectTabs extends Component { | |||
| render({ ownerUuid, selected, newProjectDialogRef, projectListingRef, | |||
| moveHere, copyHere }) { | |||
| return ( | |||
| <WBTabs tabs={ [ | |||
| { 'name': 'Projects', 'isActive': true }, | |||
| ownerUuid ? { 'name': ( <span><i class="fas fa-plus-square text-success"></i> New Project</span> ), | |||
| 'onClick': () => newProjectDialogRef.current.show(ownerUuid, | |||
| () => projectListingRef.current.fetchItems() ) } : null, | |||
| ( ownerUuid && Object.keys(selected).length > 0 ) ? | |||
| { 'name': ( <span><i class="fas fa-compress-arrows-alt text-warning"></i> Move Here</span> ), | |||
| 'onClick': moveHere } : null, | |||
| ( ownerUuid && (uuids => uuids.length > 0 && uuids.length === | |||
| uuids.map(arvadosTypeName).filter(a => (a === 'collection')).length )(Object.keys(selected)) ) ? | |||
| { 'name': ( <span><i class="fas fa-file-import text-warning"></i> Copy Here</span> ), | |||
| 'onClick': copyHere } : null | |||
| ] } /> | |||
| ); | |||
| } | |||
| } | |||
| class WBBrowse extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.renameDialogRef = createRef(); | |||
| this.deleteDialogRef = createRef(); | |||
| this.newProjectDialogRef = createRef(); | |||
| this.projectListingRef = createRef(); | |||
| this.projectTabsRef = createRef(); | |||
| this.editDescriptionDialogRef = createRef(); | |||
| this.state.selected = {}; | |||
| } | |||
| getUrl(params) { | |||
| const mode = ('mode' in params ? params.mode : this.props.mode); | |||
| if (mode === 'shared-with-me') | |||
| return '/shared-with-me/' + | |||
| ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' + | |||
| ('textSearch' in params ? params.textSearch : (this.props.textSearch || '')); | |||
| let res = '/browse/' + | |||
| ('ownerUuid' in params ? params.ownerUuid : (this.props.ownerUuid || '')) + '/' + | |||
| ('activePage' in params ? params.activePage : (this.props.activePage || '')) + '/' + | |||
| ('objTypeTab' in params ? params.objTypeTab : (this.props.objTypeTab || '')) + '/' + | |||
| ('collectionPage' in params ? params.collectionPage : (this.props.collectionPage || '')) + '/' + | |||
| ('processPage' in params ? params.processPage : (this.props.processPage || '')) + '/' + | |||
| ('workflowPage' in params ? params.workflowPage : (this.props.workflowPage || '')) + '/' + | |||
| encodeURIComponent('textSearch' in params ? params.textSearch : (this.props.textSearch || '')); | |||
| return res; | |||
| } | |||
| route(params) { | |||
| route(this.getUrl(params)); | |||
| } | |||
| renameDialog(item, callback) { | |||
| // throw Error('Not implemented'); | |||
| this.renameDialogRef.current.show(item, callback); | |||
| } | |||
| renderRenameLink(item, callback) { | |||
| return ( | |||
| <a href="#" title="Rename" onclick={ e => { e.preventDefault(); this.renameDialog(item, callback); } }> | |||
| <i class="fas fa-edit text-secondary"></i> | |||
| </a> | |||
| ); | |||
| } | |||
| renderEditDescription(item, callback) { | |||
| return ( | |||
| <a href="#" title="Edit description" onclick={ e => { e.preventDefault(); | |||
| this.editDescriptionDialogRef.current.show(item, callback); } }> | |||
| <i class="fas fa-edit text-secondary"></i> | |||
| </a> | |||
| ); | |||
| } | |||
| renderDeleteButton(item, callback) { | |||
| return ( | |||
| <button class="btn btn-outline-danger m-1" title="Delete" | |||
| onclick={ () => this.deleteDialogRef.current.show(item, callback) }> | |||
| <i class="fas fa-trash"></i> | |||
| </button> | |||
| ); | |||
| } | |||
| renderSelectionCell(item) { | |||
| const { selected } = this.state; | |||
| const { uuid } = item; | |||
| return ( | |||
| <div> | |||
| <input type="checkbox" checked={ (uuid in selected) } | |||
| onChange={ e => { | |||
| if (e.target.checked) | |||
| selected[uuid] = true; | |||
| else | |||
| delete selected[uuid]; | |||
| this.projectTabsRef.current.setState({}); | |||
| } } /> { '\u00A0' } | |||
| </div> | |||
| ); | |||
| } | |||
| renderSharingButton(item) { | |||
| return ( | |||
| <a class="btn btn-outline-success m-1" title="Share" | |||
| href={ '/sharing/' + item.uuid }> | |||
| <i class="fas fa-share-alt"></i> | |||
| </a> | |||
| ); | |||
| } | |||
| moveOrCopyOp(op) { | |||
| const { ownerUuid, app } = this.props; | |||
| const { selected } = this.state; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = new Promise(accept => accept()); | |||
| const uuids = Object.keys(selected); | |||
| for (let i = 0; i < uuids.length; i++) { | |||
| prom = prom.then(() => op(arvHost, arvToken, uuids[i], ownerUuid)); | |||
| prom = prom.then(() => ( delete selected[uuids[i]] )); | |||
| prom = prom.catch(() => {}); | |||
| } | |||
| prom = prom.then(() => this.setState({})); | |||
| } | |||
| moveHere() { | |||
| this.moveOrCopyOp(wbMoveObject); | |||
| } | |||
| copyHere() { | |||
| this.moveOrCopyOp(wbCopyCollection); | |||
| } | |||
| render({ mode, ownerUuid, activePage, app, | |||
| objTypeTab, collectionPage, processPage, workflowPage, | |||
| textSearch }, { selected }) { | |||
| const commonProps = { | |||
| renderRenameLink: (it, cb) => this.renderRenameLink(it, cb), | |||
| renderEditDescription: (it, cb) => this.renderEditDescription(it, cb), | |||
| renderDeleteButton: (it, cb) => this.renderDeleteButton(it, cb), | |||
| renderSelectionCell: it => this.renderSelectionCell(it), | |||
| renderSharingButton: it => this.renderSharingButton(it), | |||
| textSearch, | |||
| app, | |||
| appState: app.state, | |||
| arvHost: app.state.arvHost, | |||
| arvToken: app.state.arvToken, | |||
| ownerUuid | |||
| }; | |||
| const { currentUser } = app.state; | |||
| const noDefaultTab = (!ownerUuid || ownerUuid === currentUser.uuid); | |||
| return ( | |||
| <div> | |||
| <WBRenameDialog app={ app } ref={ this.renameDialogRef } /> | |||
| <WBDeleteDialog app={ app } ref={ this.deleteDialogRef } /> | |||
| <WBNewProjectDialog app={ app } ref={ this.newProjectDialogRef } /> | |||
| <WBEditDescriptionDialog app={ app } ref={ this.editDescriptionDialogRef } /> | |||
| <WBNavbarCommon app={ app } | |||
| activeItem={ mode === 'shared-with-me' ? 'shared-with-me' : | |||
| (!ownerUuid) ? 'all-projects' : | |||
| (ownerUuid === app.state.currentUser.uuid) ? 'home' : null } | |||
| textSearch={ textSearch } | |||
| textSearchNavigate={ textSearch => route(this.getUrl({ textSearch, | |||
| activePage: 0, collectionPage: 0, processPage: 0, workflowPage: 0 })) } /> | |||
| <WBArvadosCrumbs mode={ mode } uuid={ ownerUuid } app={ app } /> | |||
| <WBBrowseProjectTabs ref={ this.projectTabsRef } ownerUuid={ ownerUuid } | |||
| selected={ selected } newProjectDialogRef={ this.newProjectDialogRef } | |||
| projectListingRef={ this.projectListingRef } moveHere={ () => this.moveHere() } | |||
| copyHere={ () => this.copyHere() } /> | |||
| <WBProjectListing ref={ this.projectListingRef } | |||
| mode={ mode } | |||
| itemsPerPage="5" | |||
| activePage={ Number(activePage || 0) } | |||
| getPageUrl={ i => this.getUrl({ 'activePage': i }) } | |||
| { ...commonProps } /> | |||
| { (mode !== 'browse') ? null : ( | |||
| <WBTabs tabs={ [ | |||
| { 'id': 'collection', 'name': 'Collections', 'isActive': ((!objTypeTab && !noDefaultTab) || objTypeTab === 'collection') }, | |||
| { 'id': 'process', 'name': 'Processes', 'isActive': (objTypeTab === 'process') }, | |||
| { 'id': 'workflow', 'name': 'Workflows', 'isActive': (objTypeTab === 'workflow') } ] } | |||
| onTabChanged={ tab => this.route({ 'objTypeTab': tab['id'] }) } /> | |||
| ) } | |||
| { | |||
| (mode !== 'browse') ? null : | |||
| ((!objTypeTab && !noDefaultTab) || objTypeTab === 'collection') ? ( | |||
| <WBCollectionListing | |||
| itemsPerPage="20" | |||
| activePage={ Number(collectionPage || 0) } | |||
| getPageUrl={ i => this.getUrl({ 'collectionPage': i }) } | |||
| { ...commonProps } /> | |||
| ) : (objTypeTab === 'process') ? ( | |||
| <WBProcessListing | |||
| itemsPerPage="20" | |||
| activePage={ Number(processPage || 0) } | |||
| onPageChanged={ i => this.route({ 'processPage': i }) } | |||
| { ...commonProps } /> | |||
| ) : (objTypeTab === 'workflow') ? ( | |||
| <WBWorkflowListing | |||
| itemsPerPage="20" | |||
| page={ Number(workflowPage || 0) } | |||
| getPageUrl={ i => this.getUrl({ 'workflowPage': i }) } | |||
| { ...commonProps } /> | |||
| ) : null | |||
| } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBBrowse; | |||
| @@ -0,0 +1,32 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBCollectionContent from 'wb-collection-content'; | |||
| class WBCollectionBrowse extends Component { | |||
| render({ app, uuid, collectionPath, page }, {}) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="my-2"> | |||
| This is the collection browser for { uuid } | |||
| </div> | |||
| <WBCollectionContent app={ app } uuid={ uuid } | |||
| collectionPath={ collectionPath } page={ Number(page || 0) } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBCollectionBrowse; | |||
| @@ -0,0 +1,36 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBCommonFields from 'wb-common-fields'; | |||
| import WBCollectionFields from 'wb-collection-fields'; | |||
| class WBCollectionView extends Component { | |||
| render({ app, uuid }, {}) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="my-2"> | |||
| This is the collection view for { uuid } | |||
| </div> | |||
| <h2>Common Fields</h2> | |||
| <WBCommonFields app={ app } uuid={ uuid } /> | |||
| <h2>Collection Fields</h2> | |||
| <WBCollectionFields app={ app } uuid={ uuid } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBCollectionView; | |||
| @@ -0,0 +1,40 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBCommonFields from 'wb-common-fields'; | |||
| import WBContainerFields from 'wb-container-fields'; | |||
| import WBLiveLogs from 'wb-live-logs'; | |||
| class WBContainerView extends Component { | |||
| render({ app, uuid }) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="my-2"> | |||
| This is the container view for { uuid } | |||
| </div> | |||
| <h2>Common Fields</h2> | |||
| <WBCommonFields app={ app } uuid={ uuid } /> | |||
| <h2>Container Fields</h2> | |||
| <WBContainerFields app={ app } uuid={ uuid } /> | |||
| <h2>Live Logs</h2> | |||
| <WBLiveLogs app={ app } uuid={ uuid } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBContainerView; | |||
| @@ -0,0 +1,133 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| function contentTypeFromFilename(name) { | |||
| let ext = name.split('.'); | |||
| ext = ext[ext.length - 1].toUpperCase(); | |||
| if (ext === 'TXT') | |||
| return 'text/plain; charset=utf-8'; | |||
| if (ext === 'JPG' || ext === 'JPEG') | |||
| return 'image/jpeg'; | |||
| if (ext === 'PNG') | |||
| return 'image/png'; | |||
| return 'application/octet-stream; charset=utf-8'; | |||
| } | |||
| class WBDownloadPage extends Component { | |||
| componentDidMount() { | |||
| const { app, blocksBlobUrl, inline } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = new Promise((accept, reject) => { | |||
| const xhr = new XMLHttpRequest(); | |||
| xhr.open('GET', blocksBlobUrl); | |||
| xhr.onreadystatechange = () => { | |||
| if (xhr.readyState !== 4) | |||
| return; | |||
| if (xhr.status !== 200) | |||
| reject(xhr); | |||
| else | |||
| accept(xhr); | |||
| }; | |||
| xhr.responseType = 'blob'; | |||
| xhr.send(); | |||
| }); | |||
| prom = prom.then(xhr => xhr.response.text()); | |||
| let name, file; | |||
| const { streamSaver, location } = window; | |||
| streamSaver.mitm = location.protocol + '//' + | |||
| location.hostname + (location.port ? | |||
| ':' + location.port : '') + '/mitm.html'; | |||
| let fileStream; | |||
| let writer; | |||
| let done = false; | |||
| prom = prom.then(text => { | |||
| let _; | |||
| [ _, _, name, file ] = JSON.parse(text); | |||
| fileStream = streamSaver.createWriteStream(name, { | |||
| size: file[1], | |||
| inline: inline, | |||
| contentType: contentTypeFromFilename(name) | |||
| }); | |||
| writer = fileStream.getWriter(); | |||
| window.onunload = () => { | |||
| writer.abort() | |||
| }; | |||
| window.onbeforeunload = evt => { | |||
| if (!done) { | |||
| evt.returnValue = `Are you sure you want to leave?`; | |||
| } | |||
| }; | |||
| const filters = [ | |||
| ['service_type', '=', 'proxy'] | |||
| ]; | |||
| return makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/keep_services?filters=' + | |||
| encodeURIComponent(JSON.stringify(filters))); | |||
| }); | |||
| prom = prom.then(xhr => { | |||
| const services = xhr.response.items; | |||
| const i = Math.floor(Math.random() * services.length); | |||
| const proxy = services[i]; | |||
| let prom_1 = new Promise(accept => accept()); | |||
| for (let k = 0; k < file[0].length; k++) { | |||
| const loc = file[0][k]; | |||
| prom_1 = prom_1.then(() => makeArvadosRequest( | |||
| proxy.service_host + ':' + proxy.service_port, | |||
| arvToken, | |||
| '/' + loc[0], | |||
| { 'useSsl': proxy.service_ssl_flag, | |||
| 'responseType': 'arraybuffer' } | |||
| )); | |||
| prom_1 = prom_1.then(xhr_1 => { | |||
| const blk = xhr_1.response.slice(loc[1], loc[2]); | |||
| // const r = new Response(blk); | |||
| // r.body.pipeTo(fileStream); | |||
| return writer.write(new Uint8Array(blk)); | |||
| }); | |||
| } | |||
| return prom_1; | |||
| }); | |||
| prom = prom.then(() => { | |||
| writer.close(); | |||
| done = true; | |||
| }); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div class="container-fluid"> | |||
| <div class="card my-3"> | |||
| <div class="card-body"> | |||
| Downloading, please wait... | |||
| </div> | |||
| </div> | |||
| <div class="alert alert-danger" role="alert"> | |||
| Do not close this window until the download is finished. | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBDownloadPage; | |||
| @@ -0,0 +1,110 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| function downloadFile(arvHost, arvToken, file) { | |||
| const blockRefs = file[0]; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/keep_services'); | |||
| let proxy; | |||
| prom = prom.then(xhr => { | |||
| const services = xhr.response['items']; | |||
| const proxies = services.filter(svc => (svc.service_type === 'proxy')); | |||
| const n = Math.floor(Math.random() * proxies.length); | |||
| proxy = proxies[n]; | |||
| }); | |||
| const blocks = []; | |||
| for (let i = 0; i < blockRefs.length; i++) { | |||
| let locator, start, end; | |||
| prom = prom.then(() => { | |||
| [ locator, start, end ] = blockRefs[i]; | |||
| return makeArvadosRequest( | |||
| proxy.service_host + ':' + proxy.service_port, | |||
| arvToken, '/' + locator, | |||
| { 'useSsl': proxy.service_ssl_flag, | |||
| 'responseType': 'arraybuffer' } | |||
| ); | |||
| }); | |||
| prom = prom.then(xhr => blocks.push(xhr.response.slice(start, end))); | |||
| } | |||
| prom = prom.then(() => { | |||
| const url = URL.createObjectURL(new Blob(blocks)); | |||
| const totalSize = blocks.reduce((a, b) => a.length + b.length); | |||
| const big = new Uint8Array(totalSize); | |||
| for (let i = 0, pos = 0; i < blocks.length; i++) { | |||
| big.set(blocks[i], pos); | |||
| pos += blocks[i].length; | |||
| } | |||
| // papayaContainers[0].startPapaya(); | |||
| const poll = () => { | |||
| setTimeout(() => { | |||
| console.log('Polling Papaya startup...') | |||
| if (window.papaya && window.papaya.Container) { // window.papayaContainers && window.papayaContainers[0]) { | |||
| console.log('Great, Papaya started!'); | |||
| papaya.Container.startPapaya(); | |||
| //papaya.Container.addImage(0, big.buffer); | |||
| document.body.id = "bod"; | |||
| document.body.style.background = "#555"; | |||
| papaya.Container.addViewer("bod", { | |||
| 'binaryImages': [ big.buffer ], | |||
| 'noNewFiles': true, | |||
| }); | |||
| } else | |||
| poll(); | |||
| }, 1000); | |||
| }; | |||
| //setTimeout(poll, 10000); | |||
| poll(); | |||
| }); | |||
| } | |||
| class WBImageViewerPage extends Component { | |||
| componentDidMount() { | |||
| const { blobUrl, app } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = new Promise((accept, reject) => { | |||
| const xhr = new XMLHttpRequest(); | |||
| xhr.open('GET', blobUrl); | |||
| xhr.onreadystatechange = () => { | |||
| if (xhr.readyState !== 4) return; | |||
| if (xhr.status !== 200) reject(xhr); | |||
| else accept(xhr); | |||
| }; | |||
| xhr.responseType = 'blob'; | |||
| xhr.send(); | |||
| }); | |||
| prom = prom.then(xhr => xhr.response.text()); | |||
| prom = prom.then(data => { | |||
| data = JSON.parse(data); | |||
| downloadFile(arvHost, arvToken, data.file); | |||
| }); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div> | |||
| <script language="javascript" src="/js/papaya.js"></script> | |||
| <div id="papaya" style="width: auto; height: 100%; margin: 0;"></div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBImageViewerPage; | |||
| @@ -0,0 +1,27 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import { route } from 'preact-router'; | |||
| class WBLandingPage extends Component { | |||
| componentDidMount() { | |||
| let { arvHost, arvToken } = window.localStorage; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, '/arvados/v1/users/current'); | |||
| prom = prom.then(xhr => route('/browse/' + xhr.response['uuid'])); | |||
| prom = prom.catch(() => route('/sign-in')); | |||
| } | |||
| render() { | |||
| return ( | |||
| <div>Please wait...</div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBLandingPage; | |||
| @@ -0,0 +1,185 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import { route } from 'preact-router'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBBrowseDialog from 'wb-browse-dialog'; | |||
| import WBTable from 'wb-table'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import { wbDisableControls, wbEnableControls } from 'wb-disable-controls'; | |||
| import linkState from 'linkstate'; | |||
| import wbParseWorkflowDef from 'wb-parse-workflow-def'; | |||
| import { wbParseWorkflowInputs, wbSubmitContainerRequest } from 'wb-submit-container-request'; | |||
| import WBWorkflowInput from 'wb-workflow-input'; | |||
| import { parseKeepRef } from 'wb-process-misc'; | |||
| class WBLaunchWorkflowPage extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.browseDialogRef = createRef(); | |||
| this.state.inputs = {}; | |||
| this.state.errors = []; | |||
| this.state.placeInSubProject = true; | |||
| } | |||
| componentDidMount() { | |||
| let { app, workflowUuid } = this.props; | |||
| let { arvHost, arvToken } = app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/workflows/' + workflowUuid); | |||
| prom = prom.then(xhr => { | |||
| const def = wbParseWorkflowDef(xhr.response.definition); | |||
| const inputs = {}; | |||
| const main = def['$graph'].find(a => (a.id === '#main')); | |||
| main.inputs.map(a => (inputs[a.id] = JSON.stringify(parseKeepRef(a.default)))); | |||
| this.setState({ | |||
| 'workflow': xhr.response, | |||
| 'workflowDefinition': def, | |||
| 'defaultProcessName': xhr.response.name + ' ' + (new Date().toISOString()), | |||
| 'defaultProcessDescription': xhr.response.description, | |||
| inputs | |||
| }); | |||
| }); | |||
| } | |||
| submit() { | |||
| // first see if all inputs are parseable | |||
| const { app, workflowUuid } = this.props; | |||
| const { arvHost, arvToken, currentUser } = app.state; | |||
| const { workflowDefinition, projectUuid, | |||
| processName, processDescription, | |||
| defaultProcessName, defaultProcessDescription, | |||
| placeInSubProject } = this.state; | |||
| const errors = []; | |||
| const inputs = wbParseWorkflowInputs(workflowDefinition, | |||
| this.state.inputs, errors); | |||
| if (errors.length > 0) { | |||
| this.setState({ errors }); | |||
| return; | |||
| } | |||
| const params = { | |||
| arvHost, arvToken, inputs, | |||
| processName: processName || defaultProcessName, | |||
| processDescription: processDescription || defaultProcessDescription, | |||
| projectUuid: projectUuid || currentUser.uuid, | |||
| workflowUuid, workflowDefinition, placeInSubProject | |||
| } | |||
| wbDisableControls(); | |||
| let prom = wbSubmitContainerRequest(params); | |||
| prom = prom.then(xhr => { | |||
| wbEnableControls(); | |||
| route('/process/' + xhr.response.uuid); | |||
| }); | |||
| prom = prom.catch(exc => { | |||
| wbEnableControls(); | |||
| this.setState({ errors: [ exc.message ] }); | |||
| }); | |||
| } | |||
| render({ app, workflowUuid }, | |||
| { workflow, workflowDefinition, projectUuid, processName, processDescription, | |||
| defaultProcessName, defaultProcessDescription, errors, placeInSubProject }) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBBrowseDialog app={ app } ref={ this.browseDialogRef } /> | |||
| { workflow ? | |||
| (<form class="container-fluid"> | |||
| <h1>Launch Workflow</h1> | |||
| <div class="form-group"> | |||
| <label>Workflow</label> | |||
| <WBArvadosCrumbs app={ app } uuid={ workflowUuid } /> | |||
| </div> | |||
| <div class="form-group"> | |||
| <label for="projectUuid">Project UUID</label> | |||
| <div class="input-group mb-3"> | |||
| <input type="text" class="form-control" id="projectUuid" | |||
| placeholder="Enter Project UUID" aria-label="Project UUID" | |||
| aria-describedby="button-addon2" value={ projectUuid } | |||
| onChange={ linkState(this, 'projectUuid') } /> | |||
| <div class="input-group-append"> | |||
| <button class="btn btn-primary" type="button" | |||
| id="button-addon2" onclick={ e => { e.preventDefault(); | |||
| this.browseDialogRef.current.show('owner', false, | |||
| projectUuid => this.setState({ projectUuid })); } }>Browse</button> | |||
| </div> | |||
| </div> | |||
| { projectUuid ? ( | |||
| <WBArvadosCrumbs app={ app } uuid={ projectUuid } /> | |||
| ) : null } | |||
| </div> | |||
| <div class="form-check mb-3"> | |||
| <input class="form-check-input" type="checkbox" | |||
| checked={ placeInSubProject ? 'checked' : null } | |||
| onchange={ e => (this.state.placeInSubProject = e.target.checked) } | |||
| id="placeInSubProject" /> | |||
| <label class="form-check-label" for="placeInSubProject"> | |||
| Place in a daily sub-project | |||
| </label> | |||
| </div> | |||
| <div class="form-group"> | |||
| <label for="processName">Process Name</label> | |||
| <input type="text" class="form-control" id="processName" | |||
| placeholder={ defaultProcessName } value={ processName } | |||
| onChange={ linkState(this, 'processName') }/> | |||
| </div> | |||
| <div class="form-group"> | |||
| <label for="processDescription">Process Description</label> | |||
| <input type="text" class="form-control" id="processDescription" | |||
| placeholder={ defaultProcessDescription } value={ processDescription } | |||
| onChange={ linkState(this, 'processDescription') } /> | |||
| </div> | |||
| <div class="form-group"> | |||
| <label for="inputs">Inputs</label> | |||
| <WBTable columns={ [ 'Name', 'Value'] } | |||
| rows={ workflowDefinition.$graph.find(a => (a.id === '#main')).inputs.map(it => [ | |||
| it.label || it.id, | |||
| ( <WBWorkflowInput app={ app } inputSpec={ it } | |||
| inputsDict={ this.state.inputs } | |||
| browseDialogRef={ this.browseDialogRef } /> ) | |||
| ]) } /> | |||
| </div> | |||
| { errors.length > 0 ? ( | |||
| <div class="form-group"> | |||
| { errors.map(err => ( | |||
| <div class="alert alert-danger" role="alert"> | |||
| { err } | |||
| </div> | |||
| ))} | |||
| </div> | |||
| ) : null } | |||
| <div class="form-group"> | |||
| <button class="btn btn-success" onclick={ e => { e.preventDefault(); this.submit(); } }> | |||
| Submit | |||
| </button> | |||
| </div> | |||
| </form>) : <div>Loading...</div> } | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBLaunchWorkflowPage; | |||
| @@ -0,0 +1,95 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| import WBCommonFields from 'wb-common-fields'; | |||
| import WBContainerRequestFields from 'wb-container-request-fields'; | |||
| import WBProcessListing from 'wb-process-listing'; | |||
| import WBProcessDashboard from 'wb-process-dashboard'; | |||
| class WBProcessView extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.objectUrls = []; | |||
| } | |||
| getUrl(props) { | |||
| const page = ('page' in props ? props.page : this.props.page); | |||
| return ('/process/' + this.props.uuid + | |||
| (page ? '/' + page : '')); | |||
| } | |||
| fetchData() { | |||
| let { arvHost, arvToken } = this.props.app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/container_requests/' + this.props.uuid); | |||
| let req; | |||
| let cont; | |||
| prom = prom.then(xhr => { | |||
| req = xhr.response; | |||
| if (req.container_uuid) { | |||
| let prom_1 = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/containers/' + req.container_uuid); | |||
| prom_1 = prom_1.then(xhr => (cont = xhr.response)); | |||
| return prom_1; | |||
| } | |||
| }); | |||
| prom = prom.then(() => { | |||
| this.setState({ | |||
| 'request': req, | |||
| 'container': cont | |||
| }); | |||
| }); | |||
| } | |||
| componentDidMount() { | |||
| this.fetchData(); | |||
| } | |||
| componentWillReceiveProps(nextProps) { | |||
| this.props = nextProps; | |||
| this.setState({ 'objectUrls': [], 'request': null, 'container': null }); | |||
| this.fetchData(); | |||
| } | |||
| render({ app, uuid, page }, { container }) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="my-2"> | |||
| This is the process view for { uuid } | |||
| </div> | |||
| <h2>Children Dashboard</h2> | |||
| <WBProcessDashboard app={ app } parentProcessUuid={ uuid } lazy={ true } /> | |||
| <h2>Common Fields</h2> | |||
| <WBCommonFields app={ app } uuid={ uuid } /> | |||
| <h2>Container Request Fields</h2> | |||
| <WBContainerRequestFields app={ app } uuid={ uuid } /> | |||
| <h2>Children</h2> | |||
| <WBProcessListing app={ app } | |||
| appState={ app.state } | |||
| requestingContainerUuid={ container ? container.uuid : null } | |||
| waitForNextProps={ !container } | |||
| itemsPerPage="20" | |||
| activePage={ Number(page || 0) } | |||
| getPageUrl={ page => this.getUrl({ page }) } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBProcessView; | |||
| @@ -0,0 +1,36 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBCommonFields from 'wb-common-fields'; | |||
| import WBProjectFields from 'wb-project-fields'; | |||
| class WBProjectView extends Component { | |||
| render({ app, uuid }, {}) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="my-2"> | |||
| This is the project view for { uuid } | |||
| </div> | |||
| <h2>Common Fields</h2> | |||
| <WBCommonFields app={ app } uuid={ uuid } /> | |||
| <h2>Project Fields</h2> | |||
| <WBProjectFields app={ app } uuid={ uuid } /> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBProjectView; | |||
| @@ -0,0 +1,177 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component, createRef } from 'preact'; | |||
| import WBNavbarCommon from 'wb-navbar-common'; | |||
| import WBArvadosCrumbs from 'wb-arvados-crumbs'; | |||
| import WBNameAndUuid from 'wb-name-and-uuid'; | |||
| import WBSelect from 'wb-select'; | |||
| import WBTable from 'wb-table'; | |||
| import WBPickObjectDialog from 'wb-pick-object-dialog'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBSharingPage extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| this.state.rows = []; | |||
| this.dialogRef = createRef(); | |||
| } | |||
| componentDidMount() { | |||
| this.fetchData(); | |||
| } | |||
| fetchData() { | |||
| const { app, uuid } = this.props; | |||
| const { arvHost, arvToken } = app.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/permissions/' + encodeURIComponent(uuid) + | |||
| '?limit=100000'); | |||
| prom = prom.then(xhr => this.setState({ | |||
| 'entries': xhr.response.items, | |||
| 'rows': this.prepareRows(xhr.response.items) | |||
| })); | |||
| } | |||
| deleteEntry(it) { | |||
| it._delete = true; | |||
| this.setState({ rows: this.prepareRows(this.state.entries) }); | |||
| } | |||
| prepareRows(items) { | |||
| const { app } = this.props; | |||
| return items.filter(it => (!it._delete)).map(it => [ | |||
| ( <WBNameAndUuid app={ app } uuid={ it.tail_uuid } /> ), | |||
| ( <WBSelect value={ it.name } | |||
| options={ ['can_read', 'can_write', 'can_manage'] } | |||
| onChange={ e => this.modifyEntry(it, e.target.value) } /> ), | |||
| ( <button class="btn btn-outline-danger m-1" title="Delete" | |||
| onclick={ () => this.deleteEntry(it) }> | |||
| <i class="fas fa-trash"></i> | |||
| </button> ) | |||
| ]); | |||
| } | |||
| modifyEntry(it, newPermissionName) { | |||
| it.name = newPermissionName; | |||
| it._dirty = true; | |||
| // this.setState({ rows: this.prepareRows(this.state.entries) }); | |||
| } | |||
| addEntry(it, permissionName='can_read') { | |||
| // throw Error('Not implemented'); | |||
| const { uuid } = this.props; | |||
| let { entries } = this.state; | |||
| if (entries.filter(e => (e.tail_uuid === it.uuid)).length > 0) | |||
| return; // already in the list | |||
| const e = { | |||
| //_dirty: true, | |||
| link_class: 'permission', | |||
| head_uuid: uuid, | |||
| tail_uuid: it.uuid, | |||
| name: permissionName | |||
| }; | |||
| entries = entries.concat([e]); | |||
| this.setState({ | |||
| entries, | |||
| rows: this.prepareRows(entries) | |||
| }); | |||
| } | |||
| disableControls() { | |||
| $('input, select, button').attr('disabled', 'disabled'); | |||
| $('a').each(function() { $(this).data('old_href', $(this).attr('href')); }); | |||
| $('a').attr('href', null); | |||
| } | |||
| enableControls() { | |||
| $('input, select, button').attr('disabled', null); | |||
| $('a').each(function() { $(this).attr('href', $(this).data('old_href')); }); | |||
| } | |||
| save() { | |||
| const { entries } = this.state; | |||
| const { arvHost, arvToken } = this.props.app.state; | |||
| let prom = new Promise(accept => accept()); | |||
| this.disableControls(); | |||
| this.setState({ working: true }); | |||
| for (let i = 0; i < entries.length; i++) { | |||
| const e = entries[i]; | |||
| //if (!e._dirty && !e._delete) | |||
| //continue; | |||
| if (!e.uuid) { | |||
| prom = prom.then(() => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/links', | |||
| { 'method': 'POST', | |||
| 'data': JSON.stringify({ | |||
| 'link_class': 'permission', | |||
| 'head_uuid': e.head_uuid, | |||
| 'tail_uuid': e.tail_uuid, | |||
| 'name': e.name | |||
| }) })); | |||
| } else if (e._delete) { | |||
| prom = prom.then(() => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/links/' + e.uuid, | |||
| { 'method': 'DELETE' })); | |||
| } else if (e._dirty) { | |||
| prom = prom.then(() => makeArvadosRequest(arvHost, arvToken, | |||
| '/arvados/v1/links/' + e.uuid, | |||
| { 'method': 'PUT', | |||
| 'data': JSON.stringify({ | |||
| 'name': e.name | |||
| }) })); | |||
| } | |||
| prom = prom.catch(() => {}); | |||
| } | |||
| prom = prom.then(() => { | |||
| this.enableControls(); | |||
| this.fetchData(); | |||
| this.setState({ working: false }); | |||
| }); | |||
| } | |||
| render({ app, uuid }, { rows, working }) { | |||
| return ( | |||
| <div> | |||
| <WBNavbarCommon app={ app } /> | |||
| <WBArvadosCrumbs app={ app } uuid={ uuid } /> | |||
| <div class="container-fluid"> | |||
| <div class="my-2"> | |||
| This is the sharing management page for { uuid } | |||
| </div> | |||
| <WBTable columns={ [ 'Name', 'Permission', '' ] } | |||
| headerClasses={ [ null, null, 'w-1' ] } | |||
| rows={ rows } /> | |||
| <WBPickObjectDialog app={ app } ref={ this.dialogRef } /> | |||
| { working ? (<div class="progress my-2"> | |||
| <div class={ 'progress-bar progress-bar-striped progress-bar-animated' } | |||
| role="progressbar" aria-valuenow="100" aria-valuemin="0" | |||
| aria-valuemax="100" style="width: 100%"></div> | |||
| </div>) : null } | |||
| <button class="btn btn-outline-secondary mr-2" | |||
| onclick={ () => this.dialogRef.current.show('Select User', 'user', it => this.addEntry(it)) }>Add User...</button> | |||
| <button class="btn btn-outline-secondary mr-2" | |||
| onclick={ () => this.dialogRef.current.show('Select Group', 'group', it => this.addEntry(it), [['group_class', '=', 'role']]) }>Add Group...</button> | |||
| <button class="btn btn-primary mr-2" | |||
| onclick={ () => this.save() }>Save</button> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBSharingPage; | |||
| @@ -0,0 +1,109 @@ | |||
| // | |||
| // Copyright (C) Stanislaw Adaszewski, 2020 | |||
| // Contact: s.adaszewski@gmail.com | |||
| // Website: https://adared.ch/wba | |||
| // License: GPLv3 | |||
| // | |||
| import { h, Component } from 'preact'; | |||
| import { route } from 'preact-router'; | |||
| import WBNavbar from 'wb-navbar'; | |||
| import WBTabs from 'wb-tabs'; | |||
| import linkState from 'linkstate'; | |||
| import makeArvadosRequest from 'make-arvados-request'; | |||
| class WBSignIn extends Component { | |||
| constructor(...args) { | |||
| super(...args); | |||
| const search = new URLSearchParams(window.location.search); | |||
| this.state.arvHost = window.localStorage.arvHost; | |||
| this.state.arvToken = search.get('api_token'); | |||
| } | |||
| componentDidMount() { | |||
| // const arvHost = window.localStorage.arvHost; | |||
| // const arvToken = search.get('api_token'); | |||
| const { arvHost, arvToken } = this.state; | |||
| if (arvHost && arvToken) { | |||
| this.state.arvHost = arvHost; | |||
| this.state.arvToken = arvToken; | |||
| this.submitToken(); | |||
| } | |||
| } | |||
| submit() { | |||
| const { mode } = this.props; | |||
| if (mode === 'token') | |||
| this.submitToken(); | |||
| else if (!mode || mode === 'sso') | |||
| this.submitSingleSignOn(); | |||
| else | |||
| throw Error('Unsupported mode'); | |||
| } | |||
| submitSingleSignOn() { | |||
| const { arvHost } = this.state; | |||
| window.localStorage.arvHost = arvHost; | |||
| window.location = 'https://' + arvHost + '/login?return_to=' | |||
| + encodeURIComponent(window.location.protocol + '//' + window.location.host + '/sign-in/token'); | |||
| } | |||
| submitToken() { | |||
| let { appState } = this.props; | |||
| let { arvHost, arvToken } = this.state; | |||
| let prom = makeArvadosRequest(arvHost, arvToken, '/arvados/v1/users/current'); | |||
| prom = prom.then(xhr => { | |||
| window.localStorage['arvHost'] = arvHost; | |||
| window.localStorage['arvToken'] = arvToken; | |||
| window.localStorage['currentUser'] = JSON.stringify(xhr.response); | |||
| appState.arvHost = arvHost; | |||
| appState.arvToken = arvToken; | |||
| appState.currentUser = xhr.response; | |||
| route('/browse/' + xhr.response['uuid']); | |||
| }); | |||
| prom = prom.catch(() => { | |||
| alert('Sign in unsuccessful. Verify your input and try again.') | |||
| }); | |||
| } | |||
| render({ mode }, { arvHost, arvToken }) { | |||
| return ( | |||
| <div> | |||
| <WBNavbar /> | |||
| <div class="container my-3"> | |||
| <div class="row justify-content-center"> | |||
| <div class="col-6"> | |||
| <h1>Sign In</h1> | |||
| <WBTabs class="my-3" tabs={ [ { name: 'SSO', isActive: (!mode || mode === 'sso') }, | |||
| { name: 'Token', isActive: (mode === 'token') } ] } | |||
| onTabChanged={ t => route(t.name === 'Token' ? '/sign-in/token' : '/sign-in/sso') } /> | |||
| <form> | |||
| <div class="form-group"> | |||
| <label for="arvHost">Arvados API Host</label> | |||
| <input type="text" class="form-control" id="arvHost" | |||
| placeholder="Enter Arvados API Host" | |||
| value={ arvHost } | |||
| onInput={ linkState(this, 'arvHost') } /> | |||
| </div> | |||
| { mode === 'token' ? ( | |||
| <div class="form-group"> | |||
| <label for="arvToken">Token</label> | |||
| <input type="text" class="form-control" id="arvToken" | |||
| placeholder="Enter Arvados API Token" | |||
| value={ arvToken } | |||
| onInput={ linkState(this, 'arvToken') } /> | |||
| </div> | |||
| ) : null } | |||
| <button type="submit" class="btn btn-primary" | |||
| onclick={ e => { e.preventDefault(); this.submit(); } }>Submit</button> | |||
| </form> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| export default WBSignIn; | |||