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:
parent
c2a3872386
commit
88d649d239
@ -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 {
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -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
|
||||
|
@ -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} />}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -2,6 +2,7 @@ import { createLocalStorage } from 'utils/createLocalStorage';
|
||||
|
||||
interface IGlobalStore {
|
||||
favorites?: boolean;
|
||||
hiddenEnvironments?: Set<string>;
|
||||
}
|
||||
|
||||
export const useGlobalLocalStorage = () => {
|
||||
|
37
frontend/src/hooks/useHiddenEnvironments.ts
Normal file
37
frontend/src/hooks/useHiddenEnvironments.ts
Normal 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,
|
||||
};
|
||||
};
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user