From 53e9574242339480df9000a0a69c0e0e3ff44ec2 Mon Sep 17 00:00:00 2001 From: aldbr Date: Mon, 14 Oct 2024 14:27:16 +0200 Subject: [PATCH 1/4] feat(library): add profile details (username, vo, group, properties) --- package-lock.json | 420 +++++++++++++++--- packages/diracx-web-components/.eslintrc.json | 25 +- .../DashboardLayout/ProfileButton.tsx | 80 +++- packages/diracx-web-components/package.json | 5 +- .../test/unit-tests/ProfileButton.test.tsx | 33 +- packages/diracx-web-components/tsconfig.json | 10 +- 6 files changed, 498 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f157b02..eacea84a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4242,11 +4242,10 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -4290,14 +4289,13 @@ "license": "MIT" }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -4324,8 +4322,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@hutson/parse-repository-url": { "version": "3.0.2", @@ -9751,6 +9748,151 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/types": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.2.0.tgz", @@ -10837,19 +10979,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array.prototype.toreversed": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz", - "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" - } - }, "node_modules/array.prototype.tosorted": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", @@ -14787,17 +14916,17 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -15133,36 +15262,35 @@ } }, "node_modules/eslint-plugin-react": { - "version": "7.34.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.3.tgz", - "integrity": "sha512-aoW4MV891jkUulwDApQbPYTVZmeuSyFrudpbTAQuj5Fv8VL+o6df2xIGpw8B0hPjAaih1/Fb0om9grCdyFYemA==", + "version": "7.37.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.1.tgz", + "integrity": "sha512-xwTnwDqzbDRA8uJ7BMxPs/EXRB3i8ZfnOIp8BsxEQkT0nHPp+WWceqGgo6rKb9ctNi8GJLDT4Go5HAWELa/WMg==", "dev": true, - "license": "MIT", "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.2", - "array.prototype.toreversed": "^1.1.2", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.0.19", "estraverse": "^5.3.0", + "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.8", "object.fromentries": "^2.0.8", - "object.hasown": "^1.1.4", "object.values": "^1.2.0", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", - "string.prototype.matchall": "^4.0.11" + "string.prototype.matchall": "^4.0.11", + "string.prototype.repeat": "^1.0.0" }, "engines": { "node": ">=4" }, "peerDependencies": { - "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8" + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "node_modules/eslint-plugin-react-hooks": { @@ -22769,24 +22897,6 @@ "node": ">= 0.4" } }, - "node_modules/object.hasown": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.4.tgz", - "integrity": "sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg==", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object.values": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", @@ -26319,6 +26429,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, "node_modules/string.prototype.trim": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", @@ -29468,12 +29588,13 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^8.8.1", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", - "eslint": "^8.56.0", - "eslint-config-next": "^14.2.6", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react": "^7.37.1", "eslint-plugin-storybook": "^0.8.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", @@ -29522,6 +29643,165 @@ "react-dom": "^18.0.0" } }, + "packages/diracx-web-components/node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/parser": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "dev": true, + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "packages/diracx-web-components/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, "packages/diracx-web-components/node_modules/ansi-styles": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", @@ -29543,6 +29823,30 @@ "deep-equal": "^2.0.5" } }, + "packages/diracx-web-components/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "packages/diracx-web-components/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "packages/diracx-web-components/node_modules/pretty-format": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", diff --git a/packages/diracx-web-components/.eslintrc.json b/packages/diracx-web-components/.eslintrc.json index 425b9e28..ae5f4c90 100644 --- a/packages/diracx-web-components/.eslintrc.json +++ b/packages/diracx-web-components/.eslintrc.json @@ -1,19 +1,34 @@ { "extends": [ - "next/core-web-vitals", - "prettier", "plugin:import/recommended", "plugin:import/typescript", - "plugin:storybook/recommended" + "plugin:storybook/recommended", + "plugin:@typescript-eslint/recommended", + "plugin:react/recommended", + "prettier" ], "globals": { "JSX": "readonly" }, - "plugins": ["import"], + "plugins": ["import", "@typescript-eslint", "react"], "rules": { "import/order": ["error"], "import/no-unused-modules": ["error"], "import/no-useless-path-segments": ["error"], - "import/no-unresolved": ["off"] + "import/no-unresolved": ["off"], + "react/react-in-jsx-scope": "off" + }, + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": 2021, + "sourceType": "module", + "ecmaFeatures": { + "jsx": true + } + }, + "settings": { + "react": { + "version": "detect" + } } } diff --git a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx index 6eea72ad..e2d6ab84 100644 --- a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx @@ -1,9 +1,20 @@ "use client"; import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; -import { Logout } from "@mui/icons-material"; import { + Info, + Logout, + CorporateFare, + Groups, + Person, + ExpandMore, +} from "@mui/icons-material"; +import { + Accordion, + AccordionDetails, + AccordionSummary, Avatar, Button, + Chip, Divider, IconButton, Link, @@ -11,6 +22,7 @@ import { Menu, MenuItem, Tooltip, + Typography, } from "@mui/material"; import { cyan } from "@mui/material/colors"; import React from "react"; @@ -72,7 +84,6 @@ export function ProfileButton() { id="account-menu" open={open} onClose={handleClose} - onClick={handleClose} slotProps={{ paper: { elevation: 0, @@ -104,10 +115,71 @@ export function ProfileButton() { transformOrigin={{ horizontal: "right", vertical: "top" }} anchorOrigin={{ horizontal: "right", vertical: "bottom" }} > - - Profile + + + + + + + + + + + + + + +
+ + + + + {accessTokenPayload["preferred_username"]} +
+ + + + + {accessTokenPayload["dirac_group"]} +
+ + + + + {accessTokenPayload["vo"]} +
+
+ + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + Properties + + + ( +
  • {property}
  • + ), + )} + /> +
    +
    + { + handleClose(); + }} + > + + + + About + { handleClose(); diff --git a/packages/diracx-web-components/package.json b/packages/diracx-web-components/package.json index 25a4d2f0..84d10ae5 100644 --- a/packages/diracx-web-components/package.json +++ b/packages/diracx-web-components/package.json @@ -57,12 +57,13 @@ "@testing-library/react": "^14.2.2", "@testing-library/user-event": "^14.5.1", "@types/jest": "^29.5.12", + "@typescript-eslint/eslint-plugin": "^8.8.1", "babel-jest": "^29.7.0", "babel-plugin-module-resolver": "^5.0.2", - "eslint": "^8.56.0", - "eslint-config-next": "^14.2.6", + "eslint": "^8.57.1", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", + "eslint-plugin-react": "^7.37.1", "eslint-plugin-storybook": "^0.8.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", diff --git a/packages/diracx-web-components/test/unit-tests/ProfileButton.test.tsx b/packages/diracx-web-components/test/unit-tests/ProfileButton.test.tsx index 7c4212e9..144bf8b5 100644 --- a/packages/diracx-web-components/test/unit-tests/ProfileButton.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/ProfileButton.test.tsx @@ -1,6 +1,10 @@ import React from "react"; import { render, fireEvent } from "@testing-library/react"; -import { useOidcAccessToken, useOidc } from "@axa-fr/react-oidc"; +import { + useOidcAccessToken, + useOidc, + OidcConfiguration, +} from "@axa-fr/react-oidc"; import { ProfileButton } from "@/components/DashboardLayout/ProfileButton"; import { OIDCConfigurationContext } from "@/contexts/OIDCConfigurationProvider"; @@ -35,13 +39,29 @@ describe("", () => { (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: true }); (useOidcAccessToken as jest.Mock).mockReturnValue({ accessToken: "mockAccessToken", - accessTokenPayload: { preferred_username: "John" }, + accessTokenPayload: { + preferred_username: "John", + vo: "DiracVO", + dirac_group: "dirac_user", + dirac_properties: ["NormalUser"], + }, }); const { getByText, queryByText } = render(); fireEvent.click(getByText("J")); - expect(queryByText("Profile")).toBeInTheDocument(); + + expect(queryByText("John")).toBeInTheDocument(); + expect(queryByText("DiracVO")).toBeInTheDocument(); + expect(queryByText("dirac_user")).toBeInTheDocument(); + expect(queryByText("Properties")).toBeInTheDocument(); + expect(queryByText("About")).toBeInTheDocument(); expect(queryByText("Logout")).toBeInTheDocument(); + + // Open the "Properties" section + fireEvent.click(getByText("Properties")); + + // Ensure the "NormalUser" property is displayed within the "Properties" section + expect(queryByText("NormalUser")).toBeInTheDocument(); }); it('calls the logout function when "Logout" is clicked', () => { @@ -57,7 +77,12 @@ describe("", () => { // Mock context value const mockContextValue = { - configuration: { scope: "mockScope" }, + configuration: { + scope: "fake_scope", + client_id: "fake_id", + redirect_uri: "fake_uri", + authority: "fake_authority", + } as OidcConfiguration, setConfiguration: jest.fn(), }; diff --git a/packages/diracx-web-components/tsconfig.json b/packages/diracx-web-components/tsconfig.json index 6921613a..e95531ba 100644 --- a/packages/diracx-web-components/tsconfig.json +++ b/packages/diracx-web-components/tsconfig.json @@ -17,7 +17,13 @@ "resolveJsonModule": true, "isolatedModules": true, "jsx": "react-jsx", - "types": ["react", "react-dom", "node"], + "types": [ + "react", + "react-dom", + "node", + "jest", + "@testing-library/jest-dom" + ], "plugins": [ { "name": "next" @@ -36,6 +42,6 @@ "tsup.config.ts", "jest.setup.ts", "**/*.stories.*", - "mocks", + "mocks" ] } From 32c3a8343aef76ab079be9a2f341bc75249ddd32 Mon Sep 17 00:00:00 2001 From: aldbr Date: Wed, 16 Oct 2024 16:04:20 +0200 Subject: [PATCH 2/4] fix: tsup and eslint errors --- packages/diracx-web-components/.eslintrc.json | 1 + .../components/ApplicationList.ts | 8 +- .../BaseApp.stories.tsx} | 15 +- .../UserDashboard.tsx => BaseApp/BaseApp.tsx} | 3 +- .../components/BaseApp/index.ts | 1 + .../DashboardLayout/Dashboard.stories.tsx | 7 +- .../components/DashboardLayout/Dashboard.tsx | 4 +- .../DashboardDrawer.stories.tsx | 7 +- .../DashboardLayout/DashboardDrawer.tsx | 113 ++++---- .../components/DashboardLayout/DrawerItem.tsx | 4 +- .../DrawerItemGroup.stories.tsx | 13 +- .../DashboardLayout/DrawerItemGroup.tsx | 35 ++- .../DashboardLayout/ProfileButton.stories.tsx | 7 +- .../DashboardLayout/ProfileButton.tsx | 71 ++--- .../components/JobMonitor/JobDataService.ts | 43 +-- .../components/JobMonitor/JobDataTable.tsx | 61 ++-- .../JobMonitor/JobHistoryDialog.tsx | 3 +- .../components/JobMonitor/JobMonitor.tsx | 2 +- .../components/Login/LoginForm.tsx | 2 +- .../components/UserDashboard/index.ts | 1 - .../diracx-web-components/components/index.ts | 4 +- .../components/shared/ApplicationHeader.tsx | 2 +- .../components/shared/DataTable.tsx | 260 +++++++++--------- .../components/shared/FilterForm.tsx | 249 +++++++++-------- .../components/shared/FilterToolbar.tsx | 59 ++-- .../contexts/ApplicationsProvider.tsx | 108 ++++---- .../hooks/application.tsx | 12 +- .../diracx-web-components/hooks/metadata.tsx | 10 +- .../diracx-web-components/hooks/utils.tsx | 12 +- ...serDashboard.test.tsx => BaseApp.test.tsx} | 8 +- .../test/unit-tests/Dashboard.test.tsx | 35 ++- .../test/unit-tests/FilterForm.test.tsx | 30 +- .../test/unit-tests/FilterToolbar.test.tsx | 8 +- ...cationConfig.ts => ApplicationMetadata.ts} | 2 +- .../diracx-web-components/types/Column.ts | 6 +- .../types/DashboardGroup.ts | 8 + .../types/DashboardItem.ts | 11 + .../diracx-web-components/types/Filter.ts | 10 +- packages/diracx-web-components/types/Job.ts | 19 ++ .../diracx-web-components/types/JobHistory.ts | 7 + .../diracx-web-components/types/SearchBody.ts | 11 + .../types/UserSection.ts | 14 - packages/diracx-web-components/types/index.ts | 8 +- .../diracx-web/src/app/(dashboard)/page.tsx | 14 +- packages/diracx-web/test/e2e/dashboard.cy.ts | 6 +- packages/diracx-web/test/e2e/jobMonitor.cy.ts | 2 +- .../extensions/src/app/(dashboard)/layout.tsx | 6 +- .../extensions/src/app/(dashboard)/page.tsx | 18 +- .../extensions/src/gubbins/applicationList.ts | 4 +- ...tSections.tsx => defaultUserDashboard.tsx} | 4 +- 50 files changed, 736 insertions(+), 612 deletions(-) rename packages/diracx-web-components/components/{UserDashboard/UserDashboard.stories.tsx => BaseApp/BaseApp.stories.tsx} (88%) rename packages/diracx-web-components/components/{UserDashboard/UserDashboard.tsx => BaseApp/BaseApp.tsx} (92%) create mode 100644 packages/diracx-web-components/components/BaseApp/index.ts delete mode 100644 packages/diracx-web-components/components/UserDashboard/index.ts rename packages/diracx-web-components/test/unit-tests/{UserDashboard.test.tsx => BaseApp.test.tsx} (89%) rename packages/diracx-web-components/types/{ApplicationConfig.ts => ApplicationMetadata.ts} (77%) create mode 100644 packages/diracx-web-components/types/DashboardGroup.ts create mode 100644 packages/diracx-web-components/types/DashboardItem.ts create mode 100644 packages/diracx-web-components/types/Job.ts create mode 100644 packages/diracx-web-components/types/JobHistory.ts create mode 100644 packages/diracx-web-components/types/SearchBody.ts delete mode 100644 packages/diracx-web-components/types/UserSection.ts rename packages/extensions/src/gubbins/{defaultSections.tsx => defaultUserDashboard.tsx} (94%) diff --git a/packages/diracx-web-components/.eslintrc.json b/packages/diracx-web-components/.eslintrc.json index ae5f4c90..93679404 100644 --- a/packages/diracx-web-components/.eslintrc.json +++ b/packages/diracx-web-components/.eslintrc.json @@ -16,6 +16,7 @@ "import/no-unused-modules": ["error"], "import/no-useless-path-segments": ["error"], "import/no-unresolved": ["off"], + "react/prop-types": "off", "react/react-in-jsx-scope": "off" }, "parser": "@typescript-eslint/parser", diff --git a/packages/diracx-web-components/components/ApplicationList.ts b/packages/diracx-web-components/components/ApplicationList.ts index 6b1464a4..1c7d9de6 100644 --- a/packages/diracx-web-components/components/ApplicationList.ts +++ b/packages/diracx-web-components/components/ApplicationList.ts @@ -1,10 +1,10 @@ import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; -import ApplicationConfig from "@/types/ApplicationConfig"; import JobMonitor from "./JobMonitor/JobMonitor"; -import UserDashboard from "./UserDashboard/UserDashboard"; +import BaseApp from "./BaseApp/BaseApp"; +import ApplicationMetadata from "@/types/ApplicationMetadata"; -export const applicationList: ApplicationConfig[] = [ - { name: "Dashboard", component: UserDashboard, icon: Dashboard }, +export const applicationList: ApplicationMetadata[] = [ + { name: "Base Application", component: BaseApp, icon: Dashboard }, { name: "Job Monitor", component: JobMonitor, diff --git a/packages/diracx-web-components/components/UserDashboard/UserDashboard.stories.tsx b/packages/diracx-web-components/components/BaseApp/BaseApp.stories.tsx similarity index 88% rename from packages/diracx-web-components/components/UserDashboard/UserDashboard.stories.tsx rename to packages/diracx-web-components/components/BaseApp/BaseApp.stories.tsx index cced558e..ff68e576 100644 --- a/packages/diracx-web-components/components/UserDashboard/UserDashboard.stories.tsx +++ b/packages/diracx-web-components/components/BaseApp/BaseApp.stories.tsx @@ -2,15 +2,16 @@ import React from "react"; import { StoryObj, Meta } from "@storybook/react"; import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; import { Paper } from "@mui/material"; +import { Apps } from "@mui/icons-material"; import { useOidcAccessToken } from "../../mocks/react-oidc.mock"; import { useMUITheme } from "../../hooks/theme"; import { ApplicationsContext } from "../../contexts/ApplicationsProvider"; import { NavigationProvider } from "../../contexts/NavigationProvider"; -import UserDashboard from "./UserDashboard"; +import BaseApp from "./BaseApp"; const meta = { - title: "User Dashboard/UserDashboard", - component: UserDashboard, + title: "Base Application", + component: BaseApp, parameters: { layout: "centered", }, @@ -49,7 +50,7 @@ const meta = { { id: "example", title: "App Name", - icon: React.Component, + icon: Apps, type: "test", }, ], @@ -69,7 +70,7 @@ const meta = { useOidcAccessToken.mockRestore(); }; }, -} satisfies Meta; +} satisfies Meta; export default meta; type Story = StoryObj; @@ -82,7 +83,7 @@ export const LoggedIn: Story = { useOidcAccessToken.mockReturnValue({ accessTokenPayload: { preferred_username: "John Doe" }, }); - return ; + return ; }, }; @@ -94,6 +95,6 @@ export const LoggedOff: Story = { useOidcAccessToken.mockReturnValue({ accessTokenPayload: null, }); - return ; + return ; }, }; diff --git a/packages/diracx-web-components/components/UserDashboard/UserDashboard.tsx b/packages/diracx-web-components/components/BaseApp/BaseApp.tsx similarity index 92% rename from packages/diracx-web-components/components/UserDashboard/UserDashboard.tsx rename to packages/diracx-web-components/components/BaseApp/BaseApp.tsx index c1102256..49c7a8f0 100644 --- a/packages/diracx-web-components/components/UserDashboard/UserDashboard.tsx +++ b/packages/diracx-web-components/components/BaseApp/BaseApp.tsx @@ -1,5 +1,4 @@ "use client"; -import React from "react"; import { useOidcAccessToken } from "@axa-fr/react-oidc/"; import { useOIDCContext } from "@/hooks/oidcConfiguration"; import ApplicationHeader from "@/components/shared/ApplicationHeader"; @@ -9,7 +8,7 @@ import ApplicationHeader from "@/components/shared/ApplicationHeader"; * * @returns User Dashboard content */ -export default function UserDashboard({ +export default function BaseApplication({ headerSize, }: { /** The size of the header, optional, will default to h4 */ diff --git a/packages/diracx-web-components/components/BaseApp/index.ts b/packages/diracx-web-components/components/BaseApp/index.ts new file mode 100644 index 00000000..9a16efcb --- /dev/null +++ b/packages/diracx-web-components/components/BaseApp/index.ts @@ -0,0 +1 @@ +export { default as BaseApp } from "./BaseApp"; diff --git a/packages/diracx-web-components/components/DashboardLayout/Dashboard.stories.tsx b/packages/diracx-web-components/components/DashboardLayout/Dashboard.stories.tsx index 594ba7c4..8000f6cf 100644 --- a/packages/diracx-web-components/components/DashboardLayout/Dashboard.stories.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/Dashboard.stories.tsx @@ -7,6 +7,7 @@ import { ApplicationsContext } from "../../contexts/ApplicationsProvider"; import { NavigationProvider } from "../../contexts/NavigationProvider"; import { useOidc, useOidcAccessToken } from "../../mocks/react-oidc.mock"; import { applicationList } from "../ApplicationList"; +import { DashboardGroup } from "../../types/DashboardGroup"; import Dashboard from "./Dashboard"; const meta = { @@ -22,7 +23,9 @@ const meta = { }, decorators: [ (Story) => { - const [sections, setSections] = React.useState([ + const [userDashboard, setUserDashboard] = React.useState< + DashboardGroup[] + >([ { title: "Group Title", extended: true, @@ -47,7 +50,7 @@ const meta = { }} > diff --git a/packages/diracx-web-components/components/DashboardLayout/Dashboard.tsx b/packages/diracx-web-components/components/DashboardLayout/Dashboard.tsx index 53f8388d..3949dfba 100644 --- a/packages/diracx-web-components/components/DashboardLayout/Dashboard.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/Dashboard.tsx @@ -8,10 +8,10 @@ import { Menu } from "@mui/icons-material"; import Toolbar from "@mui/material/Toolbar"; import Stack from "@mui/material/Stack"; import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; -import { useMUITheme } from "@/hooks/theme"; import { ProfileButton } from "./ProfileButton"; import { ThemeToggleButton } from "./ThemeToggleButton"; import DashboardDrawer from "./DashboardDrawer"; +import { useMUITheme } from "@/hooks/theme"; interface DashboardProps { /** The content to be displayed in the main area */ @@ -23,7 +23,7 @@ interface DashboardProps { } /** - * Build a side bar on the left containing the available sections as well as a top bar. + * Build a side bar on the left containing the available groups as well as a top bar. * The side bar is expected to collapse if displayed on a small screen * * @param props - children, drawerWidth, logoURL diff --git a/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.stories.tsx b/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.stories.tsx index 0703d84a..7d1f633d 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.stories.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.stories.tsx @@ -8,6 +8,7 @@ import { useMUITheme } from "../../hooks/theme"; import { useOidc, useOidcAccessToken } from "../../mocks/react-oidc.mock"; import { ApplicationsContext } from "../../contexts/ApplicationsProvider"; import { applicationList } from "../ApplicationList"; +import { DashboardGroup } from "../../types"; import DashboardDrawer from "./DashboardDrawer"; const meta = { @@ -20,7 +21,9 @@ const meta = { decorators: [ (Story) => { const theme = useMUITheme(); - const [sections, setSections] = React.useState([ + const [userDashboard, setUserDashboard] = React.useState< + DashboardGroup[] + >([ { title: "Group Title", extended: true, @@ -36,7 +39,7 @@ const meta = { ]); return ( diff --git a/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.tsx b/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.tsx index 0b68f5ff..6edcb0e0 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DashboardDrawer.tsx @@ -22,10 +22,11 @@ import React, { } from "react"; import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"; -import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; -import { useMUITheme } from "@/hooks/theme"; import DrawerItemGroup from "./DrawerItemGroup"; import AppDialog from "./ApplicationDialog"; +import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; +import { useMUITheme } from "@/hooks/theme"; +import { DashboardGroup } from "@/types"; interface DashboardDrawerProps { /** The variant of the drawer. Usually temporary if on mobile and permanent otherwise. */ @@ -66,19 +67,21 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { id: string | null; }>({ type: null, id: null }); - const [popAnchorEl, setPopAnchorEl] = React.useState(null); + const [popAnchorEl, setPopAnchorEl] = React.useState( + null, + ); const [renameValue, setRenameValue] = React.useState(""); - // Define the sections that are accessible to users. - // Each section has an associated icon and path. - const [userSections, setSections] = useContext(ApplicationsContext); + // Define the applications that are accessible to users. + // Each application has an associated icon and path. + const [userDashboard, setUserDashboard] = useContext(ApplicationsContext); const logoURL = props.logoURL || "/DIRAC-logo.png"; const theme = useMUITheme(); useEffect(() => { - // Handle changes to sections when drag and drop occurs. + // Handle changes to app instances when drag and drop occurs. return monitorForElements({ onDrop({ source, location }) { const target = location.current.dropTargets[0]; @@ -93,10 +96,10 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { const groupTitle = targetData.title; const closestEdgeOfTarget = extractClosestEdge(targetData); const targetIndex = targetData.index as number; - const sourceGroup = userSections.find( + const sourceGroup = userDashboard.find( (group) => group.title == sourceData.title, ); - const targetGroup = userSections.find( + const targetGroup = userDashboard.find( (group) => group.title == groupTitle, ); const sourceIndex = sourceData.index as number; @@ -113,10 +116,10 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { } else { // If the target is a group const groupTitle = targetData.title; - const sourceGroup = userSections.find( + const sourceGroup = userDashboard.find( (group) => group.title == sourceData.title, ); - const targetGroup = userSections.find( + const targetGroup = userDashboard.find( (group) => group.title == groupTitle, ); const sourceIndex = sourceData.index as number; @@ -127,17 +130,17 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { }); /** - * Reorders sections within a group or between different groups. + * Reorders app instances within a group or between different groups. * - * @param sourceGroup - The source group from which the section is being moved. - * @param destinationGroup - The destination group where the section is being moved to. - * @param sourceIndex - The index of the section within the source group. - * @param destinationIndex - The index where the section should be placed in the destination group. - * If null, the section will be placed at the end of the destination group. + * @param sourceGroup - The source group from which the app instance is being moved. + * @param destinationGroup - The destination group where the app instance is being moved to. + * @param sourceIndex - The index of the app instance within the source group. + * @param destinationIndex - The index where the app instance should be placed in the destination group. + * If null, the app instance will be placed at the end of the destination group. */ function reorderSections( - sourceGroup: any, - destinationGroup: any, + sourceGroup: DashboardGroup | undefined, + destinationGroup: DashboardGroup | undefined, sourceIndex: number, destinationIndex: number | null = null, ) { @@ -166,11 +169,11 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { } sourceItems.splice(destinationIndex, 0, removed); - setSections((sections) => - sections.map((section) => - section.title === sourceGroup.title - ? { ...section, items: sourceItems } - : section, + setUserDashboard((groups) => + groups.map((group) => + group.title === sourceGroup.title + ? { ...group, items: sourceItems } + : group, ), ); } else { @@ -185,19 +188,19 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { } destinationItems.splice(destinationIndex, 0, removed); - setSections((sections) => - sections.map((section) => - section.title === sourceGroup.title - ? { ...section, items: sourceItems } - : section.title === destinationGroup.title - ? { ...section, items: destinationItems } - : section, + setUserDashboard((groups) => + groups.map((group) => + group.title === sourceGroup.title + ? { ...group, items: sourceItems } + : group.title === destinationGroup.title + ? { ...group, items: destinationItems } + : group, ), ); } } } - }, [setSections, userSections]); + }, [setUserDashboard, userDashboard]); /** * Handles the creation of a new app in the dashboard drawer. @@ -206,18 +209,18 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { * @param icon - The icon component for the app. */ const handleAppCreation = (appType: string, icon: SvgIconComponent) => { - let group = userSections[userSections.length - 1]; + let group = userDashboard[userDashboard.length - 1]; const empty = !group; if (empty) { //create a new group if there is no group group = { - title: `Group ${userSections.length + 1}`, + title: `Group ${userDashboard.length + 1}`, extended: false, items: [], }; } - let title = `${appType} ${userSections.reduce( + let title = `${appType} ${userDashboard.reduce( (sum, group) => sum + group.items.filter((item) => item.type === appType).length, 1, @@ -228,7 +231,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { const newApp = { title, - id: `${title}${userSections.reduce( + id: `${title}${userDashboard.reduce( (sum, group) => sum + group.items.length, 0, )}`, @@ -237,10 +240,10 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { }; group.items.push(newApp); if (empty) { - setSections((userSections) => [...userSections, group]); + setUserDashboard((userDashboard) => [...userDashboard, group]); } else { - setSections((userSections) => - userSections.map((g) => (g.title === group.title ? group : g)), + setUserDashboard((userDashboard) => + userDashboard.map((g) => (g.title === group.title ? group : g)), ); } }; @@ -273,26 +276,26 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { const handleNewGroup = () => { const newGroup = { - title: `Group ${userSections.length + 1}`, + title: `Group ${userDashboard.length + 1}`, extended: false, items: [], }; - while (userSections.some((group) => group.title === newGroup.title)) { + while (userDashboard.some((group) => group.title === newGroup.title)) { newGroup.title = `Group ${parseInt(newGroup.title.split(" ")[1]) + 1}`; } - setSections((userSections) => [...userSections, newGroup]); + setUserDashboard((userDashboard) => [...userDashboard, newGroup]); handleCloseContextMenu(); }; const handleDelete = () => { if (contextState.type === "group") { - setSections((userSections) => - userSections.filter((group) => group.title !== contextState.id), + setUserDashboard((userDashboard) => + userDashboard.filter((group) => group.title !== contextState.id), ); } else if (contextState.type === "item") { - setSections((userSections) => - userSections.map((group) => { + setUserDashboard((userDashboard) => + userDashboard.map((group) => { const newItems = group.items.filter( (item) => item.id !== contextState.id, ); @@ -303,7 +306,7 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { handleCloseContextMenu(); }; - const handleRenameClick = (event: any) => { + const handleRenameClick = (event: React.MouseEvent) => { setPopAnchorEl(event.currentTarget); }; @@ -315,12 +318,12 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { const handleRename = () => { if (contextState.type === "group") { //check if the name is already taken - if (userSections.some((group) => group.title === renameValue)) { + if (userDashboard.some((group) => group.title === renameValue)) { return; } //rename the group - setSections((userSections) => - userSections.map((group) => { + setUserDashboard((userDashboard) => + userDashboard.map((group) => { if (group.title === contextState.id) { return { ...group, title: renameValue }; } @@ -328,8 +331,8 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { }), ); } else if (contextState.type === "item") { - setSections((userSections) => - userSections.map((group) => { + setUserDashboard((userDashboard) => + userDashboard.map((group) => { const newItems = group.items.map((item) => { if (item.id === contextState.id) { return { ...item, title: renameValue }; @@ -379,9 +382,9 @@ export default function DashboardDrawer(props: DashboardDrawerProps) { > DIRAC logo - {/* Map over user sections and render them as list items in the drawer. */} + {/* Map over user app instances and render them as list items in the drawer. */} - {userSections.map((group) => ( + {userDashboard.map((group) => ( diff --git a/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx b/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx index 6c113efe..dd311d71 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx @@ -49,7 +49,7 @@ export default function DrawerItem({ const theme = useMUITheme(); const { setParam } = useSearchParamsUtils(); // Represents the closest edge to the mouse cursor - const [closestEdge, setClosestEdge]: any = useState(null); + const [closestEdge, setClosestEdge] = useState(null); const appId = useApplicationId(); @@ -89,7 +89,7 @@ export default function DrawerItem({ return () => root.unmount(); }, // Seamless transition between the preview and the real element - getOffset: ({ container }) => { + getOffset: () => { const elementPos = source.element.getBoundingClientRect(); const x = location.current.input.pageX - elementPos.x; const y = location.current.input.pageY - elementPos.y; diff --git a/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.stories.tsx b/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.stories.tsx index e5247ee8..798d2fbf 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.stories.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.stories.tsx @@ -6,6 +6,7 @@ import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; import { Dashboard } from "@mui/icons-material"; import { useMUITheme } from "../../hooks/theme"; import { useOidc, useOidcAccessToken } from "../../mocks/react-oidc.mock"; +import { DashboardGroup } from "../../types/DashboardGroup"; import DrawerItemGroup from "./DrawerItemGroup"; const meta = { @@ -59,17 +60,17 @@ export const Default: Story = { ], }, handleContextMenu: () => () => {}, - setSections: () => {}, + setUserDashboard: () => {}, }, render: (props) => { const [, updateArgs] = useArgs(); - const updateSections = (sections) => { - if (typeof sections === "function") { - sections = sections([props.group]); + const updateGroups = (groups: React.SetStateAction) => { + if (typeof groups === "function") { + groups = groups([props.group]); } - updateArgs({ group: sections[0] }); + updateArgs({ group: groups[0] }); }; - props.setSections = updateSections; + props.setUserDashboard = updateGroups; return ; }, }; diff --git a/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.tsx b/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.tsx index 73b155db..a2b07184 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DrawerItemGroup.tsx @@ -1,10 +1,10 @@ "use client"; import { Accordion, AccordionDetails, AccordionSummary } from "@mui/material"; -import { ExpandMore } from "@mui/icons-material"; +import { ExpandMore, Apps } from "@mui/icons-material"; import React, { useEffect } from "react"; import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter"; -import { UserSection } from "@/types/UserSection"; import DrawerItem from "./DrawerItem"; +import { DashboardGroup } from "@/types/DashboardGroup"; /** * Represents a group of items in a drawer. @@ -15,13 +15,13 @@ import DrawerItem from "./DrawerItem"; */ export default function DrawerItemGroup({ group: { title, extended: expanded, items }, - setSections, + setUserDashboard, handleContextMenu, }: { /** The group object containing the title, expanded state, and items. */ - group: UserSection; - /** The function to set the sections state. */ - setSections: React.Dispatch>; + group: DashboardGroup; + /** The function to set the user dashboard state. */ + setUserDashboard: React.Dispatch>; /** The function to handle the context menu. */ handleContextMenu: ( type: "group" | "item" | null, @@ -44,7 +44,7 @@ export default function DrawerItemGroup({ onDragStart: () => setHovered(true), onDrop: () => { setHovered(false); - handleChange(title)(null, true); + handleChange(title)({} as React.ChangeEvent, true); }, onDragEnter: () => setHovered(true), onDragLeave: () => setHovered(false), @@ -52,16 +52,15 @@ export default function DrawerItemGroup({ }); // Handles expansion of the accordion group - const handleChange = (title: string) => (event: any, isExpanded: any) => { - // Set the extended state of the accordion group. - setSections((sections) => - sections.map((section) => - section.title === title - ? { ...section, extended: isExpanded } - : section, - ), - ); - }; + const handleChange = + (title: string) => (_: React.ChangeEvent, isExpanded: boolean) => { + // Set the extended state of the accordion group. + setUserDashboard((groups) => + groups.map((group) => + group.title === title ? { ...group, extended: isExpanded } : group, + ), + ); + }; return ( (
    diff --git a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.stories.tsx b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.stories.tsx index 5c6b24f8..31f54922 100644 --- a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.stories.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.stories.tsx @@ -29,7 +29,12 @@ const meta = { async beforeEach() { useOidcAccessToken.mockReturnValue({ accessToken: "123456789", - accessTokenPayload: { preferred_username: "John Doe" }, + accessTokenPayload: { + preferred_username: "John Doe", + vo: "dirac", + dirac_group: "dirac_user", + dirac_properties: ["NormalUser", "AnotherProperty"], + }, }); useOidc.mockReturnValue({ login: () => {}, diff --git a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx index e2d6ab84..1dfd752a 100644 --- a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx @@ -21,6 +21,7 @@ import { ListItemIcon, Menu, MenuItem, + Stack, Tooltip, Typography, } from "@mui/material"; @@ -117,36 +118,38 @@ export function ProfileButton() { > - - - - - - - - - - - - + + + + + + + + + + + + + +
    - - - - - {accessTokenPayload["preferred_username"]} -
    - - - - - {accessTokenPayload["dirac_group"]} -
    - - - - - {accessTokenPayload["vo"]} -
    + + + + + {accessTokenPayload["preferred_username"]} +
    + + + + + {accessTokenPayload["dirac_group"]} +
    + + + + + {accessTokenPayload["vo"]} +
    @@ -159,13 +162,13 @@ export function ProfileButton() { Properties - + {accessTokenPayload["dirac_properties"]?.map( (property: string, index: number) => ( -
  • {property}
  • + ), )} - /> +
    diff --git a/packages/diracx-web-components/components/JobMonitor/JobDataService.ts b/packages/diracx-web-components/components/JobMonitor/JobDataService.ts index 623be8fd..579747eb 100644 --- a/packages/diracx-web-components/components/JobMonitor/JobDataService.ts +++ b/packages/diracx-web-components/components/JobMonitor/JobDataService.ts @@ -1,9 +1,11 @@ import useSWR, { mutate } from "swr"; -import { fetcher } from "@/hooks/utils"; import dayjs from "dayjs"; +import { fetcher } from "@/hooks/utils"; +import { Filter, SearchBody, Job } from "@/types"; +import { JobHistory } from "@/types/JobHistory"; -function processSearchBody(searchBody: any) { - searchBody.search = searchBody.search?.map((filter: any) => { +function processSearchBody(searchBody: SearchBody) { + searchBody.search = searchBody.search?.map((filter: Filter) => { if (filter.operator == "last") { return { parameter: filter.parameter, @@ -29,13 +31,16 @@ function processSearchBody(searchBody: any) { */ export const useJobs = ( accessToken: string, - searchBody: any, + searchBody: SearchBody, page: number, rowsPerPage: number, ) => { processSearchBody(searchBody); const urlGetJobs = `/api/jobs/search?page=${page + 1}&per_page=${rowsPerPage}`; - return useSWR([urlGetJobs, accessToken, "POST", searchBody], fetcher); + + return useSWR([urlGetJobs, accessToken, "POST", searchBody], (args) => + fetcher(args), + ); }; /** @@ -48,7 +53,7 @@ export const useJobs = ( */ export const refreshJobs = ( accessToken: string, - searchBody: any, + searchBody: SearchBody, page: number, rowsPerPage: number, ) => { @@ -61,16 +66,16 @@ export const refreshJobs = ( * Deletes jobs with the specified IDs. * * @param selectedIds - An array of job IDs to delete. - * @param token - The authentication token. + * @param accessToken - The authentication token. * @returns A Promise that resolves to an object containing the response headers and data. */ export function deleteJobs( selectedIds: readonly number[], - token: any, -): Promise<{ headers: Headers; data: any }> { + accessToken: string, +): Promise<{ headers: Headers; data: number[] }> { const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); const deleteUrl = `/api/jobs/?${queryString}`; - return fetcher([deleteUrl, token, "DELETE"]); + return fetcher([deleteUrl, accessToken, "DELETE"]); } /** @@ -82,11 +87,11 @@ export function deleteJobs( */ export function killJobs( selectedIds: readonly number[], - token: any, -): Promise<{ headers: Headers; data: any }> { + accessToken: string, +): Promise<{ headers: Headers; data: number[] }> { const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); const killUrl = `/api/jobs/kill?${queryString}`; - return fetcher([killUrl, token, "POST"]); + return fetcher([killUrl, accessToken, "POST"]); } /** @@ -98,11 +103,11 @@ export function killJobs( */ export function rescheduleJobs( selectedIds: readonly number[], - token: any, -): Promise<{ headers: Headers; data: any }> { + accessToken: string, +): Promise<{ headers: Headers; data: number[] }> { const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); const rescheduleUrl = `/api/jobs/reschedule?${queryString}`; - return fetcher([rescheduleUrl, token, "POST"]); + return fetcher([rescheduleUrl, accessToken, "POST"]); } /** @@ -113,8 +118,8 @@ export function rescheduleJobs( */ export function getJobHistory( jobId: number, - token: any, -): Promise<{ headers: Headers; data: any }> { + accessToken: string, +): Promise<{ headers: Headers; data: { [key: number]: JobHistory[] } }> { const historyUrl = `/api/jobs/${jobId}/status/history`; - return fetcher([historyUrl, token]); + return fetcher([historyUrl, accessToken]); } diff --git a/packages/diracx-web-components/components/JobMonitor/JobDataTable.tsx b/packages/diracx-web-components/components/JobMonitor/JobDataTable.tsx index 04e22993..0e0ea3b8 100644 --- a/packages/diracx-web-components/components/JobMonitor/JobDataTable.tsx +++ b/packages/diracx-web-components/components/JobMonitor/JobDataTable.tsx @@ -26,8 +26,6 @@ import { } from "@mui/material"; import { useOidcAccessToken } from "@axa-fr/react-oidc"; import { Delete, Clear, Replay } from "@mui/icons-material"; -import { Filter } from "@/types/Filter"; -import { Column } from "@/types/Column"; import { useOIDCContext } from "../../hooks/oidcConfiguration"; import { DataTable, MenuItem } from "../shared/DataTable"; import { JobHistoryDialog } from "./JobHistoryDialog"; @@ -39,6 +37,10 @@ import { rescheduleJobs, useJobs, } from "./JobDataService"; +import { Column } from "@/types/Column"; +import { InternalFilter } from "@/types/Filter"; +import { JobHistory } from "@/types/JobHistory"; +import { Job, SearchBody } from "@/types"; const statusColors: { [key: string]: string } = { Submitting: purple[500], @@ -61,7 +63,10 @@ const statusColors: { [key: string]: string } = { /** * Renders the status cell with colors */ -const renderStatusCell = (status: string) => { +const renderStatusCell = (status: unknown) => { + if (typeof status !== "string") { + return null; // or handle other types as needed + } return ( ([]); + const [filters, setFilters] = React.useState([]); // State for search body - const [searchBody, setSearchBody] = React.useState({}); + const [searchBody, setSearchBody] = React.useState({}); // State for job history const [isHistoryDialogOpen, setIsHistoryDialogOpen] = React.useState(false); - const [jobHistoryData, setJobHistoryData] = React.useState([]); + const [jobHistoryData, setJobHistoryData] = React.useState([]); /** * Fetches the jobs from the /api/jobs/search endpoint @@ -154,7 +159,7 @@ export function JobDataTable() { ); const dataHeader = data?.headers; - const results = data?.data; + const results = data?.data || []; // Parse the headers to get the first item, last item and number of items const contentRange = dataHeader?.get("content-range"); @@ -187,10 +192,15 @@ export function JobDataTable() { message: "Deleted successfully", severity: "success", }); - } catch (error: any) { + } catch (error: unknown) { + let errorMessage = "An unknown error occurred"; + + if (error instanceof Error) { + errorMessage = error.message; + } setSnackbarInfo({ open: true, - message: "Delete failed: " + error.message, + message: "Delete failed: " + errorMessage, severity: "error", }); } finally { @@ -213,10 +223,15 @@ export function JobDataTable() { message: "Killed successfully", severity: "success", }); - } catch (error: any) { + } catch (error: unknown) { + let errorMessage = "An unknown error occurred"; + + if (error instanceof Error) { + errorMessage = error.message; + } setSnackbarInfo({ open: true, - message: "Kill failed: " + error.message, + message: "Kill failed: " + errorMessage, severity: "error", }); } finally { @@ -239,10 +254,15 @@ export function JobDataTable() { message: "Rescheduled successfully", severity: "success", }); - } catch (error: any) { + } catch (error: unknown) { + let errorMessage = "An unknown error occurred"; + + if (error instanceof Error) { + errorMessage = error.message; + } setSnackbarInfo({ open: true, - message: "Reschedule failed: " + error.message, + message: "Reschedule failed: " + errorMessage, severity: "error", }); } finally { @@ -262,10 +282,15 @@ export function JobDataTable() { // Show the history setJobHistoryData(data[selectedId]); setIsHistoryDialogOpen(true); - } catch (error: any) { + } catch (error: unknown) { + let errorMessage = "An unknown error occurred"; + + if (error instanceof Error) { + errorMessage = error.message; + } setSnackbarInfo({ open: true, - message: "Fetching history failed: " + error.message, + message: "Fetching history failed: " + errorMessage, severity: "error", }); } finally { @@ -312,8 +337,8 @@ export function JobDataTable() { */ return ( - <> - + title="List of Jobs" page={page} setPage={setPage} @@ -363,6 +388,6 @@ export function JobDataTable() { onClose={handleHistoryClose} historyData={jobHistoryData} /> - + ); } diff --git a/packages/diracx-web-components/components/JobMonitor/JobHistoryDialog.tsx b/packages/diracx-web-components/components/JobMonitor/JobHistoryDialog.tsx index 1626f41c..96ea10b0 100644 --- a/packages/diracx-web-components/components/JobMonitor/JobHistoryDialog.tsx +++ b/packages/diracx-web-components/components/JobMonitor/JobHistoryDialog.tsx @@ -10,6 +10,7 @@ import { TableRow, } from "@mui/material"; import { Close } from "@mui/icons-material"; +import { JobHistory } from "@/types/JobHistory"; interface JobHistoryDialogProps { /** Whether the Dialog is open */ @@ -19,7 +20,7 @@ interface JobHistoryDialogProps { /** * The data for the job history dialog */ - historyData: any[]; + historyData: JobHistory[]; } /** diff --git a/packages/diracx-web-components/components/JobMonitor/JobMonitor.tsx b/packages/diracx-web-components/components/JobMonitor/JobMonitor.tsx index 8eb6cef7..6967f527 100644 --- a/packages/diracx-web-components/components/JobMonitor/JobMonitor.tsx +++ b/packages/diracx-web-components/components/JobMonitor/JobMonitor.tsx @@ -11,7 +11,7 @@ import ApplicationHeader from "@/components/shared/ApplicationHeader"; export default function JobMonitor({ headerSize, }: { - /** The size of the header, optional, will default to h4 */ + /** The size of the header, optional */ headerSize?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; }) { return ( diff --git a/packages/diracx-web-components/components/Login/LoginForm.tsx b/packages/diracx-web-components/components/Login/LoginForm.tsx index 9e71f918..13f5dc4d 100644 --- a/packages/diracx-web-components/components/Login/LoginForm.tsx +++ b/packages/diracx-web-components/components/Login/LoginForm.tsx @@ -13,12 +13,12 @@ import TextField from "@mui/material/TextField"; import { CssBaseline, Stack } from "@mui/material"; import { ThemeProvider as MUIThemeProvider } from "@mui/material/styles"; import { useOidc } from "@axa-fr/react-oidc"; +import { useMetadata, Metadata } from "../../hooks/metadata"; import { useOIDCContext } from "@/hooks/oidcConfiguration"; import { useMUITheme } from "@/hooks/theme"; import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; import { NavigationContext } from "@/contexts/NavigationProvider"; -import { useMetadata, Metadata } from "../../hooks/metadata"; /** * Login form diff --git a/packages/diracx-web-components/components/UserDashboard/index.ts b/packages/diracx-web-components/components/UserDashboard/index.ts deleted file mode 100644 index 2cc580e7..00000000 --- a/packages/diracx-web-components/components/UserDashboard/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as UserDashboard } from "./UserDashboard"; diff --git a/packages/diracx-web-components/components/index.ts b/packages/diracx-web-components/components/index.ts index 808f20dd..49a99e09 100644 --- a/packages/diracx-web-components/components/index.ts +++ b/packages/diracx-web-components/components/index.ts @@ -16,5 +16,5 @@ export * from "./OIDC"; // Shared components export * from "./shared"; -// User Dashboard -export * from "./UserDashboard"; +// Base Application +export * from "./BaseApp"; diff --git a/packages/diracx-web-components/components/shared/ApplicationHeader.tsx b/packages/diracx-web-components/components/shared/ApplicationHeader.tsx index b1a2b830..3a1bc15b 100644 --- a/packages/diracx-web-components/components/shared/ApplicationHeader.tsx +++ b/packages/diracx-web-components/components/shared/ApplicationHeader.tsx @@ -16,7 +16,7 @@ export interface ApplicationHeaderProps { */ export default function ApplicationHeader({ type, - size = "h4", + size = "h5", }: ApplicationHeaderProps) { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); diff --git a/packages/diracx-web-components/components/shared/DataTable.tsx b/packages/diracx-web-components/components/shared/DataTable.tsx index 86bc0258..05611e83 100644 --- a/packages/diracx-web-components/components/shared/DataTable.tsx +++ b/packages/diracx-web-components/components/shared/DataTable.tsx @@ -28,11 +28,12 @@ import { } from "@mui/material"; import { cyan } from "@mui/material/colors"; import { TableComponents, TableVirtuoso } from "react-virtuoso"; -import { Filter } from "@/types/Filter"; +import { FilterToolbar } from "./FilterToolbar"; +import { InternalFilter } from "@/types/Filter"; import { Column } from "@/types/Column"; import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; -import { FilterToolbar } from "./FilterToolbar"; +import { DashboardGroup, SearchBody } from "@/types"; /** * Menu item @@ -131,77 +132,6 @@ function DataTableToolbar(props: DataTableToolbarProps) { ); } -interface TableContextProps { - rowIdentifier: string; - handleClick: (event: React.MouseEvent, id: number) => void; - handleContextMenu: (event: React.MouseEvent, id: number) => void; - isSelected: (id: number) => boolean; - isMobile: boolean; -} - -// Virtuoso table components: https://virtuoso.dev/ -// Used to render large tables with virtualization, which improves performance -const VirtuosoTableComponents: TableComponents> = { - Scroller: React.forwardRef(function Scroller(props, ref) { - return ; - }), - Table: function VirtuosoTable(props) { - const { isMobile } = props.context as TableContextProps; - return ( - - ); - }, - TableHead: React.forwardRef( - function VirtuosoTableHead(props, ref) { - return ; - }, - ), - TableRow: function VirtuosoTableRow({ - item, - ...props - }: { - item: Record; - [key: string]: any; - }) { - const { rowIdentifier, handleClick, handleContextMenu, isSelected } = - props.context as TableContextProps; - - if (item) { - return ( - handleClick(event, item[rowIdentifier])} - role="checkbox" - aria-checked={isSelected(item[rowIdentifier])} - tabIndex={-1} - key={item[rowIdentifier]} - selected={isSelected(item[rowIdentifier])} - onContextMenu={(event) => - handleContextMenu(event, item[rowIdentifier]) - } - style={{ cursor: "context-menu" }} - /> - ); - } - return ; - }, - TableBody: React.forwardRef( - function VirtuosoTableBody(props, ref) { - return ; - }, - ), -}; - /** * Data table props * @property {string} title - the title of the table @@ -215,14 +145,14 @@ const VirtuosoTableComponents: TableComponents> = { * @property {function} setFilters - the function to call when the filters change * @property {function} setSearchBody - the function to call when the search body changes * @property {Column[]} columns - the columns of the table - * @property {any[]} rows - the rows of the table + * @property {T[]} rows - the rows of the table * @property {string | null} error - the error message * @property {string} rowIdentifier - the identifier for the rows * @property {boolean} isMobile - whether the table is displayed on a mobile device * @property {JSX.Element} toolbarComponents - the components to display in the toolbar * @property {MenuItem[]} menuItems - the menu items */ -interface DataTableProps { +interface DataTableProps> { /** The title of the table */ title: string; /** The current page */ @@ -248,15 +178,15 @@ interface DataTableProps { /** The function to call when the selected rows change */ setSelected: React.Dispatch>; /** The filters to apply */ - filters: Filter[]; + filters: InternalFilter[]; /** The function to call when the filters change */ - setFilters: React.Dispatch>; + setFilters: React.Dispatch>; /** The function to call when the search body changes */ - setSearchBody: (searchBody: any) => void; + setSearchBody: React.Dispatch>; /** The columns of the table */ columns: Column[]; /** The rows of the table */ - rows: any[]; + rows: T[]; /** The error message */ error: string | null; /** Whether the table is validating */ @@ -264,7 +194,7 @@ interface DataTableProps { /** Whether the table is loading */ isLoading: boolean; /** The identifier for the rows */ - rowIdentifier: string; + rowIdentifier: keyof T; /** Whether the table is displayed on a mobile device */ isMobile: boolean; /** The components to display in the toolbar */ @@ -278,7 +208,9 @@ interface DataTableProps { * * @returns a DataTable component */ -export function DataTable(props: DataTableProps) { +export function DataTable>( + props: DataTableProps, +) { const { title, page, @@ -311,56 +243,58 @@ export function DataTable(props: DataTableProps) { mouseY: number | null; id: number | null; }>({ mouseX: null, mouseY: null, id: null }); - const { getAllParam, getParam, setParam } = useSearchParamsUtils(); + const { getParam, setParam } = useSearchParamsUtils(); const appId = getParam("appId"); - const [appliedFilters, setAppliedFilters] = React.useState(filters); + const [appliedFilters, setAppliedFilters] = + React.useState(filters); const updateFiltersAndUrl = React.useCallback( - (newFilters: Filter[]) => { + (newFilters: InternalFilter[]) => { // Update the filters in the URL using the setParam function setParam( "filter", newFilters.map( (filter) => - `${filter.id}_${filter.column}_${filter.operator}_${filter.value}`, + `${filter.id}_${filter.parameter}_${filter.operator}_${filter.value}`, ), ); }, [setParam], ); - const [sections, setSections] = React.useContext(ApplicationsContext); - const updateSectionFilters = React.useCallback( - (newFilters: Filter[]) => { + const [userDashboard, setUserDashboard] = + React.useContext(ApplicationsContext); + const updateGroupFilters = React.useCallback( + (newFilters: InternalFilter[]) => { const appId = getParam("appId"); - const section = sections.find((section) => - section.items.some((item) => item.id === appId), + const group = userDashboard.find((group) => + group.items.some((item) => item.id === appId), ); - if (section) { - const newSection = { - ...section, - items: section.items.map((item) => { + if (group) { + const newGroup = { + ...group, + items: group.items.map((item) => { if (item.id === appId) { - return { ...item, data: { filters: newFilters } }; + return { ...item, data: newFilters }; } return item; }), }; - setSections((sections) => - sections.map((s) => (s.title === section.title ? newSection : s)), + setUserDashboard((groups: DashboardGroup[]) => + groups.map((s) => (s.title === group.title ? newGroup : s)), ); } }, - [getParam, sections, setSections], + [getParam, userDashboard, setUserDashboard], ); // Handle the application of filters const handleApplyFilters = () => { - // Transform list of filters into a json object + // Transform list of internal filters into filters const jsonFilters = filters.map((filter) => ({ - parameter: filter.column, + parameter: filter.parameter, operator: filter.operator, value: filter.value, values: filter.values, @@ -371,24 +305,24 @@ export function DataTable(props: DataTableProps) { // Update the filters in the URL updateFiltersAndUrl(filters); - // Update the filters in the sections - updateSectionFilters(filters); + // Update the filters in the groups + updateGroupFilters(filters); }; - const SectionItem = React.useMemo( + const DashboardItem = React.useMemo( () => - sections - .find((section) => section.items.some((item) => item.id === appId)) + userDashboard + .find((group) => group.items.some((item) => item.id === appId)) ?.items.find((item) => item.id === appId), - [appId, sections], + [appId, userDashboard], ); React.useEffect(() => { - if (SectionItem?.data?.filters) { - setFilters(SectionItem.data.filters); - setAppliedFilters(SectionItem.data.filters); - const jsonFilters = SectionItem.data.filters.map((filter: Filter) => ({ - parameter: filter.column, + if (DashboardItem?.data) { + setFilters(DashboardItem.data); + setAppliedFilters(DashboardItem.data); + const jsonFilters = DashboardItem.data.map((filter: InternalFilter) => ({ + parameter: filter.parameter, operator: filter.operator, value: filter.value, values: filter.values, @@ -398,17 +332,17 @@ export function DataTable(props: DataTableProps) { setFilters([]); setSearchBody({ search: [] }); } - }, [SectionItem?.data?.filters, setFilters, setSearchBody]); + }, [DashboardItem?.data, setFilters, setSearchBody]); // Manage sorting const handleRequestSort = ( event: React.MouseEvent, - property: string | number, + property: string, ) => { const isAsc = orderBy === property && order === "asc"; setOrder(isAsc ? "desc" : "asc"); setOrderBy(property); - setSearchBody((prevState: any) => ({ + setSearchBody((prevState: SearchBody) => ({ ...prevState, sort: [{ parameter: property, direction: isAsc ? "desc" : "asc" }], })); @@ -417,7 +351,7 @@ export function DataTable(props: DataTableProps) { // Manage selection const handleSelectAllClick = (event: React.ChangeEvent) => { if (event.target.checked) { - const newSelected = rows.map((n: any) => n[rowIdentifier]); + const newSelected = rows.map((n: T) => n[rowIdentifier] as number); setSelected(newSelected); return; } @@ -472,6 +406,83 @@ export function DataTable(props: DataTableProps) { setContextMenu({ mouseX: null, mouseY: null, id: null }); }; + // Virtuoso table components: https://virtuoso.dev/ + // Used to render large tables with virtualization, which improves performance + interface TableContextProps { + rowIdentifier: keyof T; + handleClick: (event: React.MouseEvent, id: number) => void; + handleContextMenu: (event: React.MouseEvent, id: number) => void; + isSelected: (id: number) => boolean; + isMobile: boolean; + } + + interface TableRowProps { + item: T; + context?: TableContextProps; + "data-index"?: number; + } + + const VirtuosoTableComponents: TableComponents = { + Scroller: React.forwardRef(function Scroller(props, ref) { + return ; + }), + Table: function VirtuosoTable(props) { + const { isMobile } = props.context as TableContextProps; + return ( +
    + ); + }, + TableHead: React.forwardRef( + function VirtuosoTableHead(props, ref) { + return ; + }, + ), + TableRow: function VirtuosoTableRow(props: TableRowProps) { + const { item, context } = props; + + if (!context) { + return ; + } + + const { rowIdentifier, handleClick, handleContextMenu, isSelected } = + context || {}; + + const itemId = item[rowIdentifier]; + if (typeof itemId !== "number") { + return ; + } + + return ( + handleClick(event, itemId)} + role="checkbox" + aria-checked={isSelected(itemId)} + tabIndex={-1} + key={itemId} + selected={isSelected(itemId)} + onContextMenu={(event) => handleContextMenu(event, itemId)} + style={{ cursor: "context-menu" }} + /> + ); + }, + TableBody: React.forwardRef( + function VirtuosoTableBody(props, ref) { + return ; + }, + ), + }; + // Wait for the data to load if (isValidating || isLoading) { return ( @@ -553,7 +564,7 @@ export function DataTable(props: DataTableProps) { toolbarComponents={toolbarComponents} /> - data={rows} components={VirtuosoTableComponents} context={{ @@ -565,8 +576,7 @@ export function DataTable(props: DataTableProps) { }} fixedHeaderContent={() => { const createSortHandler = - (property: string | number) => - (event: React.MouseEvent) => { + (property: string) => (event: React.MouseEvent) => { handleRequestSort(event, property); }; @@ -587,13 +597,13 @@ export function DataTable(props: DataTableProps) { {columns.map((headCell) => ( {headCell.label} {orderBy === headCell.id ? ( @@ -609,8 +619,8 @@ export function DataTable(props: DataTableProps) { ); }} - itemContent={(index: number, row: Record) => { - const isItemSelected = isSelected(row[rowIdentifier]); + itemContent={(index: number, row: T) => { + const isItemSelected = isSelected(row[rowIdentifier] as number); const labelId = `enhanced-table-checkbox-${index}`; return ( @@ -625,8 +635,10 @@ export function DataTable(props: DataTableProps) { {columns.map((column) => { const cellValue = row[column.id]; return ( - - {column.render ? column.render(cellValue) : cellValue} + + {column.render + ? column.render(cellValue) + : String(cellValue)} ); })} diff --git a/packages/diracx-web-components/components/shared/FilterForm.tsx b/packages/diracx-web-components/components/shared/FilterForm.tsx index 27bf8be4..4899d509 100644 --- a/packages/diracx-web-components/components/shared/FilterForm.tsx +++ b/packages/diracx-web-components/components/shared/FilterForm.tsx @@ -7,16 +7,17 @@ import { InputLabel, MenuItem, Select, + SelectChangeEvent, Stack, TextField, Tooltip, Typography, } from "@mui/material"; -import { Filter } from "@/types/Filter"; -import { Column } from "@/types/Column"; import { DateTimePicker, LocalizationProvider } from "@mui/x-date-pickers"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; +import { Column } from "@/types/Column"; +import { InternalFilter } from "@/types/Filter"; import "dayjs/locale/en-gb"; // needed by LocalizationProvider to format Dates to dd-mm-yyyy /** @@ -24,20 +25,20 @@ import "dayjs/locale/en-gb"; // needed by LocalizationProvider to format Dates t * @property {Column[]} columns - the columns on which to filter * @property {function} handleFilterChange - the function to call when a filter is changed * @property {function} handleFilterMenuClose - the function to call when the filter menu is closed - * @property {Filter[]} filters - the filters for the table + * @property {InternalFilter[]} filters - the filters for the table * @property {number} selectedFilterId - the id of the selected filter */ interface FilterFormProps { /** The columns of the data table */ columns: Column[]; /** The function to call when a filter is changed */ - handleFilterChange: (index: number, tempFilter: Filter) => void; + handleFilterChange: (index: number, tempFilter: InternalFilter) => void; /** The function to call when the filter menu is closed */ handleFilterMenuClose: () => void; /** The filters for the table */ - filters: Filter[]; + filters: InternalFilter[]; /** The function to set the filters */ - setFilters: React.Dispatch>; + setFilters: React.Dispatch>; /** The id of the selected filter */ selectedFilterId: number | undefined; } @@ -56,7 +57,9 @@ export function FilterForm(props: FilterFormProps) { handleFilterMenuClose, selectedFilterId, } = props; - const [tempFilter, setTempFilter] = React.useState(null); + const [tempFilter, setTempFilter] = React.useState( + null, + ); // Find the index using the filter ID const filterIndex = filters.findIndex((f) => f.id === selectedFilterId); @@ -68,7 +71,7 @@ export function FilterForm(props: FilterFormProps) { } else { setTempFilter({ id: Date.now(), - column: "", + parameter: "", operator: "eq", value: "", }); @@ -78,12 +81,12 @@ export function FilterForm(props: FilterFormProps) { if (!tempFilter) return null; const onChange = (field: string, value: string | string[] | undefined) => { - setTempFilter((prevFilter: Filter | null) => { + setTempFilter((prevFilter: InternalFilter | null) => { if (prevFilter === null) { return null; // or initialize a new Filter object as appropriate } // Ensuring all fields of Filter are always defined - const updatedFilter: Filter = { + const updatedFilter: InternalFilter = { ...prevFilter, [field]: value, }; @@ -100,27 +103,28 @@ export function FilterForm(props: FilterFormProps) { handleFilterMenuClose(); }; - const selectedColumn = columns.find((c) => c.id == tempFilter.column); + const selectedColumn = columns.find((c) => c.id == tempFilter.parameter); + + const columnType = selectedColumn?.type; + const isCategory = Array.isArray(columnType); + const isDateTime = columnType === "DateTime"; + const isNumber = columnType === "number"; - const types: { - [type: string]: { operators: string[]; defaultOperator: string }; - } = { - DateTime: { operators: ["last", "gt", "lt"], defaultOperator: "last" }, - category: { - operators: ["eq", "neq", "in", "not in", "like"], - defaultOperator: "eq", - }, - number: { - operators: ["eq", "neq", "gt", "lt", "like"], - defaultOperator: "eq", - }, - default: { - operators: ["eq", "neq", "gt", "lt", "like"], - defaultOperator: "eq", - }, + const operatorOptions = { + DateTime: ["last", "gt", "lt"], + category: ["eq", "neq", "in", "not in", "like"], + number: ["eq", "neq", "gt", "lt", "in", "not in", "like"], + default: ["eq", "neq", "gt", "lt", "like"], }; - const operatorText: { [operator: string]: string } = { + const defaultOperators = { + DateTime: "last", + category: "eq", + number: "eq", + default: "eq", + }; + + const operatorLabels: { [operator: string]: string } = { eq: "equals to", neq: "not equals to", last: "in the last", @@ -131,47 +135,66 @@ export function FilterForm(props: FilterFormProps) { like: "like", }; - function operatorSelector() { - if (tempFilter) - return ( - - Operator - - - ); - } + const getOperatorType = () => { + if (isDateTime) return "DateTime"; + if (isCategory) return "category"; + if (isNumber) return "number"; + return "default"; + }; + + const operatorType = getOperatorType(); + + const operators = operatorOptions[operatorType]; - function valueSelector() { + const operatorSelector = ( + + Operator + + + ); + + const valueSelector = () => { if (!tempFilter) return null; - if (selectedColumn?.type == "DateTime") { - if (tempFilter.operator != "last") + + const isMultiple = ["in", "not in"].includes(tempFilter.operator); + const selectValue = isMultiple + ? ((tempFilter.values || []) as string[]) + : ((tempFilter.value || "") as string); + + const handleValueChange = (e: SelectChangeEvent) => { + const value = e.target.value; + if (isMultiple) { + onChange("values", value as string[]); + } else { + onChange("value", value as string); + } + }; + + if (isDateTime) { + if (tempFilter.operator !== "last") { return ( ); - else + } else { return ( Value @@ -199,50 +222,41 @@ export function FilterForm(props: FilterFormProps) { sx={{ minWidth: 100 }} > Hour + Day Week Month Year ); + } } - if ( - typeof selectedColumn?.type == "object" && - tempFilter.operator != "like" - ) + if (isCategory && tempFilter.operator !== "like") { return ( Value ); - if (selectedColumn?.type == "number") { - if (!["in", "not in", "like"].includes(tempFilter.operator)) + } + + if (isNumber) { + if (!["in", "not in", "like"].includes(tempFilter.operator)) { return ( ); - else if (["in", "not in"].includes(tempFilter.operator)) + } else if (isMultiple) { return ( onChange("values", e.target.value.split(" "))} + value={(tempFilter.values || []).join(",")} + onChange={(e) => onChange("values", e.target.value.split(","))} /> ); + } } + return ( ); - } + }; return ( @@ -289,36 +305,41 @@ export function FilterForm(props: FilterFormProps) { - Column + Parameter - {operatorSelector()} + {operatorSelector} {valueSelector()} diff --git a/packages/diracx-web-components/components/shared/FilterToolbar.tsx b/packages/diracx-web-components/components/shared/FilterToolbar.tsx index f4e3249f..6c29d14d 100644 --- a/packages/diracx-web-components/components/shared/FilterToolbar.tsx +++ b/packages/diracx-web-components/components/shared/FilterToolbar.tsx @@ -3,9 +3,9 @@ import { FilterList, Delete, Send } from "@mui/icons-material"; import Chip from "@mui/material/Chip"; import Button from "@mui/material/Button"; import { Alert, Popover, Stack, Tooltip } from "@mui/material"; -import { Filter } from "@/types/Filter"; -import { Column } from "@/types/Column"; import { FilterForm } from "./FilterForm"; +import { InternalFilter } from "@/types/Filter"; +import { Column } from "@/types/Column"; import "@/hooks/theme"; /** @@ -16,11 +16,11 @@ interface FilterToolbarProps { /** The columns of the data table */ columns: Column[]; /** The filters to apply */ - filters: Filter[]; + filters: InternalFilter[]; /** The function to set the filters */ - setFilters: React.Dispatch>; + setFilters: React.Dispatch>; /** The applied filters */ - appliedFilters: Filter[]; + appliedFilters: InternalFilter[]; /** The function to apply the filters */ handleApplyFilters: () => void; } @@ -34,9 +34,8 @@ export function FilterToolbar(props: FilterToolbarProps) { const { columns, filters, setFilters, appliedFilters, handleApplyFilters } = props; const [anchorEl, setAnchorEl] = React.useState(null); - const [selectedFilter, setSelectedFilter] = React.useState( - null, - ); + const [selectedFilter, setSelectedFilter] = + React.useState(null); const addFilterButtonRef = React.useRef(null); // Filter actions @@ -45,7 +44,7 @@ export function FilterToolbar(props: FilterToolbarProps) { // It is just a placeholder to open the filter form const newFilter = { id: Date.now(), - column: "", + parameter: "", operator: "eq", value: "", }; @@ -57,7 +56,7 @@ export function FilterToolbar(props: FilterToolbarProps) { setFilters([]); }, [setFilters]); - const handleFilterChange = (index: number, newFilter: Filter) => { + const handleFilterChange = (index: number, newFilter: InternalFilter) => { const updatedFilters = filters.map((filter, i) => i === index ? newFilter : filter, ); @@ -91,35 +90,27 @@ export function FilterToolbar(props: FilterToolbarProps) { }, [filters, appliedFilters]); const isApplied = React.useCallback( - (filter: Filter) => { + (filter: InternalFilter) => { return appliedFilters.some((f) => f.id == filter.id); }, [appliedFilters], ); + function debounce void>( + func: T, + wait: number, + ) { + let timeout: ReturnType | undefined; + + return function executedFunction(event: KeyboardEvent) { + clearTimeout(timeout); + timeout = setTimeout(() => func(event), wait); + }; + } + // Keyboard shortcuts React.useEffect(() => { - function debounce(func: (...args: any[]) => void, wait: number) { - let timeout: ReturnType | undefined; - - return function executedFunction(...args: any[]) { - const later = () => { - clearTimeout(timeout); - func(...args); - }; - - clearTimeout(timeout); - timeout = setTimeout(later, wait); - }; - } - - const handleKeyPress = (event: { - altKey: any; - shiftKey: any; - key: string; - preventDefault: () => void; - stopPropagation: () => void; - }) => { + const handleKeyPress = (event: KeyboardEvent) => { if (event.altKey && event.shiftKey) { switch (event.key.toLowerCase()) { case "a": @@ -202,10 +193,10 @@ export function FilterToolbar(props: FilterToolbarProps) { useFlexGap sx={{ m: 1 }} > - {filters.map((filter: Filter, index: number) => ( + {filters.map((filter: InternalFilter, index: number) => ( { handleFilterMenuOpen(event); // Open the menu setSelectedFilter(filter); // Set the selected filter diff --git a/packages/diracx-web-components/contexts/ApplicationsProvider.tsx b/packages/diracx-web-components/contexts/ApplicationsProvider.tsx index 5dd46d51..425f019d 100644 --- a/packages/diracx-web-components/contexts/ApplicationsProvider.tsx +++ b/packages/diracx-web-components/contexts/ApplicationsProvider.tsx @@ -5,19 +5,20 @@ import React, { useEffect, useState, } from "react"; -import { Dashboard, FolderCopy, Monitor } from "@mui/icons-material"; +import { Monitor } from "@mui/icons-material"; import JSONCrush from "jsoncrush"; import { useSearchParamsUtils } from "@/hooks/searchParamsUtils"; import { applicationList } from "@/components/ApplicationList"; -import { UserSection } from "@/types/UserSection"; -import ApplicationConfig from "@/types/ApplicationConfig"; +import { DashboardGroup } from "@/types/DashboardGroup"; +import ApplicationMetadata from "@/types/ApplicationMetadata"; +import { DashboardItem } from "@/types/DashboardItem"; -// Create a context for the userSections state +// Create a context for the UserDashboard state export const ApplicationsContext = createContext< [ - UserSection[], - React.Dispatch>, - ApplicationConfig[], + DashboardGroup[], + React.Dispatch>, + ApplicationMetadata[], ] >([[], () => {}, []]); @@ -26,32 +27,34 @@ export const ApplicationsContext = createContext< * * @param children - The child components to be wrapped by the provider. * @param appList - The list of application configurations. - * @param defaultSections - The default user sections. + * @param defaultUserDashboard - The default user dashboard. * @returns The applications provider. */ export const ApplicationsProvider = ({ children, appList = applicationList, - defaultSections, + defaultUserDashboard, }: { children: React.ReactNode; - appList?: ApplicationConfig[]; - defaultSections?: UserSection[]; + appList?: ApplicationMetadata[]; + defaultUserDashboard?: DashboardGroup[]; }) => { - const [userSections, setSections] = useState([]); + const [userDashboard, setUserDashboard] = useState([]); const { getParam, setParam } = useSearchParamsUtils(); - // save user sections to searchParams (but not icons) - const setSectionsParams = useCallback( - (sections: UserSection[] | ((prev: UserSection[]) => UserSection[])) => { - if (typeof sections === "function") { - sections = sections(userSections); + // save user dashboard to searchParams (but not icons) + const setUserDashboardParams = useCallback( + ( + groups: DashboardGroup[] | ((prev: DashboardGroup[]) => DashboardGroup[]), + ) => { + if (typeof groups === "function") { + groups = groups(userDashboard); } - const newSections = sections.map((section) => { + const newSections = groups.map((group) => { return { - ...section, - items: section.items.map((item) => { + ...group, + items: group.items.map((item) => { return { ...item, icon: () => null, @@ -59,82 +62,65 @@ export const ApplicationsProvider = ({ }), }; }); - setParam("sections", JSONCrush.crush(JSON.stringify(newSections))); + setParam("dashboard", JSONCrush.crush(JSON.stringify(newSections))); }, - [setParam, userSections], + [setParam, userDashboard], ); // get user sections from searchParams - const sectionsParams = useMemo(() => getParam("sections"), [getParam]); + const groupsParams = useMemo(() => getParam("dashboard"), [getParam]); useEffect(() => { - if (userSections.length !== 0) return; - if (sectionsParams) { - const uncrushed = JSONCrush.uncrush(sectionsParams); + if (userDashboard.length !== 0) return; + if (groupsParams) { + const uncrushed = JSONCrush.uncrush(groupsParams); try { - const newSections = JSON.parse(uncrushed).map( - (section: { items: any[] }) => { - section.items = section.items.map((item: any) => { + const newSections: DashboardGroup[] = JSON.parse(uncrushed).map( + (group: DashboardGroup) => { + group.items = group.items.map((item: DashboardItem) => { return { ...item, //get icon from appList - icon: appList.find((app) => app.name === item.type)?.icon, + icon: + appList.find((app) => app.name === item.type)?.icon || null, }; }); - return section; + return group; }, ); - if (newSections !== userSections) { - setSections(newSections); + if (newSections !== userDashboard) { + setUserDashboard(newSections); } } catch (e) { - console.error("Error parsing user sections : ", uncrushed, e); + console.error("Error parsing user dashboard : ", uncrushed, e); } } else { - setSections( - defaultSections || [ + setUserDashboard( + defaultUserDashboard || [ { - title: "Dashboard", + title: "My dashboard", extended: true, items: [ { - title: "Dashboard", - type: "Dashboard", - id: "Dashboard0", - icon: Dashboard, - }, - { - title: "Job Monitor", + title: "My Jobs", type: "Job Monitor", id: "JobMonitor0", icon: Monitor, }, ], }, - { - title: "Other", - extended: false, - items: [ - { - title: "File Catalog", - type: "File Catalog", - id: "FileCatalog0", - icon: FolderCopy, - }, - ], - }, ], ); } - }, [appList, defaultSections, sectionsParams]); + }, [appList, defaultUserDashboard, groupsParams]); return ( { - setSections(section); - setSectionsParams(section); + userDashboard, + (group) => { + setUserDashboard(group); + setUserDashboardParams(group); }, appList, ]} diff --git a/packages/diracx-web-components/hooks/application.tsx b/packages/diracx-web-components/hooks/application.tsx index 8921285f..2df1b55e 100644 --- a/packages/diracx-web-components/hooks/application.tsx +++ b/packages/diracx-web-components/hooks/application.tsx @@ -19,19 +19,19 @@ export function useApplicationId() { * @returns the application title */ export function useApplicationTitle() { - const [sections] = useContext(ApplicationsContext); + const [userDashboard] = useContext(ApplicationsContext); const appId = useApplicationId(); return useMemo(() => { - if (!sections || !appId) return null; + if (!userDashboard || !appId) return null; - const app = sections.reduce( - (acc, section) => { + const app = userDashboard.reduce( + (acc, group) => { if (acc) return acc; - return section.items.find((app) => app.id === appId); + return group.items.find((app) => app.id === appId); }, undefined as { title: string } | undefined, ); return app?.title; - }, [sections, appId]); + }, [userDashboard, appId]); } diff --git a/packages/diracx-web-components/hooks/metadata.tsx b/packages/diracx-web-components/hooks/metadata.tsx index 6820f335..499188d9 100644 --- a/packages/diracx-web-components/hooks/metadata.tsx +++ b/packages/diracx-web-components/hooks/metadata.tsx @@ -28,12 +28,16 @@ export interface Metadata { */ export function useMetadata() { const url = `/.well-known/dirac-metadata`; - const { data, error }: SWRResponse = useSWRImmutable( + + const { + data, + error, + }: SWRResponse<{ headers: Headers; data: Metadata }, Error> = useSWRImmutable( [url], - fetcher, + (args) => fetcher(args), ); - const metadata = data?.data as Metadata; + const metadata = data?.data; return { metadata, diff --git a/packages/diracx-web-components/hooks/utils.tsx b/packages/diracx-web-components/hooks/utils.tsx index bc63d497..5679f7b7 100644 --- a/packages/diracx-web-components/hooks/utils.tsx +++ b/packages/diracx-web-components/hooks/utils.tsx @@ -6,9 +6,9 @@ import { useEffect, useState } from "react"; * @param args - URL, access token, body and method * @returns a promise */ -export const fetcher = async ( - args: [string, string?, string?, any?], -): Promise<{ headers: Headers; data: any }> => { +export async function fetcher( + args: [string, string?, string?, unknown?], +): Promise<{ headers: Headers; data: T }> { const [url, accessToken, method = "GET", body] = args; const headers = { "Content-Type": "application/json", @@ -18,16 +18,16 @@ export const fetcher = async ( const response = await fetch(url, { method: method, headers: headers, - ...(body && { body: JSON.stringify(body) }), + body: body ? JSON.stringify(body) : undefined, }); if (!response.ok) throw new Error("Failed to fetch data"); - const data = await response.json(); + const data = (await response.json()) as T; const responseHeaders = response.headers; return { headers: responseHeaders, data }; -}; +} /** * Custom hook to get the diracx installation URL diff --git a/packages/diracx-web-components/test/unit-tests/UserDashboard.test.tsx b/packages/diracx-web-components/test/unit-tests/BaseApp.test.tsx similarity index 89% rename from packages/diracx-web-components/test/unit-tests/UserDashboard.test.tsx rename to packages/diracx-web-components/test/unit-tests/BaseApp.test.tsx index a7128b12..2273a669 100644 --- a/packages/diracx-web-components/test/unit-tests/UserDashboard.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/BaseApp.test.tsx @@ -1,7 +1,7 @@ import React from "react"; import { render } from "@testing-library/react"; import { useOidcAccessToken, useOidc } from "@axa-fr/react-oidc"; -import UserDashboard from "@/components/UserDashboard/UserDashboard"; +import BaseApp from "@/components/BaseApp/BaseApp"; import { ThemeProvider } from "@/contexts/ThemeProvider"; // Mock the modules @@ -15,7 +15,7 @@ jest.mock("jsoncrush", () => ({ uncrush: jest.fn().mockImplementation((data) => data.replace("crushed-", "")), })); -describe("", () => { +describe("", () => { it("renders not authenticated message when accessTokenPayload is not defined", () => { (useOidc as jest.Mock).mockReturnValue({ isAuthenticated: false }); (useOidcAccessToken as jest.Mock).mockReturnValue({ @@ -24,7 +24,7 @@ describe("", () => { const { getByText } = render( - + , ); expect(getByText("Not authenticated")).toBeInTheDocument(); @@ -38,7 +38,7 @@ describe("", () => { const { getByText } = render( - + , ); expect(getByText("Hello TestUser")).toBeInTheDocument(); diff --git a/packages/diracx-web-components/test/unit-tests/Dashboard.test.tsx b/packages/diracx-web-components/test/unit-tests/Dashboard.test.tsx index c38badf9..677ed4c6 100644 --- a/packages/diracx-web-components/test/unit-tests/Dashboard.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/Dashboard.test.tsx @@ -1,11 +1,12 @@ import React from "react"; import { render, fireEvent } from "@testing-library/react"; import { useOidc, useOidcAccessToken } from "@axa-fr/react-oidc"; +import { Dashboard as DashboardIcon } from "@mui/icons-material"; import Dashboard from "@/components/DashboardLayout/Dashboard"; import DashboardDrawer from "@/components/DashboardLayout/DashboardDrawer"; import { ThemeProvider } from "@/contexts/ThemeProvider"; import { ApplicationsContext } from "@/contexts/ApplicationsProvider"; -import { UserSection } from "@/types/UserSection"; +import { DashboardGroup } from "@/types/DashboardGroup"; import { applicationList } from "@/components/ApplicationList"; // Mock the module @@ -19,7 +20,7 @@ jest.mock("jsoncrush", () => ({ uncrush: jest.fn().mockImplementation((data) => data.replace("crushed-", "")), })); -let mockSections: UserSection[] = [ +const mockSections: DashboardGroup[] = [ { title: "Group 1", extended: true, @@ -28,7 +29,7 @@ let mockSections: UserSection[] = [ title: "App 1", id: "app1", type: "Dashboard", - icon: () =>
    App 1 Icon
    , + icon: DashboardIcon, }, ], }, @@ -38,13 +39,7 @@ const MockApplicationProvider: React.FC<{ children: React.ReactNode }> = ({ children, }): JSX.Element => ( { - mockSections = test(); - }), - applicationList, - ]} + value={[mockSections, () => {}, applicationList]} > {children} @@ -114,19 +109,33 @@ describe("", () => { const { getByText } = render( - + , ); - expect(getByText("App 1 Icon")).toBeInTheDocument(); + expect(getByText("App 1")).toBeInTheDocument(); }); it("handles context menu", () => { const { getByText, getByTestId } = render( - + , ); diff --git a/packages/diracx-web-components/test/unit-tests/FilterForm.test.tsx b/packages/diracx-web-components/test/unit-tests/FilterForm.test.tsx index 7aa86983..fa1f8601 100644 --- a/packages/diracx-web-components/test/unit-tests/FilterForm.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/FilterForm.test.tsx @@ -12,8 +12,8 @@ describe("FilterForm", () => { { id: "column5", label: "Column 5", type: "DateTime" }, ]; const filters = [ - { id: 1, column: "column1", operator: "eq", value: "value1" }, - { id: 2, column: "column2", operator: "neq", value: "value2" }, + { id: 1, parameter: "column1", operator: "eq", value: "value1" }, + { id: 2, parameter: "column2", operator: "neq", value: "value2" }, ]; const setFilters = jest.fn(); const handleFilterChange = jest.fn(); @@ -31,13 +31,13 @@ describe("FilterForm", () => { />, ); - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const operatorSelect = screen.getByTestId("filter-form-select-operator"); - const valueInput = screen.getByLabelText("Value"); + const valueInput = screen.getByLabelText("Value") as HTMLInputElement; - expect(columnSelect).not.toHaveTextContent(); + expect(columnSelect).not.toHaveTextContent("Column"); expect(operatorSelect).toHaveTextContent("equals to"); - expect(valueInput.value).not.toBe(); + expect(valueInput.value).not.toBe("value1"); }); it("renders the filter form with correct initial values when a filter is selected", () => { @@ -52,9 +52,9 @@ describe("FilterForm", () => { />, ); - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const operatorSelect = screen.getByTestId("filter-form-select-operator"); - const valueInput = screen.getByLabelText("Value"); + const valueInput = screen.getByLabelText("Value") as HTMLInputElement; expect(columnSelect).toHaveTextContent("Column 1"); expect(operatorSelect).toHaveTextContent("equals to"); @@ -73,9 +73,9 @@ describe("FilterForm", () => { />, ); - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const operatorSelect = screen.getByTestId("filter-form-select-operator"); - const valueInput = screen.getByLabelText("Value"); + const valueInput = screen.getByLabelText("Value") as HTMLInputElement; expect(columnSelect).toHaveTextContent("Column 2"); expect(operatorSelect).toHaveTextContent("not equals to"); @@ -123,7 +123,7 @@ describe("FilterForm", () => { expect(setFilters).toHaveBeenCalledWith([ ...filters, - { id: expect.any(Number), column: "", operator: "eq", value: "" }, + { id: expect.any(Number), parameter: "", operator: "eq", value: "" }, ]); expect(handleFilterChange).not.toHaveBeenCalled(); expect(handleFilterMenuClose).toHaveBeenCalled(); @@ -144,7 +144,7 @@ describe("FilterForm", () => { const applyChangesButton = screen.getByLabelText("Finish editing filter"); // Simulate a click event on the column Select element - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const columnButton = within(columnSelect).getByRole("combobox"); fireEvent.mouseDown(columnButton); @@ -157,7 +157,7 @@ describe("FilterForm", () => { expect(setFilters).toHaveBeenCalled(); expect(handleFilterChange).toHaveBeenCalledWith(0, { id: 1, - column: "column3", + parameter: "column3", operator: "eq", value: "value1", }); @@ -176,7 +176,7 @@ describe("FilterForm", () => { />, ); - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const columnButton = within(columnSelect).getByRole("combobox"); fireEvent.mouseDown(columnButton); const columnOption = screen.getByText("Column 5"); @@ -212,7 +212,7 @@ describe("FilterForm", () => { />, ); - const columnSelect = screen.getByTestId("filter-form-select-column"); + const columnSelect = screen.getByTestId("filter-form-select-parameter"); const columnButton = within(columnSelect).getByRole("combobox"); fireEvent.mouseDown(columnButton); const columnOption = screen.getByText("Column 4"); diff --git a/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx b/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx index fc372c83..85b018db 100644 --- a/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx +++ b/packages/diracx-web-components/test/unit-tests/FilterToolbar.test.tsx @@ -12,11 +12,11 @@ describe("FilterToolbar", () => { { id: "column3", label: "Column 3" }, ]; const filters = [ - { id: 1, column: "column1", operator: "eq", value: "value1" }, - { id: 2, column: "column2", operator: "neq", value: "value2" }, + { id: 1, parameter: "column1", operator: "eq", value: "value1" }, + { id: 2, parameter: "column2", operator: "neq", value: "value2" }, ]; const appliedFilters = [ - { id: 1, column: "column1", operator: "eq", value: "value1" }, + { id: 1, parameter: "column1", operator: "eq", value: "value1" }, ]; const setFilters = jest.fn(); const handleApplyFilters = jest.fn(); @@ -64,7 +64,7 @@ describe("FilterToolbar", () => { appliedFilters.push({ id: 2, - column: "column2", + parameter: "column2", operator: "neq", value: "value2", }); diff --git a/packages/diracx-web-components/types/ApplicationConfig.ts b/packages/diracx-web-components/types/ApplicationMetadata.ts similarity index 77% rename from packages/diracx-web-components/types/ApplicationConfig.ts rename to packages/diracx-web-components/types/ApplicationMetadata.ts index 324a53d8..996345cf 100644 --- a/packages/diracx-web-components/types/ApplicationConfig.ts +++ b/packages/diracx-web-components/types/ApplicationMetadata.ts @@ -1,7 +1,7 @@ import { SvgIconComponent } from "@mui/icons-material"; import { ElementType } from "react"; -export default interface ApplicationConfig { +export default interface ApplicationMetadata { name: string; component: ElementType; icon: SvgIconComponent; diff --git a/packages/diracx-web-components/types/Column.ts b/packages/diracx-web-components/types/Column.ts index 3ed686e1..bd70fe07 100644 --- a/packages/diracx-web-components/types/Column.ts +++ b/packages/diracx-web-components/types/Column.ts @@ -2,12 +2,12 @@ * Column interface * @property {number | string} id - the id of the cell * @property {string} label - the label of the cell - * @property {((value: any) => JSX.Element) | null} render - an optional render function to display the values + * @property {((value: unknown) => JSX.Element) | null} render - an optional render function to display the values * @property {string | string[]} type - The type of the values or the list of possible values */ export interface Column { - id: number | string; + id: string; label: string; - render?: ((value: any) => JSX.Element) | null; + render?: (value: unknown) => React.ReactNode; type?: string | string[]; } diff --git a/packages/diracx-web-components/types/DashboardGroup.ts b/packages/diracx-web-components/types/DashboardGroup.ts new file mode 100644 index 00000000..2b69eb30 --- /dev/null +++ b/packages/diracx-web-components/types/DashboardGroup.ts @@ -0,0 +1,8 @@ +import { DashboardItem } from "./DashboardItem"; + +// Define the type for the Dashboard Group state +export interface DashboardGroup { + title: string; + extended: boolean; + items: DashboardItem[]; +} diff --git a/packages/diracx-web-components/types/DashboardItem.ts b/packages/diracx-web-components/types/DashboardItem.ts new file mode 100644 index 00000000..7cd7b010 --- /dev/null +++ b/packages/diracx-web-components/types/DashboardItem.ts @@ -0,0 +1,11 @@ +import { SvgIconComponent } from "@mui/icons-material"; +import { InternalFilter } from "./Filter"; + +// Define the type for the Dashboard Item state +export interface DashboardItem { + title: string; + type: string; + id: string; + icon: SvgIconComponent | null; + data?: InternalFilter[]; +} diff --git a/packages/diracx-web-components/types/Filter.ts b/packages/diracx-web-components/types/Filter.ts index 3f8eb362..a7aa48e6 100644 --- a/packages/diracx-web-components/types/Filter.ts +++ b/packages/diracx-web-components/types/Filter.ts @@ -6,9 +6,15 @@ * @property {string[]} values - the values to filter by if there are multiple */ export interface Filter { - id: number; - column: string; + parameter: string; operator: string; value?: string; values?: string[]; } + +/** Internal Filter type + * @property {number} id - the id of the filter + */ +export interface InternalFilter extends Filter { + id: number; +} diff --git a/packages/diracx-web-components/types/Job.ts b/packages/diracx-web-components/types/Job.ts new file mode 100644 index 00000000..30913df8 --- /dev/null +++ b/packages/diracx-web-components/types/Job.ts @@ -0,0 +1,19 @@ +export interface Job { + JobID: number; + JobName: string; + Site: string; + Status: string; + MinorStatus: string; + SubmissionTime: Date; + RescheduleTime: Date | null; + LastUpdateTime: Date; + StartExecTime: Date | null; + HeartBeatTime: Date | null; + EndExecTime: Date | null; + ApplicationStatus: string; + UserPriority: number; + RescheduleCounter: number; + VerifiedFlag: boolean; + AccountedFlag: string; + [key: string]: unknown; +} diff --git a/packages/diracx-web-components/types/JobHistory.ts b/packages/diracx-web-components/types/JobHistory.ts new file mode 100644 index 00000000..ff70cb28 --- /dev/null +++ b/packages/diracx-web-components/types/JobHistory.ts @@ -0,0 +1,7 @@ +export interface JobHistory { + Status: string; + MinorStatus: string; + ApplicationStatus: string; + StatusTime: string; + Source: string; +} diff --git a/packages/diracx-web-components/types/SearchBody.ts b/packages/diracx-web-components/types/SearchBody.ts new file mode 100644 index 00000000..e7ba56b0 --- /dev/null +++ b/packages/diracx-web-components/types/SearchBody.ts @@ -0,0 +1,11 @@ +import { Filter } from "./Filter"; + +interface SortOption { + parameter: string; + direction: "asc" | "desc"; +} + +export interface SearchBody { + search?: Filter[]; + sort?: SortOption[]; +} diff --git a/packages/diracx-web-components/types/UserSection.ts b/packages/diracx-web-components/types/UserSection.ts deleted file mode 100644 index 900d6377..00000000 --- a/packages/diracx-web-components/types/UserSection.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { SvgIconComponent } from "@mui/icons-material"; - -// Define the type for the userSections state -export type UserSection = { - title: string; - extended: boolean; - items: { - title: string; - type: string; - id: string; - icon: SvgIconComponent; - data?: any; - }[]; -}; diff --git a/packages/diracx-web-components/types/index.ts b/packages/diracx-web-components/types/index.ts index ad41112e..a58c732a 100644 --- a/packages/diracx-web-components/types/index.ts +++ b/packages/diracx-web-components/types/index.ts @@ -1,4 +1,8 @@ -export { type default as ApplicationConfig } from "./ApplicationConfig"; +export { type default as ApplicationMetadata } from "./ApplicationMetadata"; export * from "./Column"; export * from "./Filter"; -export * from "./UserSection"; +export * from "./DashboardGroup"; +export * from "./DashboardItem"; +export * from "./SearchBody"; +export * from "./Job"; +export * from "./JobHistory"; diff --git a/packages/diracx-web/src/app/(dashboard)/page.tsx b/packages/diracx-web/src/app/(dashboard)/page.tsx index 5aebfe6f..1cadc126 100644 --- a/packages/diracx-web/src/app/(dashboard)/page.tsx +++ b/packages/diracx-web/src/app/(dashboard)/page.tsx @@ -2,7 +2,7 @@ import React from "react"; import { useSearchParams } from "next/navigation"; import { - UserDashboard, + BaseApp, applicationList, } from "@dirac-grid/diracx-web-components/components"; import { ApplicationsContext } from "@dirac-grid/diracx-web-components/contexts"; @@ -10,18 +10,18 @@ import { ApplicationsContext } from "@dirac-grid/diracx-web-components/contexts" export default function Page() { const searchParams = useSearchParams(); const appId = searchParams.get("appId"); - const [sections] = React.useContext(ApplicationsContext); + const [userDashboard] = React.useContext(ApplicationsContext); const appType = React.useMemo(() => { - const section = sections.find((section) => - section.items.some((item) => item.id === appId), + const group = userDashboard.find((group) => + group.items.some((item) => item.id === appId), ); - return section?.items.find((item) => item.id === appId)?.type; - }, [sections, appId]); + return group?.items.find((item) => item.id === appId)?.type; + }, [userDashboard, appId]); const Component = React.useMemo(() => { return applicationList.find((app) => app.name === appType)?.component; }, [appType]); - return Component ? : ; + return Component ? : ; } diff --git a/packages/diracx-web/test/e2e/dashboard.cy.ts b/packages/diracx-web/test/e2e/dashboard.cy.ts index 82ff1548..b3e82042 100644 --- a/packages/diracx-web/test/e2e/dashboard.cy.ts +++ b/packages/diracx-web/test/e2e/dashboard.cy.ts @@ -18,7 +18,7 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { cy.url().should("include", "/auth"); }); cy.visit( - "/?appId=Dashboard0§ions=E-4tru2-93-96-0%27A1316Job70%275AOther4fals2.3.6B80%2755*%28%27title%21%27-Dashboard.B+891Job+792e~items%21E3type%21%2749extended%215%29%5D6id%21%277Monitor8Catalog9%27~A%29%2C*BFileE%5B*%01EBA987654321.-*_", + "/?appId=Dashboard0&dashboard=E-4tru2-93-96-0%27A1316Job70%275AOther4fals2.3.6B80%2755*%28%27title%21%27-Dashboard.B+891Job+792e~items%21E3type%21%2749extended%215%29%5D6id%21%277Monitor8Catalog9%27~A%29%2C*BFileE%5B*%01EBA987654321.-*_", ); }); @@ -112,7 +112,7 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { // Check if the drawer is not visible before reloading cy.contains("Job Monitor").should("not.be.visible"); cy.wait(500); - cy.url().should("include", "sections="); + cy.url().should("include", "dashboard="); cy.reload(); // Check if the drawer is still not visible after reloading @@ -121,7 +121,7 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { it("should load the state of the drawer from url", () => { cy.visit( - "/?sections=%5B%28%27title%21%27Test+Value%27~extended%21true~items%21%5B%5D%29%5D_", + "/?dashboard=%5B%28%27title%21%27Test+Value%27~extended%21true~items%21%5B%5D%29%5D_", ); // Check if there is a group with the title "Test Value" diff --git a/packages/diracx-web/test/e2e/jobMonitor.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.cy.ts index 8e8fa7d7..9abe7a99 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.cy.ts @@ -18,7 +18,7 @@ describe("Job Monitor", () => { // Visit the page where the Job Monitor is rendered cy.visit( - "/?appId=JobMonitor0§ions=6%21%27Test+Group%27~extended%21true~items%216*.~id430%27%29%2C-5%27~id51.%29%5D%29%5D*4+3-%28%27title.%27~type*%273Monitor4%21%27Job5*+26%5B-%016543.-*_", + "/?appId=JobMonitor0&userDashboard=6%21%27Test+Group%27~extended%21true~items%216*.~id430%27%29%2C-5%27~id51.%29%5D%29%5D*4+3-%28%27title.%27~type*%273Monitor4%21%27Job5*+26%5B-%016543.-*_", ); }); diff --git a/packages/extensions/src/app/(dashboard)/layout.tsx b/packages/extensions/src/app/(dashboard)/layout.tsx index c2e6e450..ad18b813 100644 --- a/packages/extensions/src/app/(dashboard)/layout.tsx +++ b/packages/extensions/src/app/(dashboard)/layout.tsx @@ -11,7 +11,7 @@ import { } from "@dirac-grid/diracx-web-components/contexts"; import { usePathname, useRouter, useSearchParams } from "next/navigation"; import { applicationList } from "@/gubbins/applicationList"; -import { defaultSections } from "@/gubbins/defaultSections"; +import { defaultUserDashboard } from "@/gubbins/defaultUserDashboard"; // Layout for the dashboard: setup the providers and the dashboard for the applications export default function DashboardLayout({ @@ -33,11 +33,11 @@ export default function DashboardLayout({ setPath={router.push} getSearchParams={() => searchParams} > - {/* ApplicationsProvider is the provider for the applications, you can give it customized application list or default sections to override them. + {/* ApplicationsProvider is the provider for the applications, you can give it customized application list or default user dashboard to override them. No need to use it if you don't want to customize the applications */} {/* OIDCSecure is used to make sure the user is authenticated before accessing the dashboard */} diff --git a/packages/extensions/src/app/(dashboard)/page.tsx b/packages/extensions/src/app/(dashboard)/page.tsx index 07192bf4..d51b6b1e 100644 --- a/packages/extensions/src/app/(dashboard)/page.tsx +++ b/packages/extensions/src/app/(dashboard)/page.tsx @@ -1,6 +1,6 @@ "use client"; import React, { useEffect } from "react"; -import { UserDashboard } from "@dirac-grid/diracx-web-components/components"; +import { BaseApp } from "@dirac-grid/diracx-web-components/components"; import { ApplicationsContext } from "@dirac-grid/diracx-web-components/contexts"; import { useSearchParamsUtils } from "@dirac-grid/diracx-web-components/hooks"; import { applicationList } from "@/gubbins/applicationList"; @@ -16,22 +16,22 @@ export default function Page() { } }, [getParam, setParam]); - // Get the user sections from the ApplicationsContext - const [sections] = React.useContext(ApplicationsContext); + // Get the user dashboard from the ApplicationsContext + const [userDashboard] = React.useContext(ApplicationsContext); // Memoize the application type based on the appId const appType = React.useMemo(() => { - const section = sections.find((section) => - section.items.some((item) => item.id === appId), + const group = userDashboard.find((group) => + group.items.some((item) => item.id === appId), ); - return section?.items.find((item) => item.id === appId)?.type; - }, [sections, appId]); + return group?.items.find((item) => item.id === appId)?.type; + }, [userDashboard, appId]); // Get the component based on the application type const Component = React.useMemo(() => { return applicationList.find((app) => app.name === appType)?.component; }, [appType]); - // Render the component if it exists, otherwise render the UserDashboard - return Component ? : ; + // Render the component if it exists, otherwise render the BaseApp + return Component ? : ; } diff --git a/packages/extensions/src/gubbins/applicationList.ts b/packages/extensions/src/gubbins/applicationList.ts index 496024de..99f0bcf8 100644 --- a/packages/extensions/src/gubbins/applicationList.ts +++ b/packages/extensions/src/gubbins/applicationList.ts @@ -1,10 +1,10 @@ import { applicationList } from "@dirac-grid/diracx-web-components/components"; -import { ApplicationConfig } from "@dirac-grid/diracx-web-components/types"; +import { ApplicationMetadata } from "@dirac-grid/diracx-web-components/types"; import { BugReport } from "@mui/icons-material"; import TestApp from "@/gubbins/components/TestApp/testApp"; // New Application List with the default ones + the Test app -const appList: ApplicationConfig[] = [ +const appList: ApplicationMetadata[] = [ ...applicationList, { name: "Test App", component: TestApp, icon: BugReport }, ]; diff --git a/packages/extensions/src/gubbins/defaultSections.tsx b/packages/extensions/src/gubbins/defaultUserDashboard.tsx similarity index 94% rename from packages/extensions/src/gubbins/defaultSections.tsx rename to packages/extensions/src/gubbins/defaultUserDashboard.tsx index 2d2caad6..0f3582f8 100644 --- a/packages/extensions/src/gubbins/defaultSections.tsx +++ b/packages/extensions/src/gubbins/defaultUserDashboard.tsx @@ -1,7 +1,7 @@ import { applicationList } from "@/gubbins/applicationList"; -// New default user sections -export const defaultSections: { +// New default user dashboard groups +export const defaultUserDashboard: { title: string; extended: boolean; items: { From eeceb59cd6cee1a772c92fcead48e229f67996c1 Mon Sep 17 00:00:00 2001 From: aldbr Date: Wed, 16 Oct 2024 17:01:08 +0200 Subject: [PATCH 3/4] fix(e2e): adapt cypress tests --- .../components/DashboardLayout/DrawerItem.tsx | 1 + packages/diracx-web/test/e2e/dashboard.cy.ts | 12 +++++------- packages/diracx-web/test/e2e/jobMonitor.cy.ts | 16 ++++++++-------- packages/diracx-web/test/e2e/login_out.cy.ts | 10 +++++++++- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx b/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx index dd311d71..994d4aa2 100644 --- a/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/DrawerItem.tsx @@ -162,6 +162,7 @@ export default function DrawerItem({ component={DragIndicator} sx={{ cursor: "grab" }} ref={handleRef} + data-testid="drag-handle" /> {closestEdge && } diff --git a/packages/diracx-web/test/e2e/dashboard.cy.ts b/packages/diracx-web/test/e2e/dashboard.cy.ts index b3e82042..96544b94 100644 --- a/packages/diracx-web/test/e2e/dashboard.cy.ts +++ b/packages/diracx-web/test/e2e/dashboard.cy.ts @@ -36,11 +36,11 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { it("should handle application addition", () => { cy.contains("Add application").click(); - cy.get("button").contains("Dashboard").click().click(); + cy.get("button").contains("Base Application").click().click(); cy.contains("Other").click(); // Check if the application is added - cy.contains("Dashboard 2").should("be.visible"); + cy.contains("Base Application 1").should("be.visible"); }); it("should handle application deletion", () => { @@ -55,12 +55,12 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { cy.get(".MuiListItemButton-root").contains("Dashboard").rightclick(); cy.contains("Rename").click(); - cy.get("input").type("Dashboard 1"); + cy.get("input").type("Base App1"); cy.get("button").contains("Rename").click(); // Check if the application is renamed cy.get(".MuiListItemButton-root") - .contains("Dashboard 1") + .contains("Base App1") .should("be.visible"); }); @@ -139,9 +139,7 @@ describe("DashboardDrawer", { retries: { runMode: 5, openMode: 3 } }, () => { dragAndDrop( cy.contains("[draggable=true]", "Job Monitor").first(), cy.contains("[data-drop-target-for-element=true]", "Other").first(), - cy.get( - '.MuiAccordionDetails-root > :nth-child(2) > .MuiButtonBase-root .css-1blhdvq-MuiListItemIcon-root > [data-testid="DragIndicatorIcon"]', - ), + cy.get('[data-testid="drag-handle"]').eq(1), ); // Check if the application is dropped diff --git a/packages/diracx-web/test/e2e/jobMonitor.cy.ts b/packages/diracx-web/test/e2e/jobMonitor.cy.ts index 9abe7a99..f7d04b62 100644 --- a/packages/diracx-web/test/e2e/jobMonitor.cy.ts +++ b/packages/diracx-web/test/e2e/jobMonitor.cy.ts @@ -18,7 +18,7 @@ describe("Job Monitor", () => { // Visit the page where the Job Monitor is rendered cy.visit( - "/?appId=JobMonitor0&userDashboard=6%21%27Test+Group%27~extended%21true~items%216*.~id430%27%29%2C-5%27~id51.%29%5D%29%5D*4+3-%28%27title.%27~type*%273Monitor4%21%27Job5*+26%5B-%016543.-*_", + "/?appId=JobMonitor0&dashboard=6%21%27My+dashboard%27~extended%21true~items%216*.~id430%27%29%2C-5%27~id51.%29%5D%29%5D*4+3-%28%27title.%27~type*%273Monitor4%21%27Job5*+26%5B-%016543.-*_&userDashboard=6%21%27Test+Group%27~extended%21true~items%216*.~id430%27%29%2C-5%27~id51.%29%5D%29%5D*4+3-%28%27title.%27~type*%273Monitor4%21%27Job5*+26%5B-%016543.-*_", ); }); @@ -30,7 +30,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); @@ -45,7 +45,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); @@ -63,7 +63,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); @@ -86,7 +86,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); @@ -98,7 +98,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test2"); @@ -117,7 +117,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); @@ -138,7 +138,7 @@ describe("Job Monitor", () => { cy.get("button").contains("Add filter").click(); cy.get( - '[data-testid="filter-form-select-column"] > .MuiSelect-select', + '[data-testid="filter-form-select-parameter"] > .MuiSelect-select', ).click(); cy.get('[data-value="JobName"]').click(); cy.get("#value").type("test"); diff --git a/packages/diracx-web/test/e2e/login_out.cy.ts b/packages/diracx-web/test/e2e/login_out.cy.ts index cab4a07b..7a6cec86 100644 --- a/packages/diracx-web/test/e2e/login_out.cy.ts +++ b/packages/diracx-web/test/e2e/login_out.cy.ts @@ -45,8 +45,16 @@ describe("Login and Logout", () => { cy.contains("Login").should("not.exist"); cy.contains("Hello admin").should("exist"); - // Logout + // Click on the user avatar cy.get(".MuiAvatar-root").click(); + // Check the user details + cy.contains("admin").should("exist"); + cy.contains("diracAdmin").should("exist"); + // Check the Properties accordion + cy.contains("Properties").click(); + cy.contains("NormalUser").should("exist"); + + // Logout cy.contains("Logout").click(); // The user is redirected back to the /auth page From 9a71c8e93543e593d625b715d5a99600ff4235fc Mon Sep 17 00:00:00 2001 From: aldbr Date: Wed, 16 Oct 2024 18:22:15 +0200 Subject: [PATCH 4/4] fix: use MUI for personal details --- .../DashboardLayout/ProfileButton.tsx | 109 +++++++++--------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx index 1dfd752a..f6b55313 100644 --- a/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx +++ b/packages/diracx-web-components/components/DashboardLayout/ProfileButton.tsx @@ -13,6 +13,7 @@ import { AccordionDetails, AccordionSummary, Avatar, + Box, Button, Chip, Divider, @@ -116,61 +117,59 @@ export function ProfileButton() { transformOrigin={{ horizontal: "right", vertical: "top" }} anchorOrigin={{ horizontal: "right", vertical: "bottom" }} > - -
    - - - - - - - - - - - - - - -
    - - - - - {accessTokenPayload["preferred_username"]} -
    - - - - - {accessTokenPayload["dirac_group"]} -
    - - - - - {accessTokenPayload["vo"]} -
    - - - - } - aria-controls="panel1a-content" - id="panel1a-header" - > - Properties - - - - {accessTokenPayload["dirac_properties"]?.map( - (property: string, index: number) => ( - - ), - )} - - - + + + + + + + + {accessTokenPayload["preferred_username"]} + + + + + + + + + + + + {accessTokenPayload["dirac_group"]} + + + + + + + + + {accessTokenPayload["vo"]} + + + + + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + Properties + + + + {accessTokenPayload["dirac_properties"]?.map( + (property: string, index: number) => ( + + ), + )} + + + +