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 { 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 {

View File

@ -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 = () => {
<FeatureOverviewMetaData />
<ConditionallyRender
condition={Boolean(uiConfig.flags.variantsPerEnvironment)}
show={<FeatureOverviewSidePanel />}
show={
<FeatureOverviewSidePanel
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
}
elseShow={<FeatureOverviewEnvSwitches />}
/>
</div>

View File

@ -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 (
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
<StyledAccordion
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
>
<StyledAccordionSummary
expandIcon={<ExpandMore titleAccess="Toggle" />}
>
<StyledHeader data-loading enabled={env.enabled}>
<StyledHeaderTitle>
<StyledEnvironmentIcon enabled={env.enabled} />
<div>
<StyledStringTruncator
text={env.name}
maxWidth="100"
maxLength={15}
/>
</div>
<ConditionallyRender
condition={!env.enabled}
show={
<Chip
size="small"
variant="outlined"
label="Disabled"
sx={{ ml: 1 }}
<ConditionallyRender
condition={
!Boolean(new Set(globalStore.hiddenEnvironments).has(env.name))
}
show={
<StyledFeatureOverviewEnvironment enabled={env.enabled}>
<StyledAccordion
data-testid={`${FEATURE_ENVIRONMENT_ACCORDION}_${env.name}`}
>
<StyledAccordionSummary
expandIcon={<ExpandMore titleAccess="Toggle" />}
>
<StyledHeader data-loading enabled={env.enabled}>
<StyledHeaderTitle>
<StyledEnvironmentIcon
enabled={env.enabled}
/>
}
/>
</StyledHeaderTitle>
<StyledContainer>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="text"
/>
<FeatureStrategyIcons
strategies={featureEnvironment?.strategies}
/>
</StyledContainer>
</StyledHeader>
<FeatureOverviewEnvironmentMetrics
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,
}}
>
<div>
<StyledStringTruncator
text={env.name}
maxWidth="100"
maxLength={15}
/>
</div>
<ConditionallyRender
condition={!env.enabled}
show={
<Chip
size="small"
variant="outlined"
label="Disabled"
sx={{ ml: 1 }}
/>
}
/>
</StyledHeaderTitle>
<StyledContainer>
<FeatureStrategyMenu
label="Add strategy"
projectId={projectId}
featureId={featureId}
environmentId={env.name}
variant="text"
/>
</Box>
<EnvironmentFooter
environmentMetric={environmentMetric}
/>
</>
}
/>
</StyledAccordionDetails>
</StyledAccordion>
</StyledFeatureOverviewEnvironment>
<FeatureStrategyIcons
strategies={
featureEnvironment?.strategies
}
/>
</StyledContainer>
</StyledHeader>
<FeatureOverviewEnvironmentMetrics
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 featureId = useRequiredPathParam('featureId');
const { feature } = useFeature(projectId, featureId);
@ -64,6 +72,8 @@ export const FeatureOverviewSidePanel = () => {
</StyledHeader>
}
feature={feature}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
<Divider />
<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 { 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<String>;
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}
</StyledLabel>
<FeatureOverviewSidePanelEnvironmentHider
environment={environment}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
/>
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={onChangeRequestToggleClose}

View File

@ -32,11 +32,15 @@ const StyledLink = styled(Link<typeof RouterLink | 'a'>)(() => ({
interface IFeatureOverviewSidePanelEnvironmentSwitchesProps {
feature: IFeatureToggle;
header: React.ReactNode;
hiddenEnvironments: Set<String>;
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 = ({
<FeatureOverviewSidePanelEnvironmentSwitch
key={environment.name}
environment={environment}
hiddenEnvironments={hiddenEnvironments}
setHiddenEnvironments={setHiddenEnvironments}
showInfoBox={() => {
setEnvironmentName(environment.name);
setShowInfoBox(true);

View File

@ -2,6 +2,7 @@ import { createLocalStorage } from 'utils/createLocalStorage';
interface IGlobalStore {
favorites?: boolean;
hiddenEnvironments?: Set<string>;
}
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.
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);
}