mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-09 00:18:00 +01:00
chore (1-3076): add remaining "project resources" to status payload (#8660)
This PR adds member, api token, and segment counts to the project status payload. It updates the schemas and adds the necessary stores to get this information. It also adds a new query to the segments store for getting project segments. I'll add tests in a follow-up.
This commit is contained in:
parent
f016a3e34d
commit
8a5771dd50
@ -1,14 +1,6 @@
|
|||||||
import { Typography, styled } from '@mui/material';
|
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 { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import {
|
import type { ReactNode, FC, PropsWithChildren } from 'react';
|
||||||
type ReactNode,
|
|
||||||
useMemo,
|
|
||||||
type FC,
|
|
||||||
type PropsWithChildren,
|
|
||||||
} from 'react';
|
|
||||||
import UsersIcon from '@mui/icons-material/Group';
|
import UsersIcon from '@mui/icons-material/Group';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import ApiKeyIcon from '@mui/icons-material/Key';
|
import ApiKeyIcon from '@mui/icons-material/Key';
|
||||||
@ -97,25 +89,32 @@ const ListItem: FC<
|
|||||||
</ListItemRow>
|
</ListItemRow>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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 = () => {
|
export const ProjectResources = () => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
const { project } = useProjectOverview(projectId);
|
const { resources, loading } = useProjectResources(projectId);
|
||||||
const { tokens } = useProjectApiTokens(projectId);
|
|
||||||
const { segments } = useSegments();
|
|
||||||
const { data: projectStatus, loading: loadingResources } =
|
|
||||||
useProjectStatus(projectId);
|
|
||||||
|
|
||||||
const segmentCount = useMemo(
|
const loadingRef = useLoading(loading, '[data-loading-resources=true]');
|
||||||
() =>
|
|
||||||
segments?.filter((segment) => segment.project === projectId)
|
|
||||||
.length ?? 0,
|
|
||||||
[segments, projectId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadingStatusRef = useLoading(true, '[data-loading-resources=true]');
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper ref={loadingStatusRef}>
|
<Wrapper ref={loadingRef}>
|
||||||
<ProjectResourcesInner>
|
<ProjectResourcesInner>
|
||||||
<Typography variant='h3' sx={{ margin: 0 }}>
|
<Typography variant='h3' sx={{ margin: 0 }}>
|
||||||
Project Resources
|
Project Resources
|
||||||
@ -126,7 +125,7 @@ export const ProjectResources = () => {
|
|||||||
linkText='Add members'
|
linkText='Add members'
|
||||||
icon={<UsersIcon />}
|
icon={<UsersIcon />}
|
||||||
>
|
>
|
||||||
{project.members} project member(s)
|
{resources.members} project member(s)
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -134,7 +133,7 @@ export const ProjectResources = () => {
|
|||||||
linkText='Add new key'
|
linkText='Add new key'
|
||||||
icon={<ApiKeyIcon />}
|
icon={<ApiKeyIcon />}
|
||||||
>
|
>
|
||||||
{tokens.length} API key(s)
|
{resources.apiTokens} API key(s)
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -142,8 +141,8 @@ export const ProjectResources = () => {
|
|||||||
linkText='View connections'
|
linkText='View connections'
|
||||||
icon={<ConnectedIcon />}
|
icon={<ConnectedIcon />}
|
||||||
>
|
>
|
||||||
{projectStatus?.resources?.connectedEnvironments}{' '}
|
{resources.connectedEnvironments} connected
|
||||||
connected environment(s)
|
environment(s)
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -151,7 +150,7 @@ export const ProjectResources = () => {
|
|||||||
linkText='Add segments'
|
linkText='Add segments'
|
||||||
icon={<SegmentsIcon />}
|
icon={<SegmentsIcon />}
|
||||||
>
|
>
|
||||||
{segmentCount} project segment(s)
|
{resources.segments} project segment(s)
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</ResourceList>
|
</ResourceList>
|
||||||
</ProjectResourcesInner>
|
</ProjectResourcesInner>
|
||||||
|
@ -6,7 +6,12 @@ const path = (projectId: string) => `api/admin/projects/${projectId}/status`;
|
|||||||
|
|
||||||
const placeholderData: ProjectStatusSchema = {
|
const placeholderData: ProjectStatusSchema = {
|
||||||
activityCountByDate: [],
|
activityCountByDate: [],
|
||||||
resources: { connectedEnvironments: 0 },
|
resources: {
|
||||||
|
connectedEnvironments: 0,
|
||||||
|
members: 0,
|
||||||
|
apiTokens: 0,
|
||||||
|
segments: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useProjectStatus = (projectId: string) => {
|
export const useProjectStatus = (projectId: string) => {
|
||||||
|
@ -13,8 +13,11 @@ export interface ProjectStatusSchema {
|
|||||||
activityCountByDate: ProjectActivitySchema;
|
activityCountByDate: ProjectActivitySchema;
|
||||||
|
|
||||||
/** Key resources within the project */
|
/** Key resources within the project */
|
||||||
resources: {
|
|
||||||
/** Handwritten placeholder */
|
/** Handwritten placeholder */
|
||||||
|
resources: {
|
||||||
connectedEnvironments: number;
|
connectedEnvironments: number;
|
||||||
|
apiTokens: number;
|
||||||
|
members: number;
|
||||||
|
segments: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,10 @@ import EventStore from '../events/event-store';
|
|||||||
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
import FakeEventStore from '../../../test/fixtures/fake-event-store';
|
||||||
import ProjectStore from '../project/project-store';
|
import ProjectStore from '../project/project-store';
|
||||||
import FakeProjectStore from '../../../test/fixtures/fake-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 = (
|
export const createProjectStatusService = (
|
||||||
db: Db,
|
db: Db,
|
||||||
@ -16,15 +20,37 @@ export const createProjectStatusService = (
|
|||||||
config.getLogger,
|
config.getLogger,
|
||||||
config.flagResolver,
|
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 = () => {
|
export const createFakeProjectStatusService = () => {
|
||||||
const eventStore = new FakeEventStore();
|
const eventStore = new FakeEventStore();
|
||||||
const projectStore = new FakeProjectStore();
|
const projectStore = new FakeProjectStore();
|
||||||
|
const apiTokenStore = new FakeApiTokenStore();
|
||||||
|
const segmentStore = new FakeSegmentStore();
|
||||||
const projectStatusService = new ProjectStatusService({
|
const projectStatusService = new ProjectStatusService({
|
||||||
eventStore,
|
eventStore,
|
||||||
projectStore,
|
projectStore,
|
||||||
|
apiTokenStore,
|
||||||
|
segmentStore,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -1,28 +1,66 @@
|
|||||||
import type { ProjectStatusSchema } from '../../openapi';
|
import type { ProjectStatusSchema } from '../../openapi';
|
||||||
import type { IEventStore, IProjectStore, IUnleashStores } from '../../types';
|
import type {
|
||||||
|
IApiTokenStore,
|
||||||
|
IEventStore,
|
||||||
|
IProjectStore,
|
||||||
|
ISegmentStore,
|
||||||
|
IUnleashStores,
|
||||||
|
} from '../../types';
|
||||||
|
|
||||||
export class ProjectStatusService {
|
export class ProjectStatusService {
|
||||||
private eventStore: IEventStore;
|
private eventStore: IEventStore;
|
||||||
private projectStore: IProjectStore;
|
private projectStore: IProjectStore;
|
||||||
|
private apiTokenStore: IApiTokenStore;
|
||||||
|
private segmentStore: ISegmentStore;
|
||||||
|
|
||||||
constructor({
|
constructor({
|
||||||
eventStore,
|
eventStore,
|
||||||
projectStore,
|
projectStore,
|
||||||
}: Pick<IUnleashStores, 'eventStore' | 'projectStore'>) {
|
apiTokenStore,
|
||||||
|
segmentStore,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashStores,
|
||||||
|
'eventStore' | 'projectStore' | 'apiTokenStore' | 'segmentStore'
|
||||||
|
>) {
|
||||||
this.eventStore = eventStore;
|
this.eventStore = eventStore;
|
||||||
this.projectStore = projectStore;
|
this.projectStore = projectStore;
|
||||||
|
this.apiTokenStore = apiTokenStore;
|
||||||
|
this.segmentStore = segmentStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
async getProjectStatus(projectId: string): Promise<ProjectStatusSchema> {
|
||||||
|
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 {
|
return {
|
||||||
resources: {
|
resources: {
|
||||||
connectedEnvironments:
|
connectedEnvironments,
|
||||||
await this.projectStore.getConnectedEnvironmentCountForProject(
|
members,
|
||||||
projectId,
|
apiTokens,
|
||||||
),
|
segments,
|
||||||
},
|
},
|
||||||
activityCountByDate:
|
activityCountByDate,
|
||||||
await this.eventStore.getProjectRecentEventActivity(projectId),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,4 +25,6 @@ export interface ISegmentStore extends Store<ISegment, number> {
|
|||||||
existsByName(name: string): Promise<boolean>;
|
existsByName(name: string): Promise<boolean>;
|
||||||
|
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
|
|
||||||
|
getProjectSegmentCount(projectId: string): Promise<number>;
|
||||||
}
|
}
|
||||||
|
@ -370,6 +370,15 @@ export default class SegmentStore implements ISegmentStore {
|
|||||||
return Boolean(rows[0]);
|
return Boolean(rows[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getProjectSegmentCount(projectId: string): Promise<number> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT COUNT(*) FROM ${T.segments} WHERE segment_project_id = ?`,
|
||||||
|
[projectId],
|
||||||
|
);
|
||||||
|
|
||||||
|
return Number(result.rows[0].count);
|
||||||
|
}
|
||||||
|
|
||||||
prefixColumns(): string[] {
|
prefixColumns(): string[] {
|
||||||
return COLUMNS.map((c) => `${T.segments}.${c}`);
|
return COLUMNS.map((c) => `${T.segments}.${c}`);
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,12 @@ test('projectStatusSchema', () => {
|
|||||||
{ date: '2022-12-14', count: 2 },
|
{ date: '2022-12-14', count: 2 },
|
||||||
{ date: '2022-12-15', count: 5 },
|
{ date: '2022-12-15', count: 5 },
|
||||||
],
|
],
|
||||||
resources: { connectedEnvironments: 2 },
|
resources: {
|
||||||
|
connectedEnvironments: 2,
|
||||||
|
apiTokens: 2,
|
||||||
|
members: 1,
|
||||||
|
segments: 0,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
|
@ -17,7 +17,12 @@ export const projectStatusSchema = {
|
|||||||
resources: {
|
resources: {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
additionalProperties: false,
|
additionalProperties: false,
|
||||||
required: ['connectedEnvironments'],
|
required: [
|
||||||
|
'connectedEnvironments',
|
||||||
|
'apiTokens',
|
||||||
|
'members',
|
||||||
|
'segments',
|
||||||
|
],
|
||||||
description: 'Key resources within the project',
|
description: 'Key resources within the project',
|
||||||
properties: {
|
properties: {
|
||||||
connectedEnvironments: {
|
connectedEnvironments: {
|
||||||
@ -26,6 +31,24 @@ export const projectStatusSchema = {
|
|||||||
description:
|
description:
|
||||||
'The number of environments that have received SDK traffic in this project.',
|
'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.',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
4
src/test/fixtures/fake-segment-store.ts
vendored
4
src/test/fixtures/fake-segment-store.ts
vendored
@ -62,4 +62,8 @@ export default class FakeSegmentStore implements ISegmentStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
destroy(): void {}
|
destroy(): void {}
|
||||||
|
|
||||||
|
async getProjectSegmentCount(): Promise<number> {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user