From 88d649d239d9c27f3bbad2a9e090e2df3ca0619b Mon Sep 17 00:00:00 2001 From: sjaanus Date: Tue, 3 Jan 2023 15:41:34 +0200 Subject: [PATCH] Allow hiding environments from the feature overview screen (#2727) --- .../ChangeRequestsTabs/ChangeRequestsTabs.tsx | 8 +- .../FeatureOverview/FeatureOverview.tsx | 10 +- .../FeatureOverviewEnvironment.tsx | 174 ++++++++++-------- .../FeatureOverviewSidePanel.tsx | 12 +- ...atureOverviewSidePanelEnvironmentHider.tsx | 44 +++++ ...tureOverviewSidePanelEnvironmentSwitch.tsx | 18 ++ ...reOverviewSidePanelEnvironmentSwitches.tsx | 6 + frontend/src/hooks/useGlobalLocalStorage.ts | 1 + frontend/src/hooks/useHiddenEnvironments.ts | 37 ++++ frontend/src/utils/storage.ts | 7 +- 10 files changed, 231 insertions(+), 86 deletions(-) create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentHider.tsx create mode 100644 frontend/src/hooks/useHiddenEnvironments.ts diff --git a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx index 93315ded63..014ddc9aad 100644 --- a/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx +++ b/frontend/src/component/changeRequest/ProjectChangeRequests/ChangeRequestsTabs/ChangeRequestsTabs.tsx @@ -17,14 +17,14 @@ import { featuresPlaceholder } from 'component/feature/FeatureToggleList/Feature import theme from 'themes/theme'; import { useSearch } from 'hooks/useSearch'; import { useSearchParams } from 'react-router-dom'; -import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell'; -import { TextCell } from '../../../common/Table/cells/TextCell/TextCell'; +import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell'; +import { TextCell } from 'component/common/Table/cells/TextCell/TextCell'; import { ChangeRequestStatusCell } from './ChangeRequestStatusCell/ChangeRequestStatusCell'; import { AvatarCell } from './AvatarCell/AvatarCell'; import { ChangeRequestTitleCell } from './ChangeRequestTitleCell/ChangeRequestTitleCell'; -import { TableBody, TableRow } from '../../../common/Table'; +import { TableBody, TableRow } from 'component/common/Table'; import { useStyles } from './ChangeRequestsTabs.styles'; -import { createLocalStorage } from '../../../../utils/createLocalStorage'; +import { createLocalStorage } from 'utils/createLocalStorage'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; export interface IChangeRequestTableProps { diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx index a3257f750b..72a4e4087f 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx @@ -14,6 +14,7 @@ import { usePageTitle } from 'hooks/usePageTitle'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; +import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments'; const FeatureOverview = () => { const { uiConfig } = useUiConfig(); @@ -22,6 +23,8 @@ const FeatureOverview = () => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const featurePath = formatFeaturePath(projectId, featureId); + const { hiddenEnvironments, setHiddenEnvironments } = + useHiddenEnvironments(); const onSidebarClose = () => navigate(featurePath); usePageTitle(featureId); @@ -31,7 +34,12 @@ const FeatureOverview = () => { } + show={ + + } elseShow={} /> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx index e6a926fda4..33269ea5f6 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/FeatureOverviewEnvironment.tsx @@ -21,6 +21,7 @@ import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureSt import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons'; +import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage'; interface IFeatureOverviewEnvironmentProps { env: IFeatureEnvironment; @@ -123,6 +124,7 @@ const FeatureOverviewEnvironment = ({ const featureId = useRequiredPathParam('featureId'); const { metrics } = useFeatureMetrics(projectId, featureId); const { feature } = useFeature(projectId, featureId); + const { value: globalStore } = useGlobalLocalStorage(); const featureMetrics = getFeatureMetrics(feature?.environments, metrics); const environmentMetric = featureMetrics.find( @@ -133,92 +135,106 @@ const FeatureOverviewEnvironment = ({ ); return ( - - - } - > - - - -
- -
- + + } + > + + + - } - /> - - - - - - - - - - - - name) - .filter(name => name !== env.name)} - /> - 0 - } - show={ - <> - +
+ +
+ + } + /> +
+ - - - - } - /> - -
-
+ + + + + + + + + name) + .filter(name => name !== env.name)} + /> + 0 + } + show={ + <> + + + + + + } + /> + + + + } + /> ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx index e4baf1cf34..4302ecc234 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx @@ -40,7 +40,15 @@ const StyledHeader = styled('h3')(({ theme }) => ({ }, })); -export const FeatureOverviewSidePanel = () => { +interface IFeatureOverviewSidePanelProps { + hiddenEnvironments: Set; + setHiddenEnvironments: (environment: string) => void; +} + +export const FeatureOverviewSidePanel = ({ + hiddenEnvironments, + setHiddenEnvironments, +}: IFeatureOverviewSidePanelProps) => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { feature } = useFeature(projectId, featureId); @@ -64,6 +72,8 @@ export const FeatureOverviewSidePanel = () => { } feature={feature} + hiddenEnvironments={hiddenEnvironments} + setHiddenEnvironments={setHiddenEnvironments} /> ({ + cursor: 'pointer', + marginLeft: 'auto', + color: theme.palette.grey[700], + '&:hover': { + opacity: 1, + }, + opacity: 0, +})); + +const VisibleOff = styled(VisibilityOff)(({ theme }) => ({ + cursor: 'pointer', + marginLeft: 'auto', + color: theme.palette.grey[700], +})); + +interface IFeatureOverviewSidePanelEnvironmentHiderProps { + environment: IFeatureEnvironment; + hiddenEnvironments: Set; + setHiddenEnvironments: (environment: string) => void; +} + +export const FeatureOverviewSidePanelEnvironmentHider = ({ + environment, + hiddenEnvironments, + setHiddenEnvironments, +}: IFeatureOverviewSidePanelEnvironmentHiderProps) => { + const toggleHiddenEnvironments = () => { + setHiddenEnvironments(environment.name); + }; + + return ( + } + elseShow={} + /> + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx index 8f4418a66c..a062086ef2 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch.tsx @@ -13,12 +13,15 @@ import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfi import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { styled } from '@mui/material'; import StringTruncator from 'component/common/StringTruncator/StringTruncator'; +import { RemoveRedEye, Star } from '@mui/icons-material'; +import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider'; const StyledContainer = styled('div')(({ theme }) => ({ marginLeft: theme.spacing(-1.5), '&:not(:last-of-type)': { marginBottom: theme.spacing(2), }, + display: 'flex', })); const StyledLabel = styled('label')(() => ({ @@ -27,11 +30,19 @@ const StyledLabel = styled('label')(() => ({ cursor: 'pointer', })); +const HideButton = styled(RemoveRedEye)(({ theme }) => ({ + cursor: 'pointer', + marginLeft: 'auto', + color: theme.palette.grey[700], +})); + interface IFeatureOverviewSidePanelEnvironmentSwitchProps { environment: IFeatureEnvironment; callback?: () => void; showInfoBox: () => void; children?: React.ReactNode; + hiddenEnvironments: Set; + setHiddenEnvironments: (environment: string) => void; } export const FeatureOverviewSidePanelEnvironmentSwitch = ({ @@ -39,6 +50,8 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ callback, showInfoBox, children, + hiddenEnvironments, + setHiddenEnvironments, }: IFeatureOverviewSidePanelEnvironmentSwitchProps) => { const { name, enabled } = environment; @@ -136,6 +149,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({ /> {children ?? defaultContent} + )(() => ({ interface IFeatureOverviewSidePanelEnvironmentSwitchesProps { feature: IFeatureToggle; header: React.ReactNode; + hiddenEnvironments: Set; + setHiddenEnvironments: (environment: string) => void; } export const FeatureOverviewSidePanelEnvironmentSwitches = ({ feature, header, + hiddenEnvironments, + setHiddenEnvironments, }: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => { const [showInfoBox, setShowInfoBox] = useState(false); const [environmentName, setEnvironmentName] = useState(''); @@ -73,6 +77,8 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({ { setEnvironmentName(environment.name); setShowInfoBox(true); diff --git a/frontend/src/hooks/useGlobalLocalStorage.ts b/frontend/src/hooks/useGlobalLocalStorage.ts index 590583e25a..7bf6f6ca5d 100644 --- a/frontend/src/hooks/useGlobalLocalStorage.ts +++ b/frontend/src/hooks/useGlobalLocalStorage.ts @@ -2,6 +2,7 @@ import { createLocalStorage } from 'utils/createLocalStorage'; interface IGlobalStore { favorites?: boolean; + hiddenEnvironments?: Set; } export const useGlobalLocalStorage = () => { diff --git a/frontend/src/hooks/useHiddenEnvironments.ts b/frontend/src/hooks/useHiddenEnvironments.ts new file mode 100644 index 0000000000..ea6a0076af --- /dev/null +++ b/frontend/src/hooks/useHiddenEnvironments.ts @@ -0,0 +1,37 @@ +import { createLocalStorage } from 'utils/createLocalStorage'; +import { useGlobalLocalStorage } from './useGlobalLocalStorage'; +import { useState } from 'react'; + +interface IGlobalStore { + favorites?: boolean; + hiddenEnvironments?: Set; +} + +export const useHiddenEnvironments = () => { + const { value: globalStore, setValue: setGlobalStore } = + useGlobalLocalStorage(); + const [hiddenEnvironments, setStoredHiddenEnvironments] = useState< + Set + >(new Set(globalStore.hiddenEnvironments)); + + const setHiddenEnvironments = (environment: string) => { + setGlobalStore(params => { + const hiddenEnvironments = new Set(params.hiddenEnvironments); + if (hiddenEnvironments.has(environment)) { + hiddenEnvironments.delete(environment); + } else { + hiddenEnvironments.add(environment); + } + setStoredHiddenEnvironments(hiddenEnvironments); + return { + ...globalStore, + hiddenEnvironments: hiddenEnvironments, + }; + }); + }; + + return { + hiddenEnvironments, + setHiddenEnvironments, + }; +}; diff --git a/frontend/src/utils/storage.ts b/frontend/src/utils/storage.ts index f07ad7ddc4..08efa3c721 100644 --- a/frontend/src/utils/storage.ts +++ b/frontend/src/utils/storage.ts @@ -12,7 +12,12 @@ export function getLocalStorageItem(key: string): T | undefined { // Does nothing if the browser denies access. export function setLocalStorageItem(key: string, value: unknown) { try { - window.localStorage.setItem(key, JSON.stringify(value)); + window.localStorage.setItem( + key, + JSON.stringify(value, (_key, value) => + value instanceof Set ? [...value] : value + ) + ); } catch (err: unknown) { console.warn(err); }