diff --git a/backend/src/api/dashboard/dashboardMetricsGet.ts b/backend/src/api/dashboard/dashboardMetricsGet.ts new file mode 100644 index 0000000000..ddf277da0a --- /dev/null +++ b/backend/src/api/dashboard/dashboardMetricsGet.ts @@ -0,0 +1,12 @@ +import DashboardService from '@/services/dashboardService' + +import Permissions from '../../security/permissions' +import PermissionChecker from '../../services/user/permissionChecker' + +export default async (req, res) => { + new PermissionChecker(req).validateHas(Permissions.values.memberRead) + + const payload = await new DashboardService(req).getMetrics(req.query) + + await req.responseHandler.success(req, res, payload) +} diff --git a/backend/src/api/dashboard/index.ts b/backend/src/api/dashboard/index.ts index 0fd01a65c3..7d370cfe07 100644 --- a/backend/src/api/dashboard/index.ts +++ b/backend/src/api/dashboard/index.ts @@ -2,4 +2,5 @@ import { safeWrap } from '../../middlewares/errorMiddleware' export default (app) => { app.get(`/dashboard`, safeWrap(require('./dashboardGet').default)) + app.get(`/dashboard/metrics`, safeWrap(require('./dashboardMetricsGet').default)) } diff --git a/backend/src/services/dashboardService.ts b/backend/src/services/dashboardService.ts index 8569634fb3..42590e26cd 100644 --- a/backend/src/services/dashboardService.ts +++ b/backend/src/services/dashboardService.ts @@ -1,6 +1,9 @@ +import { getMetrics } from '@crowd/data-access-layer/src/dashboards' import { RedisCache } from '@crowd/redis' import { DashboardTimeframe } from '@crowd/types' +import SequelizeRepository from '../database/repositories/sequelizeRepository' + import { IServiceOptions } from './IServiceOptions' interface IDashboardQueryParams { @@ -9,6 +12,10 @@ interface IDashboardQueryParams { timeframe: DashboardTimeframe } +interface IDashboardMetricsQueryParams { + segment?: string +} + export default class DashboardService { options: IServiceOptions @@ -75,4 +82,19 @@ export default class DashboardService { return JSON.parse(data) } + + async getMetrics(params: IDashboardMetricsQueryParams) { + try { + if (!params.segment) { + this.options.log.warn('No segment ID provided for metrics query') + } + + const qx = SequelizeRepository.getQueryExecutor(this.options) + const metrics = await getMetrics(qx, params.segment) + return metrics + } catch (error) { + this.options.log.error('Failed to fetch dashboard metrics', { error, params }) + throw new Error('Unable to fetch dashboard metrics') + } + } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a9dee9755a..c2de5061ec 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -14,6 +14,7 @@ "@linuxfoundation/lfx-ui-core": "^0.0.20", "@nangohq/frontend": "^0.9.0", "@nangohq/frontend-v2": "npm:@nangohq/frontend@^0.52.4", + "@sxzz/popperjs-es": "^2.11.7", "@tailwindcss/line-clamp": "^0.4.2", "@tanstack/vue-query": "^5.75.1", "@tanstack/vue-query-devtools": "^5.75.1", @@ -30,6 +31,7 @@ "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.27.5", "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "logrocket": "^3.0.1", "logrocket-vuex": "^0.0.3", "marked": "^4.3.0", @@ -2729,7 +2731,6 @@ "version": "1.10.17", "resolved": "https://registry.npmjs.org/@interactjs/core/-/core-1.10.17.tgz", "integrity": "sha512-rL9w+83HDRuXub8Ezqs+97CYLl/ne7bLT/sAeduUWaxYhsW9iOqBoob9JnkkCZOaOsYizWI1EWy0+fNc5ibtLQ==", - "peer": true, "peerDependencies": { "@interactjs/utils": "1.10.17" } @@ -2795,7 +2796,6 @@ "version": "1.10.17", "resolved": "https://registry.npmjs.org/@interactjs/modifiers/-/modifiers-1.10.17.tgz", "integrity": "sha512-Dxw8kv9VBIxnhNvQncR6CKAGMzKXczLvuAUIdSPFYtyerX/XiDulJUqhR+jVKNp/WjF1DvdBxWo0kGGLbM84LQ==", - "peer": true, "dependencies": { "@interactjs/snappers": "1.10.17" }, @@ -2862,8 +2862,7 @@ "node_modules/@interactjs/utils": { "version": "1.10.17", "resolved": "https://registry.npmjs.org/@interactjs/utils/-/utils-1.10.17.tgz", - "integrity": "sha512-sZAW08CkqgvqRjUIaLRjScjObcCzN9D75yekLA21EClYAZIhi4A+GEt2z/WqOCOksTaEPLYmQyhkpXcboc0LhQ==", - "peer": true + "integrity": "sha512-sZAW08CkqgvqRjUIaLRjScjObcCzN9D75yekLA21EClYAZIhi4A+GEt2z/WqOCOksTaEPLYmQyhkpXcboc0LhQ==" }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -3294,6 +3293,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", "optional": true, + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -3304,6 +3304,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", "optional": true, + "peer": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -6235,6 +6236,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@sxzz/popperjs-es": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@tailwindcss/line-clamp": { "version": "0.4.2", "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.2.tgz", @@ -6535,7 +6545,6 @@ "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.7.tgz", "integrity": "sha512-z0ptr6UI10VlU6l5MYhGwS4mC8DZyYer2mCoyysZtSF7p26zOX8UpbrV0YpNYLGS8K4PUFIyEr62IMFFjveSiQ==", - "peer": true, "dependencies": { "@types/lodash": "*" } @@ -6812,7 +6821,6 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.59.5.tgz", "integrity": "sha512-NJXQC4MRnF9N9yWqQE2/KLRSOLvrrlZb48NGVfBa+RuPMN6B7ZcK5jZOvhuygv4D64fRKnZI4L4p8+M+rfeQuw==", "dev": true, - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.59.5", "@typescript-eslint/types": "5.59.5", @@ -7493,7 +7501,6 @@ "version": "8.8.2", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -8309,7 +8316,6 @@ "url": "https://github.com/sponsors/ai" } ], - "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -8521,8 +8527,7 @@ "node_modules/chart.js": { "version": "3.9.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-3.9.1.tgz", - "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==", - "peer": true + "integrity": "sha512-Ro2JbLmvg83gXF5F4sniaQ+lTbSv18E+TIf2cOeiH1Iqd2PGFOtem+DUufMZsCJwFE7ywPOpfXFBwRTGq7dh6w==" }, "node_modules/chartjs-adapter-moment": { "version": "1.0.1", @@ -9826,7 +9831,6 @@ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", "dev": true, - "peer": true, "dependencies": { "ansi-colors": "^4.1.1" }, @@ -10009,7 +10013,6 @@ "integrity": "sha512-1GJtYnUxsJreHYA0Y+iQz2UEykonY66HNWOb0yXYZi9/kNrORUEHVg87eQsCtqh59PEJ5YVZJO98JHznMJSWjg==", "dev": true, "hasInstallScript": true, - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -10443,7 +10446,6 @@ "version": "8.36.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.36.0.tgz", "integrity": "sha512-Y956lmS7vDqomxlaaQAHVmeb4tNMp2FWIvU/RnU5BD3IKMD/MJPr76xdyr68P8tV1iNMvN2mRK0yy3c+UjL+bw==", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.4.0", @@ -10631,7 +10633,6 @@ "version": "2.27.5", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.27.5.tgz", "integrity": "sha512-LmEt3GVofgiGuiE+ORpnvP+kAm3h6MLZJ4Q5HCyHADofsb4VzXFsRiWj3c0OFiV+3DWFh0qg3v9gcPlfc3zRow==", - "peer": true, "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", @@ -10790,7 +10791,6 @@ "version": "9.10.0", "resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-9.10.0.tgz", "integrity": "sha512-2MgP31OBf8YilUvtakdVMc8xVbcMp7z7/iQj8LHVpXrSXHPXSJRUIGSPFI6b6pyCx/buKaFJ45ycqfHvQRiW2g==", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.3.0", "natural-compare": "^1.4.0", @@ -14439,14 +14439,12 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "peer": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "node_modules/lodash-es": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", - "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", - "peer": true + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==" }, "node_modules/lodash-unified": { "version": "1.0.3", @@ -16032,7 +16030,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -17419,7 +17416,6 @@ "resolved": "https://registry.npmjs.org/sass/-/sass-1.92.1.tgz", "integrity": "sha512-ffmsdbwqb3XeyR8jJR6KelIXARM9bFQe8A6Q3W4Klmwy5Ckd5gz7jgUNHo4UOqutU5Sk1DtKLbpDP0nLCg1xqQ==", "devOptional": true, - "peer": true, "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -17468,6 +17464,7 @@ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", "dev": true, + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -18211,7 +18208,6 @@ "version": "3.2.7", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", - "peer": true, "dependencies": { "arg": "^5.0.2", "chokidar": "^3.5.3", @@ -18410,6 +18406,7 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-5.16.8.tgz", "integrity": "sha512-QI5g1E/ef7d+PsDifb+a6nnVgC4F22Bg6T0xrBrz6iloVB4PUkkunp6V8nzoOOZJIzjWVdAGqCdlKlhLq/TbIA==", "optional": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.2", "acorn": "^8.5.0", @@ -18427,7 +18424,8 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "optional": true + "optional": true, + "peer": true }, "node_modules/test-exclude": { "version": "6.0.0", @@ -18746,7 +18744,6 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.4.tgz", "integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==", "devOptional": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -19241,7 +19238,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", @@ -19403,7 +19399,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/vue/-/vue-3.3.1.tgz", "integrity": "sha512-3Rwy4I5idbPVSDZu6I+fFh6tdDSZbauImCTqLxE7y0LpHtiDvPeY01OI7RkFPbva1nk4hoO0sv/NzosH2h60sg==", - "peer": true, "dependencies": { "@vue/compiler-dom": "3.3.1", "@vue/compiler-sfc": "3.3.1", diff --git a/frontend/package.json b/frontend/package.json index 6413ecb071..3766300d6b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -33,6 +33,7 @@ "@linuxfoundation/lfx-ui-core": "^0.0.20", "@nangohq/frontend": "^0.9.0", "@nangohq/frontend-v2": "npm:@nangohq/frontend@^0.52.4", + "@sxzz/popperjs-es": "^2.11.7", "@tailwindcss/line-clamp": "^0.4.2", "@tanstack/vue-query": "^5.75.1", "@tanstack/vue-query-devtools": "^5.75.1", @@ -49,6 +50,7 @@ "eslint-import-resolver-alias": "^1.1.2", "eslint-plugin-import": "^2.27.5", "lodash": "^4.17.21", + "lodash-es": "^4.17.21", "logrocket": "^3.0.1", "logrocket-vuex": "^0.0.3", "marked": "^4.3.0", diff --git a/frontend/src/config/integrations/confluence/config.ts b/frontend/src/config/integrations/confluence/config.ts index 76fdb7ae98..5c100c9035 100644 --- a/frontend/src/config/integrations/confluence/config.ts +++ b/frontend/src/config/integrations/confluence/config.ts @@ -4,22 +4,24 @@ import ConfluenceParams from './components/confluence-params.vue'; import ConfluenceDropdown from './components/confluence-dropdown.vue'; import LfConfluenceSettingsDrawer from './components/confluence-settings-drawer.vue'; -const image = new URL( - '@/assets/images/integrations/confluence.svg', - import.meta.url, -).href; +const image = new URL('@/assets/images/integrations/confluence.svg', import.meta.url).href; const confluence: IntegrationConfig = { key: 'confluence', name: 'Confluence', image, - description: - 'Connect Confluence to sync documentation activities from your repos.', + description: 'Connect Confluence to sync documentation activities from your repos.', connectComponent: ConfluenceConnect, connectedParamsComponent: ConfluenceParams, dropdownComponent: ConfluenceDropdown, settingComponent: LfConfluenceSettingsDrawer, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default confluence; diff --git a/frontend/src/config/integrations/devto/config.ts b/frontend/src/config/integrations/devto/config.ts index f2f7a776f6..2e1692da18 100644 --- a/frontend/src/config/integrations/devto/config.ts +++ b/frontend/src/config/integrations/devto/config.ts @@ -2,18 +2,22 @@ import { IntegrationConfig } from '@/config/integrations'; import DevtoConnect from './components/devto-connect.vue'; import DevtoParams from './components/devto-params.vue'; -const image = new URL('@/assets/images/integrations/devto.png', import.meta.url) - .href; +const image = new URL('@/assets/images/integrations/devto.png', import.meta.url).href; const devto: IntegrationConfig = { key: 'devto', name: 'DEV', image, - description: - 'Connect DEV to sync profile information and comments on articles.', + description: 'Connect DEV to sync profile information and comments on articles.', connectComponent: DevtoConnect, connectedParamsComponent: DevtoParams, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default devto; diff --git a/frontend/src/config/integrations/discord/config.ts b/frontend/src/config/integrations/discord/config.ts index f674283d86..173a7af1a5 100644 --- a/frontend/src/config/integrations/discord/config.ts +++ b/frontend/src/config/integrations/discord/config.ts @@ -2,20 +2,22 @@ import { IntegrationConfig } from '@/config/integrations'; import DiscordConnect from './components/discord-connect.vue'; import DiscordParams from './components/discord-params.vue'; -const image = new URL( - '@/assets/images/integrations/discord.png', - import.meta.url, -).href; +const image = new URL('@/assets/images/integrations/discord.png', import.meta.url).href; const discord: IntegrationConfig = { key: 'discord', name: 'Discord', image, - description: - 'Connect Discord to sync messages, threads, forum channels, and new joiners.', + description: 'Connect Discord to sync messages, threads, forum channels, and new joiners.', connectComponent: DiscordConnect, connectedParamsComponent: DiscordParams, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default discord; diff --git a/frontend/src/config/integrations/discourse/config.ts b/frontend/src/config/integrations/discourse/config.ts index a05275d063..7f9f32e520 100644 --- a/frontend/src/config/integrations/discourse/config.ts +++ b/frontend/src/config/integrations/discourse/config.ts @@ -4,22 +4,24 @@ import DiscourseConnect from './components/discourse-connect.vue'; import DiscourseParams from './components/discourse-params.vue'; import DiscourseDropdown from './components/discourse-dropdown.vue'; -const image = new URL( - '@/assets/images/integrations/discourse.png', - import.meta.url, -).href; +const image = new URL('@/assets/images/integrations/discourse.png', import.meta.url).href; const discourse: IntegrationConfig = { key: 'discourse', name: 'Discourse', image, - description: - 'Connect Discourse to sync topics, posts, and replies from your account forums.', + description: 'Connect Discourse to sync topics, posts, and replies from your account forums.', connectComponent: DiscourseConnect, connectedParamsComponent: DiscourseParams, dropdownComponent: DiscourseDropdown, settingComponent: LfDiscourseSettingsDrawer, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default discourse; diff --git a/frontend/src/config/integrations/gerrit/config.ts b/frontend/src/config/integrations/gerrit/config.ts index c4b09c6790..3783f1727b 100644 --- a/frontend/src/config/integrations/gerrit/config.ts +++ b/frontend/src/config/integrations/gerrit/config.ts @@ -4,22 +4,24 @@ import GerritConnect from './components/gerrit-connect.vue'; import GerritParams from './components/gerrit-params.vue'; import GerritDropdown from './components/gerrit-dropdown.vue'; -const image = new URL( - '@/assets/images/integrations/gerrit.png', - import.meta.url, -).href; +const image = new URL('@/assets/images/integrations/gerrit.png', import.meta.url).href; const gerrit: IntegrationConfig = { key: 'gerrit', name: 'Gerrit', image, - description: - 'Connect Gerrit to sync documentation activities from your repos.', + description: 'Connect Gerrit to sync documentation activities from your repos.', connectComponent: GerritConnect, connectedParamsComponent: GerritParams, dropdownComponent: GerritDropdown, settingComponent: LfGerritSettingsDrawer, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default gerrit; diff --git a/frontend/src/config/integrations/git/config.ts b/frontend/src/config/integrations/git/config.ts index 18d1b8c34e..9e4de87ca4 100644 --- a/frontend/src/config/integrations/git/config.ts +++ b/frontend/src/config/integrations/git/config.ts @@ -4,8 +4,7 @@ import GitDropdown from './components/git-dropdown.vue'; import GitParams from './components/git-params.vue'; import LfGitSettingsDrawer from './components/git-settings-drawer.vue'; -const image = new URL('@/assets/images/integrations/git.png', import.meta.url) - .href; +const image = new URL('@/assets/images/integrations/git.png', import.meta.url).href; const git: IntegrationConfig = { key: 'git', @@ -17,6 +16,12 @@ const git: IntegrationConfig = { connectedParamsComponent: GitParams, settingComponent: LfGitSettingsDrawer, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + ], }; export default git; diff --git a/frontend/src/config/integrations/github-nango/config.ts b/frontend/src/config/integrations/github-nango/config.ts index 992031847e..d1e754bf81 100644 --- a/frontend/src/config/integrations/github-nango/config.ts +++ b/frontend/src/config/integrations/github-nango/config.ts @@ -5,10 +5,7 @@ import GithubParams from './components/github-params.vue'; import GithubDropdown from './components/github-dropdown.vue'; import GithubMappedRepos from './components/github-mapped-repos.vue'; -const image = new URL( - '@/assets/images/integrations/github.png', - import.meta.url, -).href; +const image = new URL('@/assets/images/integrations/github.png', import.meta.url).href; const github: IntegrationConfig = { key: 'github', @@ -23,6 +20,16 @@ const github: IntegrationConfig = { mappedReposComponent: GithubMappedRepos, settingComponent: LfGithubSettingsDrawer, showProgress: false, + actionRequiredMessage: [ + { + key: 'needs-reconnect', + text: 'Reconnect your account to restore access.', + }, + { + key: 'mapping', + text: 'Select repositories to track and map them to projects.', + }, + ], }; export default github; diff --git a/frontend/src/config/integrations/github/components/github-action.vue b/frontend/src/config/integrations/github/components/github-action.vue index a661f18b09..70c17d1a1f 100644 --- a/frontend/src/config/integrations/github/components/github-action.vue +++ b/frontend/src/config/integrations/github/components/github-action.vue @@ -14,13 +14,16 @@ import { computed, ref } from 'vue'; import LfButton from '@/ui-kit/button/Button.vue'; import AppGithubSettingsDrawer from '@/config/integrations/github/components/settings/github-settings-drawer.vue'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ integration: any; -}>(); + preventAutoOpen?: boolean; +}>(), { + preventAutoOpen: false, +}); const isMapping = computed(() => props.integration.status === 'mapping'); -const isSettingsDrawerOpen = ref(props.integration.status === 'mapping'); +const isSettingsDrawerOpen = ref(props.integration.status === 'mapping' && !props.preventAutoOpen); + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/integration-tabs.vue b/frontend/src/modules/admin/modules/overview/components/fragments/integration-tabs.vue new file mode 100644 index 0000000000..03d04ea5d2 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/integration-tabs.vue @@ -0,0 +1,51 @@ + + + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/integrations-filter.vue b/frontend/src/modules/admin/modules/overview/components/fragments/integrations-filter.vue new file mode 100644 index 0000000000..c696303f75 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/integrations-filter.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/load-more.vue b/frontend/src/modules/admin/modules/overview/components/fragments/load-more.vue new file mode 100644 index 0000000000..42317c98fa --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/load-more.vue @@ -0,0 +1,87 @@ + + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/project-filter.vue b/frontend/src/modules/admin/modules/overview/components/fragments/project-filter.vue new file mode 100644 index 0000000000..3e744db60e --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/project-filter.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/project-group-filter.vue b/frontend/src/modules/admin/modules/overview/components/fragments/project-group-filter.vue new file mode 100644 index 0000000000..92338dd44b --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/project-group-filter.vue @@ -0,0 +1,205 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/status-display.vue b/frontend/src/modules/admin/modules/overview/components/fragments/status-display.vue new file mode 100644 index 0000000000..a78efd799f --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/status-display.vue @@ -0,0 +1,63 @@ + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/sub-project-filter.vue b/frontend/src/modules/admin/modules/overview/components/fragments/sub-project-filter.vue new file mode 100644 index 0000000000..a6ea4df442 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/sub-project-filter.vue @@ -0,0 +1,132 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/fragments/trend-display.vue b/frontend/src/modules/admin/modules/overview/components/fragments/trend-display.vue new file mode 100644 index 0000000000..d625b5006b --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/fragments/trend-display.vue @@ -0,0 +1,89 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/sections/integration-details.vue b/frontend/src/modules/admin/modules/overview/components/sections/integration-details.vue new file mode 100644 index 0000000000..1b134d5385 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/sections/integration-details.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/sections/integration-status.vue b/frontend/src/modules/admin/modules/overview/components/sections/integration-status.vue new file mode 100644 index 0000000000..3e12fb2638 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/sections/integration-status.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/sections/overview-filter.vue b/frontend/src/modules/admin/modules/overview/components/sections/overview-filter.vue new file mode 100644 index 0000000000..bfd7a71009 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/sections/overview-filter.vue @@ -0,0 +1,27 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/components/sections/summary.vue b/frontend/src/modules/admin/modules/overview/components/sections/summary.vue new file mode 100644 index 0000000000..c7314e43cb --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/components/sections/summary.vue @@ -0,0 +1,109 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/pages/overview.vue b/frontend/src/modules/admin/modules/overview/pages/overview.vue new file mode 100644 index 0000000000..04c1dda0c2 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/pages/overview.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/frontend/src/modules/admin/modules/overview/services/overview.api.service.ts b/frontend/src/modules/admin/modules/overview/services/overview.api.service.ts new file mode 100644 index 0000000000..a255bc9bad --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/services/overview.api.service.ts @@ -0,0 +1,180 @@ +import type { QueryFunction } from '@tanstack/vue-query'; +import { type ComputedRef, computed } from 'vue'; +import { useInfiniteQuery, useQuery } from '@tanstack/vue-query'; +import { TanstackKey } from '@/shared/types/tanstack'; +import authAxios from '@/shared/axios/auth-axios'; +import { Project } from '@/modules/lf/segments/types/Segments'; +import { IntegrationProgress } from '@/modules/integration/types/IntegrationProgress'; +import { + DashboardMetrics, + GlobalIntegrationStatusCount, + IntegrationStatusResponse, +} from '../types/overview.types'; + +export interface GlobalIntegrationStatusCountQueryParams { + platform: string | undefined + segment?: string +} + +export interface GlobalIntegrationIntegrationsQueryParams { + platform: string | undefined + status: string[] + query: string + limit: number + segment?: string +} + +export interface IntegrationProgressListQueryParams { + segments?: string[] +} + +export interface DashboardMetricsQueryParams { + segment?: string +} + +class OverviewApiService { + fetchProjectById(params: ComputedRef) { + const queryKey = computed(() => [TanstackKey.PROJECT_BY_ID, params.value]); + const queryFn = computed>(() => this.fetchProjectByIdQueryFn(() => ({ + id: params.value, + }))); + + return useQuery({ + queryKey, + queryFn, + }); + } + + fetchProjectByIdQueryFn( + query: () => Record, + ): QueryFunction { + return () => authAxios + .get(`/segment/${query().id}`, { + params: { + segments: [query().id], + }, + }) + .then((res) => res.data); + } + + fetchGlobalIntegrationStatusCount(params: ComputedRef) { + const queryKey = computed(() => [ + TanstackKey.GLOBAL_INTEGRATION_STATUS_COUNT, + params.value.platform, + params.value.segment, + ]); + const queryFn = computed>(() => this.fetchGlobalIntegrationStatusCountQueryFn(() => ({ + platform: params.value.platform, + segment: params.value.segment, + }))); + + return useQuery({ + queryKey, + queryFn, + }); + } + + fetchGlobalIntegrationStatusCountQueryFn( + query: () => Record, + ): QueryFunction { + return () => authAxios + .get('/integration/global/status', { + params: query(), + }) + .then((res) => res.data); + } + + fetchGlobalIntegrations(params: ComputedRef) { + const queryKey = computed(() => [ + TanstackKey.GLOBAL_INTEGRATIONS, + params.value.platform, + params.value.status, + params.value.query, + params.value.limit, + params.value.segment, + ]); + const queryFn = computed>(() => this.fetchGlobalIntegrationsQueryFn(() => ({ + ...params.value, + }))); + + return useInfiniteQuery< + IntegrationStatusResponse, + Error, + IntegrationStatusResponse, + readonly unknown[], + number + >({ + queryKey, + // @ts-expect-error - TanStack Query type inference issue with Vue + queryFn, + getNextPageParam: this.getNextPageIntegrationsParam, + initialPageParam: 0, + }); + } + + fetchGlobalIntegrationsQueryFn( + query: () => Record, + ): QueryFunction { + return ({ pageParam = 0 }) => authAxios + .get('/integration/global', { + params: { offset: pageParam, ...query() }, + }) + .then((res) => res.data); + } + + getNextPageIntegrationsParam(lastPage: IntegrationStatusResponse): number | undefined { + const nextPage = lastPage.offset + lastPage.limit; + const totalRows = lastPage.count; + return nextPage < totalRows ? nextPage : undefined; + } + + fetchDashboardMetrics(params: ComputedRef) { + const queryKey = computed(() => [TanstackKey.DASHBOARD_METRICS, params.value.segment]); + const queryFn = computed>(() => this.fetchDashboardMetricsQueryFn(() => ({ + ...params.value, + }))); + + return useQuery({ + queryKey, + queryFn, + }); + } + + fetchDashboardMetricsQueryFn( + query: () => Record, + ): QueryFunction { + return () => authAxios + .get('/dashboard/metrics', { + params: query(), + }) + .then((res) => res.data); + } + + fetchIntegrationProgressList(params: ComputedRef) { + const queryKey = computed(() => [TanstackKey.INTEGRATION_PROGRESS_LIST, params.value.segments]); + const queryFn = computed>(() => this.fetchIntegrationProgressListQueryFn(() => ({ + segments: params.value.segments, + }))); + + return useQuery({ + queryKey, + queryFn, + enabled: !!params.value.segments, + }); + } + + fetchIntegrationProgressListQueryFn( + query: () => Record, + ): QueryFunction { + return () => authAxios + .post( + '/integration/progress/list', + { + segments: query().segments, + }, + ) + .then((res) => res.data); + } +} + +export const OVERVIEW_API_SERVICE = new OverviewApiService(); diff --git a/frontend/src/modules/admin/modules/overview/store/mock-overview-data.ts b/frontend/src/modules/admin/modules/overview/store/mock-overview-data.ts new file mode 100644 index 0000000000..2951607195 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/store/mock-overview-data.ts @@ -0,0 +1,488 @@ +import { IntegrationStatus } from '../types/overview.types'; + +export const mockOverviewData: IntegrationStatus[] = [ + // In Progress integrations + { + grandparentId: 'd004b040-95d2-449b-b5a3-8e789418cc88', + grandparentName: 'Academy Software Foundation (ASWF)', + id: 'a92494e5-268e-44f1-bffc-f55240be77ed', + name: 'Academy Software Foundation (ASWF)', + parentId: '9d8f1f72-4d12-4502-8f2e-c2b8348c8209', + parentName: 'Academy Software Foundation (ASWF)', + platform: 'git', + segmentId: '8eb1d4d5-2536-435b-980c-f11e30943025', + settings: { remotes: ['https://github.com/AcademySoftwareFoundation/tac'] }, + status: 'in-progress', + statusDetails: '100 out of 1,000 data streams processed...', + }, + { + grandparentId: 'e005c041-96d3-450c-c6a4-9e890519dd89', + grandparentName: 'Academy Software Foundation', + id: 'b93595f6-369f-45g2-cgd4-g66351cf88fe', + name: 'Asset Repository Working Group', + parentId: 'ae9g2g73-5e13-4603-9g3f-d3c9459d9310', + parentName: 'Academy Software Foundation', + platform: 'stackoverflow', + segmentId: '9fc2e5e6-3647-546c-a91d-g22f41a54136', + settings: { tags: ['academy-software-foundation', 'asset-repository'] }, + status: 'in-progress', + statusDetails: '600 data streams being processed...', + }, + { + grandparentId: 'f106d142-a7e4-561d-d7b5-af901620ee90', + grandparentName: 'Academy Software Foundation', + id: 'ca4696g7-470g-46h3-dhf5-h77462dg99gf', + name: 'MaterialX', + parentId: 'bg0h3h84-6f24-5714-ah4g-e4da570ea421', + parentName: 'Academy Software Foundation', + platform: 'git', + segmentId: 'a0d3f6f7-4758-657d-ba2e-h33g52b65247', + settings: { remotes: ['https://github.com/AcademySoftwareFoundation/MaterialX'] }, + status: 'in-progress', + statusDetails: '600 data streams being processed...', + }, + { + grandparentId: 'g207e253-b8f5-672e-e8c6-bg012731ff01', + grandparentName: 'Academy Software Foundation', + id: 'db5707h8-581h-57i4-eig6-i88573eh00hg', + name: 'OpenAssetIO', + parentId: 'ch1i4i95-7g35-6825-bi5h-f5eb681fb532', + parentName: 'Academy Software Foundation', + platform: 'gitlab', + segmentId: 'b1e4g7g8-5869-768e-cb3f-i44h63c76358', + settings: { remotes: ['https://gitlab.com/AcademySoftwareFoundation/OpenAssetIO'] }, + status: 'in-progress', + statusDetails: '600 data streams being processed...', + }, + { + grandparentId: 'k601i697-fc29-ab6i-ic20-fk456175jj45', + grandparentName: 'Apache Software Foundation', + id: 'hf9b4blc-9c5l-9bm8-ikl0-mc2917il44lk', + name: 'Apache Kafka', + parentId: 'gl5m8md9-bk79-ac69-fm9l-j9if0c5jf976', + parentName: 'Apache Software Foundation', + platform: 'git', + segmentId: 'f5i8kbkc-9ca3-ba9i-gf7j-m88l07ga9792', + settings: { remotes: ['https://github.com/apache/kafka'] }, + status: 'in-progress', + statusDetails: '250 out of 2,500 data streams processed...', + }, + { + grandparentId: 'l702j708-gd30-bc7j-jd31-gl567286kk56', + grandparentName: 'Linux Foundation', + id: 'ig0c5cmd-ad6m-acn9-jlm1-nd3028jm55ml', + name: 'Kubernetes', + parentId: 'hm6n9ne0-cl80-bd70-gn0m-k0jg1d6kg087', + parentName: 'Cloud Native Computing Foundation', + platform: 'github', + segmentId: 'g6j9lcld-odb4-cb0j-hg8k-n99m18hb0803', + settings: { repositories: ['kubernetes/kubernetes', 'kubernetes/community'] }, + status: 'in-progress', + statusDetails: '1,200 out of 5,000 data streams processed...', + }, + { + grandparentId: 'm803k819-he41-cd8k-ke42-hm678397ll67', + grandparentName: 'Eclipse Foundation', + id: 'jh1d6dne-be7n-bdo0-kmn2-oe4139kn66nm', + name: 'Eclipse IDE', + parentId: 'in7o0of1-dm91-ce81-ho1n-l1kh2e7lh198', + parentName: 'Eclipse Foundation', + platform: 'git', + segmentId: 'h7k0mdme-pec5-dc1k-ih9l-o00n29ic1914', + settings: { remotes: ['https://github.com/eclipse/eclipse.platform'] }, + status: 'in-progress', + statusDetails: '800 out of 3,200 data streams processed...', + }, + { + grandparentId: 'n904l920-if52-de9l-lf53-in789408mm78', + grandparentName: 'Mozilla Foundation', + id: 'ki2e7eof-cf8o-cep1-lno3-pf5240lo77on', + name: 'Firefox Browser', + parentId: 'jo8p1pg2-en02-df92-ip2o-m2li3f8mi209', + parentName: 'Mozilla Corporation', + platform: 'mercurial', + segmentId: 'i8l1nenf-qfd6-ed2l-ji0m-p11o30jd2025', + settings: { repositories: ['mozilla-central', 'mozilla-beta'] }, + status: 'in-progress', + statusDetails: '450 out of 1,800 data streams processed...', + }, + { + grandparentId: 'o005m031-jg63-ef0m-mg64-jo890519nn89', + grandparentName: 'Python Software Foundation', + id: 'lj3f8fpg-dg9p-dfq2-moq4-qg6351mp88po', + name: 'Python Core', + parentId: 'kp9q2qh3-fo13-eg03-jq3p-n3mj4g9nj320', + parentName: 'Python Software Foundation', + platform: 'git', + segmentId: 'j9m2ofog-rgd7-fe3m-kj1n-q22p41ke3136', + settings: { remotes: ['https://github.com/python/cpython'] }, + status: 'in-progress', + statusDetails: '1,500 out of 6,000 data streams processed...', + }, + { + grandparentId: 'p106n142-kh74-fg1n-nh75-kp901620oo90', + grandparentName: 'Node.js Foundation', + id: 'mk4g9gqh-eh0q-egr3-npq5-rh7462nq99qp', + name: 'Node.js Runtime', + parentId: 'lq0r3ri4-gp24-fh14-kr4q-o4nk5h0ok431', + parentName: 'OpenJS Foundation', + platform: 'git', + segmentId: 'k0n3pgph-she8-gf4n-lk2o-r33q52of4247', + settings: { remotes: ['https://github.com/nodejs/node'] }, + status: 'in-progress', + statusDetails: '900 out of 3,600 data streams processed...', + }, + { + grandparentId: 'q207o253-li85-gh2o-oi86-lq012731pp01', + grandparentName: 'React Community', + id: 'nl5h0hri-fi1r-fhs4-oqr6-si8573or00rq', + name: 'React Framework', + parentId: 'mr1s4sj5-hq35-gi25-ls5r-p5ol6i1pl542', + parentName: 'Meta Open Source', + platform: 'github', + segmentId: 'l1o4qhqi-tif9-hg5o-ml3p-s44r63pg5358', + settings: { repositories: ['facebook/react', 'reactjs/react.dev'] }, + status: 'in-progress', + statusDetails: '2,100 out of 8,400 data streams processed...', + }, + { + grandparentId: 'r308p364-mj96-hi3p-pj97-mr123842qq12', + grandparentName: 'Vue.js Community', + id: 'om6i1isj-gj2s-git5-prr7-tj9684ps11sr', + name: 'Vue.js Framework', + parentId: 'ns2t5tk6-ir46-hj36-mt6s-q6pm7j2qm653', + parentName: 'Vue.js Team', + platform: 'github', + segmentId: 'm2p5riir-ujg0-ih6p-nm4q-t55s74qh6469', + settings: { repositories: ['vuejs/core', 'vuejs/vue'] }, + status: 'in-progress', + statusDetails: '750 out of 3,000 data streams processed...', + }, + { + grandparentId: 's409q475-nk07-ij4q-qk08-ns234953rr23', + grandparentName: 'Angular Team', + id: 'pn7j2jtk-hk3t-hju6-qss8-uk0795qt22ts', + name: 'Angular Framework', + parentId: 'ot3u6ul7-js57-ik47-nu7t-r7qn8k3rn764', + parentName: 'Google Open Source', + platform: 'git', + segmentId: 'n3q6sjjs-vkh1-ji7q-on5r-u66t85ri7570', + settings: { remotes: ['https://github.com/angular/angular'] }, + status: 'in-progress', + statusDetails: '1,800 out of 7,200 data streams processed...', + }, + { + grandparentId: 't50ar586-ol18-jk5r-rl19-ot345064ss34', + grandparentName: 'Django Software Foundation', + id: 'qo8k3kul-il4u-ikv7-rtt9-vl1806ru33ut', + name: 'Django Framework', + parentId: 'pu4v7vm8-kt68-jl58-ov8u-s8rf9l4sf875', + parentName: 'Django Software Foundation', + platform: 'git', + segmentId: 'o4r7tkuk-wli2-kj8r-po6s-v77u96sj8681', + settings: { remotes: ['https://github.com/django/django'] }, + status: 'in-progress', + statusDetails: '650 out of 2,600 data streams processed...', + }, + { + grandparentId: 'u601s697-pm29-kl6s-sm20-pu456175tt45', + grandparentName: 'Ruby Community', + id: 'rp9l4lvn-jm5v-jlw8-suv0-wm2917sv44vl', + name: 'Ruby on Rails', + parentId: 'qv5w8wn9-lu79-km69-pw9v-t9sg0m5tg986', + parentName: 'Rails Core Team', + platform: 'git', + segmentId: 'p5s8ulvl-xmj3-lk9s-qp7t-w88v07tk9792', + settings: { remotes: ['https://github.com/rails/rails'] }, + status: 'in-progress', + statusDetails: '1,100 out of 4,400 data streams processed...', + }, + { + grandparentId: 'v702t708-qn30-lm7t-tn31-qv567286uu56', + grandparentName: 'Laravel Community', + id: 'sq0m5mwo-kn6w-kmx9-tvw1-xn3028tw55wm', + name: 'Laravel Framework', + parentId: 'rw6x9xo0-mv80-ln70-qw0w-u0th1n6uh087', + parentName: 'Laravel LLC', + platform: 'github', + segmentId: 'q6t9vmwm-ynk4-ml0t-rq8u-x99w18ul0803', + settings: { repositories: ['laravel/framework', 'laravel/laravel'] }, + status: 'in-progress', + statusDetails: '850 out of 3,400 data streams processed...', + }, + { + grandparentId: 'w803u819-ro41-mn8u-uo42-rw678397vv67', + grandparentName: 'Spring Community', + id: 'tr1n6nxp-lo7x-lny0-uwx2-yo4139ux66xn', + name: 'Spring Framework', + parentId: 'sx7y0yp1-nw91-mo81-rx1y-v1ui2o7vi198', + parentName: 'VMware Tanzu', + platform: 'git', + segmentId: 'r7u0wnxn-zol5-nm1u-sr9v-y00x29vm1914', + settings: { remotes: ['https://github.com/spring-projects/spring-framework'] }, + status: 'in-progress', + statusDetails: '1,300 out of 5,200 data streams processed...', + }, + { + grandparentId: 'x904v920-sp52-no9v-vp53-sx789408ww78', + grandparentName: 'Express.js Community', + id: 'us2o7oyq-mp8y-moz1-vxy3-zp5240vy77yo', + name: 'Express.js Framework', + parentId: 'ty8z1zq2-ox02-np92-sy2z-w2vj3p8wj209', + parentName: 'OpenJS Foundation', + platform: 'github', + segmentId: 's8v1xoyo-apm6-on2v-ts0w-z11y30wn2025', + settings: { repositories: ['expressjs/express'] }, + status: 'in-progress', + statusDetails: '400 out of 1,600 data streams processed...', + }, + { + grandparentId: 'y005w031-tq63-op0w-wq64-ty890519xx89', + grandparentName: 'Fastify Community', + id: 'vt3p8pzr-nq9z-npa2-wyz4-aq6351wz88zp', + name: 'Fastify Framework', + parentId: 'uz9a2ar3-py13-oq03-tz3a-x3wk4q9xk320', + parentName: 'Fastify Team', + platform: 'git', + segmentId: 't9w2ypzp-bqn7-po3w-ut1x-a22z41xo3136', + settings: { remotes: ['https://github.com/fastify/fastify'] }, + status: 'in-progress', + statusDetails: '550 out of 2,200 data streams processed...', + }, + { + grandparentId: 'z106x142-ur74-pq1x-xr75-uz901620yy90', + grandparentName: 'NestJS Community', + id: 'wu4q9qas-or0a-oqb3-xza5-br7462ya99aq', + name: 'NestJS Framework', + parentId: 'va0b3bs4-qz24-pr14-ua4b-y4xl5r0yl431', + parentName: 'NestJS Team', + platform: 'github', + segmentId: 'u0x3zqaq-cro8-qq4x-vu2y-b33a52zp4247', + settings: { repositories: ['nestjs/nest'] }, + status: 'in-progress', + statusDetails: '700 out of 2,800 data streams processed...', + }, + { + grandparentId: 'a207y253-vs85-qr2y-ys86-va012731zz01', + grandparentName: 'Next.js Community', + id: 'xv5r0rbt-ps1b-prc4-yab6-cs8573zb00br', + name: 'Next.js Framework', + parentId: 'wb1c4ct5-ra35-qs25-vb5c-z5ym6s1zm542', + parentName: 'Vercel Inc.', + platform: 'git', + segmentId: 'v1y4arbs-dsp9-rr5y-wv3z-c44b63aq5358', + settings: { remotes: ['https://github.com/vercel/next.js'] }, + status: 'in-progress', + statusDetails: '1,600 out of 6,400 data streams processed...', + }, + { + grandparentId: 'b308z364-wt96-rs3z-zt97-wb123842aa12', + grandparentName: 'Nuxt.js Community', + id: 'yw6s1scu-qt2c-qsd5-zbc7-dt9684ac11cs', + name: 'Nuxt.js Framework', + parentId: 'xc2d5du6-sb46-rt36-wc6d-a6zn7t2an653', + parentName: 'NuxtLabs', + platform: 'github', + segmentId: 'w2z5bsct-etq0-sr6z-xw4a-d55c74br6469', + settings: { repositories: ['nuxt/nuxt'] }, + status: 'in-progress', + statusDetails: '950 out of 3,800 data streams processed...', + }, + { + grandparentId: 'c409a475-xu07-st4a-au08-xc234953bb23', + grandparentName: 'Gatsby Community', + id: 'zx7t2tdv-ru3d-rte6-acd8-eu0795bd22dt', + name: 'Gatsby Framework', + parentId: 'yd3e6ev7-tc57-su47-xd7e-b7ao8u3bo764', + parentName: 'Gatsby Inc.', + platform: 'git', + segmentId: 'x3a6ctdu-fuq1-ts7a-yx5b-e66d85cs7570', + settings: { remotes: ['https://github.com/gatsbyjs/gatsby'] }, + status: 'in-progress', + statusDetails: '1,050 out of 4,200 data streams processed...', + }, + { + grandparentId: 'd50bb586-yv18-tu5b-bv19-yd345064cc34', + grandparentName: 'Svelte Community', + id: 'ay8u3uew-sv4e-suf7-bde9-fv1806ce33eu', + name: 'Svelte Framework', + parentId: 'ze4f7fw8-ud68-tv58-ye8f-c8bp9v4cp875', + parentName: 'Svelte Team', + platform: 'github', + segmentId: 'y4b7duev-gvr2-ut8b-zy6c-f77e96dt8681', + settings: { repositories: ['sveltejs/svelte'] }, + status: 'in-progress', + statusDetails: '600 out of 2,400 data streams processed...', + }, + { + grandparentId: 'e601c697-zw29-uv6c-cw20-ze456175dd45', + grandparentName: 'Solid.js Community', + id: 'bz9v4vfx-tw5f-tvg8-cef0-gw2917df44fv', + name: 'Solid.js Framework', + parentId: 'af5g8gx9-ve79-uw69-zf9g-d9cq0w5eq986', + parentName: 'Solid Team', + platform: 'git', + segmentId: 'z5c8evfw-hwt3-vu9c-az7d-g88f07eu9792', + settings: { remotes: ['https://github.com/solidjs/solid'] }, + status: 'in-progress', + statusDetails: '350 out of 1,400 data streams processed...', + }, + { + grandparentId: 'f702d708-ax30-vw7d-dx31-af567286ee56', + grandparentName: 'Qwik Community', + id: 'ca0w5wgy-ux6g-uwh9-dfw1-hx3028eg55gw', + name: 'Qwik Framework', + parentId: 'bg6h9hy0-wf80-vx70-ag0h-e0dr1x6fr087', + parentName: 'Builder.io', + platform: 'github', + segmentId: 'a6d9fwgw-ixt4-wv0d-ba8e-h99g18fv0803', + settings: { repositories: ['BuilderIO/qwik'] }, + status: 'in-progress', + statusDetails: '300 out of 1,200 data streams processed...', + }, + { + grandparentId: 'g803e819-by41-wx8e-ey42-bg678397ff67', + grandparentName: 'Remix Community', + id: 'db1x6xhz-vy7h-vxi0-egx2-iy4139fh66hx', + name: 'Remix Framework', + parentId: 'ch7i0iz1-xg91-wy81-bh1i-f1es2y7gs198', + parentName: 'Remix Software Inc.', + platform: 'git', + segmentId: 'b7e0gxhx-jyu5-xw1e-cb9f-i00h29gw1914', + settings: { remotes: ['https://github.com/remix-run/remix'] }, + status: 'in-progress', + statusDetails: '800 out of 3,200 data streams processed...', + }, + { + grandparentId: 'h904f920-cz52-xy9f-fz53-ch789408gg78', + grandparentName: 'Astro Community', + id: 'ec2y7yia-wz8i-wyj1-fhy3-jz5240gi77iy', + name: 'Astro Framework', + parentId: 'di8j1ja2-yh02-xz92-ci2j-g2ft3z8ht209', + parentName: 'The Astro Technology Company', + platform: 'github', + segmentId: 'c8f1hyhz-kzv6-xy2f-dc0g-j11i30hx2025', + settings: { repositories: ['withastro/astro'] }, + status: 'in-progress', + statusDetails: '1,200 out of 4,800 data streams processed...', + }, + { + grandparentId: 'i005g031-da63-yz0g-ga64-di890519hh89', + grandparentName: 'Vite Community', + id: 'fd3z8zjb-xa9j-xzk2-giz4-ka6351hj88jz', + name: 'Vite Build Tool', + parentId: 'ej9k2kb3-zi13-ya03-dj3k-h3gt4a9ik320', + parentName: 'Vite Team', + platform: 'git', + segmentId: 'd9g2izkz-lav7-yz3g-ed1h-k22j41iy3136', + settings: { remotes: ['https://github.com/vitejs/vite'] }, + status: 'in-progress', + statusDetails: '1,400 out of 5,600 data streams processed...', + }, + { + grandparentId: 'j106h142-eb74-za1h-hb75-ej901620ii90', + grandparentName: 'Webpack Community', + id: 'ge4a9akc-yb0k-yam3-hjk5-lb7462ik99ka', + name: 'Webpack Bundler', + parentId: 'fk0l3lc4-aj24-zb14-ek4l-i4hu5b0jl431', + parentName: 'JS Foundation', + platform: 'github', + segmentId: 'e0h3jakb-mbw8-za4h-fe2i-l33k52jz4247', + settings: { repositories: ['webpack/webpack'] }, + status: 'in-progress', + statusDetails: '750 out of 3,000 data streams processed...', + }, + { + grandparentId: 'k207i253-fc85-ab2i-ic86-fk012731jj01', + grandparentName: 'Rollup Community', + id: 'hf5b0bld-zc1l-zbn4-ikl6-mc8573jl00lb', + name: 'Rollup Bundler', + parentId: 'gl1m4md5-bk35-ac25-fl5m-j5iv6c1km542', + parentName: 'Rollup Team', + platform: 'git', + segmentId: 'f1i4kblc-ncx9-ab5i-gf3j-m44l63ka5358', + settings: { remotes: ['https://github.com/rollup/rollup'] }, + status: 'in-progress', + statusDetails: '500 out of 2,000 data streams processed...', + }, + { + grandparentId: 'l308j364-gd96-bc3j-jd97-gl123842kk12', + grandparentName: 'Parcel Community', + id: 'ig6c1cme-ad2m-acn5-jlm7-nd9684km11mc', + name: 'Parcel Bundler', + parentId: 'hm2n5ne6-cl46-bd36-gm6n-k6jw7d2ln653', + parentName: 'Parcel Team', + platform: 'github', + segmentId: 'g2j5lcmd-ody0-bc6j-hg4k-n55m74lb6469', + settings: { repositories: ['parcel-bundler/parcel'] }, + status: 'in-progress', + statusDetails: '650 out of 2,600 data streams processed...', + }, + { + grandparentId: 'm409k475-he07-cd4k-ke08-hm234953ll23', + grandparentName: 'ESBuild Community', + id: 'jh7d2dnf-be3n-bdo6-kmn8-oe0795ln22mn', + name: 'ESBuild Tool', + parentId: 'in3o6of7-dm57-ce47-hn7o-l7kx8e3mo764', + parentName: 'Evan Wallace', + platform: 'git', + segmentId: 'h3k6mdne-pey1-cd7k-ih5l-o66n85mc7570', + settings: { remotes: ['https://github.com/evanw/esbuild'] }, + status: 'in-progress', + statusDetails: '900 out of 3,600 data streams processed...', + }, + { + grandparentId: 'n50al586-if18-de5l-lf19-in345064mm34', + grandparentName: 'SWC Community', + id: 'ki8e3eog-cf4o-cep7-lno9-pf1806mo33no', + name: 'SWC Compiler', + parentId: 'jo4p7pg8-en68-df58-io8p-m8ly9f4np875', + parentName: 'SWC Team', + platform: 'github', + segmentId: 'i4l7neog-qfz2-de8l-ji6m-p77o96nd8681', + settings: { repositories: ['swc-project/swc'] }, + status: 'in-progress', + statusDetails: '1,100 out of 4,400 data streams processed...', + }, + // Action Required integrations + { + grandparentId: 'h308f364-c9g6-783f-f9d7-ch123842gg12', + grandparentName: 'Academy Software Foundation', + id: 'ec6818i9-692i-68j5-fjh7-j99684fi11ih', + name: 'Community Management', + parentId: 'di2j5ja6-8h46-7936-cj6i-g6fc792gc643', + parentName: 'Academy Software Foundation', + platform: 'slack', + segmentId: 'c2f5h8h9-6970-879f-dc4g-j55i74d87469', + settings: { workspaceId: 'T0123456789', channels: ['general', 'community'] }, + status: 'mapping', + statusDetails: 'API key expired, please update credentials', + }, + { + grandparentId: 'i409g475-da07-894g-ga08-di234953hh23', + grandparentName: 'Academy Software Foundation', + id: 'fd7929ja-7a3j-79k6-gki8-ka0795gj22ji', + name: 'Developer Relations', + parentId: 'ej3k6kb7-9i57-8a47-dk7j-h7gd8a3hd754', + parentName: 'Academy Software Foundation', + platform: 'github', + segmentId: 'd3g6i9ia-7a81-98ag-ed5h-k66j85e98570', + settings: { guildId: '123456789012345678', channels: ['dev-chat', 'announcements'] }, + status: 'mapping', + statusDetails: 'Permission denied, check bot permissions', + }, + // Connection Failed integrations + { + grandparentId: 'j50ah586-eb18-9a5h-hb19-ej345064ii34', + grandparentName: 'Academy Software Foundation', + id: 'ge8a3akb-8b4k-8al7-hkj9-lb1806hk33kj', + name: 'Project Management', + parentId: 'fk4l7lc8-aj68-9b58-el8k-i8he9b4ie865', + parentName: 'Academy Software Foundation', + platform: 'jira', + segmentId: 'e4h7jajb-8b92-a9bh-fe6i-l77k96f9a681', + settings: { baseUrl: 'https://academysoftware.atlassian.net', projectKeys: ['PM', 'ASWF'] }, + status: 'error', + statusDetails: 'Unable to connect to server', + }, +]; diff --git a/frontend/src/modules/admin/modules/overview/store/overview.store.ts b/frontend/src/modules/admin/modules/overview/store/overview.store.ts new file mode 100644 index 0000000000..2a8ba9f839 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/store/overview.store.ts @@ -0,0 +1,25 @@ +import { defineStore } from 'pinia'; +import { ref } from 'vue'; +import { Project, ProjectGroup, SubProject } from '@/modules/lf/segments/types/Segments'; + +export const useOverviewStore = defineStore('overview', () => { + const selectedProjectGroupId = ref(''); + const selectedProjectGroup = ref(null); + const selectedProjectId = ref(''); + const selectedProject = ref(null); + const selectedSubProjectId = ref(''); + const selectedSubProject = ref(null); + const selectedIntegrationId = ref(null); + const integrationStatusCount = ref>({}); + + return { + selectedProjectGroupId, + selectedProjectGroup, + selectedProjectId, + selectedProject, + selectedSubProjectId, + selectedSubProject, + selectedIntegrationId, + integrationStatusCount, + }; +}); diff --git a/frontend/src/modules/admin/modules/overview/types/overview.types.ts b/frontend/src/modules/admin/modules/overview/types/overview.types.ts new file mode 100644 index 0000000000..8ec94f7a10 --- /dev/null +++ b/frontend/src/modules/admin/modules/overview/types/overview.types.ts @@ -0,0 +1,52 @@ +// import { IntegrationConfig } from '@/config/integrations' +// import { IntegrationStatusConfig } from '../../integration/config/status' +export interface OverviewTrends { + current: number + previous: number + period: string +} + +export interface IntegrationTabs { + label: string + key: string + count: number + icon: string +} + +// TODO: Check with backend team about the data structure +export interface IntegrationStatus { + grandparentId: string + grandparentName: string + id: string + name: string + parentId: string + parentName: string + platform: string + segmentId: string + settings: any + status: string + statusDetails: string +} + +export interface IntegrationStatusResponse { + count: number + limit: number + offset: number + rows: IntegrationStatus[] +} + +export interface GlobalIntegrationStatusCount { + status: string + count: number +} + +export interface DashboardMetrics { + activitiesTotal: number + activitiesLast30Days: number + organizationsTotal: number + organizationsLast30Days: number + membersTotal: number + membersLast30Days: number + projectsTotal: number + projectsLast30Days: number +} diff --git a/frontend/src/modules/admin/pages/admin-panel.page.vue b/frontend/src/modules/admin/pages/admin-panel.page.vue index b76f279cb8..53b3042d6a 100644 --- a/frontend/src/modules/admin/pages/admin-panel.page.vue +++ b/frontend/src/modules/admin/pages/admin-panel.page.vue @@ -106,6 +106,7 @@ import LfCollectionsPage from '@/modules/admin/modules/collections/pages/collect import LfInsightsProjectsPage from '@/modules/admin/modules/insights-projects/pages/insights-projects.page.vue'; import config from '@/config'; import LfCategoriesPage from '@/modules/admin/modules/categories/pages/categories.page.vue'; +import { lfIntegrationStatusesTabs } from '@/modules/admin/modules/integration/config/status'; const route = useRoute(); const router = useRouter(); @@ -177,4 +178,12 @@ watch( }, { immediate: true }, ); + +watch(activeTab, (newVal) => { + if (newVal === 'integrations') { + if (window && window.localStorage) { + window.localStorage.setItem('integrationStatusFilter', Object.keys(lfIntegrationStatusesTabs)[0]); + } + } +}); diff --git a/frontend/src/modules/layout/components/menu/menu.vue b/frontend/src/modules/layout/components/menu/menu.vue index ac73f166a3..e1d01b47ae 100644 --- a/frontend/src/modules/layout/components/menu/menu.vue +++ b/frontend/src/modules/layout/components/menu/menu.vue @@ -4,10 +4,10 @@ :class="isCollapsed ? 'w-16 min-w-16' : 'w-65 min-w-65'" >
{ const originalPush = router.push; router.push = function push(location) { - return originalPush - .call(this, location) - .catch((error) => { - console.error(error); - ProgressBar.done(); - }); + return originalPush.call(this, location).catch((error) => { + console.error(error); + ProgressBar.done(); + }); }; router.beforeEach(async (to, from, next) => { @@ -76,14 +71,10 @@ export const createRouter = () => { ProgressBar.start(); } - const matchedRoute = to.matched.find( - (m) => m.meta.middleware, - ); + const matchedRoute = to.matched.find((m) => m.meta.middleware); if (matchedRoute !== undefined) { - const middlewareArray = Array.isArray( - matchedRoute.meta.middleware, - ) + const middlewareArray = Array.isArray(matchedRoute.meta.middleware) ? matchedRoute.meta.middleware : [matchedRoute.meta.middleware]; @@ -100,8 +91,15 @@ export const createRouter = () => { // Redirect to project group landing pages if routes that require a selected project group // And no project group is selected - if (to.meta.segments?.requireSelectedProjectGroup || to.meta.segments?.optionalSelectedProjectGroup) { - if (!selectedProjectGroup.value && !to.query.projectGroup && !to.meta.segments?.optionalSelectedProjectGroup) { + if ( + to.meta.segments?.requireSelectedProjectGroup + || to.meta.segments?.optionalSelectedProjectGroup + ) { + if ( + !selectedProjectGroup.value + && !to.query.projectGroup + && !to.meta.segments?.optionalSelectedProjectGroup + ) { next('/project-groups'); return; } @@ -139,6 +137,4 @@ export const createRouter = () => { return router; }; -export { - router, -}; +export { router }; diff --git a/frontend/src/shared/modules/monitoring/types/event.ts b/frontend/src/shared/modules/monitoring/types/event.ts index b54b34a471..89cbed85e0 100644 --- a/frontend/src/shared/modules/monitoring/types/event.ts +++ b/frontend/src/shared/modules/monitoring/types/event.ts @@ -13,8 +13,8 @@ export enum EventType { } export enum PageEventKey { - PROJECT_GROUPS = 'Project groups', OVERVIEW = 'Overview', + PROJECT_GROUPS = 'Project groups', MEMBERS = 'Contributors', ORGANIZATIONS = 'Organizations', ACTIVITIES = 'Activities', diff --git a/frontend/src/shared/types/tanstack.ts b/frontend/src/shared/types/tanstack.ts index c6683f7fba..fb49d1ab67 100644 --- a/frontend/src/shared/types/tanstack.ts +++ b/frontend/src/shared/types/tanstack.ts @@ -1,10 +1,15 @@ export enum TanstackKey { - ADMIN_COLLECTIONS = 'admin-collections', - ADMIN_INSIGHTS_PROJECTS = 'admin-insights-projects', - ADMIN_PROJECT_GROUPS = 'admin-project-groups', - ADMIN_SUB_PROJECTS = 'admin-sub-projects', - MEMBER_MERGE_SUGGESTIONS_COUNT = 'member-merge-suggestions-count', - MEMBERS_LIST= 'members-list', - ORGANIZATION_MERGE_SUGGESTIONS_COUNT = 'organization-merge-suggestions-count', - ORGANIZATIONS_LIST = 'organizations-list', + ADMIN_COLLECTIONS = 'admin-collections', + ADMIN_INSIGHTS_PROJECTS = 'admin-insights-projects', + ADMIN_PROJECT_GROUPS = 'admin-project-groups', + ADMIN_SUB_PROJECTS = 'admin-sub-projects', + MEMBER_MERGE_SUGGESTIONS_COUNT = 'member-merge-suggestions-count', + MEMBERS_LIST = 'members-list', + ORGANIZATION_MERGE_SUGGESTIONS_COUNT = 'organization-merge-suggestions-count', + ORGANIZATIONS_LIST = 'organizations-list', + GLOBAL_INTEGRATION_STATUS_COUNT = 'global-integration-status-count', + GLOBAL_INTEGRATIONS = 'global-integrations', + PROJECT_BY_ID = 'project-by-id', + DASHBOARD_METRICS = 'dashboard-metrics', + INTEGRATION_PROGRESS_LIST = 'integration-progress-list', } diff --git a/frontend/src/ui-kit/index.scss b/frontend/src/ui-kit/index.scss index ac1b2a8bba..db438da436 100644 --- a/frontend/src/ui-kit/index.scss +++ b/frontend/src/ui-kit/index.scss @@ -22,4 +22,6 @@ @import 'timeline/timeline'; @import 'tooltip/tooltip'; @import 'tag/tag'; - +@import 'lfx/dropdown/dropdown'; +@import 'lfx/popover/popover'; +@import 'lfx/chip/chip'; diff --git a/frontend/src/ui-kit/lfx/chip/chip.scss b/frontend/src/ui-kit/lfx/chip/chip.scss new file mode 100644 index 0000000000..6b357aa1e4 --- /dev/null +++ b/frontend/src/ui-kit/lfx/chip/chip.scss @@ -0,0 +1,33 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +.p-chip { + @apply inline-flex items-center gap-1.5 text-sm rounded-full px-2.5 text-neutral-900 h-7; + + .c-icon { + @apply text-base; + } + + .p-chip-remove-icon { + @apply text-base cursor-pointer text-neutral-300; + } + + &-size-small { + @apply text-xs h-6; + + .c-icon { + @apply text-sm; + } + + .p-chip-remove-icon { + @apply text-sm; + } + } + + &-type-bordered { + @apply bg-white border border-solid border-neutral-200; + } + + &-type-default { + @apply bg-neutral-100 border-0; + } +} diff --git a/frontend/src/ui-kit/lfx/chip/chip.vue b/frontend/src/ui-kit/lfx/chip/chip.vue new file mode 100644 index 0000000000..d5b3d9356b --- /dev/null +++ b/frontend/src/ui-kit/lfx/chip/chip.vue @@ -0,0 +1,50 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/chip/types/chip.types.ts b/frontend/src/ui-kit/lfx/chip/types/chip.types.ts new file mode 100644 index 0000000000..ab2fc6e994 --- /dev/null +++ b/frontend/src/ui-kit/lfx/chip/types/chip.types.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +export const chipSizes = ['small', 'default'] as const; +export const chipTypes = ['bordered', 'default'] as const; +export type ChipSize = (typeof chipSizes)[number]; +export type ChipType = (typeof chipTypes)[number]; diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-group-title.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-group-title.vue new file mode 100644 index 0000000000..cdccb3cb46 --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-group-title.vue @@ -0,0 +1,15 @@ + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-item.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-item.vue new file mode 100644 index 0000000000..b383b6d183 --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-item.vue @@ -0,0 +1,69 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-search.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-search.vue new file mode 100644 index 0000000000..2aa38e2674 --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-search.vue @@ -0,0 +1,62 @@ + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-select.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-select.vue new file mode 100644 index 0000000000..310211047c --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-select.vue @@ -0,0 +1,102 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-selector.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-selector.vue new file mode 100644 index 0000000000..efc117e626 --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-selector.vue @@ -0,0 +1,40 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown-separator.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown-separator.vue new file mode 100644 index 0000000000..92421509ee --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown-separator.vue @@ -0,0 +1,13 @@ + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown.scss b/frontend/src/ui-kit/lfx/dropdown/dropdown.scss new file mode 100644 index 0000000000..6e1e5a30bc --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown.scss @@ -0,0 +1,83 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +.lfx-c-dropdown { + @apply bg-white border border-neutral-200 rounded-lg p-1 flex flex-col gap-1 max-h-96 shadow-lg; + + //@screen sm{ + // @apply ; + //} + + &__item { + @apply bg-white py-2 px-3 flex items-center gap-2 text-sm leading-5 cursor-pointer transition-all rounded-md; + + &:hover { + @apply bg-neutral-50; + } + + .c-icon { + @apply text-neutral-500; + } + + // Danger type + &--danger{ + @apply text-red-600; + + [class^="fa-"], .c-icon{ + @apply text-red-600; + } + } + + &.is-selected { + @apply font-semibold; + + .c-icon { + @apply text-neutral-900; + } + } + } + + &__separator { + @apply w-full border-b border-neutral-100 h-0; + } + + &__group-title { + @apply px-3 pt-1 text-xs font-semibold leading-5 text-neutral-400; + } + + &__selector { + @apply flex items-center gap-2 text-sm leading-5 font-medium transition-all; + @apply px-3 py-2 cursor-pointer transition-all rounded-lg select-none; + + &:hover { + @apply bg-neutral-50; + } + + &--small { + @apply px-2 py-1 text-xs gap-1.5 rounded-md; + } + + &--filled { + @apply bg-white outline outline-1 outline-neutral-200 shadow-xs; + } + } + + &__sub { + @apply relative; + + &-menu { + @apply absolute left-full top-0 z-10 ml-3 overflow-auto; + } + } +} + +.dropdown-popover.c-popover__content { + z-index: 9; +} + +.is-open { + .lfx-c-dropdown { + &__selector { + @apply bg-neutral-50; + } + } +} diff --git a/frontend/src/ui-kit/lfx/dropdown/dropdown.vue b/frontend/src/ui-kit/lfx/dropdown/dropdown.vue new file mode 100644 index 0000000000..d4687a9544 --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/dropdown.vue @@ -0,0 +1,78 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/dropdown/types/dropdown.types.ts b/frontend/src/ui-kit/lfx/dropdown/types/dropdown.types.ts new file mode 100644 index 0000000000..73dab3f91a --- /dev/null +++ b/frontend/src/ui-kit/lfx/dropdown/types/dropdown.types.ts @@ -0,0 +1,41 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +export interface DropdownOption { + label: string + value: string + description?: string +} + +export interface DropdownGroupOptions { + label: string + items: DropdownOption[] +} + +export const dropdownSizes = ['default', 'small'] as const; +export const dropdownTypes = ['filled', 'transparent'] as const; + +export type DropdownSize = (typeof dropdownSizes)[number] +export type DropdownType = (typeof dropdownTypes)[number] + +export const dropdownItemTypes = ['regular', 'danger'] as const; + +export type DropdownItemType = (typeof dropdownItemTypes)[number] + +export interface DropdownProps { + modelValue?: string + options: DropdownOption[] | DropdownGroupOptions[] + dropdownIcon?: string + placeholder?: string + disabled?: boolean + type?: DropdownType + size?: DropdownSize + showFilter?: boolean + showGroupBreaks?: boolean + icon?: string + fullWidth?: boolean + center?: boolean + prefix?: string + dropdownPosition?: 'left' | 'right' + iconOnlyMobile?: boolean + splitLines?: number[] // index of the lines to split the dropdown +} diff --git a/frontend/src/ui-kit/lfx/popover/popover.scss b/frontend/src/ui-kit/lfx/popover/popover.scss new file mode 100644 index 0000000000..5b09d2587e --- /dev/null +++ b/frontend/src/ui-kit/lfx/popover/popover.scss @@ -0,0 +1,30 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +.lfx-c-popover { + &__trigger { + @apply w-fit; + } + + &__content-overlay { + @apply fixed top-0 left-0 w-full h-full z-40; + } + + &__content { + @apply z-50 w-max overflow-visible; + width: auto !important; + + &.is-hidden { + @apply invisible opacity-0 pointer-events-none; + } + + &.is-modal { + background: linear-gradient(to bottom, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.25)); + @apply fixed top-0 left-0 h-full w-full transform translate-x-0 translate-y-0 p-12 pt-16 px-5 #{!important}; + @apply flex flex-wrap justify-center items-start; + + & > div { + @apply w-full max-h-full #{!important}; + } + } + } +} diff --git a/frontend/src/ui-kit/lfx/popover/popover.vue b/frontend/src/ui-kit/lfx/popover/popover.vue new file mode 100644 index 0000000000..8d284a09a1 --- /dev/null +++ b/frontend/src/ui-kit/lfx/popover/popover.vue @@ -0,0 +1,220 @@ + + + + + + diff --git a/frontend/src/ui-kit/lfx/popover/types/PopoverPlacement.ts b/frontend/src/ui-kit/lfx/popover/types/PopoverPlacement.ts new file mode 100644 index 0000000000..d07ed35dd7 --- /dev/null +++ b/frontend/src/ui-kit/lfx/popover/types/PopoverPlacement.ts @@ -0,0 +1,18 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +export const popoverPlacements = [ + 'bottom-start', + 'bottom', + 'bottom-end', + 'top', + 'top-start', + 'top-end', + 'left', + 'left-start', + 'left-end', + 'right', + 'right-start', + 'right-end', +] as const; + +export type PopoverPlacement = (typeof popoverPlacements)[number]; diff --git a/frontend/src/ui-kit/lfx/popover/types/PopoverTrigger.ts b/frontend/src/ui-kit/lfx/popover/types/PopoverTrigger.ts new file mode 100644 index 0000000000..f395b93ecf --- /dev/null +++ b/frontend/src/ui-kit/lfx/popover/types/PopoverTrigger.ts @@ -0,0 +1,5 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +export const popoverTrigger = ['click', 'hover'] as const; + +export type PopoverTrigger = (typeof popoverTrigger)[number]; diff --git a/frontend/src/utils/responsive.ts b/frontend/src/utils/responsive.ts new file mode 100644 index 0000000000..78ecab918b --- /dev/null +++ b/frontend/src/utils/responsive.ts @@ -0,0 +1,29 @@ +// Copyright (c) 2025 The Linux Foundation and each contributor. +// SPDX-License-Identifier: MIT +import { onMounted, onUnmounted, ref } from 'vue'; + +const useResponsive = () => { + const pageWidth = ref(0); + + const updatePageWidth = () => { + pageWidth.value = window.innerWidth; + }; + + const isMobileOrTablet = () => /Mobi|Android|iPhone|iPad|iPod/i.test(navigator?.userAgent); + + onMounted(() => { + updatePageWidth(); + window.addEventListener('resize', updatePageWidth); + }); + + onUnmounted(() => { + window.removeEventListener('resize', updatePageWidth); + }); + + return { + isMobileOrTablet, + pageWidth, + }; +}; + +export default useResponsive; diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 9483bb6125..4787e63d16 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,10 +1,9 @@ /* eslint-disable */ -const { getThemeReplacementsValues } = require('./.tailwind/colorConverter.js'); +const { getThemeReplacementsValues } = require('./.tailwind/colorConverter.js') // tailwind.config.js const plugin = require('tailwindcss/plugin') - -const themeReplacements = getThemeReplacementsValues(); +const themeReplacements = getThemeReplacementsValues() const spacing = { 0.5: '0.125rem', @@ -63,7 +62,7 @@ const spacing = { 120: '30rem', 148: '37rem', 254: '63.5rem', -}; +} /** @type {import('tailwindcss').Config} */ module.exports = { @@ -189,12 +188,12 @@ module.exports = { 3: '3px', }, boxShadow: { - DEFAULT: - '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + DEFAULT: '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', md: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)', lg: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)', xl: '0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)', '2xl': '0 25px 50px -12px rgb(0 0 0 / 0.25)', + xs: '0 1px 2px -1px rgba(0, 0, 0, 0.10), 0 1px 3px 0 rgba(0, 0, 0, 0.10)', }, fontSize: { '4xs': ['0.5rem'], @@ -209,50 +208,17 @@ module.exports = { '5xl': ['4rem'], '8xl': ['8rem'], '10xl': ['10rem'], - h1: [ - 'var(--lf-heading-1-font-size)', - 'var(--lf-heading-1-line-height)', - ], - h2: [ - 'var(--lf-heading-2-font-size)', - 'var(--lf-heading-2-line-height)', - ], - h3: [ - 'var(--lf-heading-3-font-size)', - 'var(--lf-heading-3-line-height)', - ], - h4: [ - 'var(--lf-heading-4-font-size)', - 'var(--lf-heading-4-line-height)', - ], - h5: [ - 'var(--lf-heading-5-font-size)', - 'var(--lf-heading-5-line-height)', - ], - h6: [ - 'var(--lf-heading-6-font-size)', - 'var(--lf-heading-6-line-height)', - ], - xtiny: [ - 'var(--lf-text-xtiny-font-size)', - 'var(--lf-text-xtiny-line-height)', - ], - tiny: [ - 'var(--lf-text-tiny-font-size)', - 'var(--lf-text-tiny-line-height)', - ], - small: [ - 'var(--lf-text-small-font-size)', - 'var(--lf-text-small-line-height)', - ], - medium: [ - 'var(--lf-text-medium-font-size)', - 'var(--lf-text-medium-line-height)', - ], - large: [ - 'var(--lf-text-large-font-size)', - 'var(--lf-text-large-line-height)', - ], + h1: ['var(--lf-heading-1-font-size)', 'var(--lf-heading-1-line-height)'], + h2: ['var(--lf-heading-2-font-size)', 'var(--lf-heading-2-line-height)'], + h3: ['var(--lf-heading-3-font-size)', 'var(--lf-heading-3-line-height)'], + h4: ['var(--lf-heading-4-font-size)', 'var(--lf-heading-4-line-height)'], + h5: ['var(--lf-heading-5-font-size)', 'var(--lf-heading-5-line-height)'], + h6: ['var(--lf-heading-6-font-size)', 'var(--lf-heading-6-line-height)'], + xtiny: ['var(--lf-text-xtiny-font-size)', 'var(--lf-text-xtiny-line-height)'], + tiny: ['var(--lf-text-tiny-font-size)', 'var(--lf-text-tiny-line-height)'], + small: ['var(--lf-text-small-font-size)', 'var(--lf-text-small-line-height)'], + medium: ['var(--lf-text-medium-font-size)', 'var(--lf-text-medium-line-height)'], + large: ['var(--lf-text-large-font-size)', 'var(--lf-text-large-line-height)'], }, letterSpacing: { 1: '0.0625rem', @@ -288,8 +254,7 @@ module.exports = { transitionProperty: { DEFAULT: 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', - colors: - 'color, background-color, border-color, text-decoration-color, fill, stroke', + colors: 'color, background-color, border-color, text-decoration-color, fill, stroke', }, width: { fit: 'fit-content', @@ -315,7 +280,7 @@ module.exports = { '.overflow-y-unset': { 'overflow-y': 'unset', }, - }); + }) }), ], -}; +} diff --git a/services/libs/data-access-layer/src/dashboards/base.ts b/services/libs/data-access-layer/src/dashboards/base.ts new file mode 100644 index 0000000000..dbb724c93b --- /dev/null +++ b/services/libs/data-access-layer/src/dashboards/base.ts @@ -0,0 +1,98 @@ +import { QueryExecutor } from '../queryExecutor' +import { getProjectsCount } from '../segments' + +import { IDashboardMetrics } from './types' + +export async function getMetrics( + qx: QueryExecutor, + segmentId?: string, +): Promise { + try { + const [snapshotData, projectsData] = await Promise.all([ + getSnapshotMetrics(qx, segmentId), + getProjectsCount(qx, segmentId), + ]) + + if (!snapshotData) { + // TODO: remove this mock once Tinybird sinks are available + const mockMetrics = getMockMetrics() + return { + ...mockMetrics, + projectsTotal: projectsData.projectsTotal, + projectsLast30Days: projectsData.projectsLast30Days, + } + } + + return { + ...snapshotData, + projectsTotal: projectsData.projectsTotal, + projectsLast30Days: projectsData.projectsLast30Days, + } + } catch (error: unknown) { + const msg = error instanceof Error ? error.message : '' + const code = error && typeof error === 'object' && 'code' in error ? error.code : null + + // Detect missing table + const isMissingTable = code === '42P01' || /does not exist/i.test(msg) + + if (isMissingTable) { + // TODO: remove this mock once Tinybird sinks are available + const mockMetrics = getMockMetrics() + const projectsData = await getProjectsCount(qx, segmentId) + return { + ...mockMetrics, + projectsTotal: projectsData.projectsTotal, + projectsLast30Days: projectsData.projectsLast30Days, + } + } + + throw error + } +} + +async function getSnapshotMetrics( + qx: QueryExecutor, + segmentId?: string, +): Promise | null> { + const tableName = segmentId + ? 'dashboardMetricsPerSegmentSnapshot' + : 'dashboardMetricsTotalSnapshot' + + const query = segmentId + ? ` + SELECT * + FROM "${tableName}" + WHERE "segmentId" = $(segmentId) + LIMIT 1 + ` + : ` + SELECT + "activitiesLast30Days", + "activitiesTotal", + "membersLast30Days", + "membersTotal", + "organizationsLast30Days", + "organizationsTotal", + "updatedAt" + FROM "${tableName}" + LIMIT 1 + ` + + const params = segmentId ? { segmentId } : {} + const [row] = await qx.select(query, params) + + return row || null +} + +function getMockMetrics(): IDashboardMetrics { + return { + activitiesTotal: 9926553, + activitiesLast30Days: 64329, + organizationsTotal: 104300, + organizationsLast30Days: 36, + membersTotal: 798730, + membersLast30Days: 2694, + projectsTotal: 123, + projectsLast30Days: 12312, + } +} diff --git a/services/libs/data-access-layer/src/dashboards/index.ts b/services/libs/data-access-layer/src/dashboards/index.ts new file mode 100644 index 0000000000..b097b26bf7 --- /dev/null +++ b/services/libs/data-access-layer/src/dashboards/index.ts @@ -0,0 +1,2 @@ +export * from './base' +export * from './types' diff --git a/services/libs/data-access-layer/src/dashboards/types.ts b/services/libs/data-access-layer/src/dashboards/types.ts new file mode 100644 index 0000000000..a8b3eb1066 --- /dev/null +++ b/services/libs/data-access-layer/src/dashboards/types.ts @@ -0,0 +1,10 @@ +export interface IDashboardMetrics { + activitiesTotal: number + activitiesLast30Days: number + organizationsTotal: number + organizationsLast30Days: number + membersTotal: number + membersLast30Days: number + projectsTotal: number + projectsLast30Days: number +} diff --git a/services/libs/data-access-layer/src/index.ts b/services/libs/data-access-layer/src/index.ts index c7963a6a40..05ac370cca 100644 --- a/services/libs/data-access-layer/src/index.ts +++ b/services/libs/data-access-layer/src/index.ts @@ -1,5 +1,6 @@ export * from './activities' export * from './activityRelations' +export * from './dashboards' export * from './members' export * from './organizations' export * from './prompt-history' diff --git a/services/libs/data-access-layer/src/segments/index.ts b/services/libs/data-access-layer/src/segments/index.ts index f0eed84a21..ee2bf592f8 100644 --- a/services/libs/data-access-layer/src/segments/index.ts +++ b/services/libs/data-access-layer/src/segments/index.ts @@ -260,3 +260,38 @@ export async function getGitlabRepoUrlsMappedToOtherSegments( return rows.map((r) => r.url) } + +export async function getProjectsCount( + qx: QueryExecutor, + segmentId?: string, +): Promise<{ projectsTotal: number; projectsLast30Days: number }> { + let query: string + let params: Record + + if (!segmentId) { + // Count all segments not deleted + query = ` + SELECT + COUNT(*) as "projectsTotal", + COUNT(CASE WHEN "createdAt" >= NOW() - INTERVAL '30 days' THEN 1 END) as "projectsLast30Days" + FROM segments + ` + params = {} + } else { + // Count segments where the provided segmentId is current, parent, or grandparent + query = ` + SELECT + COUNT(*) as "projectsTotal", + COUNT(CASE WHEN s."createdAt" >= NOW() - INTERVAL '30 days' THEN 1 END) as "projectsLast30Days" + FROM segments s + WHERE (s.id = $(segmentId) OR s."parentId" = $(segmentId) OR s."grandparentId" = $(segmentId)) + ` + params = { segmentId } + } + + const [result] = await qx.select(query, params) + return { + projectsTotal: parseInt(result.projectsTotal) || 0, + projectsLast30Days: parseInt(result.projectsLast30Days) || 0, + } +}