2024-09-19 12:37:35 +02:00
|
|
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
|
|
|
import {
|
2024-09-19 17:01:33 +02:00
|
|
|
IconButton,
|
|
|
|
Link,
|
2024-09-19 12:37:35 +02:00
|
|
|
List,
|
|
|
|
ListItem,
|
|
|
|
ListItemButton,
|
2024-09-19 17:01:33 +02:00
|
|
|
styled,
|
|
|
|
Typography,
|
2024-09-19 12:37:35 +02:00
|
|
|
} from '@mui/material';
|
2024-10-08 08:21:23 +02:00
|
|
|
import React, { type FC, useEffect, useRef } from 'react';
|
2024-10-03 16:20:45 +02:00
|
|
|
import LinkIcon from '@mui/icons-material/ArrowForward';
|
2024-09-20 15:53:03 +02:00
|
|
|
import { WelcomeDialog } from './WelcomeDialog';
|
|
|
|
import { useLocalStorageState } from 'hooks/useLocalStorageState';
|
2024-09-24 08:42:49 +02:00
|
|
|
import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard';
|
|
|
|
import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
|
2024-09-26 14:17:47 +02:00
|
|
|
import type {
|
2024-10-08 08:21:23 +02:00
|
|
|
PersonalDashboardSchemaFlagsItem,
|
2024-09-26 14:17:47 +02:00
|
|
|
PersonalDashboardSchemaProjectsItem,
|
|
|
|
} from '../../openapi';
|
2024-09-25 11:11:30 +02:00
|
|
|
import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
|
2024-09-27 14:02:30 +02:00
|
|
|
import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails';
|
2024-09-30 13:32:05 +02:00
|
|
|
import HelpOutline from '@mui/icons-material/HelpOutline';
|
2024-10-01 10:16:20 +02:00
|
|
|
import useLoading from '../../hooks/useLoading';
|
2024-10-01 11:33:03 +02:00
|
|
|
import { MyProjects } from './MyProjects';
|
|
|
|
import {
|
2024-10-03 09:54:27 +02:00
|
|
|
ContentGridContainer,
|
|
|
|
FlagGrid,
|
2024-10-01 11:33:03 +02:00
|
|
|
ListItemBox,
|
|
|
|
listItemStyle,
|
2024-10-03 16:20:45 +02:00
|
|
|
GridItem,
|
2024-10-01 11:33:03 +02:00
|
|
|
SpacedGridItem,
|
|
|
|
} from './Grid';
|
|
|
|
import { ContentGridNoProjects } from './ContentGridNoProjects';
|
2024-09-19 12:37:35 +02:00
|
|
|
|
2024-09-30 13:32:05 +02:00
|
|
|
const ScreenExplanation = styled('div')(({ theme }) => ({
|
|
|
|
marginBottom: theme.spacing(4),
|
|
|
|
display: 'flex',
|
|
|
|
alignItems: 'center',
|
2024-09-19 12:37:35 +02:00
|
|
|
}));
|
|
|
|
|
2024-09-19 15:25:11 +02:00
|
|
|
export const StyledCardTitle = styled('div')<{ lines?: number }>(
|
|
|
|
({ theme, lines = 2 }) => ({
|
|
|
|
fontWeight: theme.typography.fontWeightRegular,
|
|
|
|
fontSize: theme.typography.body1.fontSize,
|
|
|
|
lineClamp: `${lines}`,
|
|
|
|
WebkitLineClamp: lines,
|
|
|
|
lineHeight: '1.2',
|
|
|
|
display: '-webkit-box',
|
|
|
|
boxOrient: 'vertical',
|
|
|
|
textOverflow: 'ellipsis',
|
|
|
|
overflow: 'hidden',
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
WebkitBoxOrient: 'vertical',
|
|
|
|
wordBreak: 'break-word',
|
|
|
|
}),
|
|
|
|
);
|
2024-09-24 08:42:49 +02:00
|
|
|
const FlagListItem: FC<{
|
|
|
|
flag: { name: string; project: string; type: string };
|
|
|
|
selected: boolean;
|
|
|
|
onClick: () => void;
|
|
|
|
}> = ({ flag, selected, onClick }) => {
|
2024-10-08 08:21:23 +02:00
|
|
|
const activeFlagRef = useRef<HTMLLIElement>(null);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
if (activeFlagRef.current) {
|
|
|
|
activeFlagRef.current.scrollIntoView({
|
|
|
|
block: 'nearest',
|
|
|
|
inline: 'start',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}, []);
|
2024-09-24 08:42:49 +02:00
|
|
|
const IconComponent = getFeatureTypeIcons(flag.type);
|
|
|
|
return (
|
2024-10-08 08:21:23 +02:00
|
|
|
<ListItem
|
|
|
|
key={flag.name}
|
|
|
|
disablePadding={true}
|
|
|
|
sx={{ mb: 1 }}
|
|
|
|
ref={selected ? activeFlagRef : null}
|
|
|
|
>
|
2024-09-24 08:42:49 +02:00
|
|
|
<ListItemButton
|
2024-10-01 11:33:03 +02:00
|
|
|
sx={listItemStyle}
|
2024-09-24 08:42:49 +02:00
|
|
|
selected={selected}
|
|
|
|
onClick={onClick}
|
|
|
|
>
|
2024-10-01 11:33:03 +02:00
|
|
|
<ListItemBox>
|
2024-09-24 08:42:49 +02:00
|
|
|
<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>
|
2024-10-01 11:33:03 +02:00
|
|
|
</ListItemBox>
|
2024-09-24 08:42:49 +02:00
|
|
|
</ListItemButton>
|
|
|
|
</ListItem>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
2024-10-08 08:21:23 +02:00
|
|
|
const useDashboardState = (
|
|
|
|
projects: PersonalDashboardSchemaProjectsItem[],
|
|
|
|
flags: PersonalDashboardSchemaFlagsItem[],
|
|
|
|
) => {
|
|
|
|
type State = {
|
|
|
|
activeProject: string | undefined;
|
|
|
|
activeFlag: PersonalDashboardSchemaFlagsItem | undefined;
|
|
|
|
};
|
|
|
|
|
|
|
|
const defaultState = {
|
|
|
|
activeProject: undefined,
|
|
|
|
activeFlag: undefined,
|
|
|
|
};
|
|
|
|
|
|
|
|
const [state, setState] = useLocalStorageState<State>(
|
|
|
|
'personal-dashboard:v1',
|
|
|
|
defaultState,
|
|
|
|
);
|
2024-10-01 11:33:03 +02:00
|
|
|
|
|
|
|
useEffect(() => {
|
2024-10-08 08:21:23 +02:00
|
|
|
const setDefaultFlag =
|
|
|
|
flags.length &&
|
|
|
|
(!state.activeFlag ||
|
|
|
|
!flags.some((flag) => flag.name === state.activeFlag?.name));
|
|
|
|
const setDefaultProject =
|
|
|
|
projects.length &&
|
|
|
|
(!state.activeProject ||
|
|
|
|
!projects.some(
|
|
|
|
(project) => project.id === state.activeProject,
|
|
|
|
));
|
|
|
|
|
|
|
|
if (setDefaultFlag || setDefaultProject) {
|
|
|
|
setState({
|
|
|
|
activeFlag: setDefaultFlag ? flags[0] : state.activeFlag,
|
|
|
|
activeProject: setDefaultProject
|
|
|
|
? projects[0].id
|
|
|
|
: state.activeProject,
|
|
|
|
});
|
2024-10-01 11:33:03 +02:00
|
|
|
}
|
2024-10-08 08:21:23 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
const { activeFlag, activeProject } = state;
|
|
|
|
|
|
|
|
const setActiveFlag = (flag: PersonalDashboardSchemaFlagsItem) => {
|
|
|
|
setState({
|
|
|
|
...state,
|
|
|
|
activeFlag: flag,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
const setActiveProject = (projectId: string) => {
|
|
|
|
setState({
|
|
|
|
...state,
|
|
|
|
activeProject: projectId,
|
|
|
|
});
|
|
|
|
};
|
|
|
|
|
|
|
|
return {
|
|
|
|
activeFlag,
|
|
|
|
setActiveFlag,
|
|
|
|
activeProject,
|
|
|
|
setActiveProject,
|
|
|
|
};
|
2024-10-01 11:33:03 +02:00
|
|
|
};
|
2024-09-19 17:01:33 +02:00
|
|
|
export const PersonalDashboard = () => {
|
|
|
|
const { user } = useAuthUser();
|
|
|
|
|
|
|
|
const name = user?.name;
|
|
|
|
|
2024-10-01 10:16:20 +02:00
|
|
|
const {
|
|
|
|
personalDashboard,
|
|
|
|
refetch: refetchDashboard,
|
|
|
|
loading: personalDashboardLoading,
|
|
|
|
} = usePersonalDashboard();
|
2024-10-08 08:21:23 +02:00
|
|
|
|
|
|
|
const projects = personalDashboard?.projects || [];
|
|
|
|
|
|
|
|
const { activeProject, setActiveProject, activeFlag, setActiveFlag } =
|
|
|
|
useDashboardState(projects, personalDashboard?.flags ?? []);
|
2024-09-26 14:17:47 +02:00
|
|
|
|
2024-10-01 11:33:03 +02:00
|
|
|
const [welcomeDialog, setWelcomeDialog] = useLocalStorageState<
|
|
|
|
'open' | 'closed'
|
|
|
|
>('welcome-dialog:v1', 'open');
|
|
|
|
|
2024-10-08 08:46:14 +02:00
|
|
|
const {
|
|
|
|
personalDashboardProjectDetails,
|
|
|
|
loading: loadingDetails,
|
|
|
|
error: detailsError,
|
|
|
|
} = usePersonalDashboardProjectDetails(activeProject);
|
2024-09-23 14:23:22 +02:00
|
|
|
|
2024-10-01 11:33:03 +02:00
|
|
|
const activeProjectStage =
|
2024-09-30 14:25:56 +02:00
|
|
|
personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading';
|
2024-10-01 11:33:03 +02:00
|
|
|
const setupIncomplete =
|
|
|
|
activeProjectStage === 'onboarding-started' ||
|
|
|
|
activeProjectStage === 'first-flag-created';
|
2024-09-20 15:53:03 +02:00
|
|
|
|
2024-09-27 10:41:25 +02:00
|
|
|
const noProjects = projects.length === 0;
|
|
|
|
|
2024-10-08 08:46:14 +02:00
|
|
|
const projectStageRef = useLoading(
|
|
|
|
!detailsError && activeProjectStage === 'loading',
|
|
|
|
);
|
2024-10-01 10:16:20 +02:00
|
|
|
|
2024-09-19 12:37:35 +02:00
|
|
|
return (
|
2024-10-08 08:46:14 +02:00
|
|
|
<div>
|
2024-09-19 12:37:35 +02:00
|
|
|
<Typography component='h2' variant='h2'>
|
|
|
|
Welcome {name}
|
|
|
|
</Typography>
|
|
|
|
<ScreenExplanation>
|
2024-10-01 10:16:20 +02:00
|
|
|
<p data-loading>
|
2024-10-01 11:33:03 +02:00
|
|
|
{activeProjectStage === 'onboarded'
|
2024-09-30 15:30:18 +02:00
|
|
|
? 'We have gathered projects and flags you have favorited or owned'
|
|
|
|
: null}
|
|
|
|
{setupIncomplete
|
|
|
|
? 'Here are some tasks we think would be useful in order to get the most out of Unleash'
|
|
|
|
: null}
|
2024-10-01 11:33:03 +02:00
|
|
|
{activeProjectStage === 'loading'
|
2024-10-01 10:16:20 +02:00
|
|
|
? 'We have gathered projects and flags you have favorited or owned'
|
|
|
|
: null}
|
2024-09-30 13:32:05 +02:00
|
|
|
</p>
|
2024-10-01 10:16:20 +02:00
|
|
|
<IconButton
|
|
|
|
data-loading
|
|
|
|
size={'small'}
|
|
|
|
title='Key concepts'
|
|
|
|
onClick={() => setWelcomeDialog('open')}
|
|
|
|
>
|
|
|
|
<HelpOutline />
|
|
|
|
</IconButton>
|
2024-09-19 12:37:35 +02:00
|
|
|
</ScreenExplanation>
|
2024-09-30 13:32:05 +02:00
|
|
|
|
2024-09-30 15:40:33 +02:00
|
|
|
{noProjects && personalDashboard ? (
|
2024-09-27 10:41:25 +02:00
|
|
|
<ContentGridNoProjects
|
2024-09-30 15:40:33 +02:00
|
|
|
owners={personalDashboard.projectOwners}
|
|
|
|
admins={personalDashboard.admins}
|
2024-09-27 10:41:25 +02:00
|
|
|
/>
|
|
|
|
) : (
|
2024-10-01 11:33:03 +02:00
|
|
|
<MyProjects
|
2024-10-08 08:46:14 +02:00
|
|
|
admins={personalDashboard?.admins ?? []}
|
|
|
|
ref={projectStageRef}
|
2024-10-01 11:33:03 +02:00
|
|
|
projects={projects}
|
2024-10-08 08:21:23 +02:00
|
|
|
activeProject={activeProject || ''}
|
2024-10-01 11:33:03 +02:00
|
|
|
setActiveProject={setActiveProject}
|
|
|
|
personalDashboardProjectDetails={
|
|
|
|
personalDashboardProjectDetails
|
|
|
|
}
|
|
|
|
/>
|
2024-09-27 10:41:25 +02:00
|
|
|
)}
|
2024-10-01 11:33:03 +02:00
|
|
|
|
2024-10-03 09:54:27 +02:00
|
|
|
<ContentGridContainer>
|
|
|
|
<FlagGrid sx={{ mt: 2 }}>
|
2024-10-03 16:20:45 +02:00
|
|
|
<GridItem
|
|
|
|
gridArea='title'
|
|
|
|
sx={{ display: 'flex', alignItems: 'center' }}
|
|
|
|
>
|
2024-10-03 09:54:27 +02:00
|
|
|
<Typography variant='h3'>My feature flags</Typography>
|
2024-10-03 16:20:45 +02:00
|
|
|
</GridItem>
|
|
|
|
<GridItem
|
2024-10-03 09:54:27 +02:00
|
|
|
gridArea='lifecycle'
|
|
|
|
sx={{ display: 'flex', justifyContent: 'flex-end' }}
|
|
|
|
>
|
|
|
|
{activeFlag ? (
|
|
|
|
<FlagExposure
|
|
|
|
project={activeFlag.project}
|
|
|
|
flagName={activeFlag.name}
|
|
|
|
onArchive={refetchDashboard}
|
|
|
|
/>
|
|
|
|
) : null}
|
2024-10-03 16:20:45 +02:00
|
|
|
</GridItem>
|
2024-10-03 09:54:27 +02:00
|
|
|
<SpacedGridItem gridArea='flags'>
|
|
|
|
{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?.name
|
|
|
|
}
|
|
|
|
onClick={() => setActiveFlag(flag)}
|
|
|
|
/>
|
|
|
|
))}
|
|
|
|
</List>
|
|
|
|
) : (
|
|
|
|
<Typography>
|
|
|
|
You have not created or favorited any feature
|
|
|
|
flags. Once you do, they will show up here.
|
|
|
|
</Typography>
|
|
|
|
)}
|
|
|
|
</SpacedGridItem>
|
|
|
|
|
|
|
|
<SpacedGridItem gridArea='chart'>
|
|
|
|
{activeFlag ? (
|
|
|
|
<FlagMetricsChart flag={activeFlag} />
|
|
|
|
) : (
|
|
|
|
<PlaceholderFlagMetricsChart />
|
|
|
|
)}
|
|
|
|
</SpacedGridItem>
|
|
|
|
</FlagGrid>
|
|
|
|
</ContentGridContainer>
|
2024-09-20 15:53:03 +02:00
|
|
|
<WelcomeDialog
|
2024-09-30 13:32:05 +02:00
|
|
|
open={welcomeDialog === 'open'}
|
|
|
|
onClose={() => setWelcomeDialog('closed')}
|
2024-09-20 15:53:03 +02:00
|
|
|
/>
|
2024-09-19 12:37:35 +02:00
|
|
|
</div>
|
|
|
|
);
|
2024-09-19 09:59:07 +02:00
|
|
|
};
|
2024-09-24 13:47:21 +02:00
|
|
|
|
|
|
|
const FlagMetricsChart = React.lazy(() =>
|
|
|
|
import('./FlagMetricsChart').then((module) => ({
|
|
|
|
default: module.FlagMetricsChart,
|
|
|
|
})),
|
|
|
|
);
|
|
|
|
const PlaceholderFlagMetricsChart = React.lazy(() =>
|
|
|
|
import('./FlagMetricsChart').then((module) => ({
|
|
|
|
default: module.PlaceholderFlagMetricsChart,
|
|
|
|
})),
|
|
|
|
);
|