diff --git a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
index 8312459f74..31f7ab2848 100644
--- a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
+++ b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx
@@ -121,10 +121,12 @@ const useFlagMetrics = (
environment: string | null,
hoursBack: number,
) => {
- const { featureMetrics: metrics = [], loading } = useFeatureMetricsRaw(
- flagName,
- hoursBack,
- );
+ const {
+ featureMetrics: metrics = [],
+ loading,
+ error,
+ } = useFeatureMetricsRaw(flagName, hoursBack);
+
const sortedMetrics = useMemo(() => {
return [...metrics].sort((metricA, metricB) => {
return metricA.timestamp.localeCompare(metricB.timestamp);
@@ -151,7 +153,7 @@ const useFlagMetrics = (
return createBarChartOptions(theme, hoursBack, locationSettings);
}, [theme, hoursBack, locationSettings]);
- return { data, options, loading };
+ return { data, options, loading, error };
};
const EnvironmentSelect: FC<{
@@ -222,11 +224,22 @@ export const FlagMetricsChart: FC<{
const { environment, setEnvironment, activeEnvironments } =
useMetricsEnvironments(flag.project, flag.name);
- const { data, options, loading } = useFlagMetrics(
- flag.name,
- environment,
- hoursBack,
- );
+ const {
+ data,
+ options,
+ loading,
+ error: metricsError,
+ } = useFlagMetrics(flag.name, environment, hoursBack);
+
+ if (metricsError) {
+ return (
+
+
+
+ );
+ }
const noData = data.datasets[0].data.length === 0;
diff --git a/frontend/src/component/personalDashboard/Grid.tsx b/frontend/src/component/personalDashboard/Grid.tsx
index 11f25ecf3d..97e6a8d058 100644
--- a/frontend/src/component/personalDashboard/Grid.tsx
+++ b/frontend/src/component/personalDashboard/Grid.tsx
@@ -58,7 +58,7 @@ export const FlagGrid = styled(ContentGrid)(
);
export const GridItem = styled('div', {
- shouldForwardProp: (prop) => !['gridArea', 'sx'].includes(prop.toString()),
+ shouldForwardProp: (prop) => !['gridArea'].includes(prop.toString()),
})<{ gridArea: string }>(({ theme, gridArea }) => ({
padding: theme.spacing(2, 4),
maxHeight: '100%',
@@ -113,3 +113,20 @@ export const StyledList = styled(List)(({ theme }) => ({
maxHeight: '100%',
})({ theme }),
}));
+
+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',
+ }),
+);
diff --git a/frontend/src/component/personalDashboard/MyFlags.tsx b/frontend/src/component/personalDashboard/MyFlags.tsx
new file mode 100644
index 0000000000..fb75cda0da
--- /dev/null
+++ b/frontend/src/component/personalDashboard/MyFlags.tsx
@@ -0,0 +1,180 @@
+import { type FC, useEffect, useRef } from 'react';
+import {
+ ContentGridContainer,
+ FlagGrid,
+ ListItemBox,
+ SpacedGridItem,
+ StyledCardTitle,
+ StyledList,
+ listItemStyle,
+} from './Grid';
+import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
+import {
+ Alert,
+ IconButton,
+ Link,
+ ListItem,
+ ListItemButton,
+ Typography,
+ styled,
+} from '@mui/material';
+import LinkIcon from '@mui/icons-material/ArrowForward';
+import React from 'react';
+import type { PersonalDashboardSchemaFlagsItem } from 'openapi';
+
+const NoActiveFlagsInfo = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexFlow: 'column',
+ gap: theme.spacing(2),
+}));
+
+const FlagListItem: FC<{
+ flag: { name: string; project: string; type: string };
+ selected: boolean;
+ onClick: () => void;
+}> = ({ flag, selected, onClick }) => {
+ const activeFlagRef = useRef(null);
+ const { trackEvent } = usePlausibleTracker();
+
+ useEffect(() => {
+ if (activeFlagRef.current) {
+ activeFlagRef.current.scrollIntoView({
+ block: 'nearest',
+ inline: 'start',
+ });
+ }
+ }, []);
+ const IconComponent = getFeatureTypeIcons(flag.type);
+ const flagLink = `projects/${flag.project}/features/${flag.name}`;
+ return (
+
+
+
+
+ {flag.name}
+ {
+ trackEvent('personal-dashboard', {
+ props: {
+ eventType: `Go to flag from list`,
+ },
+ });
+ }}
+ size='small'
+ sx={{ ml: 'auto' }}
+ >
+
+
+
+
+
+ );
+};
+
+type FlagData =
+ | {
+ state: 'flags';
+ flags: PersonalDashboardSchemaFlagsItem[];
+ activeFlag: PersonalDashboardSchemaFlagsItem;
+ }
+ | {
+ state: 'no flags';
+ };
+
+type Props = {
+ hasProjects: boolean;
+ flagData: FlagData;
+ setActiveFlag: (flag: PersonalDashboardSchemaFlagsItem) => void;
+ refetchDashboard: () => void;
+};
+
+export const MyFlags: FC = ({
+ hasProjects,
+ flagData,
+ setActiveFlag,
+ refetchDashboard,
+}) => {
+ return (
+
+
+
+ {flagData.state === 'flags' ? (
+
+ {flagData.flags.map((flag) => (
+ setActiveFlag(flag)}
+ />
+ ))}
+
+ ) : hasProjects ? (
+
+
+ You have not created or favorited any feature
+ flags. Once you do, they will show up here.
+
+
+ To create a new flag, go to one of your
+ projects.
+
+
+ ) : (
+
+ You need to create or join a project to be able to
+ add a flag, or you must be given the rights by your
+ admin to add feature flags.
+
+ )}
+
+
+
+ {flagData.state === 'flags' ? (
+
+ ) : (
+
+ )}
+
+
+
+ );
+};
+
+const FlagMetricsChart = React.lazy(() =>
+ import('./FlagMetricsChart').then((module) => ({
+ default: module.FlagMetricsChart,
+ })),
+);
+const PlaceholderFlagMetricsChart = React.lazy(() =>
+ import('./FlagMetricsChart').then((module) => ({
+ default: module.PlaceholderFlagMetricsChartWithWrapper,
+ })),
+);
diff --git a/frontend/src/component/personalDashboard/MyProjects.tsx b/frontend/src/component/personalDashboard/MyProjects.tsx
index 70062e4f7f..f58b76f8eb 100644
--- a/frontend/src/component/personalDashboard/MyProjects.tsx
+++ b/frontend/src/component/personalDashboard/MyProjects.tsx
@@ -13,7 +13,6 @@ import { ConnectSDK, CreateFlag, ExistingFlag } from './ConnectSDK';
import { LatestProjectEvents } from './LatestProjectEvents';
import { RoleAndOwnerInfo } from './RoleAndOwnerInfo';
import { forwardRef, useEffect, useRef, type FC } from 'react';
-import { StyledCardTitle } from './PersonalDashboard';
import type {
PersonalDashboardProjectDetailsSchema,
PersonalDashboardSchemaAdminsItem,
@@ -28,6 +27,7 @@ import {
GridItem,
SpacedGridItem,
StyledList,
+ StyledCardTitle,
} from './Grid';
import { ContactAdmins, DataError } from './ProjectDetailsError';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
diff --git a/frontend/src/component/personalDashboard/PersonalDashboard.tsx b/frontend/src/component/personalDashboard/PersonalDashboard.tsx
index be9a8d954a..dd7be3bc85 100644
--- a/frontend/src/component/personalDashboard/PersonalDashboard.tsx
+++ b/frontend/src/component/personalDashboard/PersonalDashboard.tsx
@@ -3,201 +3,23 @@ import {
Accordion,
AccordionDetails,
AccordionSummary,
- Alert,
Button,
- IconButton,
- Link,
- ListItem,
- ListItemButton,
styled,
Typography,
} from '@mui/material';
-import React, { type FC, useEffect, useRef } from 'react';
-import LinkIcon from '@mui/icons-material/ArrowForward';
import { WelcomeDialog } from './WelcomeDialog';
import { useLocalStorageState } from 'hooks/useLocalStorageState';
import { usePersonalDashboard } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboard';
-import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons';
-import type {
- PersonalDashboardSchemaFlagsItem,
- PersonalDashboardSchemaProjectsItem,
-} from '../../openapi';
import { usePersonalDashboardProjectDetails } from 'hooks/api/getters/usePersonalDashboard/usePersonalDashboardProjectDetails';
import useLoading from '../../hooks/useLoading';
import { MyProjects } from './MyProjects';
-import {
- ContentGridContainer,
- FlagGrid,
- ListItemBox,
- listItemStyle,
- SpacedGridItem,
- StyledList,
-} from './Grid';
import { ContentGridNoProjects } from './ContentGridNoProjects';
import ExpandMore from '@mui/icons-material/ExpandMore';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useSplashApi from 'hooks/api/actions/useSplashApi/useSplashApi';
import { useAuthSplash } from 'hooks/api/getters/useAuth/useAuthSplash';
-
-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',
- }),
-);
-const FlagListItem: FC<{
- flag: { name: string; project: string; type: string };
- selected: boolean;
- onClick: () => void;
-}> = ({ flag, selected, onClick }) => {
- const activeFlagRef = useRef(null);
- const { trackEvent } = usePlausibleTracker();
-
- useEffect(() => {
- if (activeFlagRef.current) {
- activeFlagRef.current.scrollIntoView({
- block: 'nearest',
- inline: 'start',
- });
- }
- }, []);
- const IconComponent = getFeatureTypeIcons(flag.type);
- const flagLink = `projects/${flag.project}/features/${flag.name}`;
- return (
-
-
-
-
- {flag.name}
- {
- trackEvent('personal-dashboard', {
- props: {
- eventType: `Go to flag from list`,
- },
- });
- }}
- size='small'
- sx={{ ml: 'auto' }}
- >
-
-
-
-
-
- );
-};
-
-// todo: move into own file
-const useDashboardState = (
- projects: PersonalDashboardSchemaProjectsItem[],
- flags: PersonalDashboardSchemaFlagsItem[],
-) => {
- type State = {
- activeProject: string | undefined;
- activeFlag: PersonalDashboardSchemaFlagsItem | undefined;
- expandProjects: boolean;
- expandFlags: boolean;
- };
-
- const defaultState: State = {
- activeProject: undefined,
- activeFlag: undefined,
- expandProjects: true,
- expandFlags: true,
- };
-
- const [state, setState] = useLocalStorageState(
- 'personal-dashboard:v1',
- 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 ||
- !projects.some(
- (project) => project.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) => {
- updateState({
- activeFlag: flag,
- });
- };
-
- const setActiveProject = (projectId: string) => {
- 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,
- };
-};
+import { useDashboardState } from './useDashboardState';
+import { MyFlags } from './MyFlags';
const WelcomeSection = styled('div')(({ theme }) => ({
display: 'flex',
@@ -264,6 +86,28 @@ const NoActiveFlagsInfo = styled('div')(({ theme }) => ({
gap: theme.spacing(2),
}));
+type DashboardState =
+ | {
+ state: 'flags and projects';
+ // regular state; show everything
+ activeFlag: any;
+ activeProject: any;
+ }
+ | {
+ state: 'projects, no flags';
+ // show projects as normal, tell the user to create a flag
+ activeProject: any;
+ }
+ | {
+ state: 'no projects, no flags';
+ // no projects and no flags; show information about admins, project owners, and tell the user to join a project to create a flags
+ }
+ | {
+ state: 'flags, no projects';
+ // show info about admins + project owners, regular flags
+ activeFlag: any;
+ };
+
export const PersonalDashboard = () => {
const { user } = useAuthUser();
const { trackEvent } = usePlausibleTracker();
@@ -272,8 +116,11 @@ export const PersonalDashboard = () => {
const name = user?.name;
- const { personalDashboard, refetch: refetchDashboard } =
- usePersonalDashboard();
+ const {
+ personalDashboard,
+ refetch: refetchDashboard,
+ loading: personalDashboardLoading,
+ } = usePersonalDashboard();
const projects = personalDashboard?.projects || [];
@@ -385,70 +232,22 @@ export const PersonalDashboard = () => {
-
-
-
- {personalDashboard &&
- personalDashboard.flags.length > 0 ? (
-
- {personalDashboard.flags.map((flag) => (
-
- setActiveFlag(flag)
- }
- />
- ))}
-
- ) : activeProject ? (
-
-
- You have not created or favorited
- any feature flags. Once you do, they
- will show up here.
-
-
- To create a new flag, go to one of
- your projects.
-
-
- ) : (
-
- You need to create or join a project to
- be able to add a flag, or you must be
- given the rights by your admin to add
- feature flags.
-
- )}
-
-
-
- {activeFlag ? (
-
- ) : (
-
- )}
-
-
-
+ 0}
+ flagData={
+ personalDashboard &&
+ personalDashboard.flags.length &&
+ activeFlag
+ ? {
+ state: 'flags' as const,
+ activeFlag,
+ flags: personalDashboard.flags,
+ }
+ : { state: 'no flags' as const }
+ }
+ setActiveFlag={setActiveFlag}
+ refetchDashboard={refetchDashboard}
+ />
{
);
};
-
-const FlagMetricsChart = React.lazy(() =>
- import('./FlagMetricsChart').then((module) => ({
- default: module.FlagMetricsChart,
- })),
-);
-const PlaceholderFlagMetricsChart = React.lazy(() =>
- import('./FlagMetricsChart').then((module) => ({
- default: module.PlaceholderFlagMetricsChartWithWrapper,
- })),
-);
diff --git a/frontend/src/component/personalDashboard/useDashboardState.ts b/frontend/src/component/personalDashboard/useDashboardState.ts
new file mode 100644
index 0000000000..e1275c089c
--- /dev/null
+++ b/frontend/src/component/personalDashboard/useDashboardState.ts
@@ -0,0 +1,95 @@
+import { useLocalStorageState } from 'hooks/useLocalStorageState';
+import type {
+ PersonalDashboardSchemaFlagsItem,
+ PersonalDashboardSchemaProjectsItem,
+} from 'openapi';
+import { useEffect } from 'react';
+
+export const useDashboardState = (
+ projects: PersonalDashboardSchemaProjectsItem[],
+ flags: PersonalDashboardSchemaFlagsItem[],
+) => {
+ type State = {
+ activeProject: string | undefined;
+ activeFlag: PersonalDashboardSchemaFlagsItem | undefined;
+ expandProjects: boolean;
+ expandFlags: boolean;
+ };
+
+ const defaultState: State = {
+ activeProject: undefined,
+ activeFlag: undefined,
+ expandProjects: true,
+ expandFlags: true,
+ };
+
+ const [state, setState] = useLocalStorageState(
+ 'personal-dashboard:v1',
+ 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 ||
+ !projects.some(
+ (project) => project.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) => {
+ updateState({
+ activeFlag: flag,
+ });
+ };
+
+ const setActiveProject = (projectId: string) => {
+ 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,
+ };
+};