From e6d95764bc1c06f2cccdcd6eaacd3935d9345373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20G=C3=B6bel?= <dgoebel@techfak.uni-bielefeld.de> Date: Fri, 9 Feb 2024 16:01:48 +0100 Subject: [PATCH] Add help page to create clowm info for existing workflows #95 --- package-lock.json | 250 ++++----- package.json | 2 + src/App.vue | 2 +- src/components/CopyToClipboardIcon.vue | 1 + src/components/DraggableLists.vue | 104 ++++ .../ParameterSchemaFormComponent.vue | 6 +- .../form-mode/ParameterBooleanInput.vue | 15 +- .../form-mode/ParameterEnumInput.vue | 26 +- .../form-mode/ParameterGroupForm.vue | 49 +- .../form-mode/ParameterNumberInput.vue | 20 +- .../form-mode/ParameterStringInput.vue | 39 +- .../workflows/WorkflowDocumentationTabs.vue | 2 +- .../workflows/WorkflowWithVersionsCard.vue | 12 + .../workflows/modals/ParameterModal.vue | 9 +- src/router/workflowRoutes.ts | 12 + src/utils/DownloadJson.ts | 6 + src/views/admin/AdminResourcesView.vue | 2 +- src/views/admin/AdminUsersView.vue | 2 +- src/views/object-storage/BucketsView.vue | 5 +- src/views/object-storage/S3KeysView.vue | 4 +- src/views/resources/ListResourcesView.vue | 2 +- src/views/resources/MyResourcesView.vue | 2 +- src/views/resources/ReviewResourceView.vue | 2 +- src/views/workflows/CreateClowmInfoView.vue | 485 ++++++++++++++++++ .../workflows/ListWorkflowExecutionsView.vue | 2 +- src/views/workflows/ListWorkflowsView.vue | 14 +- src/views/workflows/MyWorkflowsView.vue | 2 +- src/views/workflows/ReviewWorkflowsView.vue | 2 +- 28 files changed, 817 insertions(+), 262 deletions(-) create mode 100644 src/components/DraggableLists.vue create mode 100644 src/utils/DownloadJson.ts create mode 100644 src/views/workflows/CreateClowmInfoView.vue diff --git a/package-lock.json b/package-lock.json index fd24c5c..8927f4f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "pinia": "~2.1.0", "semver": "~7.5.0", "showdown": "~2.1.0", + "sortablejs": "^1.15.2", "vue": "~3.4.0", "vue-router": "~4.2.0", "vue3-cookies": "~1.0.0" @@ -39,6 +40,7 @@ "@types/node": "^18.19.5", "@types/semver": "~7.5.1", "@types/showdown": "~2.0.1", + "@types/sortablejs": "^1.15.7", "@vitejs/plugin-vue": "~5.0.0", "@vue/eslint-config-prettier": "~8.0.0", "@vue/eslint-config-typescript": "~11.0.3", @@ -211,16 +213,16 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@aws-sdk/client-s3": { - "version": "3.507.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.507.0.tgz", - "integrity": "sha512-rRLiC5Ly3e7kZVNoRsG6JhZ8Yat5uEnDeShdWNdHchyTO88AaEnHaeyiVG9ecmKI8jYl6NbWSHB8xL0l9KIr/w==", + "version": "3.509.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.509.0.tgz", + "integrity": "sha512-yeZJ1892Lj8S2zE0HerVt/ZJWaxemoEV3tzn5XDjExK6666cUajSwfmX036T51pEBwjqsTPz0ZJB1rlV7VFTIA==", "dependencies": { "@aws-crypto/sha1-browser": "3.0.0", "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", "@aws-sdk/client-sts": "3.507.0", "@aws-sdk/core": "3.496.0", - "@aws-sdk/credential-provider-node": "3.507.0", + "@aws-sdk/credential-provider-node": "3.509.0", "@aws-sdk/middleware-bucket-endpoint": "3.502.0", "@aws-sdk/middleware-expect-continue": "3.502.0", "@aws-sdk/middleware-flexible-checksums": "3.502.0", @@ -500,9 +502,9 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.507.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.507.0.tgz", - "integrity": "sha512-tkQnmOLkRBXfMLgDYHzogrqTNdtl0Im0ipzJb2IV5hfM5NoTfCf795e9A9isgwjSP/g/YEU0xQWxa4lq8LRtuA==", + "version": "3.509.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.509.0.tgz", + "integrity": "sha512-uXT8wIq1k+m0mS/pC9U1cUTIjUB7/4PgxyiYsTxYPIULtWnQXltAlcPU3QzKTJMP60sqftRYZ2jFDLAVsipQxw==", "dependencies": { "@aws-sdk/credential-provider-env": "3.502.0", "@aws-sdk/credential-provider-http": "3.503.1", @@ -569,9 +571,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.507.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.507.0.tgz", - "integrity": "sha512-cwCBGw6W71d0PrFSRizBd7WdyzsZnRWVftnOvNkyjIE5vFifdFbpdVaFidMVUZmvUQvS3Vgkx1LeXPT98EBWUQ==", + "version": "3.509.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.509.0.tgz", + "integrity": "sha512-VivrFLMmLJkf27PNY+LDO0KqH3BCeH7j5zK3l7xQgJWmPiLcBvk2UXuANZKrP7V82+/kI/AXFaXJuycedEj8yA==", "dependencies": { "@smithy/abort-controller": "^2.1.1", "@smithy/middleware-endpoint": "^2.4.1", @@ -772,9 +774,9 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.507.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.507.0.tgz", - "integrity": "sha512-A3EGvXMeOvnG+qtAsmlcQyLP7+PlCePS+PsVqLm3Pz3C16avOTxTqOZIkYCqBBX2fnASr2qUr0d3cezBfsU7PQ==", + "version": "3.509.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.509.0.tgz", + "integrity": "sha512-6EwvUtoObMZ2s7cx3zDJBgcfvqXQ8ABoDNXcSm3Y8/hdhJq8ovICwanTSWkx6ylFw+TmPt0Qo57U4SeCQd0qYA==", "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.502.0", "@aws-sdk/types": "3.502.0", @@ -1772,9 +1774,9 @@ } }, "node_modules/@smithy/core": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.1.tgz", - "integrity": "sha512-tf+NIu9FkOh312b6M9G4D68is4Xr7qptzaZGZUREELF8ysE1yLKphqt7nsomjKZVwW7WE5pDDex9idowNGRQ/Q==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-1.3.2.tgz", + "integrity": "sha512-tYDmTp0f2TZVE18jAOH1PnmkngLQ+dOGUlMd1u67s87ieueNeyqhja6z/Z4MxhybEiXKOWFOmGjfTZWFxljwJw==", "dependencies": { "@smithy/middleware-endpoint": "^2.4.1", "@smithy/middleware-retry": "^2.1.1", @@ -2245,9 +2247,9 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.1.1.tgz", - "integrity": "sha512-tYVrc+w+jSBfBd267KDnvSGOh4NMz+wVH7v4CClDbkdPfnjvImBZsOURncT5jsFwR9KCuDyPoSZq4Pa6+eCUrA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-2.2.0.tgz", + "integrity": "sha512-iFJp/N4EtkanFpBUtSrrIbtOIBf69KNuve03ic1afhJ9/korDxdM0c6cCH4Ehj/smI9pDCfVv+bqT3xZjF2WaA==", "dependencies": { "@smithy/config-resolver": "^2.1.1", "@smithy/credential-provider-imds": "^2.2.1", @@ -2416,9 +2418,9 @@ } }, "node_modules/@types/node": { - "version": "18.19.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", - "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", + "version": "18.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.15.tgz", + "integrity": "sha512-AMZ2UWx+woHNfM11PyAEQmfSxi05jm9OlkxczuHeEqmvwPkYj6MWv44gbzDPefYOLysTOFyI3ziiy2ONmUZfpA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -2436,6 +2438,12 @@ "integrity": "sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==", "dev": true }, + "node_modules/@types/sortablejs": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.7.tgz", + "integrity": "sha512-PvgWCx1Lbgm88FdQ6S7OGvLIjWS66mudKPlfdrWil0TjsO5zmoZmzoKiiwRShs1dwPgrlkr0N4ewuy0/+QUXYQ==", + "dev": true + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -2672,12 +2680,12 @@ } }, "node_modules/@vue/compiler-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.15.tgz", - "integrity": "sha512-XcJQVOaxTKCnth1vCxEChteGuwG6wqnUHxAm1DO3gCz0+uXKaJNx8/digSz4dLALCy8n2lKq24jSUs8segoqIw==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.18.tgz", + "integrity": "sha512-F7YK8lMK0iv6b9/Gdk15A67wM0KKZvxDxed0RR60C1z9tIJTKta+urs4j0RTN5XqHISzI3etN3mX0uHhjmoqjQ==", "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/shared": "3.4.15", + "@babel/parser": "^7.23.9", + "@vue/shared": "3.4.18", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.0.2" @@ -2689,26 +2697,26 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.15.tgz", - "integrity": "sha512-wox0aasVV74zoXyblarOM3AZQz/Z+OunYcIHe1OsGclCHt8RsRm04DObjefaI82u6XDzv+qGWZ24tIsRAIi5MQ==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.18.tgz", + "integrity": "sha512-24Eb8lcMfInefvQ6YlEVS18w5Q66f4+uXWVA+yb7praKbyjHRNuKVWGuinfSSjM0ZIiPi++QWukhkgznBaqpEA==", "dependencies": { - "@vue/compiler-core": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-core": "3.4.18", + "@vue/shared": "3.4.18" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.15.tgz", - "integrity": "sha512-LCn5M6QpkpFsh3GQvs2mJUOAlBQcCco8D60Bcqmf3O3w5a+KWS5GvYbrrJBkgvL1BDnTp+e8q0lXCLgHhKguBA==", - "dependencies": { - "@babel/parser": "^7.23.6", - "@vue/compiler-core": "3.4.15", - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.18.tgz", + "integrity": "sha512-rG5tqtnzwrVpMqAQ7FHtvHaV70G6LLfJIWLYZB/jZ9m/hrnZmIQh+H3ewnC5onwe/ibljm9+ZupxeElzqCkTAw==", + "dependencies": { + "@babel/parser": "^7.23.9", + "@vue/compiler-core": "3.4.18", + "@vue/compiler-dom": "3.4.18", + "@vue/compiler-ssr": "3.4.18", + "@vue/shared": "3.4.18", "estree-walker": "^2.0.2", - "magic-string": "^0.30.5", + "magic-string": "^0.30.6", "postcss": "^8.4.33", "source-map-js": "^1.0.2" } @@ -2730,12 +2738,12 @@ } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.15.tgz", - "integrity": "sha512-1jdeQyiGznr8gjFDadVmOJqZiLNSsMa5ZgqavkPZ8O2wjHv0tVuAEsw5hTdUoUW4232vpBbL/wJhzVW/JwY1Uw==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.18.tgz", + "integrity": "sha512-hSlv20oUhPxo2UYUacHgGaxtqP0tvFo6ixxxD6JlXIkwzwoZ9eKK6PFQN4hNK/R13JlNyldwWt/fqGBKgWJ6nQ==", "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-dom": "3.4.18", + "@vue/shared": "3.4.18" } }, "node_modules/@vue/devtools-api": { @@ -2831,48 +2839,48 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.15.tgz", - "integrity": "sha512-55yJh2bsff20K5O84MxSvXKPHHt17I2EomHznvFiJCAZpJTNW8IuLj1xZWMLELRhBK3kkFV/1ErZGHJfah7i7w==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.18.tgz", + "integrity": "sha512-7uda2/I0jpLiRygprDo5Jxs2HJkOVXcOMlyVlY54yRLxoycBpwGJRwJT9EdGB4adnoqJDXVT2BilUAYwI7qvmg==", "dependencies": { - "@vue/shared": "3.4.15" + "@vue/shared": "3.4.18" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.15.tgz", - "integrity": "sha512-6E3by5m6v1AkW0McCeAyhHTw+3y17YCOKG0U0HDKDscV4Hs0kgNT5G+GCHak16jKgcCDHpI9xe5NKb8sdLCLdw==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.18.tgz", + "integrity": "sha512-7mU9diCa+4e+8/wZ7Udw5pwTH10A11sZ1nldmHOUKJnzCwvZxfJqAtw31mIf4T5H2FsLCSBQT3xgioA9vIjyDQ==", "dependencies": { - "@vue/reactivity": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/reactivity": "3.4.18", + "@vue/shared": "3.4.18" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.15.tgz", - "integrity": "sha512-EVW8D6vfFVq3V/yDKNPBFkZKGMFSvZrUQmx196o/v2tHKdwWdiZjYUBS+0Ez3+ohRyF8Njwy/6FH5gYJ75liUw==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.18.tgz", + "integrity": "sha512-2y1Mkzcw1niSfG7z3Qx+2ir9Gb4hdTkZe5p/I8x1aTIKQE0vY0tPAEUPhZm5tx6183gG3D/KwHG728UR0sIufA==", "dependencies": { - "@vue/runtime-core": "3.4.15", - "@vue/shared": "3.4.15", + "@vue/runtime-core": "3.4.18", + "@vue/shared": "3.4.18", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.15.tgz", - "integrity": "sha512-3HYzaidu9cHjrT+qGUuDhFYvF/j643bHC6uUN9BgM11DVy+pM6ATsG6uPBLnkwOgs7BpJABReLmpL3ZPAsUaqw==", + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.18.tgz", + "integrity": "sha512-YJd1wa7mzUN3NRqLEsrwEYWyO+PUBSROIGlCc3J/cvn7Zu6CxhNLgXa8Z4zZ5ja5/nviYO79J1InoPeXgwBTZA==", "dependencies": { - "@vue/compiler-ssr": "3.4.15", - "@vue/shared": "3.4.15" + "@vue/compiler-ssr": "3.4.18", + "@vue/shared": "3.4.18" }, "peerDependencies": { - "vue": "3.4.15" + "vue": "3.4.18" } }, "node_modules/@vue/shared": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.15.tgz", - "integrity": "sha512-KzfPTxVaWfB+eGcGdbSf4CWdaXcGDqckoeXUh7SB3fZdEtzPCK2Vq9B/lRRL3yutax/LWITz+SwvgyOxz5V75g==" + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.18.tgz", + "integrity": "sha512-CxouGFxxaW5r1WbrSmWwck3No58rApXgRSBxrqgnY1K+jk20F6DrXJkHdH9n4HVT+/B6G2CAn213Uq3npWiy8Q==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -3207,16 +3215,10 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -3229,6 +3231,9 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } @@ -4157,13 +4162,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.1.tgz", - "integrity": "sha512-KmuibvwbWaM4BHcBRYwJfZ1JxyJeBwB8ct9YYu67SvYdbEIlcQ2e56dHxfbobqW38GXo8/zDFqJeGtHiVbWyQw==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", "dev": true, "dependencies": { "call-bind": "^1.0.5", - "es-errors": "^1.3.0" + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" }, "engines": { "node": ">= 0.4" @@ -5507,9 +5513,9 @@ } }, "node_modules/postcss": { - "version": "8.4.34", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.34.tgz", - "integrity": "sha512-4eLTO36woPSocqZ1zIrFD2K1v6wH7pY1uBh0JIM2KKfrVtGvPFiAku6aNOP0W1Wr9qwnaCsF0Z+CrVnryB2A8Q==", + "version": "8.4.35", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz", + "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==", "funding": [ { "type": "opencollective", @@ -5866,13 +5872,13 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", - "get-intrinsic": "^1.2.2", + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", "is-regex": "^1.1.4" }, "engines": { @@ -5914,14 +5920,15 @@ } }, "node_modules/set-function-length": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.1.tgz", + "integrity": "sha512-j4t6ccc+VsKwYHso+kElc5neZpjtq9EnRICFZtWyBsLojhmeF/ZBd/elqm22WJh/BziDe/SBiOeAt0m2mfLD0g==", "dev": true, "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.2", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.1" }, @@ -5997,14 +6004,18 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.5.tgz", + "integrity": "sha512-QcgiIWV4WV7qWExbN5llt6frQB/lBven9pqliLXfGPB+K9ZYXxDozp0wLkHS24kWCm+6YXH/f0HhnObZnZOBnQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6019,6 +6030,11 @@ "node": ">=8" } }, + "node_modules/sortablejs": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/sortablejs/-/sortablejs-1.15.2.tgz", + "integrity": "sha512-FJF5jgdfvoKn1MAKSdGs33bIqLi3LmsgVTliuX6iITj834F+JRQZN90Z93yql8h0K2t0RwDPBmxwlbZfDcxNZA==" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -6301,14 +6317,14 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.1.tgz", + "integrity": "sha512-RSqu1UEuSlrBhHTWC8O9FnPjOduNs4M7rJ4pRKoEjtx1zUNOPN2sSXHLDX+Y2WPbHIxbvg4JFo2DNAEfPIKWoQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", - "is-typed-array": "^1.1.10" + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -6914,15 +6930,15 @@ } }, "node_modules/vue": { - "version": "3.4.15", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.15.tgz", - "integrity": "sha512-jC0GH4KkWLWJOEQjOpkqU1bQsBwf4R1rsFtw5GQJbjHVKWDzO6P0nWWBTmjp1xSemAioDFj1jdaK1qa3DnMQoQ==", - "dependencies": { - "@vue/compiler-dom": "3.4.15", - "@vue/compiler-sfc": "3.4.15", - "@vue/runtime-dom": "3.4.15", - "@vue/server-renderer": "3.4.15", - "@vue/shared": "3.4.15" + "version": "3.4.18", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.18.tgz", + "integrity": "sha512-0zLRYamFRe0wF4q2L3O24KQzLyLpL64ye1RUToOgOxuWZsb/FhaNRdGmeozdtVYLz6tl94OXLaK7/WQIrVCw1A==", + "dependencies": { + "@vue/compiler-dom": "3.4.18", + "@vue/compiler-sfc": "3.4.18", + "@vue/runtime-dom": "3.4.18", + "@vue/server-renderer": "3.4.18", + "@vue/shared": "3.4.18" }, "peerDependencies": { "typescript": "*" diff --git a/package.json b/package.json index ef34c45..0d6de8d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "pinia": "~2.1.0", "semver": "~7.5.0", "showdown": "~2.1.0", + "sortablejs": "^1.15.2", "vue": "~3.4.0", "vue-router": "~4.2.0", "vue3-cookies": "~1.0.0" @@ -46,6 +47,7 @@ "@types/node": "^18.19.5", "@types/semver": "~7.5.1", "@types/showdown": "~2.0.1", + "@types/sortablejs": "^1.15.7", "@vitejs/plugin-vue": "~5.0.0", "@vue/eslint-config-prettier": "~8.0.0", "@vue/eslint-config-typescript": "~11.0.3", diff --git a/src/App.vue b/src/App.vue index 1ae0d2e..4497959 100644 --- a/src/App.vue +++ b/src/App.vue @@ -87,7 +87,7 @@ onBeforeMount(() => { <template> <NavbarTop /> - <div class="container-xxl mt-4 flex-grow-1"> + <div class="container-xxl mt-4 flex-grow-1 py-2"> <div id="global-toast-container" class="toast-container position-fixed top-toast end-0 p-3" diff --git a/src/components/CopyToClipboardIcon.vue b/src/components/CopyToClipboardIcon.vue index b0d34b4..6e85078 100644 --- a/src/components/CopyToClipboardIcon.vue +++ b/src/components/CopyToClipboardIcon.vue @@ -47,6 +47,7 @@ onMounted(() => { </bootstrap-toast> <button v-if="props.button" @click="copyToClipboard" class="btn btn-primary"> Copy to Clipboard + <font-awesome-icon icon="fa-solid fa-clipboard" class="ms-1" /> </button> <span v-else diff --git a/src/components/DraggableLists.vue b/src/components/DraggableLists.vue new file mode 100644 index 0000000..bb72d1d --- /dev/null +++ b/src/components/DraggableLists.vue @@ -0,0 +1,104 @@ +<script setup lang="ts"> +import { onMounted, ref } from "vue"; +import Sortable from "sortablejs"; + +const leftList = defineModel<string[]>("leftList", { required: true }); +const rightList = defineModel<string[]>("rightList", { required: true }); + +const leftListElement = ref<HTMLUListElement | undefined>(undefined); +const rightListElement = ref<HTMLUListElement | undefined>(undefined); + +onMounted(() => { + if (leftListElement.value && rightListElement.value) { + new Sortable(leftListElement.value, { + group: "shared", + animation: 150, + sort: false, // To disable sorting: set sort to false + onRemove: (evt) => { + leftList.value.splice(evt.oldIndex ?? 0, 1); + }, + onAdd: (evt) => { + leftList.value.splice( + evt.newIndex ?? 0, + 0, + (evt.item as HTMLLIElement).innerText, + ); + }, + }); + + new Sortable(rightListElement.value, { + group: "shared", + animation: 150, + sort: false, // To disable sorting: set sort to false + onRemove: (evt) => { + rightList.value.splice(evt.oldIndex ?? 0, 1); + }, + onAdd: (evt) => { + rightList.value.splice( + evt.newIndex ?? 0, + 0, + (evt.item as HTMLLIElement).innerText, + ); + }, + }); + } +}); +</script> + +<template> + <div class="row"> + <div class="col-6 d-flex flex-column justify-content-start"> + <h5><slot name="leftHeader" /></h5> + <ul + id="items" + class="list-group flex-fill border border-dashed p-1 overflow-y-scroll" + ref="leftListElement" + style="max-height: 40vh" + > + <li + v-for="(left, index) in leftList" + class="list-group-item" + :key="left" + @click=" + rightList.push(left); + leftList.splice(index, 1); + " + > + {{ left }} + </li> + </ul> + </div> + <div class="col-6 d-flex flex-column justify-content-start"> + <h5><slot name="rightHeader" /></h5> + <ul + id="items" + class="list-group flex-fill border border-dashed p-1 overflow-y-scroll" + ref="rightListElement" + style="max-height: 40vh" + > + <li + v-for="(right, index) in rightList" + class="list-group-item" + :key="right" + @click=" + leftList.push(right); + rightList.splice(index, 1); + " + > + {{ right }} + </li> + </ul> + </div> + </div> +</template> + +<style scoped> +li:hover { + cursor: grab; + background: var(--bs-secondary-bg-subtle); +} + +.border-dashed { + border-style: dashed !important; +} +</style> diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue index 4a93a52..ed64353 100644 --- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue +++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue @@ -183,6 +183,7 @@ function startWorkflow() { errorToast?.hide(); formState.validated = true; formState.errorType = undefined; + console.log(formState.formInput); if (launchForm.value?.checkValidity()) { const schemaValid = validateSchema(formState.formInput); @@ -232,7 +233,8 @@ function scroll(selectedAnchor: string) { // ============================================================================= onMounted(() => { if (props.schema) updateSchema(props.schema); - if (props.clowmInfo?.exampleParameters) Tooltip.getOrCreateInstance("#exampleDataButton"); + if (props.clowmInfo?.exampleParameters) + Tooltip.getOrCreateInstance("#exampleDataButton"); bucketRepository.fetchBuckets(); bucketRepository.fetchOwnPermissions(); keyRepository.fetchS3Keys(); @@ -271,7 +273,7 @@ onMounted(() => { }) " /> - <div class="row mb-5 align-items-start"> + <div class="row align-items-start"> <form v-if="props.schema" class="col-9" diff --git a/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue index a30bd4e..54e519c 100644 --- a/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue +++ b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue @@ -1,6 +1,8 @@ <script setup lang="ts"> import { computed } from "vue"; +const model = defineModel<boolean | undefined>({ required: true }); + const props = defineProps({ parameter: { type: Object, @@ -14,9 +16,6 @@ const props = defineProps({ type: String, required: true, }, - modelValue: { - type: Boolean, - }, helpId: { type: String, }, @@ -26,10 +25,6 @@ const randomIDSuffix = Math.random().toString(16).substring(2, 8); const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]); const iconPresent = computed<boolean>(() => props.parameter["fa_icon"]); - -const emit = defineEmits<{ - (e: "update:modelValue", value: boolean): void; -}>(); </script> <template> @@ -47,8 +42,7 @@ const emit = defineEmits<{ :name="'inlineRadioOptions' + randomIDSuffix" :id="'trueOption' + randomIDSuffix" :value="true" - :checked="props.modelValue" - @input="emit('update:modelValue', true)" + v-model="model" /> </div> <div class="form-check form-check-inline"> @@ -58,8 +52,7 @@ const emit = defineEmits<{ :name="'inlineRadioOptions' + randomIDSuffix" :id="'falseOption' + randomIDSuffix" :value="false" - @input="emit('update:modelValue', false)" - :checked="!props.modelValue" + v-model="model" /> <label class="form-check-label" :for="'falseOption' + randomIDSuffix" >False</label diff --git a/src/components/parameter-schema/form-mode/ParameterEnumInput.vue b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue index 03ec622..a92092c 100644 --- a/src/components/parameter-schema/form-mode/ParameterEnumInput.vue +++ b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue @@ -1,5 +1,7 @@ <script setup lang="ts"> -import { computed, ref } from "vue"; +import { computed } from "vue"; + +const model = defineModel<string | undefined>({ required: true }); const props = defineProps({ parameter: { @@ -14,41 +16,23 @@ const props = defineProps({ type: String, required: true, }, - modelValue: { - type: String, - }, helpId: { type: String, }, }); const possibleValues = computed<string[]>(() => props.parameter["enum"]); - -const enumSelection = ref<HTMLSelectElement | undefined>(undefined); - -const emit = defineEmits<{ - (e: "update:modelValue", value: string | undefined): void; -}>(); - -function updateValue() { - emit("update:modelValue", enumSelection.value?.value); -} </script> <template> <select ref="enumSelection" - :value="props.modelValue" - @input="updateValue" + v-model="model" class="form-select border border-secondary" :required="required" :aria-describedby="props.helpId" > - <option - v-for="val in possibleValues" - :key="val" - :selected="props.modelValue === val" - > + <option v-for="val in possibleValues" :key="val"> {{ val }} </option> </select> diff --git a/src/components/parameter-schema/form-mode/ParameterGroupForm.vue b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue index be65227..836ff27 100644 --- a/src/components/parameter-schema/form-mode/ParameterGroupForm.vue +++ b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue @@ -1,11 +1,14 @@ <script setup lang="ts"> import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; -import { computed, type PropType, watch } from "vue"; +import { computed, type PropType } from "vue"; import ParameterNumberInput from "@/components/parameter-schema/form-mode/ParameterNumberInput.vue"; import MarkdownRenderer from "@/components/MarkdownRenderer.vue"; import ParameterBooleanInput from "@/components/parameter-schema/form-mode/ParameterBooleanInput.vue"; import ParameterEnumInput from "@/components/parameter-schema/form-mode/ParameterEnumInput.vue"; import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue"; +import type { WorkflowParameters } from "@/types/WorkflowParameters"; + +const model = defineModel<WorkflowParameters>({ required: true }); const props = defineProps({ parameterGroup: { @@ -19,10 +22,6 @@ const props = defineProps({ type: String, required: true, }, - modelValue: { - type: Object, - required: true, - }, showHidden: { type: Boolean, default: false, @@ -52,14 +51,6 @@ const parameters = computed<Record<string, never>>( () => props.parameterGroup["properties"], ); -const formInput = computed(() => props.modelValue); -const emit = defineEmits<{ - ( - e: "update:modelValue", - value: Record<string, number | string | boolean | undefined>, - ): void; -}>(); - function parameterRequired( // eslint-disable-next-line @typescript-eslint/no-explicit-any parameterGroup: Record<string, any>, @@ -68,20 +59,10 @@ function parameterRequired( return ( parameterGroup["required"]?.includes(parameterName) || // parameter is required parameterGroup["dependentRequired"]?.[parameterName] // parameter is required when another parameter is set - ?.map((param: string) => formInput.value[param]) + ?.map((param: string) => model.value[param]) ?.reduce((acc: boolean, val: string) => acc || val, false) ); } - -watch( - formInput, - (newVal) => { - emit("update:modelValue", newVal); - }, - { - deep: true, - }, -); </script> <template> @@ -129,41 +110,29 @@ watch( :parameter="parameter" :help-id="parameterName + '-help'" :required="parameterRequired(parameterGroup, parameterName)" - :model-value="formInput[parameterName]" - @update:model-value=" - (newValue) => (formInput[parameterName] = newValue) - " + v-model="model[parameterName]" /> <parameter-boolean-input v-else-if="parameter['type'] === 'boolean'" :parameter-name="parameterName" :parameter="parameter" :help-id="parameterName + '-help'" - :model-value="formInput[parameterName]" - @update:model-value=" - (newValue) => (formInput[parameterName] = newValue) - " + v-model="model[parameterName]" /> <template v-else-if="parameter['type'] === 'string'"> <parameter-enum-input v-if="parameter['enum']" :parameter-name="parameterName" :parameter="parameter" - :model-value="formInput[parameterName]" :required="parameterRequired(parameterGroup, parameterName)" - @update:model-value=" - (newValue) => (formInput[parameterName] = newValue) - " + v-model="model[parameterName]" /> <parameter-string-input v-else :parameter-name="parameterName" :parameter="parameter" - :model-value="formInput[parameterName]" :required="parameterRequired(parameterGroup, parameterName)" - @update:model-value=" - (newValue) => (formInput[parameterName] = newValue) - " + v-model="model[parameterName]" :remove-advanced="!showOptional" :clowm-resource="resourceParameters?.includes(parameterName)" /> diff --git a/src/components/parameter-schema/form-mode/ParameterNumberInput.vue b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue index 55963a2..0cc7ca6 100644 --- a/src/components/parameter-schema/form-mode/ParameterNumberInput.vue +++ b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue @@ -1,5 +1,6 @@ <script setup lang="ts"> -import { ref } from "vue"; +const model = defineModel<number | undefined>({ required: true }); + const props = defineProps({ parameter: { type: Object, @@ -13,37 +14,22 @@ const props = defineProps({ type: String, required: true, }, - modelValue: { - type: Number, - }, helpId: { type: String, }, }); - -const emit = defineEmits<{ - (e: "update:modelValue", value: number | undefined): void; -}>(); - -const numberInput = ref<HTMLInputElement | undefined>(undefined); - -function updateValue() { - emit("update:modelValue", Number(numberInput.value?.value)); -} </script> <template> <input class="form-control border border-secondary" type="number" - ref="numberInput" :max="props.parameter['maximum']" :min="props.parameter['minimum']" :step="props.parameter['type'] === 'integer' ? 1 : 0.0001" - :value="props.modelValue" + v-model="model" :required="props.required" :aria-describedby="props.helpId" - @input="updateValue" /> </template> diff --git a/src/components/parameter-schema/form-mode/ParameterStringInput.vue b/src/components/parameter-schema/form-mode/ParameterStringInput.vue index a41cc04..fe6d014 100644 --- a/src/components/parameter-schema/form-mode/ParameterStringInput.vue +++ b/src/components/parameter-schema/form-mode/ParameterStringInput.vue @@ -10,6 +10,8 @@ const s3objectRepository = useS3ObjectStore(); const resourceRepository = useResourceStore(); const randomIDSuffix = Math.random().toString(16).substring(2, 8); +const model = defineModel<string | undefined>({ required: true }); + const props = defineProps({ parameter: { type: Object, @@ -23,9 +25,6 @@ const props = defineProps({ type: String, required: true, }, - modelValue: { - type: String, - }, helpId: { type: String, }, @@ -39,10 +38,6 @@ const props = defineProps({ }, }); -const emit = defineEmits<{ - (e: "update:modelValue", value?: string): void; -}>(); - const s3Path = reactive<{ bucket: string; key?: string; @@ -61,10 +56,8 @@ const selectedResource = reactive<{ const formState = reactive<{ advancedInput: boolean; - stringVal?: string; }>({ advancedInput: false, - stringVal: undefined, }); const stringInput = ref<HTMLInputElement | undefined>(undefined); @@ -131,22 +124,17 @@ watch( }, ); -watch(() => formState.stringVal, updateValue); watch(selectedResource, () => { if (clowmResource.value && !formState.advancedInput) { updateStringFromResource(); } }); -watch( - () => props.modelValue, - (newVal) => { - if (formState.stringVal != newVal) { - formState.stringVal = newVal; - formState.advancedInput = true; - } - }, -); +watch(model, (newVal, oldVal) => { + if (newVal && newVal !== oldVal) { + formState.advancedInput = true; + } +}); watch( () => formState.advancedInput, @@ -161,18 +149,14 @@ watch( }, ); -function updateValue() { - emit("update:modelValue", formState.stringVal); -} - function updateStringFromS3() { - formState.stringVal = !s3Path.bucket + model.value = !s3Path.bucket ? undefined : `s3://${s3Path.bucket}${s3Path.key ? "/" + s3Path.key : ""}`; } function updateStringFromResource() { - formState.stringVal = + model.value = resourceRepository.resourceMapping[selectedResource.resourceId]?.versions[ selectedResource.resourceVersionIndex ]?.cluster_path ?? undefined; @@ -188,8 +172,7 @@ function updateKeysInBucket(bucketName?: string) { } onMounted(() => { - formState.stringVal = props.modelValue; - if (formState.stringVal) { + if (model.value) { formState.advancedInput = true; } }); @@ -265,7 +248,7 @@ onMounted(() => { class="form-control border border-secondary" :class="{ 'rounded-end': props.removeAdvanced && !helpTextPresent }" type="text" - v-model="formState.stringVal" + v-model="model" :required="props.required" :aria-describedby="props.helpId" :pattern="pattern" diff --git a/src/components/workflows/WorkflowDocumentationTabs.vue b/src/components/workflows/WorkflowDocumentationTabs.vue index 7ce8afc..7ebecac 100644 --- a/src/components/workflows/WorkflowDocumentationTabs.vue +++ b/src/components/workflows/WorkflowDocumentationTabs.vue @@ -102,7 +102,7 @@ const activeTab = computed<string>( ></span> </p> </div> - <div v-else class="px-2 pt-3 border border-top-0 mb-2"> + <div v-else class="px-2 pt-3 border border-top-0"> <div v-if="activeTab === 'description'"> <markdown-renderer v-if="props.descriptionMarkdown" diff --git a/src/components/workflows/WorkflowWithVersionsCard.vue b/src/components/workflows/WorkflowWithVersionsCard.vue index defb55e..3def45d 100644 --- a/src/components/workflows/WorkflowWithVersionsCard.vue +++ b/src/components/workflows/WorkflowWithVersionsCard.vue @@ -126,6 +126,18 @@ onMounted(() => { <span>{{ props.workflow.name }}</span> </div> <div> + <router-link + :to="{ + name: 'workflows-clowminfo', + query: { + workflow_id: workflow.workflow_id, + workflow_version_id: sortedVersions(props.workflow.versions)[0] + ?.workflow_version_id, + }, + }" + class="btn btn-primary" + >Clowm + </router-link> <button type="button" class="btn btn-outline-info me-2" diff --git a/src/components/workflows/modals/ParameterModal.vue b/src/components/workflows/modals/ParameterModal.vue index 31c33ca..5ae588a 100644 --- a/src/components/workflows/modals/ParameterModal.vue +++ b/src/components/workflows/modals/ParameterModal.vue @@ -9,6 +9,7 @@ import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import type { WorkflowExecutionOut, WorkflowVersion } from "@/client/workflow"; import { useNameStore } from "@/stores/names"; import { useWorkflowStore } from "@/stores/workflows"; +import { createDownloadUrl } from "@/utils/DownloadJson"; import type { WorkflowParameters } from "@/types/WorkflowParameters"; const nameRepository = useNameStore(); @@ -80,10 +81,10 @@ const parameterDownloadUrl = computed<string | undefined>(() => { if (parameters.value == undefined) { return undefined; } - const blob = new Blob([JSON.stringify(parameters.value, undefined, 2)], { - type: "application/json", - }); - return URL.createObjectURL(blob); + return createDownloadUrl( + JSON.stringify(parameters.value, null, 2), + "application/json", + ); }); const workflowName = computed<string>(() => { diff --git a/src/router/workflowRoutes.ts b/src/router/workflowRoutes.ts index f4d076c..4441fcb 100644 --- a/src/router/workflowRoutes.ts +++ b/src/router/workflowRoutes.ts @@ -20,6 +20,18 @@ export const workflowRoutes: RouteRecordRaw[] = [ requiresDeveloperRole: true, }, }, + { + path: "developer/workflows/clowminfo", + name: "workflows-clowminfo", + component: () => import("../views/workflows/CreateClowmInfoView.vue"), + props: (route) => ({ + workflow_version_id: route.query.workflow_version_id ?? undefined, + workflow_id: route.query.workflow_id ?? undefined, + }), + meta: { + requiresDeveloperRole: true, + }, + }, { path: "reviewer/workflows", name: "workflows-reviewer", diff --git a/src/utils/DownloadJson.ts b/src/utils/DownloadJson.ts new file mode 100644 index 0000000..9ad1f78 --- /dev/null +++ b/src/utils/DownloadJson.ts @@ -0,0 +1,6 @@ +export function createDownloadUrl(content: string, mimeType?: string): string { + const blob = new Blob([content], { + type: mimeType, + }); + return URL.createObjectURL(blob); +} diff --git a/src/views/admin/AdminResourcesView.vue b/src/views/admin/AdminResourcesView.vue index 095336b..9528652 100644 --- a/src/views/admin/AdminResourcesView.vue +++ b/src/views/admin/AdminResourcesView.vue @@ -147,7 +147,7 @@ function resetForm() { :resource="resourceState.inspectResource" /> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + class="row border-bottom mb-4 justify-content-between align-items-center" > <h2>Manage Resources</h2> </div> diff --git a/src/views/admin/AdminUsersView.vue b/src/views/admin/AdminUsersView.vue index a1f3e14..58eeccc 100644 --- a/src/views/admin/AdminUsersView.vue +++ b/src/views/admin/AdminUsersView.vue @@ -43,7 +43,7 @@ function searchUsers() { <template> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + class="row border-bottom mb-4 justify-content-between align-items-center" > <h2>Manage Users</h2> </div> diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index b19d7d8..1797014 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -92,11 +92,10 @@ onMounted(() => { modalID="create-bucket-modal" v-if="!authStore.foreignUser" /> - <div class="row m-2 border-bottom"> - <div class="col-12"></div> + <div class="row border-bottom"> <h2 class="mb-2">My Data Buckets</h2> </div> - <div class="row m-2 mt-4"> + <div class="row mt-4"> <div class="col-3"> <div class="d-flex justify-content-between"> <button diff --git a/src/views/object-storage/S3KeysView.vue b/src/views/object-storage/S3KeysView.vue index 8fce1fe..018415a 100644 --- a/src/views/object-storage/S3KeysView.vue +++ b/src/views/object-storage/S3KeysView.vue @@ -80,11 +80,11 @@ onMounted(() => { <bootstrap-toast toast-id="successKeyToast"> Successfully deleted S3 Key {{ keyState.deletedKey }} </bootstrap-toast> - <div class="row m-2 border-bottom mt-4"> + <div class="row border-bottom mt-4"> <div class="col-12"></div> <h2 class="mb-2">S3 Keys</h2> </div> - <div class="row m-2 mt-4"> + <div class="row mt-4"> <div class="col-4"> <div class="d-flex justify-content-between mb-4"> <button diff --git a/src/views/resources/ListResourcesView.vue b/src/views/resources/ListResourcesView.vue index b615344..5787adc 100644 --- a/src/views/resources/ListResourcesView.vue +++ b/src/views/resources/ListResourcesView.vue @@ -53,7 +53,7 @@ onMounted(() => { </script> <template> - <div class="row m-2 border-bottom mb-4"> + <div class="row border-bottom mb-4"> <h2 class="mb-2">Available Resources</h2> </div> <div class="d-flex m-2 mb-3 align-items-center justify-content-start"> diff --git a/src/views/resources/MyResourcesView.vue b/src/views/resources/MyResourcesView.vue index e9a3d23..5b2b365 100644 --- a/src/views/resources/MyResourcesView.vue +++ b/src/views/resources/MyResourcesView.vue @@ -62,7 +62,7 @@ onMounted(() => { modal-id="updateResourceModal" /> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2" + class="row border-bottom mb-4 justify-content-between align-items-center pb-2" > <h2 class="w-fit">My Resources</h2> <div class="w-fit"> diff --git a/src/views/resources/ReviewResourceView.vue b/src/views/resources/ReviewResourceView.vue index 78f6785..e682c79 100644 --- a/src/views/resources/ReviewResourceView.vue +++ b/src/views/resources/ReviewResourceView.vue @@ -75,7 +75,7 @@ onMounted(() => { :resource="resourceState.inspectResource" /> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + class="row border-bottom mb-4 justify-content-between align-items-center" > <h2 class="w-fit">Resource Requests</h2> <span diff --git a/src/views/workflows/CreateClowmInfoView.vue b/src/views/workflows/CreateClowmInfoView.vue new file mode 100644 index 0000000..0c2c089 --- /dev/null +++ b/src/views/workflows/CreateClowmInfoView.vue @@ -0,0 +1,485 @@ +<script setup lang="ts"> +import CopyToClipboardIcon from "@/components/CopyToClipboardIcon.vue"; +import { + computed, + onMounted, + onBeforeMount, + reactive, + watch, + onUnmounted, +} from "vue"; +import { createDownloadUrl } from "@/utils/DownloadJson"; +import type { ClowmInfo } from "@/types/ClowmInfo"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import { useWorkflowStore } from "@/stores/workflows"; +import { DocumentationEnum } from "@/client/workflow"; +import DraggableLists from "@/components/DraggableLists.vue"; + +const props = defineProps<{ + workflow_id?: string; + workflow_version_id?: string; +}>(); + +const workflowRepository = useWorkflowStore(); + +const infoState = reactive<ClowmInfo>({ + inputParameters: [], + outputParameters: [], + exampleParameters: undefined, + dois: undefined, + resourceParameters: undefined, +}); + +const parameterPools = reactive<{ + input: string[]; + output: string[]; + resources: string[]; + examples: string[]; +}>({ + examples: [], + input: [], + output: [], + resources: [], +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const parameterSchema = computed<Record<string, Record<string, any>>>(() => { + const schema = + workflowRepository.documentationFiles[props.workflow_version_id ?? ""] + ?.parameter_schema; + const a = schema?.["properties"] ?? {}; + for (const group in schema?.["definitions"] ?? {}) { + for (const param in schema?.["definitions"]?.[group]?.["properties"] ?? + {}) { + a[param] = schema["definitions"][group]["properties"][param]; + } + } + return a; +}); + +function getParameterType(param: string): string | undefined { + return parameterSchema.value[param]?.["type"]; +} + +const infoStateString = computed<string>(() => + JSON.stringify(infoState, null, 2), +); +const downloadUrl = computed<string>(() => + createDownloadUrl(infoStateString.value, "application/json"), +); + +watch( + () => workflowRepository.documentationFiles[props.workflow_version_id ?? ""], + (newVal) => { + updateParameterPools(newVal); + }, + { + deep: true, + }, +); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function updateParameterPools(newVal?: Record<DocumentationEnum, any>) { + if (newVal?.parameter_schema) { + const parameters = extractParameterList(newVal.parameter_schema); + parameterPools.input = parameters.slice(); + parameterPools.output = parameters.slice(); + parameterPools.resources = parameters.slice(); + parameterPools.examples = parameters.slice(); + infoState.inputParameters = []; + infoState.outputParameters = []; + infoState.exampleParameters = undefined; + infoState.resourceParameters = undefined; + } + if (newVal?.clowm_info) { + Object.assign(infoState, JSON.parse(JSON.stringify(newVal.clowm_info))); + parameterPools.input = parameterPools.input.filter( + (param) => !infoState.inputParameters.includes(param), + ); + parameterPools.resources = parameterPools.resources?.filter( + (param) => !infoState.resourceParameters?.includes(param), + ); + parameterPools.output = parameterPools.output.filter( + (param) => !infoState.outputParameters.includes(param), + ); + parameterPools.examples = parameterPools.examples?.filter( + (param) => + !Object.keys(infoState.exampleParameters ?? {}).includes(param), + ); + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function extractParameterList(schema: Record<string, any>): string[] { + const groupedParameters = Object.keys(schema["definitions"] ?? {}).reduce( + (acc: string[], val) => [ + ...acc, + ...Object.keys(schema["definitions"][val]["properties"]), + ], + [], + ); + const singleParameters = Object.keys(schema["properties"] ?? {}); + return [...groupedParameters, ...singleParameters]; +} + +function addDoi() { + if (infoState.dois == undefined) { + infoState.dois = [""]; + } else { + infoState.dois.push(""); + } +} + +function removeDoi(index: number) { + if (infoState.dois?.length === 1) { + infoState.dois = undefined; + } + infoState.dois?.splice(index, 1); +} + +onBeforeMount(() => { + if (props.workflow_id && props.workflow_version_id) { + workflowRepository.fetchWorkflowDocumentation( + props.workflow_id, + props.workflow_version_id, + DocumentationEnum.PARAMETER_SCHEMA, + ); + workflowRepository.fetchWorkflowDocumentation( + props.workflow_id, + props.workflow_version_id, + DocumentationEnum.CLOWM_INFO, + ); + } +}); + +onMounted(() => { + if (props.workflow_id && props.workflow_version_id) { + updateParameterPools( + workflowRepository.documentationFiles[props.workflow_version_id], + ); + } +}); + +onUnmounted(() => { + console.log("Unmounted"); +}); +</script> + +<template> + <div class="row border-bottom mb-4"> + <h2 class="mb-2">Create <code>clowm_info.json</code></h2> + </div> + <div class="accordion mb-2" id="clowmInfoAccordion"> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button + class="accordion-button" + type="button" + data-bs-toggle="collapse" + data-bs-target="#clowmInfoAccordion-input" + aria-expanded="true" + aria-controls="clowmInfoAccordion-input" + > + Input Parameters + </button> + </h2> + <div + id="clowmInfoAccordion-input" + class="accordion-collapse collapse show" + data-bs-parent="#clowmInfoAccordion" + > + <div class="accordion-body"> + <draggable-lists + :left-list="parameterPools.input" + :right-list="infoState.inputParameters" + > + <template #leftHeader>Workflow Parameters</template> + <template #rightHeader>Input Parameters</template> + </draggable-lists> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button + class="accordion-button collapsed" + type="button" + data-bs-toggle="collapse" + data-bs-target="#clowmInfoAccordion-output" + aria-expanded="false" + aria-controls="clowmInfoAccordion-output" + > + Output Parameters + </button> + </h2> + <div + id="clowmInfoAccordion-output" + class="accordion-collapse collapse" + data-bs-parent="#clowmInfoAccordion" + > + <div class="accordion-body"> + <draggable-lists + :left-list="parameterPools.output" + :right-list="infoState.outputParameters" + > + <template #leftHeader>Workflow Parameters</template> + <template #rightHeader>Output Parameters</template> + </draggable-lists> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button + class="accordion-button collapsed" + type="button" + data-bs-toggle="collapse" + data-bs-target="#clowmInfoAccordion-resource" + aria-expanded="false" + aria-controls="clowmInfoAccordion-resource" + > + Resource Parameters + </button> + </h2> + <div + id="clowmInfoAccordion-resource" + class="accordion-collapse collapse" + data-bs-parent="#clowmInfoAccordion" + > + <div class="accordion-body"> + <button + v-if="infoState.resourceParameters == undefined" + type="button" + class="btn btn-primary" + @click="infoState.resourceParameters = []" + > + This workflow needs resources + </button> + <draggable-lists + :left-list="parameterPools.resources" + :right-list="infoState.resourceParameters" + v-else + > + <template #leftHeader>Workflow Parameters</template> + <template #rightHeader>Resources Parameters</template> + </draggable-lists> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button + class="accordion-button collapsed" + type="button" + data-bs-toggle="collapse" + data-bs-target="#clowmInfoAccordion-example" + aria-expanded="false" + aria-controls="clowmInfoAccordion-example" + > + Example Parameters + </button> + </h2> + <div + id="clowmInfoAccordion-example" + class="accordion-collapse collapse" + data-bs-parent="#clowmInfoAccordion" + > + <div class="accordion-body"> + <button + v-if="infoState.exampleParameters == undefined" + type="button" + class="btn btn-primary" + @click="infoState.exampleParameters = {}" + > + I have Examples + </button> + <template v-else> + <div + class="d-flex flex-wrap overflow-y-scroll p-1 border rounded border-dashed mb-2" + style="max-height: 30vh" + > + <div + class="w-fit border px-2 rounded cursor-pointer m-1 parameter-container" + v-for="(param, index) in parameterPools.examples" + :key="param" + @click=" + parameterPools.examples.splice(index, 1); + infoState.exampleParameters[param] = ''; + " + > + {{ param }} + </div> + </div> + <table class="table table-bordered"> + <thead> + <tr> + <th scope="col"><b>Parameter</b></th> + <th scope="col"><b>Value</b></th> + </tr> + </thead> + <tbody> + <tr + v-for="param in Object.keys(infoState.exampleParameters)" + :key="param" + > + <td style="width: 10%">{{ param }}</td> + <td class="d-flex justify-content-between align-items-center"> + <input + v-if=" + getParameterType(param) === 'number' || + getParameterType(param) === 'integer' + " + type="number" + class="form-control form-control-sm flex-grow" + v-model="infoState.exampleParameters[param]" + :step="getParameterType(param) === 'integer' ? 1 : 0.0001" + :min="parameterSchema[param]['minimum']" + :max="parameterSchema[param]['maximum']" + /> + <div + v-else-if="getParameterType(param) === 'boolean'" + class="flex-grow" + > + <div class="form-check form-check-inline"> + <label + class="form-check-label" + :for="'trueOption' + param.replace(/\./g, '')" + >True</label + > + <input + class="form-check-input" + type="radio" + :name=" + 'inlineRadioOptions' + param.replace(/\./g, '') + " + :id="'trueOption' + param.replace(/\./g, '')" + :value="true" + v-model="infoState.exampleParameters[param]" + /> + </div> + <div class="form-check form-check-inline"> + <input + class="form-check-input" + type="radio" + :name=" + 'inlineRadioOptions' + param.replace(/\./g, '') + " + :id="'falseOption' + param.replace(/\./g, '')" + :value="false" + v-model="infoState.exampleParameters[param]" + /> + <label + class="form-check-label" + :for="'falseOption' + param.replace(/\./g, '')" + >False</label + > + </div> + </div> + <select + v-else-if="parameterSchema[param]?.['enum']" + class="form-select form-select-sm flex-grow" + v-model="infoState.exampleParameters[param]" + > + <option + v-for="option in parameterSchema[param]?.['enum']" + :key="option" + :value="option" + > + {{ option }} + </option> + </select> + <input + v-else + type="text" + class="form-control form-control-sm flex-grow" + v-model="infoState.exampleParameters[param]" + :pattern="parameterSchema[param]?.['pattern']" + /> + <button + type="button" + class="btn btn-outline-danger btn-sm ms-2" + @click=" + delete infoState.exampleParameters[param]; + parameterPools.examples.push(param); + " + > + Remove + </button> + </td> + </tr> + </tbody> + </table> + </template> + </div> + </div> + </div> + <div class="accordion-item"> + <h2 class="accordion-header"> + <button + class="accordion-button collapsed" + type="button" + data-bs-toggle="collapse" + data-bs-target="#clowmInfoAccordion-dois" + aria-expanded="false" + aria-controls="clowmInfoAccordion-dois" + > + DOIs + </button> + </h2> + <div + id="clowmInfoAccordion-dois" + class="accordion-collapse collapse" + data-bs-parent="#clowmInfoAccordion" + > + <div class="accordion-body"> + <button type="button" class="btn btn-primary" @click="addDoi"> + Add DOI + </button> + <div v-if="infoState.dois"> + <div + class="d-flex my-2" + v-for="(doi, index) in infoState.dois" + :key="index" + > + <button + type="button" + class="btn btn-outline-danger btn-sm" + @click="removeDoi(index)" + > + Remove + </button> + <input + type="text" + class="form-control mx-2" + v-model="infoState.dois[index]" + maxlength="48" + style="max-width: 400px" + /> + <div class="align-self-center"> + Link: + <a + :href="'https://doi.org/' + doi" + target="_blank" + class="align-self-center" + >{{ doi }}</a + > + </div> + </div> + </div> + </div> + </div> + </div> + </div> + <pre class="rounded w-100"><code>{{ infoState }}</code></pre> + <copy-to-clipboard-icon button :text="infoStateString" /> + <a class="btn btn-primary ms-2" :href="downloadUrl" download="clowm_info.json" + >Download to file + <font-awesome-icon icon="fa-solid fa-download" class="ms-1" /> + </a> +</template> + +<style scoped> +.parameter-container:hover { + background: var(--bs-secondary-bg-subtle); +} +</style> diff --git a/src/views/workflows/ListWorkflowExecutionsView.vue b/src/views/workflows/ListWorkflowExecutionsView.vue index c3d87aa..5b96f03 100644 --- a/src/views/workflows/ListWorkflowExecutionsView.vue +++ b/src/views/workflows/ListWorkflowExecutionsView.vue @@ -165,7 +165,7 @@ onUnmounted(() => { :execution-id="executionsState.executionParameters" /> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center" + class="row border-bottom mb-4 justify-content-between align-items-center" > <h2 class="mb-2 w-fit">My Workflow Executions</h2> <div class="w-fit"> diff --git a/src/views/workflows/ListWorkflowsView.vue b/src/views/workflows/ListWorkflowsView.vue index 56ccb5a..4c54cf1 100644 --- a/src/views/workflows/ListWorkflowsView.vue +++ b/src/views/workflows/ListWorkflowsView.vue @@ -51,15 +51,15 @@ function filterWorkflowWithoutVersion(workflow: WorkflowOut): boolean { } const processedWorkflows = computed<WorkflowOut[]>(() => { - return [ - ...workflowRepository.workflows.filter( - (workflow) => - filterWorkflowByString(workflow) && - filterWorkflowWithoutVersion(workflow), - ), - ].sort((a, b) => + const temp = workflowRepository.workflows.filter( + (workflow) => + filterWorkflowByString(workflow) && + filterWorkflowWithoutVersion(workflow), + ); + temp.sort((a, b) => filterFunctionMapping[workflowsState.sortByAttribute](a, b) ? 1 : -1, ); + return temp; }); onMounted(() => { diff --git a/src/views/workflows/MyWorkflowsView.vue b/src/views/workflows/MyWorkflowsView.vue index 48d1c28..8b5b935 100644 --- a/src/views/workflows/MyWorkflowsView.vue +++ b/src/views/workflows/MyWorkflowsView.vue @@ -114,7 +114,7 @@ onMounted(() => { modal-i-d="updateWorkflowCredentialsModal" /> <div - class="row m-2 border-bottom mb-4 justify-content-between align-items-center pb-2" + class="row border-bottom mb-4 justify-content-between align-items-center pb-2" > <h2 class="w-fit">My Workflows</h2> <button diff --git a/src/views/workflows/ReviewWorkflowsView.vue b/src/views/workflows/ReviewWorkflowsView.vue index 8507574..ad15851 100644 --- a/src/views/workflows/ReviewWorkflowsView.vue +++ b/src/views/workflows/ReviewWorkflowsView.vue @@ -63,7 +63,7 @@ onMounted(() => { </script> <template> - <div class="row m-2 border-bottom mb-4"> + <div class="row border-bottom mb-4"> <h2 class="mb-2">Workflow Reviews</h2> </div> <div v-if="workflowsState.loading" class="text-center mt-5"> -- GitLab