diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx index d9c684f6de0c..8ede97688e97 100644 --- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx +++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx @@ -22,6 +22,8 @@ import { WelcomeDialog } from './WelcomeDialog'; import { useLocalStorageState } from 'hooks/useLocalStorageState'; import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview'; import { ProjectSetupComplete } from './ProjectSetupComplete'; +import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard'; +import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; const ScreenExplanation = styled(Typography)(({ theme }) => ({ marginTop: theme.spacing(1), @@ -136,6 +138,38 @@ const useProjects = () => { return { projects, activeProject, setActiveProject }; }; +const FlagListItem: FC<{ + flag: { name: string; project: string; type: string }; + selected: boolean; + onClick: () => void; +}> = ({ flag, selected, onClick }) => { + const IconComponent = getFeatureTypeIcons(flag.type); + return ( + + + + + {flag.name} + + + + + + + ); +}; + export const PersonalDashboard = () => { const { user } = useAuthUser(); @@ -143,6 +177,14 @@ export const PersonalDashboard = () => { const { projects, activeProject, setActiveProject } = useProjects(); + const { personalDashboard } = usePersonalDashboard(); + const [activeFlag, setActiveFlag] = useState(null); + useEffect(() => { + if (personalDashboard?.flags.length) { + setActiveFlag(personalDashboard.flags[0].name); + } + }, [JSON.stringify(personalDashboard)]); + const { project: activeProjectOverview, loading } = useProjectOverview(activeProject); @@ -256,11 +298,28 @@ export const PersonalDashboard = () => { - - You have not created or favorited any feature flags. - Once you do, the will show up here. - + {personalDashboard && personalDashboard.flags.length > 0 ? ( + + {personalDashboard.flags.map((flag) => ( + setActiveFlag(flag.name)} + /> + ))} + + ) : ( + + You have not created or favorited any feature flags. + Once you do, they will show up here. + + )} + Feature flag metrics diff --git a/frontend/src/hooks/api/getters/usePersonalDashboard/usePersonalDashboard.ts b/frontend/src/hooks/api/getters/usePersonalDashboard/usePersonalDashboard.ts new file mode 100644 index 000000000000..af855485b533 --- /dev/null +++ b/frontend/src/hooks/api/getters/usePersonalDashboard/usePersonalDashboard.ts @@ -0,0 +1,31 @@ +import useSWR from 'swr'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler'; +import type { PersonalDashboardSchema } from 'openapi'; + +export interface IPersonalDashboardOutput { + personalDashboard?: PersonalDashboardSchema; + refetch: () => void; + loading: boolean; + error?: Error; +} + +export const usePersonalDashboard = (): IPersonalDashboardOutput => { + const { data, error, mutate } = useSWR( + formatApiPath('api/admin/personal-dashboard'), + fetcher, + ); + + return { + personalDashboard: data, + loading: !error && !data, + refetch: () => mutate(), + error, + }; +}; + +const fetcher = (path: string) => { + return fetch(path) + .then(handleErrorResponses('Personal Dashboard')) + .then((res) => res.json()); +}; diff --git a/frontend/src/openapi/models/personalDashboardSchemaFlagsItem.ts b/frontend/src/openapi/models/personalDashboardSchemaFlagsItem.ts index 0f2596dbc9e1..75a31ef0c58c 100644 --- a/frontend/src/openapi/models/personalDashboardSchemaFlagsItem.ts +++ b/frontend/src/openapi/models/personalDashboardSchemaFlagsItem.ts @@ -7,4 +7,6 @@ export type PersonalDashboardSchemaFlagsItem = { /** The name of the flag */ name: string; + project: string; + type: string; }; diff --git a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts index 59c60b7213d1..682988dc4578 100644 --- a/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/fake-personal-dashboard-read-model.ts @@ -1,9 +1,12 @@ -import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; +import type { + IPersonalDashboardReadModel, + PersonalFeature, +} from './personal-dashboard-read-model-type'; export class FakePersonalDashboardReadModel implements IPersonalDashboardReadModel { - async getPersonalFeatures(userId: number): Promise<{ name: string }[]> { + async getPersonalFeatures(userId: number): Promise { return []; } } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts index 260b0bac1e6c..830dc9629f72 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-controller.e2e.test.ts @@ -55,9 +55,9 @@ test('should return personal dashboard with own flags and favorited flags', asyn expect(body).toMatchObject({ projects: [], flags: [ - { name: 'my_feature_c' }, - { name: 'my_feature_d' }, - { name: 'other_feature_b' }, + { name: 'my_feature_d', type: 'release', project: 'default' }, + { name: 'my_feature_c', type: 'release', project: 'default' }, + { name: 'other_feature_b', type: 'release', project: 'default' }, ], }); }); diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts index 234b6878c1c2..f63e43ae080b 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model-type.ts @@ -1,3 +1,5 @@ +export type PersonalFeature = { name: string; type: string; project: string }; + export interface IPersonalDashboardReadModel { - getPersonalFeatures(userId: number): Promise<{ name: string }[]>; + getPersonalFeatures(userId: number): Promise; } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts index 8d6074aa2cc9..317113cd755c 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-read-model.ts @@ -1,5 +1,8 @@ import type { Db } from '../../db/db'; -import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; +import type { + IPersonalDashboardReadModel, + PersonalFeature, +} from './personal-dashboard-read-model-type'; export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { private db: Db; @@ -8,15 +11,34 @@ export class PersonalDashboardReadModel implements IPersonalDashboardReadModel { this.db = db; } - getPersonalFeatures(userId: number): Promise<{ name: string }[]> { - return this.db<{ name: string }>('favorite_features') + async getPersonalFeatures(userId: number): Promise { + const result = await this.db<{ + name: string; + type: string; + project: string; + }>('favorite_features') + .join('features', 'favorite_features.feature', 'features.name') .where('favorite_features.user_id', userId) - .select('feature as name') + .whereNull('features.archived_at') + .select( + 'features.name as name', + 'features.type', + 'features.project', + 'features.created_at', + ) .union(function () { - this.select('name') + this.select('name', 'type', 'project', 'created_at') .from('features') - .where('features.created_by_user_id', userId); + .where('features.created_by_user_id', userId) + .whereNull('features.archived_at'); }) - .orderBy('name', 'asc'); + .orderBy('created_at', 'desc') + .limit(100); + + return result.map((row) => ({ + name: row.name, + type: row.type, + project: row.project, + })); } } diff --git a/src/lib/features/personal-dashboard/personal-dashboard-service.ts b/src/lib/features/personal-dashboard/personal-dashboard-service.ts index e24b34a5a31e..4837d93324b5 100644 --- a/src/lib/features/personal-dashboard/personal-dashboard-service.ts +++ b/src/lib/features/personal-dashboard/personal-dashboard-service.ts @@ -1,4 +1,7 @@ -import type { IPersonalDashboardReadModel } from './personal-dashboard-read-model-type'; +import type { + IPersonalDashboardReadModel, + PersonalFeature, +} from './personal-dashboard-read-model-type'; export class PersonalDashboardService { private personalDashboardReadModel: IPersonalDashboardReadModel; @@ -7,7 +10,7 @@ export class PersonalDashboardService { this.personalDashboardReadModel = personalDashboardReadModel; } - getPersonalFeatures(userId: number): Promise<{ name: string }[]> { + getPersonalFeatures(userId: number): Promise { return this.personalDashboardReadModel.getPersonalFeatures(userId); } } diff --git a/src/lib/openapi/spec/personal-dashboard-schema.ts b/src/lib/openapi/spec/personal-dashboard-schema.ts index 4c902a2455da..ec98f861e4c3 100644 --- a/src/lib/openapi/spec/personal-dashboard-schema.ts +++ b/src/lib/openapi/spec/personal-dashboard-schema.ts @@ -36,6 +36,16 @@ export const personalDashboardSchema = { example: 'my-flag', description: 'The name of the flag', }, + project: { + type: 'string', + example: 'my-project-id', + description: 'The id of the feature project', + }, + type: { + type: 'string', + example: 'release', + description: 'The type of the feature flag', + }, }, }, description: 'A list of flags a user created or favorited', diff --git a/src/server-dev.ts b/src/server-dev.ts index 3517154721a7..f702cdbcc2ef 100644 --- a/src/server-dev.ts +++ b/src/server-dev.ts @@ -54,6 +54,7 @@ process.nextTick(async () => { addonUsageMetrics: true, onboardingMetrics: true, onboardingUI: true, + personalDashboardUI: true, }, }, authentication: {