1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-25 00:07:47 +01:00

Allow hiding environments from the feature overview screen (#2727)

This commit is contained in:
sjaanus 2023-01-03 15:41:34 +02:00 committed by GitHub
parent c2a3872386
commit 88d649d239
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 231 additions and 86 deletions

View File

@ -17,14 +17,14 @@ import { featuresPlaceholder } from 'component/feature/FeatureToggleList/Feature
import theme from 'themes/theme'; import theme from 'themes/theme';
import { useSearch } from 'hooks/useSearch'; import { useSearch } from 'hooks/useSearch';
import { useSearchParams } from 'react-router-dom'; import { useSearchParams } from 'react-router-dom';
import { TimeAgoCell } from '../../../common/Table/cells/TimeAgoCell/TimeAgoCell'; import { TimeAgoCell } from 'component/common/Table/cells/TimeAgoCell/TimeAgoCell';
import { TextCell } from '../../../common/Table/cells/TextCell/TextCell'; import { TextCell } from 'component/common/Table/cells/TextCell/TextCell';
import { ChangeRequestStatusCell } from './ChangeRequestStatusCell/ChangeRequestStatusCell'; import { ChangeRequestStatusCell } from './ChangeRequestStatusCell/ChangeRequestStatusCell';
import { AvatarCell } from './AvatarCell/AvatarCell'; import { AvatarCell } from './AvatarCell/AvatarCell';
import { ChangeRequestTitleCell } from './ChangeRequestTitleCell/ChangeRequestTitleCell'; import { ChangeRequestTitleCell } from './ChangeRequestTitleCell/ChangeRequestTitleCell';
import { TableBody, TableRow } from '../../../common/Table'; import { TableBody, TableRow } from 'component/common/Table';
import { useStyles } from './ChangeRequestsTabs.styles'; import { useStyles } from './ChangeRequestsTabs.styles';
import { createLocalStorage } from '../../../../utils/createLocalStorage'; import { createLocalStorage } from 'utils/createLocalStorage';
import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns'; import { useConditionallyHiddenColumns } from 'hooks/useConditionallyHiddenColumns';
export interface IChangeRequestTableProps { export interface IChangeRequestTableProps {

View File

@ -14,6 +14,7 @@ import { usePageTitle } from 'hooks/usePageTitle';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel';
import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments';
const FeatureOverview = () => { const FeatureOverview = () => {
const { uiConfig } = useUiConfig(); const { uiConfig } = useUiConfig();
@ -22,6 +23,8 @@ const FeatureOverview = () => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const featurePath = formatFeaturePath(projectId, featureId); const featurePath = formatFeaturePath(projectId, featureId);
const { hiddenEnvironments, setHiddenEnvironments } =
useHiddenEnvironments();
const onSidebarClose = () => navigate(featurePath); const onSidebarClose = () => navigate(featurePath);
usePageTitle(featureId); usePageTitle(featureId);
@ -31,7 +34,12 @@ const FeatureOverview = () => {
<FeatureOverviewMetaData /> <FeatureOverviewMetaData />
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig.flags.variantsPerEnvironment)} condition={Boolean(uiConfig.flags.variantsPerEnvironment)}
show={<FeatureOverviewSidePanel />} show={
<FeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
}
elseShow={<FeatureOverviewEnvSwitches />} elseShow={<FeatureOverviewEnvSwitches />}
/> />
</div> </div>

View File

@ -21,6 +21,7 @@ import { FeatureStrategyMenu } from 'component/feature/FeatureStrategy/FeatureSt
import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds'; import { FEATURE_ENVIRONMENT_ACCORDION } from 'utils/testIds';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons'; import { FeatureStrategyIcons } from 'component/feature/FeatureStrategy/FeatureStrategyIcons/FeatureStrategyIcons';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
interface IFeatureOverviewEnvironmentProps { interface IFeatureOverviewEnvironmentProps {
env: IFeatureEnvironment; env: IFeatureEnvironment;
@ -123,6 +124,7 @@ const FeatureOverviewEnvironment = ({
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { metrics } = useFeatureMetrics(projectId, featureId); const { metrics } = useFeatureMetrics(projectId, featureId);
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
const { value: globalStore } = useGlobalLocalStorage();
const featureMetrics = getFeatureMetrics(feature?.environments, metrics); const featureMetrics = getFeatureMetrics(feature?.environments, metrics);
const environmentMetric = featureMetrics.find( const environmentMetric = featureMetrics.find(
@ -133,92 +135,106 @@ const FeatureOverviewEnvironment = ({
); );
return ( return (
<StyledFeatureOverviewEnvironment enabled={env.enabled}> <ConditionallyRender
<StyledAccordion condition={
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`} !Boolean(new Set(globalStore.hiddenEnvironments).has(env.name))
> }
<StyledAccordionSummary show={
expandIcon={<ExpandMore titleAccess="Toggle" />} <StyledFeatureOverviewEnvironment enabled={env.enabled}>
> <StyledAccordion
<StyledHeader data-loading enabled={env.enabled}> data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
<StyledHeaderTitle> >
<StyledEnvironmentIcon enabled={env.enabled} /> <StyledAccordionSummary
<div> expandIcon={<ExpandMore titleAccess="Toggle" />}
<StyledStringTruncator >
text={env.name} <StyledHeader data-loading enabled={env.enabled}>
maxWidth="100" <StyledHeaderTitle>
maxLength={15} <StyledEnvironmentIcon
/> enabled={env.enabled}
</div>
<ConditionallyRender
condition={!env.enabled}
show={
<Chip
size="small"
variant="outlined"
label="Disabled"
sx={{ ml: 1 }}
/> />
} <div>
/> <StyledStringTruncator
</StyledHeaderTitle> text={env.name}
<StyledContainer> maxWidth="100"
<FeatureStrategyMenu maxLength={15}
label="Add strategy" />
projectId={projectId} </div>
featureId={featureId} <ConditionallyRender
environmentId={env.name} condition={!env.enabled}
variant="text" show={
/> <Chip
<FeatureStrategyIcons size="small"
strategies={featureEnvironment?.strategies} variant="outlined"
/> label="Disabled"
</StyledContainer> sx={{ ml: 1 }}
</StyledHeader> />
}
<FeatureOverviewEnvironmentMetrics />
environmentMetric={environmentMetric} </StyledHeaderTitle>
disabled={!env.enabled} <StyledContainer>
/>
</StyledAccordionSummary>
<StyledAccordionDetails enabled={env.enabled}>
<StyledEnvironmentAccordionBody
featureEnvironment={featureEnvironment}
isDisabled={!env.enabled}
otherEnvironments={feature?.environments
.map(({ name }) => name)
.filter(name => name !== env.name)}
/>
<ConditionallyRender
condition={
(featureEnvironment?.strategies?.length || 0) > 0
}
show={
<>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
py: 1,
}}
>
<FeatureStrategyMenu <FeatureStrategyMenu
label="Add strategy" label="Add strategy"
projectId={projectId} projectId={projectId}
featureId={featureId} featureId={featureId}
environmentId={env.name} environmentId={env.name}
variant="text"
/> />
</Box> <FeatureStrategyIcons
<EnvironmentFooter strategies={
environmentMetric={environmentMetric} featureEnvironment?.strategies
/> }
</> />
} </StyledContainer>
/> </StyledHeader>
</StyledAccordionDetails>
</StyledAccordion> <FeatureOverviewEnvironmentMetrics
</StyledFeatureOverviewEnvironment> environmentMetric={environmentMetric}
disabled={!env.enabled}
/>
</StyledAccordionSummary>
<StyledAccordionDetails enabled={env.enabled}>
<StyledEnvironmentAccordionBody
featureEnvironment={featureEnvironment}
isDisabled={!env.enabled}
otherEnvironments={feature?.environments
.map(({ name }) => name)
.filter(name => name !== env.name)}
/>
<ConditionallyRender
condition={
(featureEnvironment?.strategies?.length ||
0) > 0
}
show={
<>
<Box
sx={{
display: 'flex',
justifyContent: 'center',
py: 1,
}}
>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
/>
</Box>
<EnvironmentFooter
environmentMetric={
environmentMetric
}
/>
</>
}
/>
</StyledAccordionDetails>
</StyledAccordion>
</StyledFeatureOverviewEnvironment>
}
/>
); );
}; };

View File

@ -40,7 +40,15 @@ const StyledHeader = styled('h3')(({ theme }) => ({
}, },
})); }));
export const FeatureOverviewSidePanel = () => { interface IFeatureOverviewSidePanelProps {
hiddenEnvironments: Set<String>;
setHiddenEnvironments: (environment: string) => void;
}
export const FeatureOverviewSidePanel = ({
hiddenEnvironments,
setHiddenEnvironments,
}: IFeatureOverviewSidePanelProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { feature } = useFeature(projectId, featureId); const { feature } = useFeature(projectId, featureId);
@ -64,6 +72,8 @@ export const FeatureOverviewSidePanel = () => {
</StyledHeader> </StyledHeader>
} }
feature={feature} feature={feature}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/> />
<Divider /> <Divider />
<FeatureOverviewSidePanelDetails <FeatureOverviewSidePanelDetails

View File

@ -0,0 +1,44 @@
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import { styled } from '@mui/material';
import { Visibility, VisibilityOff } from '@mui/icons-material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
const Visible = styled(Visibility)(({ theme }) => ({
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<String>;
setHiddenEnvironments: (environment: string) => void;
}
export const FeatureOverviewSidePanelEnvironmentHider = ({
environment,
hiddenEnvironments,
setHiddenEnvironments,
}: IFeatureOverviewSidePanelEnvironmentHiderProps) => {
const toggleHiddenEnvironments = () => {
setHiddenEnvironments(environment.name);
};
return (
<ConditionallyRender
condition={hiddenEnvironments.has(environment.name)}
show={<VisibleOff onClick={toggleHiddenEnvironments} />}
elseShow={<Visible onClick={toggleHiddenEnvironments} />}
/>
);
};

View File

@ -13,12 +13,15 @@ import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfi
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import StringTruncator from 'component/common/StringTruncator/StringTruncator'; import StringTruncator from 'component/common/StringTruncator/StringTruncator';
import { RemoveRedEye, Star } from '@mui/icons-material';
import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider';
const StyledContainer = styled('div')(({ theme }) => ({ const StyledContainer = styled('div')(({ theme }) => ({
marginLeft: theme.spacing(-1.5), marginLeft: theme.spacing(-1.5),
'&:not(:last-of-type)': { '&:not(:last-of-type)': {
marginBottom: theme.spacing(2), marginBottom: theme.spacing(2),
}, },
display: 'flex',
})); }));
const StyledLabel = styled('label')(() => ({ const StyledLabel = styled('label')(() => ({
@ -27,11 +30,19 @@ const StyledLabel = styled('label')(() => ({
cursor: 'pointer', cursor: 'pointer',
})); }));
const HideButton = styled(RemoveRedEye)(({ theme }) => ({
cursor: 'pointer',
marginLeft: 'auto',
color: theme.palette.grey[700],
}));
interface IFeatureOverviewSidePanelEnvironmentSwitchProps { interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
environment: IFeatureEnvironment; environment: IFeatureEnvironment;
callback?: () => void; callback?: () => void;
showInfoBox: () => void; showInfoBox: () => void;
children?: React.ReactNode; children?: React.ReactNode;
hiddenEnvironments: Set<String>;
setHiddenEnvironments: (environment: string) => void;
} }
export const FeatureOverviewSidePanelEnvironmentSwitch = ({ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
@ -39,6 +50,8 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
callback, callback,
showInfoBox, showInfoBox,
children, children,
hiddenEnvironments,
setHiddenEnvironments,
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => { }: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
const { name, enabled } = environment; const { name, enabled } = environment;
@ -136,6 +149,11 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
/> />
{children ?? defaultContent} {children ?? defaultContent}
</StyledLabel> </StyledLabel>
<FeatureOverviewSidePanelEnvironmentHider
environment={environment}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
<ChangeRequestDialogue <ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen} isOpen={changeRequestDialogDetails.isOpen}
onClose={onChangeRequestToggleClose} onClose={onChangeRequestToggleClose}

View File

@ -32,11 +32,15 @@ const StyledLink = styled(Link<typeof RouterLink | 'a'>)(() => ({
interface IFeatureOverviewSidePanelEnvironmentSwitchesProps { interface IFeatureOverviewSidePanelEnvironmentSwitchesProps {
feature: IFeatureToggle; feature: IFeatureToggle;
header: React.ReactNode; header: React.ReactNode;
hiddenEnvironments: Set<String>;
setHiddenEnvironments: (environment: string) => void;
} }
export const FeatureOverviewSidePanelEnvironmentSwitches = ({ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
feature, feature,
header, header,
hiddenEnvironments,
setHiddenEnvironments,
}: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => { }: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => {
const [showInfoBox, setShowInfoBox] = useState(false); const [showInfoBox, setShowInfoBox] = useState(false);
const [environmentName, setEnvironmentName] = useState(''); const [environmentName, setEnvironmentName] = useState('');
@ -73,6 +77,8 @@ export const FeatureOverviewSidePanelEnvironmentSwitches = ({
<FeatureOverviewSidePanelEnvironmentSwitch <FeatureOverviewSidePanelEnvironmentSwitch
key={environment.name} key={environment.name}
environment={environment} environment={environment}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
showInfoBox={() => { showInfoBox={() => {
setEnvironmentName(environment.name); setEnvironmentName(environment.name);
setShowInfoBox(true); setShowInfoBox(true);

View File

@ -2,6 +2,7 @@ import { createLocalStorage } from 'utils/createLocalStorage';
interface IGlobalStore { interface IGlobalStore {
favorites?: boolean; favorites?: boolean;
hiddenEnvironments?: Set<string>;
} }
export const useGlobalLocalStorage = () => { export const useGlobalLocalStorage = () => {

View File

@ -0,0 +1,37 @@
import { createLocalStorage } from 'utils/createLocalStorage';
import { useGlobalLocalStorage } from './useGlobalLocalStorage';
import { useState } from 'react';
interface IGlobalStore {
favorites?: boolean;
hiddenEnvironments?: Set<string>;
}
export const useHiddenEnvironments = () => {
const { value: globalStore, setValue: setGlobalStore } =
useGlobalLocalStorage();
const [hiddenEnvironments, setStoredHiddenEnvironments] = useState<
Set<string>
>(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,
};
};

View File

@ -12,7 +12,12 @@ export function getLocalStorageItem<T>(key: string): T | undefined {
// Does nothing if the browser denies access. // Does nothing if the browser denies access.
export function setLocalStorageItem(key: string, value: unknown) { export function setLocalStorageItem(key: string, value: unknown) {
try { 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) { } catch (err: unknown) {
console.warn(err); console.warn(err);
} }