diff --git a/Dockerfile b/Dockerfile index 3293d59d4f883e4b6fd059eba5e27759020c7a9a..d640ca8ee17bf98f7caaaa857f04058310a14768 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ RUN npm run build-only # production stage FROM nginx:stable-alpine as production-stage EXPOSE 80 -HEALTHCHECK --interval=10s --timeout=2s CMD curl --head -f http://localhost || exit 1 +HEALTHCHECK --interval=5s --timeout=2s CMD curl --head -f http://localhost || exit 1 COPY nginx.conf /etc/nginx/conf.d/default.conf COPY --from=build-stage /app/dist /usr/share/nginx/html COPY --from=build-stage /app/src/assets/env.template.js /tmp diff --git a/package-lock.json b/package-lock.json index 4a1883630ec573fea9a35768ea5a8c122b97f241..dd0ffb929e33117ce344179a8befa93f58057531 100644 --- a/package-lock.json +++ b/package-lock.json @@ -70,9 +70,9 @@ } }, "node_modules/@apidevtools/json-schema-ref-parser": { - "version": "11.5.5", - "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.5.5.tgz", - "integrity": "sha512-hv/aXDILyroHioVW27etFMV+IX6FyNn41YwbeGIAt5h/7fUTQvHI5w3ols8qYAT8aQt3kzexq5ZwxFDxNHIhdQ==", + "version": "11.6.0", + "resolved": "https://registry.npmjs.org/@apidevtools/json-schema-ref-parser/-/json-schema-ref-parser-11.6.0.tgz", + "integrity": "sha512-I+d5/XrazqY86/kGsmjVercjjJ+w6MVXJj7vnHfUgXzaoLJAl0/tPk2WXVpHUeRqHqyJ6AGkXBqx6Dc3wJkrCQ==", "dev": true, "dependencies": { "@jsdevtools/ono": "^7.1.3", @@ -1575,9 +1575,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.14.3.tgz", - "integrity": "sha512-X9alQ3XM6I9IlSlmC8ddAvMSyG1WuHk5oUnXGw+yUBs3BFoTizmG1La/Gr8fVJvDWAq+zlYTZ9DBgrlKRVY06g==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.16.4.tgz", + "integrity": "sha512-GkhjAaQ8oUTOKE4g4gsZ0u8K/IHU1+2WQSgS1TwTcYvL+sjbaQjNHFXbOJ6kgqGHIO1DfUhI/Sphi9GkRT9K+Q==", "cpu": [ "arm" ], @@ -1588,9 +1588,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.14.3.tgz", - "integrity": "sha512-eQK5JIi+POhFpzk+LnjKIy4Ks+pwJ+NXmPxOCSvOKSNRPONzKuUvWE+P9JxGZVxrtzm6BAYMaL50FFuPe0oWMQ==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.16.4.tgz", + "integrity": "sha512-Bvm6D+NPbGMQOcxvS1zUl8H7DWlywSXsphAeOnVeiZLQ+0J6Is8T7SrjGTH29KtYkiY9vld8ZnpV3G2EPbom+w==", "cpu": [ "arm64" ], @@ -1601,9 +1601,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.14.3.tgz", - "integrity": "sha512-Od4vE6f6CTT53yM1jgcLqNfItTsLt5zE46fdPaEmeFHvPs5SjZYlLpHrSiHEKR1+HdRfxuzXHjDOIxQyC3ptBA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.16.4.tgz", + "integrity": "sha512-i5d64MlnYBO9EkCOGe5vPR/EeDwjnKOGGdd7zKFhU5y8haKhQZTN2DgVtpODDMxUr4t2K90wTUJg7ilgND6bXw==", "cpu": [ "arm64" ], @@ -1614,9 +1614,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.14.3.tgz", - "integrity": "sha512-0IMAO21axJeNIrvS9lSe/PGthc8ZUS+zC53O0VhF5gMxfmcKAP4ESkKOCwEi6u2asUrt4mQv2rjY8QseIEb1aw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.16.4.tgz", + "integrity": "sha512-WZupV1+CdUYehaZqjaFTClJI72fjJEgTXdf4NbW69I9XyvdmztUExBtcI2yIIU6hJtYvtwS6pkTkHJz+k08mAQ==", "cpu": [ "x64" ], @@ -1627,9 +1627,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.14.3.tgz", - "integrity": "sha512-ge2DC7tHRHa3caVEoSbPRJpq7azhG+xYsd6u2MEnJ6XzPSzQsTKyXvh6iWjXRf7Rt9ykIUWHtl0Uz3T6yXPpKw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.16.4.tgz", + "integrity": "sha512-ADm/xt86JUnmAfA9mBqFcRp//RVRt1ohGOYF6yL+IFCYqOBNwy5lbEK05xTsEoJq+/tJzg8ICUtS82WinJRuIw==", "cpu": [ "arm" ], @@ -1640,9 +1640,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.14.3.tgz", - "integrity": "sha512-ljcuiDI4V3ySuc7eSk4lQ9wU8J8r8KrOUvB2U+TtK0TiW6OFDmJ+DdIjjwZHIw9CNxzbmXY39wwpzYuFDwNXuw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.16.4.tgz", + "integrity": "sha512-tJfJaXPiFAG+Jn3cutp7mCs1ePltuAgRqdDZrzb1aeE3TktWWJ+g7xK9SNlaSUFw6IU4QgOxAY4rA+wZUT5Wfg==", "cpu": [ "arm" ], @@ -1653,9 +1653,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.14.3.tgz", - "integrity": "sha512-Eci2us9VTHm1eSyn5/eEpaC7eP/mp5n46gTRB3Aar3BgSvDQGJZuicyq6TsH4HngNBgVqC5sDYxOzTExSU+NjA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.16.4.tgz", + "integrity": "sha512-7dy1BzQkgYlUTapDTvK997cgi0Orh5Iu7JlZVBy1MBURk7/HSbHkzRnXZa19ozy+wwD8/SlpJnOOckuNZtJR9w==", "cpu": [ "arm64" ], @@ -1666,9 +1666,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.14.3.tgz", - "integrity": "sha512-UrBoMLCq4E92/LCqlh+blpqMz5h1tJttPIniwUgOFJyjWI1qrtrDhhpHPuFxULlUmjFHfloWdixtDhSxJt5iKw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.16.4.tgz", + "integrity": "sha512-zsFwdUw5XLD1gQe0aoU2HVceI6NEW7q7m05wA46eUAyrkeNYExObfRFQcvA6zw8lfRc5BHtan3tBpo+kqEOxmg==", "cpu": [ "arm64" ], @@ -1679,9 +1679,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.14.3.tgz", - "integrity": "sha512-5aRjvsS8q1nWN8AoRfrq5+9IflC3P1leMoy4r2WjXyFqf3qcqsxRCfxtZIV58tCxd+Yv7WELPcO9mY9aeQyAmw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.16.4.tgz", + "integrity": "sha512-p8C3NnxXooRdNrdv6dBmRTddEapfESEUflpICDNKXpHvTjRRq1J82CbU5G3XfebIZyI3B0s074JHMWD36qOW6w==", "cpu": [ "ppc64" ], @@ -1692,9 +1692,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.14.3.tgz", - "integrity": "sha512-sk/Qh1j2/RJSX7FhEpJn8n0ndxy/uf0kI/9Zc4b1ELhqULVdTfN6HL31CDaTChiBAOgLcsJ1sgVZjWv8XNEsAQ==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.16.4.tgz", + "integrity": "sha512-Lh/8ckoar4s4Id2foY7jNgitTOUQczwMWNYi+Mjt0eQ9LKhr6sK477REqQkmy8YHY3Ca3A2JJVdXnfb3Rrwkng==", "cpu": [ "riscv64" ], @@ -1705,9 +1705,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.14.3.tgz", - "integrity": "sha512-jOO/PEaDitOmY9TgkxF/TQIjXySQe5KVYB57H/8LRP/ux0ZoO8cSHCX17asMSv3ruwslXW/TLBcxyaUzGRHcqg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.16.4.tgz", + "integrity": "sha512-1xwwn9ZCQYuqGmulGsTZoKrrn0z2fAur2ujE60QgyDpHmBbXbxLaQiEvzJWDrscRq43c8DnuHx3QorhMTZgisQ==", "cpu": [ "s390x" ], @@ -1718,9 +1718,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.14.3.tgz", - "integrity": "sha512-8ybV4Xjy59xLMyWo3GCfEGqtKV5M5gCSrZlxkPGvEPCGDLNla7v48S662HSGwRd6/2cSneMQWiv+QzcttLrrOA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.16.4.tgz", + "integrity": "sha512-LuOGGKAJ7dfRtxVnO1i3qWc6N9sh0Em/8aZ3CezixSTM+E9Oq3OvTsvC4sm6wWjzpsIlOCnZjdluINKESflJLA==", "cpu": [ "x64" ], @@ -1731,9 +1731,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.14.3.tgz", - "integrity": "sha512-s+xf1I46trOY10OqAtZ5Rm6lzHre/UiLA1J2uOhCFXWkbZrJRkYBPO6FhvGfHmdtQ3Bx793MNa7LvoWFAm93bg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.16.4.tgz", + "integrity": "sha512-ch86i7KkJKkLybDP2AtySFTRi5fM3KXp0PnHocHuJMdZwu7BuyIKi35BE9guMlmTpwwBTB3ljHj9IQXnTCD0vA==", "cpu": [ "x64" ], @@ -1744,9 +1744,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.14.3.tgz", - "integrity": "sha512-+4h2WrGOYsOumDQ5S2sYNyhVfrue+9tc9XcLWLh+Kw3UOxAvrfOrSMFon60KspcDdytkNDh7K2Vs6eMaYImAZg==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.16.4.tgz", + "integrity": "sha512-Ma4PwyLfOWZWayfEsNQzTDBVW8PZ6TUUN1uFTBQbF2Chv/+sjenE86lpiEwj2FiviSmSZ4Ap4MaAfl1ciF4aSA==", "cpu": [ "arm64" ], @@ -1757,9 +1757,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.14.3.tgz", - "integrity": "sha512-T1l7y/bCeL/kUwh9OD4PQT4aM7Bq43vX05htPJJ46RTI4r5KNt6qJRzAfNfM+OYMNEVBWQzR2Gyk+FXLZfogGw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.16.4.tgz", + "integrity": "sha512-9m/ZDrQsdo/c06uOlP3W9G2ENRVzgzbSXmXHT4hwVaDQhYcRpi9bgBT0FTG9OhESxwK0WjQxYOSfv40cU+T69w==", "cpu": [ "ia32" ], @@ -1770,9 +1770,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.14.3.tgz", - "integrity": "sha512-/BypzV0H1y1HzgYpxqRaXGBRqfodgoBBCcsrujT6QRcakDQdfU+Lq9PENPh5jB4I44YWq+0C2eHsHya+nZY1sA==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.16.4.tgz", + "integrity": "sha512-YunpoOAyGLDseanENHmbFvQSfVL5BxW3k7hhy0eN4rb3gS/ct75dVD0EXOWIqFT/nE8XYW6LP6vz6ctKRi0k9A==", "cpu": [ "x64" ], @@ -2495,16 +2495,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.0.tgz", - "integrity": "sha512-GJWR0YnfrKnsRoluVO3PRb9r5aMZriiMMM/RHj5nnTrBy1/wIgk76XCtCKcnXGjpZQJQRFtGV9/0JJ6n30uwpQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", + "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/type-utils": "7.7.0", - "@typescript-eslint/utils": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/type-utils": "7.7.1", + "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.3.1", @@ -2530,15 +2530,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.0.tgz", - "integrity": "sha512-fNcDm3wSwVM8QYL4HKVBggdIPAy9Q41vcvC/GtDobw3c4ndVT3K6cqudUmjHPw8EAp4ufax0o58/xvWaP2FmTg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", + "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4" }, "engines": { @@ -2558,13 +2558,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.0.tgz", - "integrity": "sha512-/8INDn0YLInbe9Wt7dK4cXLDYp0fNHP5xKLHvZl3mOT5X17rK/YShXaiNmorl+/U4VKCVIjJnx4Ri5b0y+HClw==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", + "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0" + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2575,13 +2575,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.0.tgz", - "integrity": "sha512-bOp3ejoRYrhAlnT/bozNQi3nio9tIgv3U5C0mVDdZC7cpcQEDZXvq8inrHYghLVwuNABRqrMW5tzAv88Vy77Sg==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", + "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.0", - "@typescript-eslint/utils": "7.7.0", + "@typescript-eslint/typescript-estree": "7.7.1", + "@typescript-eslint/utils": "7.7.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2602,9 +2602,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.0.tgz", - "integrity": "sha512-G01YPZ1Bd2hn+KPpIbrAhEWOn5lQBrjxkzHkWvP6NucMXFtfXoevK82hzQdpfuQYuhkvFDeQYbzXCjR1z9Z03w==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", + "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2615,13 +2615,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.0.tgz", - "integrity": "sha512-8p71HQPE6CbxIBy2kWHqM1KGrC07pk6RJn40n0DSc6bMOBBREZxSDJ+BmRzc8B5OdaMh1ty3mkuWRg4sCFiDQQ==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", + "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/visitor-keys": "7.7.0", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/visitor-keys": "7.7.1", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2643,17 +2643,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.0.tgz", - "integrity": "sha512-LKGAXMPQs8U/zMRFXDZOzmMKgFv3COlxUQ+2NMPhbqgVm6R1w+nU1i4836Pmxu9jZAuIeyySNrN/6Rc657ggig==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", + "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.15", "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.0", - "@typescript-eslint/types": "7.7.0", - "@typescript-eslint/typescript-estree": "7.7.0", + "@typescript-eslint/scope-manager": "7.7.1", + "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/typescript-estree": "7.7.1", "semver": "^7.6.0" }, "engines": { @@ -2668,12 +2668,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.0.tgz", - "integrity": "sha512-h0WHOj8MhdhY8YWkzIF30R379y0NqyOHExI9N9KCzvmu05EgG4FumeYa3ccfKUSphyWkWQE1ybVrgz/Pbam6YA==", + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", + "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.0", + "@typescript-eslint/types": "7.7.1", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2704,40 +2704,40 @@ } }, "node_modules/@volar/language-core": { - "version": "2.2.0-alpha.8", - "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.8.tgz", - "integrity": "sha512-Ew1Iw7/RIRNuDLn60fWJdOLApAlfTVPxbPiSLzc434PReC9kleYtaa//Wo2WlN1oiRqneW0pWQQV0CwYqaimLQ==", + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.2.0-alpha.10.tgz", + "integrity": "sha512-njVJLtpu0zMvDaEk7K5q4BRpOgbyEUljU++un9TfJoJNhxG0z/hWwpwgTRImO42EKvwIxF3XUzeMk+qatAFy7Q==", "dev": true, "dependencies": { - "@volar/source-map": "2.2.0-alpha.8" + "@volar/source-map": "2.2.0-alpha.10" } }, "node_modules/@volar/source-map": { - "version": "2.2.0-alpha.8", - "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.8.tgz", - "integrity": "sha512-E1ZVmXFJ5DU4fWDcWHzi8OLqqReqIDwhXvIMhVdk6+VipfMVv4SkryXu7/rs4GA/GsebcRyJdaSkKBB3OAkIcA==", + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.2.0-alpha.10.tgz", + "integrity": "sha512-nrdWApVkP5cksAnDEyy1JD9rKdwOJsEq1B+seWO4vNXmZNcxQQCx4DULLBvKt7AzRUAQiAuw5aQkb9RBaSqdVA==", "dev": true, "dependencies": { "muggle-string": "^0.4.0" } }, "node_modules/@volar/typescript": { - "version": "2.2.0-alpha.8", - "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.8.tgz", - "integrity": "sha512-RLbRDI+17CiayHZs9HhSzlH0FhLl/+XK6o2qoiw2o2GGKcyD1aDoY6AcMd44acYncTOrqoTNoY6LuCiRyiJiGg==", + "version": "2.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.2.0-alpha.10.tgz", + "integrity": "sha512-GCa0vTVVdA9ULUsu2Rx7jwsIuyZQPvPVT9o3NrANTbYv+523Ao1gv3glC5vzNSDPM6bUl37r94HbCj7KINQr+g==", "dev": true, "dependencies": { - "@volar/language-core": "2.2.0-alpha.8", + "@volar/language-core": "2.2.0-alpha.10", "path-browserify": "^1.0.1" } }, "node_modules/@vue/compiler-core": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.22.tgz", - "integrity": "sha512-FBDRCBE/rFPA8OfTUrARx2c49N7zoImlGT7hsFikv0pZxQlFhffQwewpEXaLynZW0/DspVXmNA+QQ9dXINpWmg==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.24.tgz", + "integrity": "sha512-vbW/tgbwJYj62N/Ww99x0zhFTkZDTcGh3uwJEuadZ/nF9/xuFMC4693P9r+3sxGXISABpDKvffY5ApH9pmdd1A==", "dependencies": { - "@babel/parser": "^7.24.1", - "@vue/shared": "3.4.22", + "@babel/parser": "^7.24.4", + "@vue/shared": "3.4.24", "entities": "^4.5.0", "estree-walker": "^2.0.2", "source-map-js": "^1.2.0" @@ -2749,26 +2749,26 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-dom": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.22.tgz", - "integrity": "sha512-YkAS+jZc6Ip360kT3lZbMQZteiYBbHDSVKr94Jdd8Zjr7VjSkkXKAFFR/FW+2tNtBYXOps6xrWlOquy3GeYB0w==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.24.tgz", + "integrity": "sha512-4XgABML/4cNndVsQndG6BbGN7+EoisDwi3oXNovqL/4jdNhwvP8/rfRMTb6FxkxIxUUtg6AI1/qZvwfSjxJiWA==", "dependencies": { - "@vue/compiler-core": "3.4.22", - "@vue/shared": "3.4.22" + "@vue/compiler-core": "3.4.24", + "@vue/shared": "3.4.24" } }, "node_modules/@vue/compiler-sfc": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.22.tgz", - "integrity": "sha512-Pncp5Vc8E2Ef1o5uveO8WA1IqM7rt0R1jN8D4qitQYOUxC97iITGYA8oMInQ3UcDS7ip+SegyA2HbAEB4V6NMQ==", - "dependencies": { - "@babel/parser": "^7.24.1", - "@vue/compiler-core": "3.4.22", - "@vue/compiler-dom": "3.4.22", - "@vue/compiler-ssr": "3.4.22", - "@vue/shared": "3.4.22", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.24.tgz", + "integrity": "sha512-nRAlJUK02FTWfA2nuvNBAqsDZuERGFgxZ8sGH62XgFSvMxO2URblzulExsmj4gFZ8e+VAyDooU9oAoXfEDNxTA==", + "dependencies": { + "@babel/parser": "^7.24.4", + "@vue/compiler-core": "3.4.24", + "@vue/compiler-dom": "3.4.24", + "@vue/compiler-ssr": "3.4.24", + "@vue/shared": "3.4.24", "estree-walker": "^2.0.2", - "magic-string": "^0.30.8", + "magic-string": "^0.30.10", "postcss": "^8.4.38", "source-map-js": "^1.2.0" } @@ -2779,23 +2779,20 @@ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" }, "node_modules/@vue/compiler-sfc/node_modules/magic-string": { - "version": "0.30.9", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.9.tgz", - "integrity": "sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==", + "version": "0.30.10", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz", + "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" } }, "node_modules/@vue/compiler-ssr": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.22.tgz", - "integrity": "sha512-ycb2sL0SW6AkgVMrvaU/TIAEk7FQWyv/oYya44E/V9xURM+ij9Oev5bVobSS7GLJzkUieWW3SrYcK/PZpb5i4A==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.24.tgz", + "integrity": "sha512-ZsAtr4fhaUFnVcDqwW3bYCSDwq+9Gk69q2r/7dAHDrOMw41kylaMgOP4zRnn6GIEJkQznKgrMOGPMFnLB52RbQ==", "dependencies": { - "@vue/compiler-dom": "3.4.22", - "@vue/shared": "3.4.22" + "@vue/compiler-dom": "3.4.24", + "@vue/shared": "3.4.24" } }, "node_modules/@vue/devtools-api": { @@ -2842,12 +2839,12 @@ } }, "node_modules/@vue/language-core": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.13.tgz", - "integrity": "sha512-oQgM+BM66SU5GKtUMLQSQN0bxHFkFpLSSAiY87wVziPaiNQZuKVDt/3yA7GB9PiQw0y/bTNL0bOc0jM/siYjKg==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-2.0.14.tgz", + "integrity": "sha512-3q8mHSNcGTR7sfp2X6jZdcb4yt8AjBXAfKk0qkZIh7GAJxOnoZ10h5HToZglw4ToFvAnq+xu/Z2FFbglh9Icag==", "dev": true, "dependencies": { - "@volar/language-core": "2.2.0-alpha.8", + "@volar/language-core": "2.2.0-alpha.10", "@vue/compiler-dom": "^3.4.0", "@vue/shared": "^3.4.0", "computeds": "^0.0.1", @@ -2865,48 +2862,48 @@ } }, "node_modules/@vue/reactivity": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.22.tgz", - "integrity": "sha512-+golHRRfcGoahBrhoTauFNIIAhxntRV3BI8HHqVvCdsuWivxW1MI0E9AOXVsz4H/ZlWM1ahudWTX6PhUrNR2yQ==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.24.tgz", + "integrity": "sha512-nup3fSYg4i4LtNvu9slF/HF/0dkMQYfepUdORBcMSsankzRPzE7ypAFurpwyRBfU1i7Dn1kcwpYsE1wETSh91g==", "dependencies": { - "@vue/shared": "3.4.22" + "@vue/shared": "3.4.24" } }, "node_modules/@vue/runtime-core": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.22.tgz", - "integrity": "sha512-cbA8lcL4g1907EdY1a1KmP5IRWfbqjgBRcgJPkF//yn96XSC1/VAJBZiAGLiyw0P77Rw2Ao7d9U51vU1GC6yUQ==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.24.tgz", + "integrity": "sha512-c7iMfj6cJMeAG3s5yOn9Rc5D9e2/wIuaozmGf/ICGCY3KV5H7mbTVdvEkd4ZshTq7RUZqj2k7LMJWVx+EBiY1g==", "dependencies": { - "@vue/reactivity": "3.4.22", - "@vue/shared": "3.4.22" + "@vue/reactivity": "3.4.24", + "@vue/shared": "3.4.24" } }, "node_modules/@vue/runtime-dom": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.22.tgz", - "integrity": "sha512-AXxRHrFkLX1y2+70CO2wDKRxW0WZcQKTOXS31AK+jZ1RLPtI6sEHVpYNfyE9WgbgXOqPtX4gfIfuoFYi8iCu2w==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.24.tgz", + "integrity": "sha512-uXKzuh/Emfad2Y7Qm0ABsLZZV6H3mAJ5ZVqmAOlrNQRf+T5mxpPGZBfec1hkP41t6h6FwF6RSGCs/gd8WbuySQ==", "dependencies": { - "@vue/runtime-core": "3.4.22", - "@vue/shared": "3.4.22", + "@vue/runtime-core": "3.4.24", + "@vue/shared": "3.4.24", "csstype": "^3.1.3" } }, "node_modules/@vue/server-renderer": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.22.tgz", - "integrity": "sha512-okiNxiCOhJlx6IOrTZvhIVwf2UYKay0hnIPqWu4h19bkNv1gmG4Ic6U3zXY287AWF26lQuFMa515Qzc+R0aAYg==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.24.tgz", + "integrity": "sha512-H+DLK4sQF6sRgzKyofmlEVBIV/9KrQU6HIV7nt6yIwSGGKvSwlV8pqJlebUKLpbXaNHugdSfAbP6YmXF69lxow==", "dependencies": { - "@vue/compiler-ssr": "3.4.22", - "@vue/shared": "3.4.22" + "@vue/compiler-ssr": "3.4.24", + "@vue/shared": "3.4.24" }, "peerDependencies": { - "vue": "3.4.22" + "vue": "3.4.24" } }, "node_modules/@vue/shared": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.22.tgz", - "integrity": "sha512-cg7R9XNk4ovV3bKka/1a464O2oY0l5Fyt0rwGR4hSJRPjUJ0WVjrPdsr4W0JbUriwiM8EKcCcCjeKN5pRMs2Zg==" + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.24.tgz", + "integrity": "sha512-BW4tajrJBM9AGAknnyEw5tO2xTmnqgup0VTnDAMcxYmqOX0RG0b9aSUGAbEKolD91tdwpA6oCwbltoJoNzpItw==" }, "node_modules/@vue/tsconfig": { "version": "0.5.1", @@ -5912,9 +5909,9 @@ } }, "node_modules/rollup": { - "version": "4.14.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", - "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", + "version": "4.16.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.16.4.tgz", + "integrity": "sha512-kuaTJSUbz+Wsb2ATGvEknkI12XV40vIiHmLuFlejoo7HtDok/O5eDDD0UpCVY5bBX5U5RYo8wWP83H7ZsqVEnA==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5927,22 +5924,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.14.3", - "@rollup/rollup-android-arm64": "4.14.3", - "@rollup/rollup-darwin-arm64": "4.14.3", - "@rollup/rollup-darwin-x64": "4.14.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.14.3", - "@rollup/rollup-linux-arm-musleabihf": "4.14.3", - "@rollup/rollup-linux-arm64-gnu": "4.14.3", - "@rollup/rollup-linux-arm64-musl": "4.14.3", - "@rollup/rollup-linux-powerpc64le-gnu": "4.14.3", - "@rollup/rollup-linux-riscv64-gnu": "4.14.3", - "@rollup/rollup-linux-s390x-gnu": "4.14.3", - "@rollup/rollup-linux-x64-gnu": "4.14.3", - "@rollup/rollup-linux-x64-musl": "4.14.3", - "@rollup/rollup-win32-arm64-msvc": "4.14.3", - "@rollup/rollup-win32-ia32-msvc": "4.14.3", - "@rollup/rollup-win32-x64-msvc": "4.14.3", + "@rollup/rollup-android-arm-eabi": "4.16.4", + "@rollup/rollup-android-arm64": "4.16.4", + "@rollup/rollup-darwin-arm64": "4.16.4", + "@rollup/rollup-darwin-x64": "4.16.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.16.4", + "@rollup/rollup-linux-arm-musleabihf": "4.16.4", + "@rollup/rollup-linux-arm64-gnu": "4.16.4", + "@rollup/rollup-linux-arm64-musl": "4.16.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.16.4", + "@rollup/rollup-linux-riscv64-gnu": "4.16.4", + "@rollup/rollup-linux-s390x-gnu": "4.16.4", + "@rollup/rollup-linux-x64-gnu": "4.16.4", + "@rollup/rollup-linux-x64-musl": "4.16.4", + "@rollup/rollup-win32-arm64-msvc": "4.16.4", + "@rollup/rollup-win32-ia32-msvc": "4.16.4", + "@rollup/rollup-win32-x64-msvc": "4.16.4", "fsevents": "~2.3.2" } }, @@ -6643,9 +6640,9 @@ } }, "node_modules/vite": { - "version": "5.2.9", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.9.tgz", - "integrity": "sha512-uOQWfuZBlc6Y3W/DTuQ1Sr+oIXWvqljLvS881SVmAj00d5RdgShLcuXWxseWPd4HXwiYBFW/vXHfKFeqj9uQnw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.10.tgz", + "integrity": "sha512-PAzgUZbP7msvQvqdSD+ErD5qGnSFiGOoWmV5yAKUEI0kdhjbH6nMWVyZQC/hSc4aXwc0oJ9aEdIiF9Oje0JFCw==", "dev": true, "dependencies": { "esbuild": "^0.20.1", @@ -6698,15 +6695,15 @@ } }, "node_modules/vue": { - "version": "3.4.22", - "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.22.tgz", - "integrity": "sha512-CIx7NiP+n5WHBCG/fDNaUPP4qbQ5CIa8XIHZE3HpfS/rb2vmSIsp74BxsZyrrGKF0vHW3GoToqP3l0hzrMTecw==", + "version": "3.4.24", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.24.tgz", + "integrity": "sha512-NPdx7dLGyHmKHGRRU5bMRYVE+rechR+KDU5R2tSTNG36PuMwbfAJ+amEvOAw7BPfZp5sQulNELSLm5YUkau+Sg==", "dependencies": { - "@vue/compiler-dom": "3.4.22", - "@vue/compiler-sfc": "3.4.22", - "@vue/runtime-dom": "3.4.22", - "@vue/server-renderer": "3.4.22", - "@vue/shared": "3.4.22" + "@vue/compiler-dom": "3.4.24", + "@vue/compiler-sfc": "3.4.24", + "@vue/runtime-dom": "3.4.24", + "@vue/server-renderer": "3.4.24", + "@vue/shared": "3.4.24" }, "peerDependencies": { "typescript": "*" @@ -6751,9 +6748,9 @@ } }, "node_modules/vue-router": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz", - "integrity": "sha512-dqUcs8tUeG+ssgWhcPbjHvazML16Oga5w34uCUmsk7i0BcnskoLGwjpa15fqMr2Fa5JgVBrdL2MEgqz6XZ/6IQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.2.tgz", + "integrity": "sha512-hKQJ1vDAZ5LVkKEnHhmm1f9pMiWIBNGF5AwU67PdH7TyXCj/a4hTccuUuYCAMgJK6rO/NVYtQIEN3yL8CECa7Q==", "dependencies": { "@vue/devtools-api": "^6.5.1" }, @@ -6775,13 +6772,13 @@ } }, "node_modules/vue-tsc": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.13.tgz", - "integrity": "sha512-a3nL3FvguCWVJUQW/jFrUxdeUtiEkbZoQjidqvMeBK//tuE2w6NWQAbdrEpY2+6nSa4kZoKZp8TZUMtHpjt4mQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.0.14.tgz", + "integrity": "sha512-DgAO3U1cnCHOUO7yB35LENbkapeRsBZ7Ugq5hGz/QOHny0+1VQN8eSwSBjYbjLVPfvfw6EY7sNPjbuHHUhckcg==", "dev": true, "dependencies": { - "@volar/typescript": "2.2.0-alpha.8", - "@vue/language-core": "2.0.13", + "@volar/typescript": "2.2.0-alpha.10", + "@vue/language-core": "2.0.14", "semver": "^7.5.4" }, "bin": { diff --git a/src/App.vue b/src/App.vue index 88d98f85e2cafe4eba35222987a776f66fbc6d40..62e5c5084f580afc492cd0e7fc5254a6c2d84d04 100644 --- a/src/App.vue +++ b/src/App.vue @@ -108,7 +108,7 @@ onMounted(() => { if (userRepository.authenticated) { resourceRepository.fetchPublicResources(); workflowRepository.fetchWorkflows(); - bucketRepository.fetchBuckets(); + bucketRepository.fetchOwnBuckets(); s3KeyRepository.fetchS3Keys(); if (!userRepository.foreignUser) { bucketRepository.fetchOwnPermissions(); diff --git a/src/client/auth/index.ts b/src/client/auth/index.ts index 016081e45f1b86aa33a4ac54f09bfecbeb88a5f8..199503503a6fc5e2d9dc5c6e8a5882105f6c98c3 100644 --- a/src/client/auth/index.ts +++ b/src/client/auth/index.ts @@ -9,7 +9,7 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { ErrorDetail } from './models/ErrorDetail'; export type { HTTPValidationError } from './models/HTTPValidationError'; -export type { OIDCProvider } from './models/OIDCProvider'; +export { OIDCProvider } from './models/OIDCProvider'; export { RoleEnum } from './models/RoleEnum'; export type { User } from './models/User'; export type { ValidationError } from './models/ValidationError'; diff --git a/src/client/auth/models/OIDCProvider.ts b/src/client/auth/models/OIDCProvider.ts index 55d0ffe88b5308c7482c32344c7397a340b2770b..b8e466e6bb596bbb6ce7902ff364dd875f2473d6 100644 --- a/src/client/auth/models/OIDCProvider.ts +++ b/src/client/auth/models/OIDCProvider.ts @@ -2,4 +2,6 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type OIDCProvider = string; +export enum OIDCProvider { + LIFESCIENCE = 'lifescience', +} diff --git a/src/client/s3proxy/index.ts b/src/client/s3proxy/index.ts index 69ca1247859c17ce872d0b36ceef666bc065a3c2..37a68b12fa1d1b590ba411ecb44acaa2199c6396 100644 --- a/src/client/s3proxy/index.ts +++ b/src/client/s3proxy/index.ts @@ -13,8 +13,8 @@ export type { BucketOut } from './models/BucketOut'; export type { BucketPermissionIn } from './models/BucketPermissionIn'; export type { BucketPermissionOut } from './models/BucketPermissionOut'; export type { BucketPermissionParameters } from './models/BucketPermissionParameters'; +export type { BucketSizeLimits } from './models/BucketSizeLimits'; export { BucketType } from './models/BucketType'; -export { Constraint } from './models/Constraint'; export type { ErrorDetail } from './models/ErrorDetail'; export type { HTTPValidationError } from './models/HTTPValidationError'; export { Permission } from './models/Permission'; diff --git a/src/client/s3proxy/models/BucketOut.ts b/src/client/s3proxy/models/BucketOut.ts index 87a20c6eac77702b29452506e250b1a9583a8554..be4829455e8185d9955156a9aedde65e79d19a4c 100644 --- a/src/client/s3proxy/models/BucketOut.ts +++ b/src/client/s3proxy/models/BucketOut.ts @@ -2,11 +2,18 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Constraint } from './Constraint'; /** * Schema for answering a request with a bucket. */ export type BucketOut = { + /** + * Size limit of the bucket in KiB + */ + size_limit?: (number | null); + /** + * Number of objects limit of the bucket + */ + object_limit?: (number | null); /** * Name of the bucket */ @@ -23,10 +30,6 @@ export type BucketOut = { * UID of the owner */ owner_id: string; - /** - * Constraint for the owner of the bucket - */ - owner_constraint?: (Constraint | null); /** * Flag if the bucket is anonymously readable */ diff --git a/src/client/s3proxy/models/BucketSizeLimits.ts b/src/client/s3proxy/models/BucketSizeLimits.ts new file mode 100644 index 0000000000000000000000000000000000000000..22e59a73fb77094c3c60adc896ee9c60157b2ce6 --- /dev/null +++ b/src/client/s3proxy/models/BucketSizeLimits.ts @@ -0,0 +1,15 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type BucketSizeLimits = { + /** + * Size limit of the bucket in KiB + */ + size_limit?: (number | null); + /** + * Number of objects limit of the bucket + */ + object_limit?: (number | null); +}; + diff --git a/src/client/s3proxy/models/Constraint.ts b/src/client/s3proxy/models/Constraint.ts deleted file mode 100644 index f33ad3b6dd99f415ef7c58226d0f026946cad402..0000000000000000000000000000000000000000 --- a/src/client/s3proxy/models/Constraint.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* generated using openapi-typescript-codegen -- do not edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -/** - * Enumeration for the possible permission on a bucket. - */ -export enum Constraint { - READ = 'READ', - WRITE = 'WRITE', -} diff --git a/src/client/s3proxy/services/BucketService.ts b/src/client/s3proxy/services/BucketService.ts index 1984645f7bec8774a6318dcfdb59c0f9e4e32c92..b30e775be92d1ecbecc7d0dde1cc8b296b4ee82a 100644 --- a/src/client/s3proxy/services/BucketService.ts +++ b/src/client/s3proxy/services/BucketService.ts @@ -5,6 +5,7 @@ import type { Body_Bucket_update_bucket_public_state } from '../models/Body_Bucket_update_bucket_public_state'; import type { BucketIn } from '../models/BucketIn'; import type { BucketOut } from '../models/BucketOut'; +import type { BucketSizeLimits } from '../models/BucketSizeLimits'; import type { BucketType } from '../models/BucketType'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; @@ -133,8 +134,8 @@ export class BucketService { }); } /** - * update public status - * Toggle the buckets public state. A bucket with an owner constraint can't be made public. + * Update public status + * Update the buckets public state. * * Permission `bucket:update` required if the current user is the owner of the bucket, * otherwise `bucket:update_any` required. @@ -164,4 +165,35 @@ export class BucketService { }, }); } + /** + * Update bucket limits + * Update the buckets size limits. + * + * Permission `bucket:update_any` required. + * @param bucketName Name of bucket + * @param requestBody + * @returns BucketOut Successful Response + * @throws ApiError + */ + public static bucketUpdateBucketLimits( + bucketName: string, + requestBody: BucketSizeLimits, + ): CancelablePromise<BucketOut> { + return __request(OpenAPI, { + method: 'PATCH', + url: '/buckets/{bucket_name}/limits', + path: { + 'bucket_name': bucketName, + }, + body: requestBody, + mediaType: 'application/json', + errors: { + 400: `Error decoding JWT Token`, + 401: `Not authenticated`, + 403: `Not authorized`, + 404: `Entity not Found`, + 422: `Validation Error`, + }, + }); + } } diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 11802cc744409b3f7f2886de688e696f6d247d8c..857988fced70fd30e3578f372738d229da21f9ca 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -216,6 +216,13 @@ watch( >Users </router-link> </li> + <li> + <router-link + class="dropdown-item" + :to="{ name: 'admin-buckets' }" + >Buckets + </router-link> + </li> <li> <router-link class="dropdown-item" diff --git a/src/components/admin/UpdateBucketLimitsModal.vue b/src/components/admin/UpdateBucketLimitsModal.vue new file mode 100644 index 0000000000000000000000000000000000000000..741679f98044a3fdf911baea8f22cb66b9d6e1c7 --- /dev/null +++ b/src/components/admin/UpdateBucketLimitsModal.vue @@ -0,0 +1,236 @@ +<script setup lang="ts"> +import { computed, onMounted, reactive, ref, watch } from "vue"; +import type { BucketOut, BucketSizeLimits } from "@/client/s3proxy"; +import BootstrapModal from "@/components/modals/BootstrapModal.vue"; +import { filesize } from "filesize"; +import { useBucketStore } from "@/stores/buckets"; +import BootstrapToast from "@/components/BootstrapToast.vue"; +import { Modal, Toast, Tooltip } from "bootstrap"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; + +const props = defineProps<{ + modalId: string; + bucket: BucketOut; +}>(); + +type DataUnits = "MiB" | "GiB" | "TiB" | "KB" | "MB" | "GB" | "TB"; +const unitCalculatorState = reactive<{ + selectedUnit: DataUnits; + input: number; +}>({ + selectedUnit: "GB", + input: 1, +}); +const unitCalculatorResult = computed<number>(() => { + switch (unitCalculatorState.selectedUnit) { + case "KB": + return 1000 * (unitCalculatorState.input / 1024); + case "MB": + return 1000000 * (unitCalculatorState.input / 1024); + case "GB": + return 1000000000 * (unitCalculatorState.input / 1024); + case "TB": + return 1000000000000 * (unitCalculatorState.input / 1024); + case "MiB": + return 1024 * unitCalculatorState.input; + case "GiB": + return 1048576 * unitCalculatorState.input; + case "TiB": + return 1073741824 * unitCalculatorState.input; + default: + return 1; + } +}); + +const sizeState = reactive<{ + limits: BucketSizeLimits; + loading: boolean; +}>({ + limits: { + size_limit: undefined, + object_limit: undefined, + }, + loading: false, +}); + +const emit = defineEmits<{ + (e: "updated-limits", bucket: BucketOut): void; +}>(); + +let successToast: Toast; +let modal: Modal | null = null; +const sizeForm = ref<HTMLFormElement | undefined>(undefined); + +watch( + () => props.bucket, + (newBucket, oldBucket) => { + if (newBucket.name != oldBucket.name) { + sizeState.limits.size_limit = newBucket.size_limit; + sizeState.limits.object_limit = newBucket.object_limit; + } + }, +); + +const bucketRepository = useBucketStore(); + +const randomIDSuffix = Math.random().toString(16).substring(2, 8); +const formId = `update-bucket-limits-modal-${randomIDSuffix}`; +const successToastId = `update-bucket-limits-success-toast-${randomIDSuffix}`; + +function updateLimits() { + if (sizeForm.value?.checkValidity()) { + sizeState.loading = true; + bucketRepository + .updateBucketLimits( + props.bucket.name, + sizeState.limits.size_limit ? sizeState.limits.size_limit : null, + sizeState.limits.object_limit ? sizeState.limits.object_limit : null, + ) + .then((bucket) => { + successToast?.show(); + emit("updated-limits", bucket); + modal?.hide(); + }) + .finally(() => { + sizeState.loading = false; + }); + } +} + +function copyFromCalculatorToForm() { + sizeState.limits.size_limit = Math.round(unitCalculatorResult.value); +} + +onMounted(() => { + successToast = Toast.getOrCreateInstance(`#${successToastId}`); + modal = Modal.getOrCreateInstance(`#${props.modalId}`); + new Tooltip(`#copy-to-size-limit-form-${randomIDSuffix}`); +}); +</script> + +<template> + <bootstrap-toast :toast-id="successToastId"> + Updated limits for bucket {{ bucket.name }} + </bootstrap-toast> + <bootstrap-modal + :modalId="props.modalId" + :static-backdrop="true" + modal-label="Update bucket size limits" + size-modifier-modal="lg" + > + <template #header + >Update limits for bucket <i>{{ bucket.name }}</i></template + > + <template #body> + <form :id="formId" @submit.prevent="updateLimits()" ref="sizeForm"> + <div class="row mb-2"> + <div class="col-5"> + <label :for="`#size-limit-${randomIDSuffix}`" class="form-label" + >Size Limit: + <template v-if="sizeState.limits.size_limit" + >{{ filesize(1024 * sizeState.limits.size_limit) }} or + {{ filesize(1024 * sizeState.limits.size_limit, { base: 2 }) }} + </template> + <template v-else>No Limits</template> + </label> + <div class="d-flex align-content-center"> + <input + :id="`size-limit-${randomIDSuffix}`" + style="text-align: right" + type="number" + v-model="sizeState.limits.size_limit" + placeholder="No limits" + min="1" + max="4294967295" + step="1" + class="form-control" + aria-label="size limit" + /> + <div class="ms-1 fs-5">KiB</div> + </div> + </div> + <div class="col-6 offset-md-1"> + <label :for="`#object-limit-${randomIDSuffix}`" class="form-label" + >Size Limit + </label> + <input + :id="`object-limit-${randomIDSuffix}`" + type="number" + class="form-control" + v-model="sizeState.limits.object_limit" + placeholder="No limits" + min="1" + max="4294967295" + step="1" + aria-label="object limit" + /> + </div> + </div> + </form> + <h4 class="mt-4">Unit calculator</h4> + <div class="d-flex justify-content-center align-content-center"> + <div class="input-group w-fit"> + <input + class="form-control" + style="text-align: right" + type="number" + min="1" + max="1000" + step="0.01" + v-model="unitCalculatorState.input" + /> + <select + v-model="unitCalculatorState.selectedUnit" + class="form-select" + > + <option>KB</option> + <option>MiB</option> + <option>MB</option> + <option>GiB</option> + <option>GB</option> + <option>TiB</option> + <option>TB</option> + </select> + </div> + <div class="fs-4 mx-4"> + <font-awesome-icon icon="fa-solid fa-arrow-right-long" /> + </div> + <div class="d-flex align-content-center"> + <div class="input-group"> + <span + :id="`copy-to-size-limit-form-${randomIDSuffix}`" + class="input-group-text hover-info cursor-pointer" + data-bs-toggle="tooltip" + data-bs-title="Copy to form" + @click="copyFromCalculatorToForm()" + ><font-awesome-icon icon="fa-solid fa-copy" + /></span> + <input + style="text-align: right" + type="number" + class="form-control" + readonly + :value="unitCalculatorResult" + /> + </div> + <div class="ms-1 fs-5">KiB</div> + </div> + </div> + </template> + <template #footer> + <button type="button" class="btn btn-secondary" data-bs-dismiss="modal"> + Close + </button> + <button + type="submit" + :form="formId" + class="btn btn-primary" + :disabled="sizeState.loading" + > + Save + </button> + </template> + </bootstrap-modal> +</template> + +<style scoped></style> diff --git a/src/components/modals/SearchUserModal.vue b/src/components/modals/SearchUserModal.vue index b6396eb488c24adc477b6fc2ce6c575a1e443135..6416a0a5da16a3ba645655041632acdf9d43a55a 100644 --- a/src/components/modals/SearchUserModal.vue +++ b/src/components/modals/SearchUserModal.vue @@ -88,8 +88,8 @@ function searchUser(name: string) { modal-label="Search User Modal" v-on="{ 'hidden.bs.modal': modalClosed, 'shown.bs.modal': modalShown }" > - <template v-slot:header>Search User</template> - <template v-slot:body> + <template #header>Search User</template> + <template #body> <div class="input-group mt-2 mb-4"> <span class="input-group-text" id="objects-search-wrapping" ><font-awesome-icon icon="fa-solid fa-magnifying-glass" diff --git a/src/components/object-storage/BucketLimitProgressBar.vue b/src/components/object-storage/BucketLimitProgressBar.vue new file mode 100644 index 0000000000000000000000000000000000000000..e0ebc5add47c6effb41243ce75ae0b0936837024 --- /dev/null +++ b/src/components/object-storage/BucketLimitProgressBar.vue @@ -0,0 +1,48 @@ +<script setup lang="ts"> +import { computed } from "vue"; + +const props = defineProps<{ + currentVal: number; + maximum: number; + label?: string; +}>(); + +const percentage = computed<number>( + () => (100 * props.currentVal) / props.maximum, +); + +const cssWidth = computed<number>(() => + Math.min(100, Math.round(percentage.value)), +); + +const colorClass = computed(() => { + if (percentage.value > 90) { + return "text-bg-danger"; + } else if (percentage.value > 75) { + return "text-bg-warning"; + } + return "text-bg-success text-dark"; +}); +</script> + +<template> + <div + class="progress" + role="progressbar" + aria-label="Warning example" + :aria-valuenow="cssWidth" + aria-valuemin="0" + aria-valuemax="100" + > + <div + class="progress-bar overflow-visible" + :class="colorClass" + :style="{ width: cssWidth + '%' }" + > + <template v-if="label">{{ label }}</template> + <template v-else> {{ percentage.toFixed(2) }}% </template> + </div> + </div> +</template> + +<style scoped></style> diff --git a/src/components/object-storage/BucketListItem.vue b/src/components/object-storage/BucketListItem.vue index 4a5bdcd89d924fefcdec0521a483936d07aa5888..e27f9906282f234c8cfbaaff8cc8c82d02e4ab2b 100644 --- a/src/components/object-storage/BucketListItem.vue +++ b/src/components/object-storage/BucketListItem.vue @@ -1,6 +1,5 @@ <script setup lang="ts"> import type { BucketOut, BucketPermissionOut } from "@/client/s3proxy"; -import { Constraint } from "@/client/s3proxy"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; import PermissionModal from "@/components/object-storage/modals/PermissionModal.vue"; import BucketDetailModal from "@/components/object-storage/modals/BucketDetailModal.vue"; @@ -16,6 +15,7 @@ import { environment } from "@/environment"; import { useS3ObjectStore } from "@/stores/s3objects"; import { filesize } from "filesize"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import BucketLimitProgressBar from "@/components/object-storage/BucketLimitProgressBar.vue"; const props = defineProps<{ active: boolean; @@ -70,7 +70,7 @@ function permissionDeleted() { function toggleBucketPublicState() { requestState.loading = true; bucketRepository - .togglePublicState(props.bucket.name, !props.bucket.public) + .updatePublicState(props.bucket.name, !props.bucket.public) .then(() => { successToast?.show(); }) @@ -154,13 +154,7 @@ onMounted(() => { }" > <span class="text-truncate flex-grow-3"> - <template v-if="bucket.owner_constraint === Constraint.READ" - >download-bucket</template - > - <template v-else-if="bucket.owner_constraint === Constraint.WRITE" - >upload-bucket</template - > - <template v-else>{{ bucket.name }}</template> + {{ bucket.name }} </span> <div class="text-nowrap"> <font-awesome-icon @@ -211,7 +205,7 @@ onMounted(() => { class="px-2 rounded-bottom border shadow-sm border-3 border-top-0 border-primary" > <div v-if="permission" class="ms-1 pt-1 text-info">Foreign Bucket</div> - <table class="table table-sm table-borderless mb-0"> + <table class="table table-sm table-borderless mb-0 align-middle"> <tbody> <tr v-if="permission"> <th scope="row" class="fw-bold">Permission:</th> @@ -264,15 +258,31 @@ onMounted(() => { </tr> <tr> <th scope="row" class="fw-bold">Objects:</th> - <td>{{ bucketMeta[0] }}</td> + <td v-if="bucket.object_limit"> + <bucket-limit-progress-bar + :maximum="bucket.object_limit" + :current-val="bucketMeta[0]" + :label="bucketMeta[0] + '/' + bucket.object_limit" + /> + </td> + <td v-else>{{ bucketMeta[0] }}</td> </tr> <tr> <th scope="row" class="fw-bold">Size:</th> - <td> - {{ filesize(bucketMeta[1], { base: 2, standard: "jedec" }) }} + <td v-if="bucket.size_limit"> + <bucket-limit-progress-bar + :maximum="1024 * bucket.size_limit" + :current-val="bucketMeta[1]" + :label=" + filesize(bucketMeta[1]) + + '/' + + filesize(1024 * bucket.size_limit) + " + /> </td> + <td v-else>{{ filesize(bucketMeta[1]) }}</td> </tr> - <tr v-if="bucket.owner_constraint == undefined"> + <tr> <th scope="row"> <div :class="{ 'form-check': !loading && permission == undefined }" diff --git a/src/components/object-storage/modals/BucketDetailModal.vue b/src/components/object-storage/modals/BucketDetailModal.vue index 7115147c0940f9743770f48fd3dd93a86c1faa45..2a2b0b1e5b08443a195484abd4ad86d076425e90 100644 --- a/src/components/object-storage/modals/BucketDetailModal.vue +++ b/src/components/object-storage/modals/BucketDetailModal.vue @@ -7,6 +7,7 @@ import { useS3ObjectStore } from "@/stores/s3objects"; import { computed } from "vue"; import { environment } from "../../../environment"; import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import BucketLimitProgressBar from "@/components/object-storage/BucketLimitProgressBar.vue"; const props = defineProps<{ modalId: string; @@ -40,7 +41,7 @@ const s3Link = computed<string>(() => "s3://" + props.bucket.name); <template v-slot:body> <div class="container-fluid"> - <table class="table table-hover table-sm table-borderless"> + <table class="table table-sm table-borderless"> <tbody> <tr> <th scope="row" class="col-3">Name</th> @@ -58,12 +59,30 @@ const s3Link = computed<string>(() => "s3://" + props.bucket.name); </tr> <tr> <th scope="row">Objects</th> - <td>{{ bucketMeta[0] }}</td> + <td> + {{ bucketMeta[0] + }}<span v-if="bucket.object_limit" + >/{{ bucket.object_limit }}</span + > + <bucket-limit-progress-bar + v-if="bucket.object_limit" + :maximum="bucket.object_limit" + :current-val="bucketMeta[0]" + /> + </td> </tr> <tr> <th scope="row">Size</th> <td> - {{ filesize(bucketMeta[1], { base: 2, standard: "jedec" }) }} + {{ filesize(bucketMeta[1]) + }}<span v-if="bucket.size_limit" + >/{{ filesize(1024 * bucket.size_limit) }}</span + > + <bucket-limit-progress-bar + v-if="bucket.size_limit" + :maximum="1024 * bucket.size_limit" + :current-val="bucketMeta[1]" + /> </td> </tr> <tr> diff --git a/src/components/object-storage/modals/CopyObjectModal.vue b/src/components/object-storage/modals/CopyObjectModal.vue index ca3b28db7f9b6e8e48c11f8c6564af5d7a1b99a0..d03cfcbae1e64e562c59dfebb3fc5cffd4b47166 100644 --- a/src/components/object-storage/modals/CopyObjectModal.vue +++ b/src/components/object-storage/modals/CopyObjectModal.vue @@ -6,6 +6,7 @@ import type { _Object as S3Object } from "@aws-sdk/client-s3"; import { useBucketStore } from "@/stores/buckets"; import { useS3ObjectStore } from "@/stores/s3objects"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import type { AbortController } from "@smithy/types"; const objectRepository = useS3ObjectStore(); @@ -19,10 +20,14 @@ const formState = reactive<{ destKey: string; destBucket: string; uploading: boolean; + err: string; + abortController?: AbortController; }>({ destKey: "", destBucket: "", uploading: false, + err: "", + abortController: undefined, }); const bucketRepository = useBucketStore(); @@ -43,6 +48,7 @@ function copyObject() { if (props.srcObject.Key == undefined) { return; } + formState.abortController = new AbortController(); formState.uploading = true; objectRepository .copyObject( @@ -50,6 +56,7 @@ function copyObject() { props.srcObject, formState.destBucket, formState.destKey, + formState.abortController, ) .then(() => { copyModal?.hide(); @@ -57,11 +64,12 @@ function copyObject() { formState.destBucket = ""; }) .catch((e) => { - console.error(e); + formState.err = e.Code; errorToast?.show(); }) .finally(() => { formState.uploading = false; + formState.abortController = undefined; }); } @@ -92,7 +100,7 @@ onMounted(() => { color-class="danger" > There has been some Error.<br /> - Try again later + Code: {{ formState.err }} </bootstrap-toast> <bootstrap-modal :modalId="modalId" @@ -164,17 +172,18 @@ onMounted(() => { Close </button> <button - :disabled="formState.uploading" + v-if="formState.uploading" + class="btn btn-danger" + @click="formState.abortController?.abort()" + > + Cancel + </button> + <button + v-else type="submit" :form="'copyObjectForm' + randomIDSuffix" class="btn btn-primary" > - <span - v-if="formState.uploading" - class="spinner-border spinner-border-sm" - role="status" - aria-hidden="true" - ></span> Copy </button> </template> diff --git a/src/components/object-storage/modals/CreateFolderModal.vue b/src/components/object-storage/modals/CreateFolderModal.vue index e0b5b398f054829eb347c9a7033eb7b41660ed9b..b7c5b48ebbfbad5fa42d1fd04a0cdc62bd21b851 100644 --- a/src/components/object-storage/modals/CreateFolderModal.vue +++ b/src/components/object-storage/modals/CreateFolderModal.vue @@ -24,9 +24,11 @@ const currentFolders = computed<string[]>(() => props.keyPrefix.split("/")); const formState = reactive<{ folderName: string; uploading: boolean; + err: string; }>({ folderName: "", uploading: false, + err: "", }); function uploadFolder() { @@ -52,7 +54,7 @@ function uploadFolder() { formState.folderName = ""; }) .catch((e) => { - console.error(e); + formState.err = e.name; errorToast?.show(); }) .finally(() => { @@ -76,7 +78,7 @@ onMounted(() => { color-class="danger" > There has been some Error.<br /> - Try again later + Code: {{ formState.err }} </bootstrap-toast> <bootstrap-modal :modalId="modalId" diff --git a/src/components/object-storage/modals/UploadObjectModal.vue b/src/components/object-storage/modals/UploadObjectModal.vue index eca85fd56cb1927ac00566be0f56f49f23fcd7af..c23951006b0aa115b2509c31ba7af98fd522ae36 100644 --- a/src/components/object-storage/modals/UploadObjectModal.vue +++ b/src/components/object-storage/modals/UploadObjectModal.vue @@ -5,6 +5,7 @@ import { Modal, Toast } from "bootstrap"; import { partial } from "filesize"; import { useS3ObjectStore } from "@/stores/s3objects"; import BootstrapToast from "@/components/BootstrapToast.vue"; +import type { AbortController } from "@smithy/types"; const fsize = partial({ base: 2, standard: "jedec" }); const objectRepository = useS3ObjectStore(); @@ -31,18 +32,22 @@ watch( }, ); -const formState = reactive({ - file: {}, - key: "", - uploading: false, - uploadDone: 0, - uploadTotal: 1, -} as { - file: File; +const formState = reactive<{ + file?: File; key: string; uploading: boolean; uploadDone: number; uploadTotal: number; + err: string; + abortController?: AbortController; +}>({ + file: undefined, + key: "", + uploading: false, + uploadDone: 0, + uploadTotal: 1, + err: "", + abortController: undefined, }); const uploadProgress = computed<number>(() => @@ -54,19 +59,32 @@ const editObject = computed<boolean>( ); function uploadObject() { + if (formState.file == undefined) { + return; + } const key = props.keyPrefix.length > 0 ? props.keyPrefix + "/" + formState.key : formState.key; formState.uploadDone = 0; formState.uploading = true; + formState.abortController = new AbortController(); + formState.uploadTotal = formState.file.size; objectRepository - .uploadObjectFile(props.bucketName, key, formState.file, (progress) => { - if (progress.loaded != null && progress.total != null) { - formState.uploadDone = progress.loaded; - formState.uploadTotal = progress.total; - } - }) + .uploadObjectFile( + props.bucketName, + key, + formState.file, + (progress) => { + if (progress.loaded != null) { + formState.uploadDone = progress.loaded; + } + if (progress.total != null) { + formState.uploadTotal = progress.total; + } + }, + formState.abortController, + ) .then(() => { uploadModal?.hide(); successToast?.show(); @@ -76,11 +94,12 @@ function uploadObject() { } }) .catch((e) => { - console.error(e); + formState.err = e.name; errorToast?.show(); }) .finally(() => { formState.uploading = false; + formState.abortController = undefined; }); } @@ -110,7 +129,7 @@ onMounted(() => { color-class="danger" > There has been some Error.<br /> - Try again later + Code: {{ formState.err }} </bootstrap-toast> <bootstrap-modal :modal-id="modalId" @@ -135,6 +154,7 @@ onMounted(() => { </template> <template #body> <div class="container-fluid"> + <p>{{ formState.err }}</p> <div class="row"> <form class="col-7" @@ -207,7 +227,7 @@ onMounted(() => { {{ uploadProgress }}% </div> </div> - <span v-if="formState.uploadDone > 0"> + <span> {{ fsize(formState.uploadDone) }} / {{ fsize(formState.uploadTotal) }} </span> @@ -216,17 +236,19 @@ onMounted(() => { Close </button> <button - :disabled="formState.uploading" + v-if="formState.uploading" + type="button" + class="btn btn-danger" + @click="formState.abortController?.abort()" + > + Cancel + </button> + <button + v-else type="submit" :form="'uploadObjectForm' + randomIDSuffix" class="btn btn-primary" > - <span - v-if="formState.uploading" - class="spinner-border spinner-border-sm" - role="status" - aria-hidden="true" - ></span> Upload </button> </template> diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue index eb230b0057ca5a7b8ca5fa3d8034eafbb08fac9d..1de06571a5b3910fa6e21d6c1be1a4512a71ad21 100644 --- a/src/components/parameter-schema/ParameterSchemaFormComponent.vue +++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue @@ -254,7 +254,7 @@ onMounted(() => { if (props.schema) updateSchema(props.schema); if (props.clowmInfo?.exampleParameters) Tooltip.getOrCreateInstance("#exampleDataButton"); - bucketRepository.fetchBuckets(); + bucketRepository.fetchOwnBuckets(); bucketRepository.fetchOwnPermissions(); keyRepository.fetchS3Keys(); resourceRepository.fetchPublicResources(); diff --git a/src/router/adminRoutes.ts b/src/router/adminRoutes.ts index 00093a7c7622da258d96fb6a2305e76a39e11fbc..0567cbdb46f65f1584f72a6117626751edc73ce2 100644 --- a/src/router/adminRoutes.ts +++ b/src/router/adminRoutes.ts @@ -19,6 +19,15 @@ export const adminRoutes: RouteRecordRaw[] = [ title: "Manage Users", }, }, + { + path: "admin/buckets", + name: "admin-buckets", + component: () => import("../views/admin/AdminBucketsView.vue"), + meta: { + requiresAdminRole: true, + title: "Manage Buckets", + }, + }, { path: "admin/sync-requests", name: "admin-sync-requests", diff --git a/src/stores/buckets.ts b/src/stores/buckets.ts index 6c13662648423e263ab02b66d3a9e914d5a7e109..53ada471028e68102c4666d224c551dd06c396ad 100644 --- a/src/stores/buckets.ts +++ b/src/stores/buckets.ts @@ -2,7 +2,7 @@ import { defineStore } from "pinia"; import { BucketPermissionService, BucketService, - Constraint, + BucketType, Permission, } from "@/client/s3proxy"; import type { @@ -30,12 +30,6 @@ export const useBucketStore = defineStore({ buckets(): BucketOut[] { const tempList = Object.values(this.bucketMapping); tempList.sort((bucketA, bucketB) => { - if (bucketA.owner_constraint) { - return -1; - } - if (bucketB.owner_constraint) { - return 1; - } return bucketA.name > bucketB.name ? 1 : -1; }); return tempList; @@ -63,11 +57,7 @@ export const useBucketStore = defineStore({ return false; } // If the bucket doesn't exist, then false - if (this.bucketMapping[bucketName] == undefined) { - return false; - } - // If there is a constraint on the bucket, then false otherwise true - return this.bucketMapping[bucketName].owner_constraint == null; + return this.bucketMapping[bucketName] != undefined; }; }, writableBuckets(): BucketOut[] { @@ -75,11 +65,7 @@ export const useBucketStore = defineStore({ if (this.ownPermissions[bucket.name] != undefined) { return this.ownPermissions[bucket.name].permission !== "READ"; } - // If the user owns the bucket, check the bucket constraint - return ( - bucket.owner_constraint == null || - bucket.owner_constraint == Constraint.WRITE - ); + return true; }); }, writableBucket(): (bucketName: string) => boolean { @@ -88,14 +74,8 @@ export const useBucketStore = defineStore({ if (this.ownPermissions[bucketName] != undefined) { return this.ownPermissions[bucketName].permission !== "READ"; } - // If the user owns the bucket, check the bucket constraint - if (this.bucketMapping[bucketName] != undefined) { - return ( - this.bucketMapping[bucketName].owner_constraint == null || - this.bucketMapping[bucketName].owner_constraint == Constraint.WRITE - ); - } - return false; + // If the bucket doesn't exist, then false + return this.bucketMapping[bucketName] != undefined; }; }, readableBucket(): (bucketName: string) => boolean { @@ -106,16 +86,29 @@ export const useBucketStore = defineStore({ } // If the user owns the bucket, check the bucket constraint if (this.bucketMapping[bucketName] != null) { - return ( - this.bucketMapping[bucketName].owner_constraint == null || - this.bucketMapping[bucketName].owner_constraint == Constraint.READ - ); + return true; } return false; }; }, }, actions: { + fetchBuckets( + ownerId?: string, + bucketType?: BucketType, + ): Promise<BucketOut[]> { + return BucketService.bucketListBuckets(ownerId, bucketType).then( + (buckets) => { + const userRepository = useAuthStore(); + userRepository.fetchUsernames( + buckets + .map((bucket) => bucket.owner_id) + .filter((owner) => owner != userRepository.currentUID), + ); + return buckets; + }, + ); + }, fetchOwnPermissions( onFinally?: () => void, ): Promise<BucketPermissionOut[]> { @@ -140,22 +133,16 @@ export const useBucketStore = defineStore({ delete this.ownPermissions[bucketName]; delete this.bucketMapping[bucketName]; }, - fetchBuckets(onFinally?: () => void): Promise<BucketOut[]> { + fetchOwnBuckets(onFinally?: () => void): Promise<BucketOut[]> { if (this.buckets.length > 0) { onFinally?.(); } const userStore = useAuthStore(); - return BucketService.bucketListBuckets(userStore.currentUID) + return this.fetchBuckets(userStore.currentUID) .then((buckets) => { for (const bucket of buckets) { this.bucketMapping[bucket.name] = bucket; } - const userRepository = useAuthStore(); - userRepository.fetchUsernames( - buckets - .map((bucket) => bucket.owner_id) - .filter((owner) => owner != userRepository.currentUID), - ); return buckets; }) .finally(onFinally); @@ -267,15 +254,27 @@ export const useBucketStore = defineStore({ return permissionOut; }); }, - togglePublicState( + async updatePublicState( bucketName: string, public_: boolean, ): Promise<BucketOut> { - return BucketService.bucketUpdateBucketPublicState(bucketName, { - public: public_, - }).then((bucket) => { - this.bucketMapping[bucketName] = bucket; - return bucket; + const bucket = await BucketService.bucketUpdateBucketPublicState( + bucketName, + { + public: public_, + }, + ); + this.bucketMapping[bucketName] = bucket; + return bucket; + }, + updateBucketLimits( + bucketName: string, + size_limit?: number | null, + object_limit?: number | null, + ): Promise<BucketOut> { + return BucketService.bucketUpdateBucketLimits(bucketName, { + size_limit: size_limit, + object_limit: object_limit, }); }, }, diff --git a/src/stores/s3objects.ts b/src/stores/s3objects.ts index e314ae5a2b69e679997eb655aa47d7ed0bba6589..149a3eb0bf89f6ffab861d0606b89812d09342a1 100644 --- a/src/stores/s3objects.ts +++ b/src/stores/s3objects.ts @@ -19,6 +19,7 @@ import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import dayjs from "dayjs"; import { Upload } from "@aws-sdk/lib-storage"; import type { Progress } from "@aws-sdk/lib-storage"; +import type { AbortController } from "@smithy/types"; export const useS3ObjectStore = defineStore({ id: "s3objects", @@ -192,6 +193,7 @@ export const useS3ObjectStore = defineStore({ srcObject: S3Object, destBucket: string, destKey: string, + abortController?: AbortController, ): Promise<S3Object> { if (srcObject.Key == undefined) { return Promise.resolve({}); @@ -201,21 +203,24 @@ export const useS3ObjectStore = defineStore({ CopySource: encodeURI(`/${srcBucket}/${srcObject.Key}`), Key: destKey, }); - return this.client.send(command).then(() => { - const newObj = { - Key: destKey, - Size: srcObject.Size, - LastModified: dayjs().toDate(), - }; - this._pushObject(destBucket, newObj); - return newObj; - }); + return this.client + .send(command, { abortSignal: abortController?.signal }) + .then(() => { + const newObj = { + Key: destKey, + Size: srcObject.Size, + LastModified: dayjs().toDate(), + }; + this._pushObject(destBucket, newObj); + return newObj; + }); }, async uploadObjectFile( bucketName: string, key: string, file: File, onProgress?: (progress: Progress) => void, + abortController?: AbortController, ): Promise<S3Object> { const parallelUploads3 = new Upload({ client: this.client, @@ -227,6 +232,7 @@ export const useS3ObjectStore = defineStore({ }, queueSize: 4, // optional concurrency configuration leavePartsOnError: false, // optional manually handle dropped parts + abortController: abortController, }); if (onProgress != undefined) { parallelUploads3.on("httpUploadProgress", onProgress); diff --git a/src/views/admin/AdminBucketsView.vue b/src/views/admin/AdminBucketsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..ae81c053c0766e001d4dcae6ccba16c27215cd09 --- /dev/null +++ b/src/views/admin/AdminBucketsView.vue @@ -0,0 +1,216 @@ +<script setup lang="ts"> +import { computed, reactive } from "vue"; +import { type BucketOut, BucketType } from "@/client/s3proxy"; +import SearchUserModal from "@/components/modals/SearchUserModal.vue"; +import type { User } from "@/client/auth"; +import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue"; +import { useBucketStore } from "@/stores/buckets"; +import { useNameStore } from "@/stores/names"; +import { filesize } from "filesize"; +import dayjs from "dayjs"; +import UpdateBucketLimitsModal from "@/components/admin/UpdateBucketLimitsModal.vue"; + +const bucketState = reactive<{ + buckets: BucketOut[]; + ownerId: string; + loading: boolean; + searched: boolean; + filterString: string; + updateLimitsBucket: BucketOut; +}>({ + buckets: [], + ownerId: "", + loading: false, + searched: false, + filterString: "", + updateLimitsBucket: { + name: "", + description: "", + owner_id: "", + public: false, + created_at: 0, + }, +}); + +const bucketRepository = useBucketStore(); +const nameRepository = useNameStore(); + +const updateLimitsModalId = "admin-update-bucket-limits-modal"; + +const filteredBuckets = computed<BucketOut[]>(() => + bucketState.buckets.filter((bucket) => + bucket.name.includes(bucketState.filterString), + ), +); + +function updateUser(user: User) { + bucketState.ownerId = user.uid; +} + +function resetForm() { + bucketState.ownerId = ""; + bucketState.filterString = ""; + bucketState.buckets = []; + bucketState.searched = false; +} + +function searchBuckets() { + bucketState.loading = true; + bucketRepository + .fetchBuckets( + bucketState.ownerId ? bucketState.ownerId : undefined, + BucketType.OWN, + ) + .then((buckets) => { + bucketState.buckets = buckets; + }) + .finally(() => { + bucketState.loading = false; + bucketState.searched = true; + }); +} + +function limitsUpdated(bucket: BucketOut) { + const bucketIndex = bucketState.buckets.findIndex( + (b) => b.name == bucket.name, + ); + if (bucketIndex > -1) { + bucketState.buckets[bucketIndex] = bucket; + } +} +</script> + +<template> + <search-user-modal + modal-id="admin-bucket-search-user-modal" + @user-found="updateUser" + /> + <update-bucket-limits-modal + :bucket="bucketState.updateLimitsBucket" + :modal-id="updateLimitsModalId" + @updated-limits="limitsUpdated" + /> + <div class="border-bottom mb-4"> + <h2>Manage Buckets</h2> + </div> + <form @submit.prevent="searchBuckets" id="admin-bucket-search-form"> + <div class="d-flex justify-content-evenly align-content-center mb-4"> + <div class="flex-fill mx-2"> + <label for="admin-bucket-name-search" class="form-label" + >Name of bucket</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-magnifying-glass" /> + </div> + <input + id="admin-bucket-name-search" + type="text" + class="form-control" + placeholder="Search Buckets" + maxlength="32" + v-model.trim="bucketState.filterString" + /> + </div> + </div> + <div class="flex-fill mx-2"> + <label for="admin-bucket-user-search" class="form-label" + >Name of the owner</label + > + <div class="input-group"> + <div class="input-group-text"> + <font-awesome-icon icon="fa-solid fa-user" /> + </div> + <input + id="admin-bucket-user-search" + type="text" + class="form-control" + readonly + :value="nameRepository.getName(bucketState.ownerId)" + placeholder="Search for owner" + data-bs-toggle="modal" + data-bs-target="#admin-bucket-search-user-modal" + /> + </div> + </div> + </div> + <button + type="submit" + class="btn btn-primary w-fit" + :disabled="bucketState.loading" + > + Search + </button> + <button + type="button" + class="btn-primary btn w-fit ms-4" + :disabled="bucketState.loading" + @click="resetForm" + > + Reset + </button> + </form> + <table class="table table-striped align-middle" v-if="bucketState.buckets"> + <thead> + <tr> + <th scope="col"><b>Name</b></th> + <th scope="col" class="description-column">Description</th> + <th scope="col">Created at</th> + <th scope="col">Size Limit</th> + <th scope="col">Object Limit</th> + <th scope="col">Owner</th> + <th scope="col"></th> + </tr> + </thead> + <tbody v-if="filteredBuckets.length === 0"> + <tr> + <td colspan="7" class="text-center fst-italic fw-light"> + <template v-if="bucketState.searched" + >No bucket found with specified filters + </template> + <template v-else>Select a filter and search for buckets</template> + </td> + </tr> + </tbody> + <tbody v-else> + <tr v-for="bucket in filteredBuckets" :key="bucket.name"> + <th scope="row">{{ bucket.name }}</th> + <td class="description-column">{{ bucket.description }}</td> + <td> + {{ dayjs.unix(bucket.created_at).format("DD.MM.YYYY HH:mm:ss") }} + </td> + <td> + <span v-if="bucket.size_limit"> + {{ filesize(1024 * bucket.size_limit) }} + </span> + <span v-else>-</span> + </td> + <td> + <span v-if="bucket.object_limit"> + {{ bucket.object_limit }} + </span> + <span v-else>-</span> + </td> + <td>{{ nameRepository.getName(bucket.owner_id) }}</td> + <td> + <button + type="button" + class="btn btn-secondary btn-sm" + @click="bucketState.updateLimitsBucket = bucket" + data-bs-toggle="modal" + :data-bs-target="'#' + updateLimitsModalId" + > + Edit limits + </button> + </td> + </tr> + </tbody> + </table> +</template> + +<style scoped> +.description-column { + word-break: break-all; + max-width: 400px; +} +</style> diff --git a/src/views/object-storage/BucketView.vue b/src/views/object-storage/BucketView.vue index 0608ca0c4d2623a2eb3369637d6155d11520029c..73f9c56745f54ce5c71623f202f21fd4a185808f 100644 --- a/src/views/object-storage/BucketView.vue +++ b/src/views/object-storage/BucketView.vue @@ -683,7 +683,7 @@ function getObjectFileName(key: string): string { > </td> <td> - {{ filesize(obj.Size ?? 0, { base: 2, standard: "jedec" }) }} + {{ filesize(obj.Size ?? 0) }} </td> <!-- Show buttons with dropdown menu if row is an object --> <td class="text-end"> diff --git a/src/views/object-storage/BucketsView.vue b/src/views/object-storage/BucketsView.vue index fc184434ea38ef5031edf34aedfb71c4512a6a22..1781a606aa1eeadabf61cc9333c9009d17a312e9 100644 --- a/src/views/object-storage/BucketsView.vue +++ b/src/views/object-storage/BucketsView.vue @@ -33,7 +33,7 @@ let refreshTimeout: NodeJS.Timeout | undefined = undefined; function fetchBuckets() { bucketRepository.fetchOwnPermissions(); bucketRepository - .fetchBuckets(() => { + .fetchOwnBuckets(() => { bucketsState.loading = false; }) .then((buckets) => { @@ -176,7 +176,6 @@ onMounted(() => { created_at: 0, owner_id: '', public: false, - owner_constraint: null, }" ></bucket-list-item> </div>