diff --git a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx
index 5ebdafc6d5..2510467f70 100644
--- a/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureEnvironmentSeen/FeatureEnvironmentSeen.tsx
@@ -74,6 +74,11 @@ const TooltipContainer: FC<{
);
};
+const LineBox = styled(Box)({
+ display: 'grid',
+ placeItems: 'center',
+});
+
export const FeatureEnvironmentSeen = ({
featureLastSeen,
environments,
@@ -91,9 +96,9 @@ export const FeatureEnvironmentSeen = ({
tooltip='No usage reported from connected applications'
>
-
+
-
+
);
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx
index e02f419221..5b53229e52 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure.tsx
@@ -12,7 +12,8 @@ export const FlagExposure: FC<{
project: string;
flagName: string;
onArchive: () => void;
-}> = ({ project, flagName, onArchive }) => {
+ className?: string;
+}> = ({ project, flagName, onArchive, className }) => {
const { feature, refetchFeature } = useFeature(project, flagName);
const lastSeenEnvironments: ILastSeenEnvironments[] =
feature.environments?.map((env) => ({
@@ -27,7 +28,7 @@ export const FlagExposure: FC<{
useState(false);
return (
-
+
= ({ owners, admins }) => {
return (
-
- My projects
-
-
- Potential next steps
-
diff --git a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
index 0bf4d30ec0..3e2b98eac9 100644
--- a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
+++ b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
@@ -23,6 +23,7 @@ import {
createPlaceholderBarChartOptions,
} from './createChartOptions';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
+import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
const defaultYes = [0, 14, 28, 21, 33, 31, 31, 22, 26, 37, 31, 14, 21, 14, 0];
@@ -140,7 +141,7 @@ const EnvironmentSelect: FC<{
);
};
-const MetricsSelectors = styled(Box)(({ theme }) => ({
+const ExposureAndSelectors = styled(Box)(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(2),
@@ -154,9 +155,14 @@ const ChartContainer = styled('div')(({ theme }) => ({
alignItems: 'center',
}));
+const StyledExposure = styled(FlagExposure)({
+ alignItems: 'center',
+});
+
export const FlagMetricsChart: FC<{
flag: { name: string; project: string };
-}> = ({ flag }) => {
+ onArchive: () => void;
+}> = ({ flag, onArchive }) => {
const [hoursBack, setHoursBack] = useState(48);
const { environment, setEnvironment, activeEnvironments } =
@@ -168,7 +174,12 @@ export const FlagMetricsChart: FC<{
return (
-
+
+
{environment ? (
-
+
{noData ? (
diff --git a/frontend/src/component/personalDashboard/Grid.tsx b/frontend/src/component/personalDashboard/Grid.tsx
index 2038a36d91..bce36d2e05 100644
--- a/frontend/src/component/personalDashboard/Grid.tsx
+++ b/frontend/src/component/personalDashboard/Grid.tsx
@@ -10,7 +10,6 @@ const ContentGrid = styled('article')(({ theme }) => {
backgroundColor: theme.palette.divider,
borderRadius: `${theme.shape.borderRadiusLarge}px`,
overflow: 'hidden',
- border: `0.5px solid ${theme.palette.divider}`,
gap: `1px`,
display: 'flex',
flexFlow: 'column nowrap',
@@ -41,7 +40,6 @@ export const ProjectGrid = styled(ContentGrid)(
gridTemplateColumns: '1fr 1fr 1fr',
display: 'grid',
gridTemplateAreas: `
- "header header header"
"projects box1 box2"
". owners owners"
`,
@@ -53,7 +51,6 @@ export const FlagGrid = styled(ContentGrid)(
gridTemplateColumns: '1fr 1fr 1fr',
display: 'grid',
gridTemplateAreas: `
- "title lifecycle lifecycle"
"flags chart chart"
`,
}),
diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx
index 32ac8cb7cb..451fa1b147 100644
--- a/frontend/src/component/personalDashboard/MyProjects.tsx
+++ b/frontend/src/component/personalDashboard/MyProjects.tsx
@@ -189,9 +189,6 @@ export const MyProjects = forwardRef<
return (
-
- My projects
-
{
});
};
-// @ts-ignore
+// @ts-expect-error The return type here isn't correct, but it's not
+// an issue for the tests. We just need to override it because it's
+// not implemented in jsdom.
HTMLCanvasElement.prototype.getContext = () => {};
//scrollIntoView is not implemented in jsdom
@@ -134,7 +136,7 @@ test('Render personal dashboard for a long running project', async () => {
await screen.findByText('projectName');
await screen.findByText('10'); // members
await screen.findByText('100'); // features
- await screen.findByText('80%'); // health
+ await screen.findAllByText('80%'); // health
await screen.findByText('Project health');
await screen.findByText('70%'); // avg health past window
diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx
index d0f4747e14..d5ba350b8d 100644
--- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx
+++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx
@@ -1,5 +1,8 @@
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import {
+ Accordion,
+ AccordionDetails,
+ AccordionSummary,
Button,
IconButton,
Link,
@@ -19,7 +22,6 @@ import type {
PersonalDashboardSchemaFlagsItem,
PersonalDashboardSchemaProjectsItem,
} from '../../openapi';
-import { FlagExposure } from 'component/feature/FeatureView/FeatureOverview/FeatureLifecycle/FlagExposure';
import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails';
import useLoading from '../../hooks/useLoading';
import { MyProjects } from './MyProjects';
@@ -28,16 +30,10 @@ import {
FlagGrid,
ListItemBox,
listItemStyle,
- GridItem,
SpacedGridItem,
} from './Grid';
import { ContentGridNoProjects } from './ContentGridNoProjects';
-
-const ScreenExplanation = styled('div')(({ theme }) => ({
- marginBottom: theme.spacing(4),
- display: 'flex',
- alignItems: 'center',
-}));
+import ExpandMore from '@mui/icons-material/ExpandMore';
export const StyledCardTitle = styled('div')<{ lines?: number }>(
({ theme, lines = 2 }) => ({
@@ -102,6 +98,7 @@ const FlagListItem: FC<{
);
};
+// todo: move into own file
const useDashboardState = (
projects: PersonalDashboardSchemaProjectsItem[],
flags: PersonalDashboardSchemaFlagsItem[],
@@ -109,11 +106,15 @@ const useDashboardState = (
type State = {
activeProject: string | undefined;
activeFlag: PersonalDashboardSchemaFlagsItem | undefined;
+ expandProjects: boolean;
+ expandFlags: boolean;
};
- const defaultState = {
+ const defaultState: State = {
activeProject: undefined,
activeFlag: undefined,
+ expandProjects: true,
+ expandFlags: true,
};
const [state, setState] = useLocalStorageState(
@@ -121,11 +122,20 @@ const useDashboardState = (
defaultState,
);
+ const updateState = (newState: Partial) =>
+ setState({ ...defaultState, ...state, ...newState });
+
useEffect(() => {
+ const updates: Partial = {};
const setDefaultFlag =
flags.length &&
(!state.activeFlag ||
!flags.some((flag) => flag.name === state.activeFlag?.name));
+
+ if (setDefaultFlag) {
+ updates.activeFlag = flags[0];
+ }
+
const setDefaultProject =
projects.length &&
(!state.activeProject ||
@@ -133,37 +143,48 @@ const useDashboardState = (
(project) => project.id === state.activeProject,
));
- if (setDefaultFlag || setDefaultProject) {
- setState({
- activeFlag: setDefaultFlag ? flags[0] : state.activeFlag,
- activeProject: setDefaultProject
- ? projects[0].id
- : state.activeProject,
- });
+ if (setDefaultProject) {
+ updates.activeProject = projects[0].id;
}
- });
+
+ if (Object.keys(updates).length) {
+ updateState(updates);
+ }
+ }, [
+ JSON.stringify(projects, null, 2),
+ JSON.stringify(flags, null, 2),
+ JSON.stringify(state, null, 2),
+ ]);
const { activeFlag, activeProject } = state;
const setActiveFlag = (flag: PersonalDashboardSchemaFlagsItem) => {
- setState({
- ...state,
+ updateState({
activeFlag: flag,
});
};
const setActiveProject = (projectId: string) => {
- setState({
- ...state,
+ updateState({
activeProject: projectId,
});
};
+ const toggleSectionState = (section: 'flags' | 'projects') => {
+ const property = section === 'flags' ? 'expandFlags' : 'expandProjects';
+ updateState({
+ [property]: !(state[property] ?? true),
+ });
+ };
+
return {
activeFlag,
setActiveFlag,
activeProject,
setActiveProject,
+ expandFlags: state.expandFlags ?? true,
+ expandProjects: state.expandProjects ?? true,
+ toggleSectionState,
};
};
@@ -173,13 +194,57 @@ const WelcomeSection = styled('div')(({ theme }) => ({
gap: theme.spacing(1),
flexFlow: 'row wrap',
alignItems: 'baseline',
- marginBottom: theme.spacing(2),
}));
-const ViewKeyConceptsButton = styled(Button)(({ theme }) => ({
+const ViewKeyConceptsButton = styled(Button)({
fontWeight: 'normal',
padding: 0,
margin: 0,
+});
+
+const SectionAccordion = styled(Accordion)(({ theme }) => ({
+ border: `1px solid ${theme.palette.divider}`,
+ borderRadius: theme.shape.borderRadiusMedium,
+ backgroundColor: theme.palette.background.paper,
+ boxShadow: 'none',
+ '& .expanded': {
+ '&:before': {
+ opacity: '0 !important',
+ },
+ },
+
+ // add a top border to the region when the accordion is collapsed.
+ // This retains the border between the summary and the region
+ // during the collapsing animation
+ "[aria-expanded='false']+.MuiCollapse-root .MuiAccordion-region": {
+ borderTop: `1px solid ${theme.palette.divider}`,
+ },
+
+ overflow: 'hidden',
+}));
+
+const StyledAccordionSummary = styled(AccordionSummary)(({ theme }) => ({
+ border: 'none',
+ padding: theme.spacing(2, 4),
+ margin: 0,
+ // increase specificity to override the default margin
+ '&>.MuiAccordionSummary-content.MuiAccordionSummary-content': {
+ margin: '0',
+ },
+ "&[aria-expanded='true']": {
+ // only add the border when it's open
+ borderBottom: `1px solid ${theme.palette.divider}`,
+ },
+}));
+
+const StyledAccordionDetails = styled(AccordionDetails)({
+ padding: 0,
+});
+
+const MainContent = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: theme.spacing(2),
}));
export const PersonalDashboard = () => {
@@ -187,32 +252,30 @@ export const PersonalDashboard = () => {
const name = user?.name;
- const {
- personalDashboard,
- refetch: refetchDashboard,
- loading: personalDashboardLoading,
- } = usePersonalDashboard();
+ const { personalDashboard, refetch: refetchDashboard } =
+ usePersonalDashboard();
const projects = personalDashboard?.projects || [];
- const { activeProject, setActiveProject, activeFlag, setActiveFlag } =
- useDashboardState(projects, personalDashboard?.flags ?? []);
+ const {
+ activeProject,
+ setActiveProject,
+ activeFlag,
+ setActiveFlag,
+ toggleSectionState,
+ expandFlags,
+ expandProjects,
+ } = useDashboardState(projects, personalDashboard?.flags ?? []);
const [welcomeDialog, setWelcomeDialog] = useLocalStorageState<
'open' | 'closed'
>('welcome-dialog:v1', 'open');
- const {
- personalDashboardProjectDetails,
- loading: loadingDetails,
- error: detailsError,
- } = usePersonalDashboardProjectDetails(activeProject);
+ const { personalDashboardProjectDetails, error: detailsError } =
+ usePersonalDashboardProjectDetails(activeProject);
const activeProjectStage =
personalDashboardProjectDetails?.onboardingStatus.status ?? 'loading';
- const setupIncomplete =
- activeProjectStage === 'onboarding-started' ||
- activeProjectStage === 'first-flag-created';
const noProjects = projects.length === 0;
@@ -221,7 +284,7 @@ export const PersonalDashboard = () => {
);
return (
-
+
Welcome {name}
@@ -239,84 +302,113 @@ export const PersonalDashboard = () => {
- {noProjects && personalDashboard ? (
-
- ) : (
- toggleSectionState('projects')}
+ >
+
}
- />
- )}
+ id='projects-panel-header'
+ aria-controls='projects-panel-content'
+ >
+
+ My projects
+
+
+
+ {noProjects && personalDashboard ? (
+
+ ) : (
+
+ )}
+
+
-
-
-
- My feature flags
-
-
- {activeFlag ? (
-
- ) : null}
-
-
- {personalDashboard &&
- personalDashboard.flags.length > 0 ? (
-
- {personalDashboard.flags.map((flag) => (
- setActiveFlag(flag)}
+ toggleSectionState('flags')}
+ >
+
+ }
+ id='flags-panel-header'
+ aria-controls='flags-panel-content'
+ >
+
+ My feature flags
+
+
+
+
+
+
+ {personalDashboard &&
+ personalDashboard.flags.length > 0 ? (
+
+ {personalDashboard.flags.map((flag) => (
+
+ setActiveFlag(flag)
+ }
+ />
+ ))}
+
+ ) : (
+
+ You have not created or favorited any
+ feature flags. Once you do, they will
+ show up here.
+
+ )}
+
+
+
+ {activeFlag ? (
+
- ))}
-
- ) : (
-
- You have not created or favorited any feature
- flags. Once you do, they will show up here.
-
- )}
-
-
-
- {activeFlag ? (
-
- ) : (
-
- )}
-
-
-
+ ) : (
+
+ )}
+
+
+
+
+
setWelcomeDialog('closed')}
/>
-
+
);
};