diff --git a/frontend/src/component/project/Project/ProjectStatus/ProjectResources.tsx b/frontend/src/component/project/Project/ProjectStatus/ProjectResources.tsx index 27f6beb6d2..78df2231fe 100644 --- a/frontend/src/component/project/Project/ProjectStatus/ProjectResources.tsx +++ b/frontend/src/component/project/Project/ProjectStatus/ProjectResources.tsx @@ -1,14 +1,6 @@ import { Typography, styled } from '@mui/material'; -import { useProjectApiTokens } from 'hooks/api/getters/useProjectApiTokens/useProjectApiTokens'; -import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; -import { useSegments } from 'hooks/api/getters/useSegments/useSegments'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; -import { - type ReactNode, - useMemo, - type FC, - type PropsWithChildren, -} from 'react'; +import type { ReactNode, FC, PropsWithChildren } from 'react'; import UsersIcon from '@mui/icons-material/Group'; import { Link } from 'react-router-dom'; import ApiKeyIcon from '@mui/icons-material/Key'; @@ -97,25 +89,32 @@ const ListItem: FC< ); +const useProjectResources = (projectId: string) => { + const { data, loading } = useProjectStatus(projectId); + + const { resources } = data ?? { + resources: { + members: 0, + apiTokens: 0, + connectedEnvironments: 0, + segments: 0, + }, + }; + + return { + resources, + loading, + }; +}; + export const ProjectResources = () => { const projectId = useRequiredPathParam('projectId'); - const { project } = useProjectOverview(projectId); - const { tokens } = useProjectApiTokens(projectId); - const { segments } = useSegments(); - const { data: projectStatus, loading: loadingResources } = - useProjectStatus(projectId); + const { resources, loading } = useProjectResources(projectId); - const segmentCount = useMemo( - () => - segments?.filter((segment) => segment.project === projectId) - .length ?? 0, - [segments, projectId], - ); - - const loadingStatusRef = useLoading(true, '[data-loading-resources=true]'); + const loadingRef = useLoading(loading, '[data-loading-resources=true]'); return ( - + Project Resources @@ -126,7 +125,7 @@ export const ProjectResources = () => { linkText='Add members' icon={} > - {project.members} project member(s) + {resources.members} project member(s) { linkText='Add new key' icon={} > - {tokens.length} API key(s) + {resources.apiTokens} API key(s) { linkText='View connections' icon={} > - {projectStatus?.resources?.connectedEnvironments}{' '} - connected environment(s) + {resources.connectedEnvironments} connected + environment(s) { linkText='Add segments' icon={} > - {segmentCount} project segment(s) + {resources.segments} project segment(s) diff --git a/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts b/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts index 0781863f9b..11755d59ad 100644 --- a/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts +++ b/frontend/src/hooks/api/getters/useProjectStatus/useProjectStatus.ts @@ -6,7 +6,12 @@ const path = (projectId: string) => `api/admin/projects/${projectId}/status`; const placeholderData: ProjectStatusSchema = { activityCountByDate: [], - resources: { connectedEnvironments: 0 }, + resources: { + connectedEnvironments: 0, + members: 0, + apiTokens: 0, + segments: 0, + }, }; export const useProjectStatus = (projectId: string) => { diff --git a/frontend/src/openapi/models/projectStatusSchema.ts b/frontend/src/openapi/models/projectStatusSchema.ts index d10d690f63..6bb8f730fb 100644 --- a/frontend/src/openapi/models/projectStatusSchema.ts +++ b/frontend/src/openapi/models/projectStatusSchema.ts @@ -13,8 +13,11 @@ export interface ProjectStatusSchema { activityCountByDate: ProjectActivitySchema; /** Key resources within the project */ + /** Handwritten placeholder */ resources: { - /** Handwritten placeholder */ connectedEnvironments: number; + apiTokens: number; + members: number; + segments: number; }; } diff --git a/src/lib/features/project-status/createProjectStatusService.ts b/src/lib/features/project-status/createProjectStatusService.ts index 6db4fa1a45..8ad9cda800 100644 --- a/src/lib/features/project-status/createProjectStatusService.ts +++ b/src/lib/features/project-status/createProjectStatusService.ts @@ -4,6 +4,10 @@ import EventStore from '../events/event-store'; import FakeEventStore from '../../../test/fixtures/fake-event-store'; import ProjectStore from '../project/project-store'; import FakeProjectStore from '../../../test/fixtures/fake-project-store'; +import FakeApiTokenStore from '../../../test/fixtures/fake-api-token-store'; +import { ApiTokenStore } from '../../db/api-token-store'; +import SegmentStore from '../segment/segment-store'; +import FakeSegmentStore from '../../../test/fixtures/fake-segment-store'; export const createProjectStatusService = ( db: Db, @@ -16,15 +20,37 @@ export const createProjectStatusService = ( config.getLogger, config.flagResolver, ); - return new ProjectStatusService({ eventStore, projectStore }); + const apiTokenStore = new ApiTokenStore( + db, + config.eventBus, + config.getLogger, + config.flagResolver, + ); + const segmentStore = new SegmentStore( + db, + config.eventBus, + config.getLogger, + config.flagResolver, + ); + + return new ProjectStatusService({ + eventStore, + projectStore, + apiTokenStore, + segmentStore, + }); }; export const createFakeProjectStatusService = () => { const eventStore = new FakeEventStore(); const projectStore = new FakeProjectStore(); + const apiTokenStore = new FakeApiTokenStore(); + const segmentStore = new FakeSegmentStore(); const projectStatusService = new ProjectStatusService({ eventStore, projectStore, + apiTokenStore, + segmentStore, }); return { diff --git a/src/lib/features/project-status/project-status-service.ts b/src/lib/features/project-status/project-status-service.ts index 4603c6905f..ae45732625 100644 --- a/src/lib/features/project-status/project-status-service.ts +++ b/src/lib/features/project-status/project-status-service.ts @@ -1,28 +1,66 @@ import type { ProjectStatusSchema } from '../../openapi'; -import type { IEventStore, IProjectStore, IUnleashStores } from '../../types'; +import type { + IApiTokenStore, + IEventStore, + IProjectStore, + ISegmentStore, + IUnleashStores, +} from '../../types'; export class ProjectStatusService { private eventStore: IEventStore; private projectStore: IProjectStore; + private apiTokenStore: IApiTokenStore; + private segmentStore: ISegmentStore; constructor({ eventStore, projectStore, - }: Pick) { + apiTokenStore, + segmentStore, + }: Pick< + IUnleashStores, + 'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore' + >) { this.eventStore = eventStore; this.projectStore = projectStore; + this.apiTokenStore = apiTokenStore; + this.segmentStore = segmentStore; } async getProjectStatus(projectId: string): Promise { + const [ + connectedEnvironments, + members, + apiTokens, + segments, + activityCountByDate, + ] = await Promise.all([ + this.projectStore.getConnectedEnvironmentCountForProject(projectId), + this.projectStore.getMembersCountByProject(projectId), + this.apiTokenStore + .getAll() + .then( + (tokens) => + tokens.filter( + (token) => + token.project === projectId || + token.projects.includes(projectId), + ).length, + ), + + this.segmentStore.getProjectSegmentCount(projectId), + this.eventStore.getProjectRecentEventActivity(projectId), + ]); + return { resources: { - connectedEnvironments: - await this.projectStore.getConnectedEnvironmentCountForProject( - projectId, - ), + connectedEnvironments, + members, + apiTokens, + segments, }, - activityCountByDate: - await this.eventStore.getProjectRecentEventActivity(projectId), + activityCountByDate, }; } } diff --git a/src/lib/features/segment/segment-store-type.ts b/src/lib/features/segment/segment-store-type.ts index eb6f417a59..b90bfc6e4a 100644 --- a/src/lib/features/segment/segment-store-type.ts +++ b/src/lib/features/segment/segment-store-type.ts @@ -25,4 +25,6 @@ export interface ISegmentStore extends Store { existsByName(name: string): Promise; count(): Promise; + + getProjectSegmentCount(projectId: string): Promise; } diff --git a/src/lib/features/segment/segment-store.ts b/src/lib/features/segment/segment-store.ts index 890ce7cc0b..9f1a751c22 100644 --- a/src/lib/features/segment/segment-store.ts +++ b/src/lib/features/segment/segment-store.ts @@ -370,6 +370,15 @@ export default class SegmentStore implements ISegmentStore { return Boolean(rows[0]); } + async getProjectSegmentCount(projectId: string): Promise { + const result = await this.db.raw( + `SELECT COUNT(*) FROM ${T.segments} WHERE segment_project_id = ?`, + [projectId], + ); + + return Number(result.rows[0].count); + } + prefixColumns(): string[] { return COLUMNS.map((c) => `${T.segments}.${c}`); } diff --git a/src/lib/openapi/spec/project-status-schema.test.ts b/src/lib/openapi/spec/project-status-schema.test.ts index 23a3ad34ed..cdf8db4514 100644 --- a/src/lib/openapi/spec/project-status-schema.test.ts +++ b/src/lib/openapi/spec/project-status-schema.test.ts @@ -7,7 +7,12 @@ test('projectStatusSchema', () => { { date: '2022-12-14', count: 2 }, { date: '2022-12-15', count: 5 }, ], - resources: { connectedEnvironments: 2 }, + resources: { + connectedEnvironments: 2, + apiTokens: 2, + members: 1, + segments: 0, + }, }; expect( diff --git a/src/lib/openapi/spec/project-status-schema.ts b/src/lib/openapi/spec/project-status-schema.ts index 5a14a64324..9a4e03a40f 100644 --- a/src/lib/openapi/spec/project-status-schema.ts +++ b/src/lib/openapi/spec/project-status-schema.ts @@ -17,7 +17,12 @@ export const projectStatusSchema = { resources: { type: 'object', additionalProperties: false, - required: ['connectedEnvironments'], + required: [ + 'connectedEnvironments', + 'apiTokens', + 'members', + 'segments', + ], description: 'Key resources within the project', properties: { connectedEnvironments: { @@ -26,6 +31,24 @@ export const projectStatusSchema = { description: 'The number of environments that have received SDK traffic in this project.', }, + apiTokens: { + type: 'number', + minimum: 0, + description: + 'The number of API tokens created specifically for this project.', + }, + members: { + type: 'number', + minimum: 0, + description: + 'The number of users who have been granted roles in this project. Does not include users who have access via groups.', + }, + segments: { + type: 'number', + minimum: 0, + description: + 'The number of segments that are scoped to this project.', + }, }, }, }, diff --git a/src/test/fixtures/fake-segment-store.ts b/src/test/fixtures/fake-segment-store.ts index 96241181a3..53f8a39379 100644 --- a/src/test/fixtures/fake-segment-store.ts +++ b/src/test/fixtures/fake-segment-store.ts @@ -62,4 +62,8 @@ export default class FakeSegmentStore implements ISegmentStore { } destroy(): void {} + + async getProjectSegmentCount(): Promise { + return 0; + } }