IF YOU WOULD LIKE TO GET AN ACCOUNT, please write an email to s dot adaszewski at gmail dot com. User accounts are meant only to report issues and/or generate pull requests. This is a purpose-specific Git hosting for ADARED projects. Thank you for your understanding!
Browse Source

Merge pull request 'master' (#1) from sadaszewski1/wba:master into master

master
sadaszewski 3 years ago
parent
commit
f98744314b
100 changed files with 7842 additions and 0 deletions
  1. +9
    -0
      .gitignore
  2. +77
    -0
      backend/keephelper/__main__.py
  3. +42
    -0
      backend/nginx/conf/nginx.conf
  4. +15
    -0
      backend/srv.py
  5. +4
    -0
      backend/supervisord/supervisord.conf
  6. +10
    -0
      dockerfiles/wba/Dockerfile
  7. +18
    -0
      dockerfiles/wba/files/default.conf
  8. +41
    -0
      dockerfiles/wba/files/wba/nginx/conf/nginx.conf
  9. +0
    -0
      dockerfiles/wba/files/wba/nginx/run/.keep
  10. +0
    -0
      dockerfiles/wba/files/wba/nginx/tmp/.keep
  11. +0
    -0
      dockerfiles/wba/files/wba/supervisord/run/.keep
  12. +8
    -0
      dockerfiles/wba/files/wba/supervisord/supervisord.conf
  13. +731
    -0
      frontend/package-lock.json
  14. +32
    -0
      frontend/package.json
  15. +67
    -0
      frontend/rollup.config.js
  16. +27
    -0
      frontend/src/css/index.css
  17. +25
    -0
      frontend/src/html/index.html
  18. +20
    -0
      frontend/src/js/arvados/base/arvados-object-name.js
  19. +28
    -0
      frontend/src/js/arvados/base/arvados-type-name.js
  20. +34
    -0
      frontend/src/js/arvados/base/detect-hashes.js
  21. +34
    -0
      frontend/src/js/arvados/base/detect-uuids.js
  22. +51
    -0
      frontend/src/js/arvados/base/fetch-object-parents.js
  23. +67
    -0
      frontend/src/js/arvados/base/make-arvados-request.js
  24. +35
    -0
      frontend/src/js/arvados/base/url-for-object.js
  25. +18
    -0
      frontend/src/js/arvados/base/wb-delete-object.js
  26. +45
    -0
      frontend/src/js/arvados/base/wb-fetch-objects.js
  27. +79
    -0
      frontend/src/js/arvados/base/wb-id-tools.js
  28. +20
    -0
      frontend/src/js/arvados/base/wb-move-object.js
  29. +23
    -0
      frontend/src/js/arvados/base/wb-rename-object.js
  30. +23
    -0
      frontend/src/js/arvados/base/wb-update-field.js
  31. +32
    -0
      frontend/src/js/arvados/collection/wb-copy-collection.js
  32. +64
    -0
      frontend/src/js/arvados/collection/wb-download-file.js
  33. +35
    -0
      frontend/src/js/arvados/collection/wb-manifest-worker-wrapper.js
  34. +21
    -0
      frontend/src/js/arvados/process/wb-input-spec-info.js
  35. +18
    -0
      frontend/src/js/arvados/process/wb-parse-workflow-def.js
  36. +18
    -0
      frontend/src/js/arvados/process/wb-process-misc.js
  37. +27
    -0
      frontend/src/js/arvados/process/wb-process-state-name.js
  38. +169
    -0
      frontend/src/js/arvados/process/wb-submit-container-request.js
  39. +32
    -0
      frontend/src/js/arvados/process/wb-uuids-to-cwl.js
  40. +55
    -0
      frontend/src/js/component/wb-arvados-crumbs.js
  41. +251
    -0
      frontend/src/js/component/wb-collection-content.js
  42. +99
    -0
      frontend/src/js/component/wb-collection-fields.js
  43. +191
    -0
      frontend/src/js/component/wb-collection-listing.js
  44. +79
    -0
      frontend/src/js/component/wb-common-fields.js
  45. +114
    -0
      frontend/src/js/component/wb-container-fields.js
  46. +139
    -0
      frontend/src/js/component/wb-container-request-fields.js
  47. +22
    -0
      frontend/src/js/component/wb-inline-search.js
  48. +85
    -0
      frontend/src/js/component/wb-json-editor.js
  49. +50
    -0
      frontend/src/js/component/wb-json-viewer.js
  50. +66
    -0
      frontend/src/js/component/wb-lazy-inline-name.js
  51. +82
    -0
      frontend/src/js/component/wb-live-logs.js
  52. +130
    -0
      frontend/src/js/component/wb-name-and-uuid.js
  53. +38
    -0
      frontend/src/js/component/wb-navbar-common.js
  54. +60
    -0
      frontend/src/js/component/wb-path-display.js
  55. +141
    -0
      frontend/src/js/component/wb-process-dashboard.js
  56. +155
    -0
      frontend/src/js/component/wb-process-listing.js
  57. +64
    -0
      frontend/src/js/component/wb-process-state.js
  58. +77
    -0
      frontend/src/js/component/wb-project-fields.js
  59. +110
    -0
      frontend/src/js/component/wb-project-listing.js
  60. +75
    -0
      frontend/src/js/component/wb-user-listing.js
  61. +112
    -0
      frontend/src/js/component/wb-workflow-fields.js
  62. +78
    -0
      frontend/src/js/component/wb-workflow-input.js
  63. +116
    -0
      frontend/src/js/component/wb-workflow-listing.js
  64. +50
    -0
      frontend/src/js/deprecated/fetch-project-parents.js
  65. +57
    -0
      frontend/src/js/deprecated/wb-arvados-collection.js
  66. +42
    -0
      frontend/src/js/deprecated/wb-browse-dialog.js
  67. +187
    -0
      frontend/src/js/deprecated/wb-collection-manifest.js
  68. +72
    -0
      frontend/src/js/deprecated/wb-download-file.js
  69. +188
    -0
      frontend/src/js/deprecated/wb-manifest-reader.js
  70. +46
    -0
      frontend/src/js/deprecated/wb-project-crumbs.js
  71. +89
    -0
      frontend/src/js/deprecated/wb-rootdir-wrapper.js
  72. +125
    -0
      frontend/src/js/dialog/wb-browse-dialog-collection-content.js
  73. +78
    -0
      frontend/src/js/dialog/wb-browse-dialog-collection-list.js
  74. +113
    -0
      frontend/src/js/dialog/wb-browse-dialog-project-list.js
  75. +78
    -0
      frontend/src/js/dialog/wb-browse-dialog-user-list.js
  76. +269
    -0
      frontend/src/js/dialog/wb-browse-dialog.js
  77. +60
    -0
      frontend/src/js/dialog/wb-delete-dialog.js
  78. +59
    -0
      frontend/src/js/dialog/wb-edit-description-dialog.js
  79. +73
    -0
      frontend/src/js/dialog/wb-new-project-dialog.js
  80. +118
    -0
      frontend/src/js/dialog/wb-pick-object-dialog.js
  81. +52
    -0
      frontend/src/js/dialog/wb-rename-dialog.js
  82. +154
    -0
      frontend/src/js/dialog/wb-toolbox-dialog.js
  83. +13
    -0
      frontend/src/js/index.js
  84. +120
    -0
      frontend/src/js/misc/wb-apply-promise-ordering.js
  85. +19
    -0
      frontend/src/js/misc/wb-disable-controls.js
  86. +19
    -0
      frontend/src/js/misc/wb-format-date.js
  87. +18
    -0
      frontend/src/js/misc/wb-format-special-value.js
  88. +120
    -0
      frontend/src/js/page/wb-app.js
  89. +255
    -0
      frontend/src/js/page/wb-browse.js
  90. +32
    -0
      frontend/src/js/page/wb-collection-browse.js
  91. +36
    -0
      frontend/src/js/page/wb-collection-view.js
  92. +40
    -0
      frontend/src/js/page/wb-container-view.js
  93. +133
    -0
      frontend/src/js/page/wb-download-page.js
  94. +110
    -0
      frontend/src/js/page/wb-image-viewer-page.js
  95. +27
    -0
      frontend/src/js/page/wb-landing-page.js
  96. +185
    -0
      frontend/src/js/page/wb-launch-workflow-page.js
  97. +95
    -0
      frontend/src/js/page/wb-process-view.js
  98. +36
    -0
      frontend/src/js/page/wb-project-view.js
  99. +177
    -0
      frontend/src/js/page/wb-sharing-page.js
  100. +109
    -0
      frontend/src/js/page/wb-sign-in.js

+ 9
- 0
.gitignore View File

@@ -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

+ 77
- 0
backend/keephelper/__main__.py View File

@@ -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()

+ 42
- 0
backend/nginx/conf/nginx.conf View File

@@ -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;
}
}
}

+ 15
- 0
backend/srv.py View File

@@ -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()


+ 4
- 0
backend/supervisord/supervisord.conf View File

@@ -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

+ 10
- 0
dockerfiles/wba/Dockerfile View File

@@ -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"

+ 18
- 0
dockerfiles/wba/files/default.conf View File

@@ -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;
}
}


+ 41
- 0
dockerfiles/wba/files/wba/nginx/conf/nginx.conf View File

@@ -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
dockerfiles/wba/files/wba/nginx/run/.keep View File


+ 0
- 0
dockerfiles/wba/files/wba/nginx/tmp/.keep View File


+ 0
- 0
dockerfiles/wba/files/wba/supervisord/run/.keep View File


+ 8
- 0
dockerfiles/wba/files/wba/supervisord/supervisord.conf View File

@@ -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

+ 731
- 0
frontend/package-lock.json View File

@@ -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"
}
}
}
}

+ 32
- 0
frontend/package.json View File

@@ -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"
}
}

+ 67
- 0
frontend/rollup.config.js View File

@@ -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()
]
}

+ 27
- 0
frontend/src/css/index.css View File

@@ -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;
}

+ 25
- 0
frontend/src/html/index.html View File

@@ -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>

+ 20
- 0
frontend/src/js/arvados/base/arvados-object-name.js View File

@@ -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;

+ 28
- 0
frontend/src/js/arvados/base/arvados-type-name.js View File

@@ -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;

+ 34
- 0
frontend/src/js/arvados/base/detect-hashes.js View File

@@ -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;

+ 34
- 0
frontend/src/js/arvados/base/detect-uuids.js View File

@@ -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;

+ 51
- 0
frontend/src/js/arvados/base/fetch-object-parents.js View File

@@ -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;

+ 67
- 0
frontend/src/js/arvados/base/make-arvados-request.js View File

@@ -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;

+ 35
- 0
frontend/src/js/arvados/base/url-for-object.js View File

@@ -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;

+ 18
- 0
frontend/src/js/arvados/base/wb-delete-object.js View File

@@ -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;

+ 45
- 0
frontend/src/js/arvados/base/wb-fetch-objects.js View File

@@ -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;

+ 79
- 0
frontend/src/js/arvados/base/wb-id-tools.js View File

@@ -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;

+ 20
- 0
frontend/src/js/arvados/base/wb-move-object.js View File

@@ -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;

+ 23
- 0
frontend/src/js/arvados/base/wb-rename-object.js View File

@@ -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;

+ 23
- 0
frontend/src/js/arvados/base/wb-update-field.js View File

@@ -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;

+ 32
- 0
frontend/src/js/arvados/collection/wb-copy-collection.js View File

@@ -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;

+ 64
- 0
frontend/src/js/arvados/collection/wb-download-file.js View File

@@ -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;

+ 35
- 0
frontend/src/js/arvados/collection/wb-manifest-worker-wrapper.js View File

@@ -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;

+ 21
- 0
frontend/src/js/arvados/process/wb-input-spec-info.js View File

@@ -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;

+ 18
- 0
frontend/src/js/arvados/process/wb-parse-workflow-def.js View File

@@ -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;

+ 18
- 0
frontend/src/js/arvados/process/wb-process-misc.js View File

@@ -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 }

+ 27
- 0
frontend/src/js/arvados/process/wb-process-state-name.js View File

@@ -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;

+ 169
- 0
frontend/src/js/arvados/process/wb-submit-container-request.js View File

@@ -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 };

+ 32
- 0
frontend/src/js/arvados/process/wb-uuids-to-cwl.js View File

@@ -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;

+ 55
- 0
frontend/src/js/component/wb-arvados-crumbs.js View File

@@ -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;

+ 251
- 0
frontend/src/js/component/wb-collection-content.js View File

@@ -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;

+ 99
- 0
frontend/src/js/component/wb-collection-fields.js View File

@@ -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;

+ 191
- 0
frontend/src/js/component/wb-collection-listing.js View File

@@ -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;

+ 79
- 0
frontend/src/js/component/wb-common-fields.js View File

@@ -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;

+ 114
- 0
frontend/src/js/component/wb-container-fields.js View File

@@ -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;

+ 139
- 0
frontend/src/js/component/wb-container-request-fields.js View File

@@ -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;

+ 22
- 0
frontend/src/js/component/wb-inline-search.js View File

@@ -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;

+ 85
- 0
frontend/src/js/component/wb-json-editor.js View File

@@ -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;

+ 50
- 0
frontend/src/js/component/wb-json-viewer.js View File

@@ -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;

+ 66
- 0
frontend/src/js/component/wb-lazy-inline-name.js View File

@@ -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;

+ 82
- 0
frontend/src/js/component/wb-live-logs.js View File

@@ -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;

+ 130
- 0
frontend/src/js/component/wb-name-and-uuid.js View File

@@ -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;

+ 38
- 0
frontend/src/js/component/wb-navbar-common.js View File

@@ -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;

+ 60
- 0
frontend/src/js/component/wb-path-display.js View File

@@ -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;

+ 141
- 0
frontend/src/js/component/wb-process-dashboard.js View File

@@ -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;

+ 155
- 0
frontend/src/js/component/wb-process-listing.js View File

@@ -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;

+ 64
- 0
frontend/src/js/component/wb-process-state.js View File

@@ -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;

+ 77
- 0
frontend/src/js/component/wb-project-fields.js View File

@@ -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;

+ 110
- 0
frontend/src/js/component/wb-project-listing.js View File

@@ -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;

+ 75
- 0
frontend/src/js/component/wb-user-listing.js View File

@@ -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;

+ 112
- 0
frontend/src/js/component/wb-workflow-fields.js View File

@@ -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;

+ 78
- 0
frontend/src/js/component/wb-workflow-input.js View File

@@ -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;

+ 116
- 0
frontend/src/js/component/wb-workflow-listing.js View File

@@ -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;

+ 50
- 0
frontend/src/js/deprecated/fetch-project-parents.js View File

@@ -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;

+ 57
- 0
frontend/src/js/deprecated/wb-arvados-collection.js View File

@@ -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;

+ 42
- 0
frontend/src/js/deprecated/wb-browse-dialog.js View File

@@ -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">&times;</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;

+ 187
- 0
frontend/src/js/deprecated/wb-collection-manifest.js View File

@@ -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 };

+ 72
- 0
frontend/src/js/deprecated/wb-download-file.js View File

@@ -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;

+ 188
- 0
frontend/src/js/deprecated/wb-manifest-reader.js View File

@@ -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;

+ 46
- 0
frontend/src/js/deprecated/wb-project-crumbs.js View File

@@ -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;

+ 89
- 0
frontend/src/js/deprecated/wb-rootdir-wrapper.js View File

@@ -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;

+ 125
- 0
frontend/src/js/dialog/wb-browse-dialog-collection-content.js View File

@@ -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;

+ 78
- 0
frontend/src/js/dialog/wb-browse-dialog-collection-list.js View File

@@ -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;

+ 113
- 0
frontend/src/js/dialog/wb-browse-dialog-project-list.js View File

@@ -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;

+ 78
- 0
frontend/src/js/dialog/wb-browse-dialog-user-list.js View File

@@ -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;

+ 269
- 0
frontend/src/js/dialog/wb-browse-dialog.js View File

@@ -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">&times;</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;

+ 60
- 0
frontend/src/js/dialog/wb-delete-dialog.js View File

@@ -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;

+ 59
- 0
frontend/src/js/dialog/wb-edit-description-dialog.js View File

@@ -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;

+ 73
- 0
frontend/src/js/dialog/wb-new-project-dialog.js View File

@@ -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;

+ 118
- 0
frontend/src/js/dialog/wb-pick-object-dialog.js View File

@@ -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;

+ 52
- 0
frontend/src/js/dialog/wb-rename-dialog.js View File

@@ -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;

+ 154
- 0
frontend/src/js/dialog/wb-toolbox-dialog.js View File

@@ -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">&times;</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;

+ 13
- 0
frontend/src/js/index.js View File

@@ -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);

+ 120
- 0
frontend/src/js/misc/wb-apply-promise-ordering.js View File

@@ -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;

+ 19
- 0
frontend/src/js/misc/wb-disable-controls.js View File

@@ -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 };

+ 19
- 0
frontend/src/js/misc/wb-format-date.js View File

@@ -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;

+ 18
- 0
frontend/src/js/misc/wb-format-special-value.js View File

@@ -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;

+ 120
- 0
frontend/src/js/page/wb-app.js View File

@@ -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;

+ 255
- 0
frontend/src/js/page/wb-browse.js View File

@@ -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;

+ 32
- 0
frontend/src/js/page/wb-collection-browse.js View File

@@ -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;

+ 36
- 0
frontend/src/js/page/wb-collection-view.js View File

@@ -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;

+ 40
- 0
frontend/src/js/page/wb-container-view.js View File

@@ -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;

+ 133
- 0
frontend/src/js/page/wb-download-page.js View File

@@ -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;

+ 110
- 0
frontend/src/js/page/wb-image-viewer-page.js View File

@@ -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;

+ 27
- 0
frontend/src/js/page/wb-landing-page.js View File

@@ -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;

+ 185
- 0
frontend/src/js/page/wb-launch-workflow-page.js View File

@@ -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;

+ 95
- 0
frontend/src/js/page/wb-process-view.js View File

@@ -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;

+ 36
- 0
frontend/src/js/page/wb-project-view.js View File

@@ -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;

+ 177
- 0
frontend/src/js/page/wb-sharing-page.js View File

@@ -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;

+ 109
- 0
frontend/src/js/page/wb-sign-in.js View File

@@ -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;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save