diff --git a/DEVELOPING.md b/DEVELOPING.md index 43f07429d385b7ce75263efa6dd5249c5a2e49ad..a623fcedc22c3a9bc121cbf378c437d4798fced1 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -10,9 +10,7 @@ npm install This has to be done only once. Export the environment variables with the appropriate values and use the `envsubst` command to populate the template and create the file the `public/env.js` ```shell -export AUTH_API_BASE_URL=http://localhost:9999/api/auth-service -export WORKFLOW_API_BASE_URL=http://localhost:9999/api/workflow-service -export S3PROXY_API_BASE_URL=http://localhost:9999/api/s3proxy-service +export API_BASE_URL=http://localhost:9999/api export S3_URL=http://localhost:9998 envsubst < src/assets/env.template.js > public/env.js ``` @@ -33,7 +31,8 @@ npm run dev ``` ## Create axios client -To create the axios client from the OpenAPI definition of the backend, start the backend and execute the following command +To create the axios client from the OpenAPI definition of the backend, start the backend and execute the following commands ```shell +curl -o openapi-clowm.json -L http://localhost:9999/api/openapi.json npm run generate-client ``` diff --git a/Dockerfile b/Dockerfile index d640ca8ee17bf98f7caaaa857f04058310a14768..9232a56c2ed56c7ec10638a956378f08265f8b1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # build stage -FROM node:20 as build-stage +FROM node:20 AS build-stage WORKDIR /app COPY package.json ./ COPY package-lock.json ./ @@ -8,7 +8,7 @@ COPY . . RUN npm run build-only # production stage -FROM nginx:stable-alpine as production-stage +FROM nginx:stable-alpine AS production-stage EXPOSE 80 HEALTHCHECK --interval=5s --timeout=2s CMD curl --head -f http://localhost || exit 1 COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/package-lock.json b/package-lock.json index 8a5906d87daf2a2b6b1fb36b49d3c6e7182d4a72..751da57fae74e0a71ba2af6e4da1993d067d7152 100644 --- a/package-lock.json +++ b/package-lock.json @@ -50,7 +50,7 @@ "@vue/eslint-config-typescript": "~14.0.0", "@vue/tsconfig": "~0.5.0", "eslint": "~9.12.0", - "eslint-plugin-vue": "~9.28.0", + "eslint-plugin-vue": "~9.29.0", "highlight.js": "^11.9.0", "npm-run-all": "~4.1.5", "prettier": "~3.3.0", @@ -282,18 +282,18 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.669.0.tgz", - "integrity": "sha512-diqkwSqWjgAZ5NdYOtpYI/JlwsFDmo7x2Q+QOHevFaCEp4vgZOwJyXE3DQbKoO1X10fMH2vOUULgonPJd4ClUQ==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.670.0.tgz", + "integrity": "sha512-8Pwu1K+PgbYpXDaGKNy5hEbRH5FXHlfXJOhtV4oEDroL7ngix3ZUVWN9oIVVSDK02y1oQS1jCSEGUiUiauzb0g==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.669.0", - "@aws-sdk/client-sts": "3.669.0", + "@aws-sdk/client-sso-oidc": "3.670.0", + "@aws-sdk/client-sts": "3.670.0", "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.669.0", + "@aws-sdk/credential-provider-node": "3.670.0", "@aws-sdk/middleware-bucket-endpoint": "3.667.0", "@aws-sdk/middleware-expect-continue": "3.667.0", "@aws-sdk/middleware-flexible-checksums": "3.669.0", @@ -308,7 +308,7 @@ "@aws-sdk/signature-v4-multi-region": "3.669.0", "@aws-sdk/types": "3.667.0", "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.670.0", "@aws-sdk/util-user-agent-node": "3.669.0", "@aws-sdk/xml-builder": "3.662.0", "@smithy/config-resolver": "^3.0.9", @@ -351,9 +351,9 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.669.0.tgz", - "integrity": "sha512-WNpfNYIHzehLv98F+KolJglXNjJOTbOvIbSZ2XAnebVLmXCWeEEd1r4dIH0oI2dHtLbQ/h3uqaeQhsVQjLAxpw==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.670.0.tgz", + "integrity": "sha512-J+oz6uSsDvk4pimMDnKJb1wsV216zTrejvMTIL4RhUD1QPIVVOpteTdUShcjZUIZnkcJZGI+cym/SFK0kuzTpg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", @@ -366,7 +366,7 @@ "@aws-sdk/region-config-resolver": "3.667.0", "@aws-sdk/types": "3.667.0", "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.670.0", "@aws-sdk/util-user-agent-node": "3.669.0", "@smithy/config-resolver": "^3.0.9", "@smithy/core": "^2.4.8", @@ -400,15 +400,15 @@ } }, "node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.669.0.tgz", - "integrity": "sha512-E7uYOS3Axhe36Zeq6iLC9kjF1mMEyCQ4fXud11h22rbjq7PFUtN2Omekrch37eUx3BFj1jMePnuTnT98t5LWnw==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.670.0.tgz", + "integrity": "sha512-4qDK2L36Q4J1lfemaHHd9ZxqKRaos3STp44qPAHf/8QyX6Uk5sXgZNVO2yWM7SIEtVKwwBh/fZAsdBkGPBfZcw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.669.0", + "@aws-sdk/credential-provider-node": "3.670.0", "@aws-sdk/middleware-host-header": "3.667.0", "@aws-sdk/middleware-logger": "3.667.0", "@aws-sdk/middleware-recursion-detection": "3.667.0", @@ -416,7 +416,7 @@ "@aws-sdk/region-config-resolver": "3.667.0", "@aws-sdk/types": "3.667.0", "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.670.0", "@aws-sdk/util-user-agent-node": "3.669.0", "@smithy/config-resolver": "^3.0.9", "@smithy/core": "^2.4.8", @@ -449,20 +449,20 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.669.0" + "@aws-sdk/client-sts": "^3.670.0" } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.669.0.tgz", - "integrity": "sha512-1XdOBtHKCVxVkEDiy+oktJNSsySj3ADyh58KpHaqgvCQKV3vmfkr0YO5dG4kqq+TflNwdfl1YgMuOUiCulOWeQ==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.670.0.tgz", + "integrity": "sha512-bExrNo8ZVWorS3cjMZKQnA2HWqDmAzcZoSN/cPVoPFNkHwdl1lzPxvcLzmhpIr48JHgKfybBjrbluDZfIYeEog==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/client-sso-oidc": "3.669.0", + "@aws-sdk/client-sso-oidc": "3.670.0", "@aws-sdk/core": "3.667.0", - "@aws-sdk/credential-provider-node": "3.669.0", + "@aws-sdk/credential-provider-node": "3.670.0", "@aws-sdk/middleware-host-header": "3.667.0", "@aws-sdk/middleware-logger": "3.667.0", "@aws-sdk/middleware-recursion-detection": "3.667.0", @@ -470,7 +470,7 @@ "@aws-sdk/region-config-resolver": "3.667.0", "@aws-sdk/types": "3.667.0", "@aws-sdk/util-endpoints": "3.667.0", - "@aws-sdk/util-user-agent-browser": "3.667.0", + "@aws-sdk/util-user-agent-browser": "3.670.0", "@aws-sdk/util-user-agent-node": "3.669.0", "@smithy/config-resolver": "^3.0.9", "@smithy/core": "^2.4.8", @@ -563,16 +563,16 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.669.0.tgz", - "integrity": "sha512-YHhfH7w29BmMPnOK0BrBhEy2IRFRSRGSCyz3jtqpG883CZ2Lxan/AzaJDfKRdz350KPgbMMBwbPkIrqNIsg8iw==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.670.0.tgz", + "integrity": "sha512-TB1gacUj75leaTt2JsCTzygDSIk4ksv9uZoR7VenlgFPRktyOeT+fapwIVBeB2Qg7b9uxAY2K5XkKstDZyBEEw==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/core": "3.667.0", "@aws-sdk/credential-provider-env": "3.667.0", "@aws-sdk/credential-provider-http": "3.667.0", "@aws-sdk/credential-provider-process": "3.667.0", - "@aws-sdk/credential-provider-sso": "3.669.0", + "@aws-sdk/credential-provider-sso": "3.670.0", "@aws-sdk/credential-provider-web-identity": "3.667.0", "@aws-sdk/types": "3.667.0", "@smithy/credential-provider-imds": "^3.2.4", @@ -585,20 +585,20 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-sts": "^3.669.0" + "@aws-sdk/client-sts": "^3.670.0" } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.669.0.tgz", - "integrity": "sha512-O506azQcq6N1gnDX870MXXL9LHlhX0k6BlLMM6IDClxVDnlNkK3+n2cAEKSy8HwZJcRlekcsKz/AS2CxjPY+fg==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.670.0.tgz", + "integrity": "sha512-zwNrRYzubk4CaZ7zebeDhxsm8QtNWkbGKopZPOaZSnd5uqUGRcmx4ccVRngWUK68XDP44aEUWC8iU5Pc7btpHQ==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/credential-provider-env": "3.667.0", "@aws-sdk/credential-provider-http": "3.667.0", - "@aws-sdk/credential-provider-ini": "3.669.0", + "@aws-sdk/credential-provider-ini": "3.670.0", "@aws-sdk/credential-provider-process": "3.667.0", - "@aws-sdk/credential-provider-sso": "3.669.0", + "@aws-sdk/credential-provider-sso": "3.670.0", "@aws-sdk/credential-provider-web-identity": "3.667.0", "@aws-sdk/types": "3.667.0", "@smithy/credential-provider-imds": "^3.2.4", @@ -629,12 +629,12 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.669.0.tgz", - "integrity": "sha512-HvpMJQ8xZuEGjadARVOfORPZx4U23PC5Jf6Fj+/NWK4VxEXhvf8J037fvGp3xRds5wUeuBBbhWX+Cbt0lbLCwQ==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.670.0.tgz", + "integrity": "sha512-5PkA8BOy4q57Vhe9AESoHKZ7vjRbElNPKjXA4qC01xY+DitClRFz4O3B9sMzFp0PHlz9nDVSXXKgq0yzF/nAag==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.669.0", + "@aws-sdk/client-sso": "3.670.0", "@aws-sdk/core": "3.667.0", "@aws-sdk/token-providers": "3.667.0", "@aws-sdk/types": "3.667.0", @@ -667,9 +667,9 @@ } }, "node_modules/@aws-sdk/lib-storage": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.669.0.tgz", - "integrity": "sha512-2tk+APdadz4zjGeWz3cR3vjiQoAeAOhpYdVlLc9HbbtwxCj6eHctK6HJl5vCzCSzAL6bkZqWoMFMkura5Bm5MA==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-storage/-/lib-storage-3.670.0.tgz", + "integrity": "sha512-uOXSyzOClpX6s4+Srtq/hLBenK/glbexUpOQfOSfVTm/Wh7oB9liAJhYi69nZgSjdqkAB4/YtPYUgPyrcLqHiw==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^3.1.5", @@ -684,7 +684,7 @@ "node": ">=16.0.0" }, "peerDependencies": { - "@aws-sdk/client-s3": "^3.669.0" + "@aws-sdk/client-s3": "^3.670.0" } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { @@ -875,9 +875,9 @@ } }, "node_modules/@aws-sdk/s3-request-presigner": { - "version": "3.669.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.669.0.tgz", - "integrity": "sha512-vlSiieoqw2+xRqjwuRtb6mzqPi7xN8djd9OorHCAwnDryVRWQucRi8ZRf5+BT6Bc+3sbqV3DTgw847EA3rNq2w==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/s3-request-presigner/-/s3-request-presigner-3.670.0.tgz", + "integrity": "sha512-cTTQWJhmCRNH9NZbTqAhx3aXmC+p7gYzj7kNOyzdTA6D4jJOQZekxFFEQ0T2BPFYPR9Elk77W0yK+Nv9wMVD4g==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/signature-v4-multi-region": "3.669.0", @@ -997,9 +997,9 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.667.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.667.0.tgz", - "integrity": "sha512-y1pKlNzNpxzddM0QSnfIfIbi3Z9LTag1VDjYyZRbEGGSVip2J00qKsET+979nRezWMyJgw5GPBQR3Y+rN+jh0Q==", + "version": "3.670.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.670.0.tgz", + "integrity": "sha512-iRynWWazqEcCKwGMcQcywKTDLdLvqts1Yx474U64I9OKQXXwhOwhXbF5CAPSRta86lkVNAVYJa/0Bsv45pNn1A==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "3.667.0", @@ -1691,20 +1691,23 @@ } }, "node_modules/@hey-api/client-axios": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/@hey-api/client-axios/-/client-axios-0.2.7.tgz", - "integrity": "sha512-3691It5Bt87/kS1K5+vPt6RdSk/gCnkiaEgjrasgRWKHktJ727f+7QWs+KfmCTSGeXf5ODTu7zNOBwzVkLzGkA==", + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@hey-api/client-axios/-/client-axios-0.2.9.tgz", + "integrity": "sha512-K4T0KdBKl4LKbAVtfL1E9y9l2Q79NskptPBQcV1+RayjADCJctOtwdjwJitkpxj31UfQ79TSKHH96mMs9HrSsQ==", "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/mrlubos" + }, "peerDependencies": { "axios": ">= 1.0.0 < 2" } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.53.9", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.53.9.tgz", - "integrity": "sha512-5fdjxM7N9IQXXLGZLezRnaP/s/wnh4OProHyX0pdO9T7sVfUk6sO4tmlqjsbmtL3Woh3yLJYFWf/mjuhtcXEpA==", + "version": "0.53.11", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.53.11.tgz", + "integrity": "sha512-PaO+o0jDhfHVS5SjtonP5CzP/NYoW8dVZUn8WthSgzpgPts8AiWYXplOyk5uEnM4ZxbkZbeTiREwaNLnJmXlTQ==", "dev": true, - "license": "MIT", + "license": "FSL-1.1-MIT", "dependencies": { "@apidevtools/json-schema-ref-parser": "11.7.0", "c12": "2.0.1", @@ -1717,6 +1720,9 @@ "engines": { "node": "^18.0.0 || >=20.0.0" }, + "funding": { + "url": "https://github.com/sponsors/mrlubos" + }, "peerDependencies": { "typescript": "^5.x" } @@ -4957,9 +4963,9 @@ } }, "node_modules/eslint-plugin-vue": { - "version": "9.28.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.28.0.tgz", - "integrity": "sha512-ShrihdjIhOTxs+MfWun6oJWuk+g/LAhN+CiuOl/jjkG3l0F2AuK5NMTaWqyvBgkFtpYmyks6P4603mLmhNJW8g==", + "version": "9.29.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.29.0.tgz", + "integrity": "sha512-hamyjrBhNH6Li6R1h1VF9KHfshJlKgKEg3ARbGTn72CMNDSMhWbgC7NdkRDEh25AFW+4SDATzyNM+3gWuZii8g==", "dev": true, "license": "MIT", "dependencies": { @@ -8668,9 +8674,9 @@ } }, "node_modules/vite": { - "version": "5.4.8", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", - "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 1e19e55b67ca313b633818cc8580d6d1309f433b..0ae94ecac2da95988f8ffdd38a8c38b29832ea82 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "@vue/eslint-config-typescript": "~14.0.0", "@vue/tsconfig": "~0.5.0", "eslint": "~9.12.0", - "eslint-plugin-vue": "~9.28.0", + "eslint-plugin-vue": "~9.29.0", "highlight.js": "^11.9.0", "npm-run-all": "~4.1.5", "prettier": "~3.3.0", diff --git a/src/App.vue b/src/App.vue index 4a20ed47680f1a9a6a28d22fbda726e0bb846978..cf641530a85f5003aec824e0fdb32ff9202bd93d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -13,6 +13,10 @@ import { useWorkflowStore } from "@/stores/workflows"; import { useBucketStore } from "@/stores/buckets"; import { useS3KeyStore } from "@/stores/s3keys"; import { stringify as param_stringify } from "qs"; +import { useS3ObjectStore } from "@/stores/s3objects"; +import { useOTRStore } from "@/stores/otrs"; +import { OwnershipTypeEnum } from "@/client"; +import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; const { cookies } = useCookies(); const router = useRouter(); @@ -23,6 +27,9 @@ const resourceRepository = useResourceStore(); const workflowRepository = useWorkflowStore(); const bucketRepository = useBucketStore(); const s3KeyRepository = useS3KeyStore(); +const objectRepository = useS3ObjectStore(); +const otrRepository = useOTRStore(); +const executionRepository = useWorkflowExecutionStore(); onBeforeMount(() => { client.setConfig({ @@ -81,6 +88,12 @@ onBeforeMount(() => { window._paq.push(["trackPageView"]); window._paq.push(["enableLinkTracking"]); }); + router.beforeEach((to) => { + // redirect path that start with '/dashboard' to ensure backwards compatibility + if (/^\/dashboard\/[\w]+/.test(to.fullPath)) { + return to.fullPath.slice(10); + } + }); router.beforeEach(async (to) => { // make sure the user is authenticated if (to.meta.public) { @@ -123,18 +136,33 @@ onBeforeMount(() => { onMounted(() => { if (userRepository.authenticated) { + Promise.all([ + s3KeyRepository.fetchS3Keys(), + bucketRepository.fetchOwnBuckets(), + ]).then(() => { + Promise.all( + bucketRepository.buckets + .filter((bucket) => bucket.owner_id === userRepository.currentUID) + .map((bucket) => objectRepository.fetchMultipartUploads(bucket.name)), + ); + }); resourceRepository.fetchPublicResources(); workflowRepository.fetchWorkflows(); - bucketRepository.fetchOwnBuckets(); - s3KeyRepository.fetchS3Keys(); + otrRepository.fetchOwnOtrs(OwnershipTypeEnum.BUCKET); + executionRepository.fetchExecutions(); if (!userRepository.foreignUser) { bucketRepository.fetchOwnPermissions(); } if (userRepository.workflowDev || userRepository.admin) { workflowRepository.fetchOwnWorkflows(); + otrRepository.fetchOwnOtrs(OwnershipTypeEnum.WORKFLOW); } if (userRepository.resourceMaintainer || userRepository.admin) { resourceRepository.fetchOwnResources(); + otrRepository.fetchOwnOtrs(OwnershipTypeEnum.RESOURCE); + } + if (userRepository.admin) { + resourceRepository.fetchSyncRequests(); } } }); @@ -143,7 +171,7 @@ onMounted(() => { <template> <AppHeader /> <div class="container-xxl mt-2 flex-grow-1 py-2"> - <router-view></router-view> + <router-view /> </div> <AppFooter /> </template> diff --git a/src/components/BootstrapCard.vue b/src/components/BootstrapCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..a089d79a2a9f4bd8adc528feb9ee5f4e7a82e65a --- /dev/null +++ b/src/components/BootstrapCard.vue @@ -0,0 +1,24 @@ +<script setup lang="ts"> +import { useSlots } from "vue"; + +const slots = useSlots(); +</script> + +<template> + <div class="card"> + <div class="card-body"> + <h5 class="card-title"> + <slot name="title" /> + </h5> + <h6 v-if="slots.subtitle" class="card-subtitle mb-2 text-body-secondary"> + <slot name="subtitle" /> + </h6> + <div class="card-text"> + <slot name="body" /> + </div> + <slot name="footer" /> + </div> + </div> +</template> + +<style scoped></style> diff --git a/src/router/adminRoutes.ts b/src/router/adminRoutes.ts index e5e6e9ca7a16017b977368b6c2d0d318e09f607b..0a16749294172a426488b5ca9189feac7a8413d5 100644 --- a/src/router/adminRoutes.ts +++ b/src/router/adminRoutes.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; export const adminRoutes: RouteRecordRaw[] = [ { - path: "admin/resources", + path: "/admin/resources", name: "admin-resources", component: () => import("../views/admin/AdminResourcesView.vue"), meta: { @@ -11,7 +11,7 @@ export const adminRoutes: RouteRecordRaw[] = [ }, }, { - path: "admin/users", + path: "/admin/users", name: "admin-users", component: () => import("../views/admin/AdminUsersView.vue"), meta: { @@ -20,7 +20,7 @@ export const adminRoutes: RouteRecordRaw[] = [ }, }, { - path: "admin/buckets", + path: "/admin/buckets", name: "admin-buckets", component: () => import("../views/admin/AdminBucketsView.vue"), meta: { @@ -29,7 +29,7 @@ export const adminRoutes: RouteRecordRaw[] = [ }, }, { - path: "admin/executions", + path: "/admin/executions", name: "admin-executions", component: () => import("../views/admin/AdminWorkflowExecutionView.vue"), meta: { @@ -38,7 +38,7 @@ export const adminRoutes: RouteRecordRaw[] = [ }, }, { - path: "admin/sync-requests", + path: "/admin/sync-requests", name: "admin-sync-requests", component: () => import("../views/admin/AdminSyncRequestsView.vue"), meta: { diff --git a/src/router/index.ts b/src/router/index.ts index 344598c79426cd5ea53da8e8f59950ab015abe0c..81ae69161f77647ce34babd5f3dfc6b8a4509161 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -9,17 +9,18 @@ import { userRoutes } from "@/router/userRoutes"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), routes: [ + ...workflowRoutes, + ...s3Routes, + ...resourceRoutes, + ...adminRoutes, + ...userRoutes, { path: "/dashboard", name: "dashboard", component: () => import("../views/DashboardView.vue"), - children: [ - ...resourceRoutes, - ...s3Routes, - ...workflowRoutes, - ...adminRoutes, - ...userRoutes, - ], + meta: { + title: "Dashboard", + }, }, { path: "/login", @@ -47,7 +48,7 @@ const router = createRouter({ { path: "/", redirect: { - name: "buckets", + name: "dashboard", }, }, { diff --git a/src/router/resourceRoutes.ts b/src/router/resourceRoutes.ts index 4dd57177d3222e77e3541c2cb387b79da3d3b654..04070719717fb2228a1db7037ee11907e44a5e61 100644 --- a/src/router/resourceRoutes.ts +++ b/src/router/resourceRoutes.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; export const resourceRoutes: RouteRecordRaw[] = [ { - path: "resources", + path: "/resources", name: "resources", component: () => import("../views/resources/ListResourcesView.vue"), meta: { @@ -10,7 +10,7 @@ export const resourceRoutes: RouteRecordRaw[] = [ }, }, { - path: "maintainer/resources", + path: "/maintainer/resources", name: "resource-maintainer", component: () => import("../views/resources/MyResourcesView.vue"), meta: { @@ -19,7 +19,7 @@ export const resourceRoutes: RouteRecordRaw[] = [ }, }, { - path: "reviewer/resources", + path: "/reviewer/resources", name: "resource-review", component: () => import("../views/resources/ReviewResourceView.vue"), meta: { diff --git a/src/router/s3Routes.ts b/src/router/s3Routes.ts index 9ec2a42967d2b38ebde016b69d6ade39811c2ea5..d7f70b49e46f747cbe76dc496d52697a4d9f1b20 100644 --- a/src/router/s3Routes.ts +++ b/src/router/s3Routes.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; export const s3Routes: RouteRecordRaw[] = [ { - path: "object-storage/buckets", + path: "/object-storage/buckets", name: "buckets", component: () => import("../views/object-storage/BucketsView.vue"), props: (route) => ({ @@ -21,7 +21,7 @@ export const s3Routes: RouteRecordRaw[] = [ ], }, { - path: "object-storage/s3-keys", + path: "/object-storage/s3-keys", name: "s3_keys", meta: { title: "S3 Keys", @@ -29,7 +29,7 @@ export const s3Routes: RouteRecordRaw[] = [ component: () => import("../views/object-storage/S3KeysView.vue"), }, { - path: "object-storage/multipart-uploads", + path: "/object-storage/multipart-uploads", name: "s3_multipart-uploads", meta: { title: "Multipart Uploads", diff --git a/src/router/userRoutes.ts b/src/router/userRoutes.ts index 1ca6ca2e9ffa57e212fa8f8c2fa1f8362d269c73..5dee8da1ee4bfefb531ab4ac31334461fddf2094 100644 --- a/src/router/userRoutes.ts +++ b/src/router/userRoutes.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; export const userRoutes: RouteRecordRaw[] = [ { - path: "api-tokens", + path: "/api-tokens", name: "api-tokens", component: () => import("../views/user/ListApiTokenView.vue"), meta: { diff --git a/src/router/workflowRoutes.ts b/src/router/workflowRoutes.ts index a0e896ace2c23cd89da536367b10d41500e8536c..942690dceb9ca6d19775e178fc892f24ce016ed9 100644 --- a/src/router/workflowRoutes.ts +++ b/src/router/workflowRoutes.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from "vue-router"; export const workflowRoutes: RouteRecordRaw[] = [ { - path: "workflow-executions", + path: "/workflow-executions", name: "workflow-executions", meta: { title: "My Workflow Executions", @@ -11,7 +11,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ import("../views/workflows/ListWorkflowExecutionsView.vue"), }, { - path: "workflows", + path: "/workflows", name: "workflows", meta: { title: "Workflows", @@ -19,7 +19,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ component: () => import("../views/workflows/ListWorkflowsView.vue"), }, { - path: "developer/workflows", + path: "/developer/workflows", name: "workflows-developer", component: () => import("../views/workflows/MyWorkflowsView.vue"), meta: { @@ -28,7 +28,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ }, }, { - path: "developer/workflows/clowminfo", + path: "/developer/workflows/clowminfo", name: "workflows-clowminfo", component: () => import("../views/workflows/CreateClowmInfoView.vue"), props: (route) => ({ @@ -41,7 +41,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ }, }, { - path: "reviewer/workflows", + path: "/reviewer/workflows", name: "workflows-reviewer", component: () => import("../views/workflows/ReviewWorkflowsView.vue"), meta: { @@ -50,7 +50,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ }, }, { - path: "workflows/arbitrary", + path: "/workflows/arbitrary", name: "arbitrary-workflow", component: () => import("../views/workflows/ArbitraryWorkflowView.vue"), meta: { @@ -63,7 +63,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ }), }, { - path: "workflows/:workflowId", + path: "/workflows/:workflowId", name: "workflow", component: () => import("../views/workflows/WorkflowView.vue"), props: (route) => ({ @@ -97,7 +97,7 @@ export const workflowRoutes: RouteRecordRaw[] = [ ], }, { - path: "workflows/:workflowId/version/:versionId/parameters", + path: "/workflows/:workflowId/version/:versionId/parameters", name: "workflow-parameter-translation", component: () => import("../views/workflows/CreateParameterTranslationView.vue"), diff --git a/src/views/DashboardView.vue b/src/views/DashboardView.vue index 86643068a1375e4447a445adbfa36a96fe8ac32c..685cb7c0ecf4bb9c6441765f617219df1a98e012 100644 --- a/src/views/DashboardView.vue +++ b/src/views/DashboardView.vue @@ -1,7 +1,520 @@ -<script setup lang="ts"></script> +<script setup lang="ts"> +import { useUserStore } from "@/stores/users"; +import { useBucketStore } from "@/stores/buckets"; +import { useS3KeyStore } from "@/stores/s3keys"; +import { environment } from "@/environment"; +import { useS3ObjectStore } from "@/stores/s3objects"; +import { computed } from "vue"; +import BootstrapCard from "@/components/BootstrapCard.vue"; +import { useOTRStore } from "@/stores/otrs"; +import { useWorkflowStore } from "@/stores/workflows"; +import { + ResourceVersionStatus, + WorkflowExecutionStatus, + WorkflowVersionStatus, +} from "@/client/types.gen"; +import type { + ResourceOut, + WorkflowOut, + WorkflowVersion, +} from "@/client/types.gen"; +import dayjs from "dayjs"; +import { useWorkflowExecutionStore } from "@/stores/workflowExecutions"; +import { useResourceStore } from "@/stores/resources"; + +const authStore = useUserStore(); +const bucketStore = useBucketStore(); +const keyStore = useS3KeyStore(); +const objectStore = useS3ObjectStore(); +const otrStore = useOTRStore(); +const workflowStore = useWorkflowStore(); +const executionStore = useWorkflowExecutionStore(); +const resourceStore = useResourceStore(); + +function sortWorkflowsByUpdate(a: WorkflowOut, b: WorkflowOut): number { + return a.versions[0].created_at <= b.versions[0].created_at ? 1 : -1; +} + +function sortVersionsByCreation( + a: WorkflowVersion, + b: WorkflowVersion, +): number { + return a.created_at < b.created_at ? 1 : -1; +} + +function sortWorkflows(workflows: WorkflowOut[]): WorkflowOut[] { + const temp = JSON.parse( + JSON.stringify( + workflows.filter((workflow) => workflow.versions.length > 0), + ), + ); + for (const index in temp) { + const versions = [...temp[index].versions]; + versions.sort(sortVersionsByCreation); + temp[index].versions = versions; + } + temp.sort(sortWorkflowsByUpdate); + return temp; +} + +const activeUploadBuckets = computed<string[]>(() => { + return Object.keys(objectStore.multiPartUploadsMapping).filter( + (bucketName) => objectStore.multiPartUploadsMapping[bucketName].length > 0, + ); +}); + +const processedWorkflows = computed<WorkflowOut[]>(() => + sortWorkflows(workflowStore.workflows), +); + +const processedOwnWorkflows = computed<WorkflowOut[]>(() => + sortWorkflows(workflowStore.ownWorkflows), +); + +const pendingOwnResourceReviews = computed<number>(() => + accumulateResourceStatus( + resourceStore.ownResources, + ResourceVersionStatus.WAIT_FOR_REVIEW, + ), +); + +const pendingResourceReviews = computed<number>(() => + accumulateResourceStatus( + resourceStore.reviewableResources, + ResourceVersionStatus.WAIT_FOR_REVIEW, + ), +); + +const pendingOwnResourceUploads = computed<number>(() => + accumulateResourceStatus( + resourceStore.ownResources, + ResourceVersionStatus.RESOURCE_REQUESTED, + ), +); + +const pendingOwnWorkflowReviews = computed<number>(() => + accumulateWorkflowStatus( + processedOwnWorkflows.value, + WorkflowVersionStatus.CREATED, + ), +); + +const pendingWorkflowReviews = computed<number>(() => + accumulateWorkflowStatus( + workflowStore.reviewableWorkflows, + WorkflowVersionStatus.CREATED, + ), +); + +function accumulateResourceStatus( + resources: ResourceOut[], + status: ResourceVersionStatus, +): number { + return resources.reduce( + (acc, cur) => + acc + + cur.versions.reduce( + (accVer, curVer) => accVer + (curVer.status == status ? 1 : 0), + 0, + ), + 0, + ); +} + +function accumulateWorkflowStatus( + workflows: WorkflowOut[], + status: WorkflowVersionStatus, +): number { + return workflows.reduce( + (acc, cur) => + acc + + cur.versions.reduce( + (accVer: number, curVer: WorkflowVersion) => + accVer + (curVer.status == status ? 1 : 0), + 0, + ), + 0, + ); +} +</script> <template> - <router-view /> + <h2>Dashboard</h2> + <div class="row p-1 pe-3"> + <div + class="border rounded col-md-4 order-md-last p-2 mb-md-0 mb-2" + style="min-height: 100px" + > + <h2>News</h2> + <div class="fst-italic text-center text-secondary mb-2 mt-md-4 mt-lg-1"> + Coming soon ... + </div> + </div> + <div class="col-md-8"> + <h3>Files</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill"> + <template #title> + <router-link :to="{ name: 'buckets' }" + >My Data Buckets + </router-link> + </template> + <template #body> + <div>Access to {{ bucketStore.buckets.length }} buckets</div> + <ul class="mb-0"> + <li> + {{ + bucketStore.buckets.filter( + (bucket) => bucket.owner_id == authStore.currentUID, + ).length + }} + own bucket(s) + </li> + <li> + {{ + bucketStore.buckets.filter( + (bucket) => bucket.owner_id != authStore.currentUID, + ).length + }} + shared bucket(s) + </li> + </ul> + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill"> + <template #title> + <router-link :to="{ name: 'buckets' }" + >Bucket Ownership Transfers + </router-link> + </template> + <template #body> + <div> + {{ otrStore.bucketOtrs.length }} open bucket ownership transfer + requests + </div> + <ul class="mb-0"> + <li> + {{ + otrStore.bucketOtrs.filter( + (otr) => otr.current_owner_uid == authStore.currentUID, + ).length + }} + pending request to others + </li> + <li> + {{ + otrStore.bucketOtrs.filter( + (otr) => otr.new_owner_uid == authStore.currentUID, + ).length + }} + request to accept / reject + </li> + </ul> + </template> + </bootstrap-card> + <bootstrap-card class="m-2 w-fit hover-shadow flex-fill"> + <template #title> + <router-link :to="{ name: 's3_keys' }">S3 Bucket Keys</router-link> + </template> + <template #body> + <div>{{ keyStore.keys.length }} active keys</div> + <div>S3 Endpoint:</div> + <a :href="environment.S3_URL">{{ environment.S3_URL }}</a> + </template> + </bootstrap-card> + <bootstrap-card class="m-2 w-fit hover-shadow flex-fill"> + <template #title> + <router-link :to="{ name: 's3_multipart-uploads' }" + >Multipart Uploads + </router-link> + </template> + <template #body> + <div class="card-text"> + {{ activeUploadBuckets.length }} active multipart uploads + </div> + <template v-if="activeUploadBuckets.length > 0"> + <div>Buckets</div> + <ul> + <li v-for="bucketName in activeUploadBuckets" :key="bucketName"> + {{ bucketName }}: + {{ objectStore.multiPartUploadsMapping[bucketName].length }} + </li> + </ul> + </template> + </template> + </bootstrap-card> + </div> + <h3>Workflows</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'workflows' }" + >{{ processedWorkflows.length }} Available Workflows + </router-link> + </template> + <template #body> + <div>Latest workflows</div> + <ul class="mb-0"> + <li + v-for="workflow in processedWorkflows.slice(0, 3)" + :key="workflow.workflow_id" + > + {{ workflow.name }} <br /> + <span class="text-secondary" style="font-size: 0.8rem"> + Last update + {{ + dayjs.unix(workflow.versions[0].created_at).fromNow() + }}</span + > + </li> + </ul> + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'workflow-executions' }" + >Workflow Executions + </router-link> + </template> + <template #body> + <div> + Overall {{ executionStore.executions.length }} workflow executions + </div> + <ul class="mb-0"> + <li> + {{ + executionStore.executions.filter( + (execution) => + execution.status === WorkflowExecutionStatus.RUNNING || + execution.status === WorkflowExecutionStatus.PENDING || + execution.status === WorkflowExecutionStatus.SCHEDULED, + ).length + }} + running executions + </li> + <li> + {{ + executionStore.executions.filter( + (execution) => + execution.status === WorkflowExecutionStatus.SUCCESS, + ).length + }} + successful executions + </li> + <li> + {{ + executionStore.executions.filter( + (execution) => + execution.status === WorkflowExecutionStatus.ERROR, + ).length + }} + erroneous executions + </li> + </ul> + </template> + </bootstrap-card> + </div> + <template v-if="authStore.workflowDev"> + <h3>Developer</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'workflows-developer' }" + >My workflows + </router-link> + </template> + <template #body> + <div> + {{ processedOwnWorkflows.length }} own workflows, + {{ pendingOwnWorkflowReviews }} pending reviews + </div> + <ul v-if="processedOwnWorkflows.length > 0" class="mb-0"> + <li + v-for="workflow in processedOwnWorkflows.slice(0, 3)" + :key="workflow.workflow_id" + > + {{ workflow.name }}<br /> + <span class="text-secondary" style="font-size: 0.8rem"> + Last update + {{ + dayjs.unix(workflow.versions[0].created_at).fromNow() + }}</span + > + </li> + </ul> + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill"> + <template #title> + <router-link :to="{ name: 'workflows-developer' }" + >Workflow Ownership Transfers + </router-link> + </template> + <template #body> + <div> + {{ otrStore.workflowOtrs.length }} open workflow ownership + transfer requests + </div> + <ul class="mb-0"> + <li> + {{ + otrStore.workflowOtrs.filter( + (otr) => otr.current_owner_uid == authStore.currentUID, + ).length + }} + pending request to others + </li> + <li> + {{ + otrStore.workflowOtrs.filter( + (otr) => otr.new_owner_uid == authStore.currentUID, + ).length + }} + request to accept / reject + </li> + </ul> + </template> + </bootstrap-card> + </div> + </template> + <template v-if="authStore.resourceMaintainer"> + <h3>Resource Maintainer</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'resource-maintainer' }" + >My resources + </router-link> + </template> + <template #body> + <div>{{ resourceStore.ownResources.length }} own resources</div> + <ul v-if="processedOwnWorkflows.length > 0" class="mb-0"> + <li class="mb-0"> + {{ pendingOwnResourceUploads }} + pending uploads + </li> + <li class="mb-0"> + {{ pendingOwnResourceReviews }} + pending reviews + </li> + </ul> + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill"> + <template #title> + <router-link :to="{ name: 'resource-maintainer' }" + >Resource Ownership Transfers + </router-link> + </template> + <template #body> + <div> + {{ otrStore.resourceOtrs.length }} open workflow ownership + transfer requests + </div> + <ul class="mb-0"> + <li> + {{ + otrStore.resourceOtrs.filter( + (otr) => otr.current_owner_uid == authStore.currentUID, + ).length + }} + pending request to others + </li> + <li> + {{ + otrStore.resourceOtrs.filter( + (otr) => otr.new_owner_uid == authStore.currentUID, + ).length + }} + request to accept / reject + </li> + </ul> + </template> + </bootstrap-card> + </div> + </template> + <template v-if="authStore.reviewer"> + <h3>Reviewer</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'workflows-reviewer' }" + >Workflow Reviews + </router-link> + </template> + <template #body> + {{ pendingWorkflowReviews }} pending reviews + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'resource-review' }" + >Resource Reviews + </router-link> + </template> + <template #body> + {{ pendingResourceReviews }} pending reviews + </template> + </bootstrap-card> + </div> + </template> + <template v-if="authStore.admin"> + <h3>Admin</h3> + <div class="d-flex flex-wrap align-items-center"> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'admin-sync-requests' }" + >Resource Sync Requests + </router-link> + </template> + <template #body> + {{ resourceStore.syncRequests.length }} pending sync requests + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'admin-users' }">Users</router-link> + </template> + <template #body> + Invite or find users and update their roles + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'admin-buckets' }" + >Buckets + </router-link> + </template> + <template #body> + Manage all buckets and update their quotas + </template> + </bootstrap-card> + + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'admin-resources' }" + >Resources + </router-link> + </template> + <template #body> + Inspect all resources and manage their status + </template> + </bootstrap-card> + <bootstrap-card class="hover-shadow m-2 flex-fill w-fit"> + <template #title> + <router-link :to="{ name: 'admin-executions' }" + >Workflow Executions + </router-link> + </template> + <template #body> + Search for workflow executions with extensive filters and inspect + them + </template> + </bootstrap-card> + </div> + </template> + </div> + </div> </template> <style scoped></style>