mirror of
https://github.com/Unleash/unleash.git
synced 2025-01-25 00:07:47 +01:00
feat: Personal flags UI component (#8221)
This commit is contained in:
parent
4e8d9a2319
commit
fee2143edf
@ -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 (
|
||||
<ListItem key={flag.name} disablePadding={true} sx={{ mb: 1 }}>
|
||||
<ListItemButton
|
||||
sx={projectStyle}
|
||||
selected={selected}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ProjectBox>
|
||||
<IconComponent color='primary' />
|
||||
<StyledCardTitle>{flag.name}</StyledCardTitle>
|
||||
<IconButton
|
||||
component={Link}
|
||||
href={`projects/${flag.project}/features/${flag.name}`}
|
||||
size='small'
|
||||
sx={{ ml: 'auto' }}
|
||||
>
|
||||
<LinkIcon
|
||||
titleAccess={`projects/${flag.project}/features/${flag.name}`}
|
||||
/>
|
||||
</IconButton>
|
||||
</ProjectBox>
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
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<string | null>(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 = () => {
|
||||
</SpacedGridItem>
|
||||
<SpacedGridItem item lg={8} md={1} />
|
||||
<SpacedGridItem item lg={4} md={1}>
|
||||
<Typography>
|
||||
You have not created or favorited any feature flags.
|
||||
Once you do, the will show up here.
|
||||
</Typography>
|
||||
{personalDashboard && personalDashboard.flags.length > 0 ? (
|
||||
<List
|
||||
disablePadding={true}
|
||||
sx={{ maxHeight: '400px', overflow: 'auto' }}
|
||||
>
|
||||
{personalDashboard.flags.map((flag) => (
|
||||
<FlagListItem
|
||||
key={flag.name}
|
||||
flag={flag}
|
||||
selected={flag.name === activeFlag}
|
||||
onClick={() => setActiveFlag(flag.name)}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
) : (
|
||||
<Typography>
|
||||
You have not created or favorited any feature flags.
|
||||
Once you do, they will show up here.
|
||||
</Typography>
|
||||
)}
|
||||
</SpacedGridItem>
|
||||
|
||||
<SpacedGridItem item lg={8} md={1}>
|
||||
<Typography sx={{ mb: 4 }}>Feature flag metrics</Typography>
|
||||
<PlaceholderFlagMetricsChart />
|
||||
|
@ -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());
|
||||
};
|
@ -7,4 +7,6 @@
|
||||
export type PersonalDashboardSchemaFlagsItem = {
|
||||
/** The name of the flag */
|
||||
name: string;
|
||||
project: string;
|
||||
type: string;
|
||||
};
|
||||
|
@ -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<PersonalFeature[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -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' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
@ -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<PersonalFeature[]>;
|
||||
}
|
||||
|
@ -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<PersonalFeature[]> {
|
||||
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,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
@ -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<PersonalFeature[]> {
|
||||
return this.personalDashboardReadModel.getPersonalFeatures(userId);
|
||||
}
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -54,6 +54,7 @@ process.nextTick(async () => {
|
||||
addonUsageMetrics: true,
|
||||
onboardingMetrics: true,
|
||||
onboardingUI: true,
|
||||
personalDashboardUI: true,
|
||||
},
|
||||
},
|
||||
authentication: {
|
||||
|
Loading…
Reference in New Issue
Block a user