From 8c2ed5dc30fe95a1bf73e99aacb134f0c523e5fa Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 14 Oct 2024 11:45:37 +0200 Subject: [PATCH] Refactor front end code pt 1 (#8438) This PR is the first in the front end code refactoring. It moves My Flags out into a separate file and includes some extra error handling (such as if the name of the flag causes problems for the API). ![image](https://github.com/user-attachments/assets/5aec8f0c-de79-4b7d-b56b-42297b872ec5) --- .../personalDashboard/FlagMetricsChart.tsx | 33 +- .../src/component/personalDashboard/Grid.tsx | 19 +- .../component/personalDashboard/MyFlags.tsx | 180 +++++++++++ .../personalDashboard/MyProjects.tsx | 2 +- .../personalDashboard/PersonalDashboard.tsx | 302 +++--------------- .../personalDashboard/useDashboardState.ts | 95 ++++++ 6 files changed, 362 insertions(+), 269 deletions(-) create mode 100644 frontend/src/component/personalDashboard/MyFlags.tsx create mode 100644 frontend/src/component/personalDashboard/useDashboardState.ts diff --git a/frontend/src/component/personalDashboard/FlagMetricsChart.tsx b/frontend/src/component/personalDashboard/FlagMetricsChart.tsx index 8312459f74ed..31f7ab284887 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 11f25ecf3d6f..97e6a8d05834 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 000000000000..fb75cda0da2c --- /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 70062e4f7f12..f58b76f8ebb1 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 be9a8d954a76..dd7be3bc8513 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 000000000000..e1275c089c42 --- /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, + }; +};