1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

feat: improved feature overview sidepanel env toggles (#2487)

https://linear.app/unleash/issue/2-423/update-feature-toggle-overview-sidepanel
This commit is contained in:
Nuno Góis 2022-11-22 08:50:31 +00:00 committed by GitHub
parent 97372cf48c
commit 3dca3d53f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 310 additions and 1 deletions

View File

@ -11,8 +11,12 @@ import {
} from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
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';
const FeatureOverview = () => {
const { uiConfig } = useUiConfig();
const { classes: styles } = useStyles();
const navigate = useNavigate();
const projectId = useRequiredPathParam('projectId');
@ -25,7 +29,11 @@ const FeatureOverview = () => {
<div className={styles.container}>
<div>
<FeatureOverviewMetaData />
<FeatureOverviewEnvSwitches />
<ConditionallyRender
condition={Boolean(uiConfig.flags.variantsPerEnvironment)}
show={<FeatureOverviewSidePanel />}
elseShow={<FeatureOverviewEnvSwitches />}
/>
</div>
<div className={styles.mainContent}>
<FeatureOverviewEnvironments />

View File

@ -0,0 +1,61 @@
import { styled } from '@mui/material';
import { HelpIcon } from 'component/common/HelpIcon/HelpIcon';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches';
const StyledContainer = styled('div')(({ theme }) => ({
borderRadius: theme.shape.borderRadiusLarge,
backgroundColor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
padding: '1.5rem',
maxWidth: '350px',
minWidth: '350px',
marginRight: '1rem',
marginTop: '1rem',
[theme.breakpoints.down(1000)]: {
marginBottom: '1rem',
width: '100%',
maxWidth: 'none',
minWidth: 'auto',
},
}));
const StyledHeader = styled('h3')(({ theme }) => ({
display: 'flex',
gap: theme.spacing(1),
alignItems: 'center',
fontSize: theme.fontSizes.bodySize,
margin: 0,
marginBottom: theme.spacing(3),
// Make the help icon align with the text.
'& > :last-child': {
position: 'relative',
top: 1,
},
}));
export const FeatureOverviewSidePanel = () => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { feature } = useFeature(projectId, featureId);
return (
<StyledContainer>
<FeatureOverviewSidePanelEnvironmentSwitches
header={
<StyledHeader data-loading>
Enabled in environments ({feature.environments.length})
<HelpIcon
tooltip="When a feature is switched off in an environment, it will always return false. When switched on, it will return true or false depending on its strategies."
placement="top"
/>
</StyledHeader>
}
feature={feature}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,146 @@
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import useToast from 'hooks/useToast';
import { IFeatureEnvironment } from 'interfaces/featureToggle';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { styled } from '@mui/material';
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
const StyledContainer = styled('div')(({ theme }) => ({
'&:not(:last-of-type)': {
marginBottom: theme.spacing(2),
},
}));
const StyledLabel = styled('label')(({ theme }) => ({
display: 'inline-flex',
alignItems: 'center',
cursor: 'pointer',
}));
interface IFeatureOverviewSidePanelEnvironmentSwitchProps {
env: IFeatureEnvironment;
callback?: () => void;
showInfoBox: () => void;
children?: React.ReactNode;
}
export const FeatureOverviewSidePanelEnvironmentSwitch = ({
env,
callback,
showInfoBox,
children,
}: IFeatureOverviewSidePanelEnvironmentSwitchProps) => {
const projectId = useRequiredPathParam('projectId');
const featureId = useRequiredPathParam('featureId');
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi();
const { refetchFeature } = useFeature(projectId, featureId);
const { setToastData, setToastApiError } = useToast();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const {
onChangeRequestToggle,
onChangeRequestToggleClose,
onChangeRequestToggleConfirm,
changeRequestDialogDetails,
} = useChangeRequestToggle(projectId);
const handleToggleEnvironmentOn = async () => {
try {
await toggleFeatureEnvironmentOn(projectId, featureId, env.name);
setToastData({
type: 'success',
title: `Available in ${env.name}`,
text: `${featureId} is now available in ${env.name} based on its defined strategies.`,
});
refetchFeature();
if (callback) {
callback();
}
} catch (error: unknown) {
if (
error instanceof Error &&
error.message === ENVIRONMENT_STRATEGY_ERROR
) {
showInfoBox();
} else {
setToastApiError(formatUnknownError(error));
}
}
};
const handleToggleEnvironmentOff = async () => {
try {
await toggleFeatureEnvironmentOff(projectId, featureId, env.name);
setToastData({
type: 'success',
title: `Unavailable in ${env.name}`,
text: `${featureId} is unavailable in ${env.name} and its strategies will no longer have any effect.`,
});
refetchFeature();
if (callback) {
callback();
}
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
};
const toggleEnvironment = async (e: React.ChangeEvent) => {
if (isChangeRequestConfigured(env.name)) {
e.preventDefault();
onChangeRequestToggle(featureId, env.name, !env.enabled);
return;
}
if (env.enabled) {
await handleToggleEnvironmentOff();
return;
}
await handleToggleEnvironmentOn();
};
const defaultContent = (
<>
{' '}
<span data-loading>{env.enabled ? 'enabled' : 'disabled'} in</span>
&nbsp;
<StringTruncator text={env.name} maxWidth="120" maxLength={15} />
</>
);
return (
<StyledContainer>
<StyledLabel>
<PermissionSwitch
permission={UPDATE_FEATURE_ENVIRONMENT}
projectId={projectId}
checked={env.enabled}
onChange={toggleEnvironment}
environmentId={env.name}
/>
{children ?? defaultContent}
</StyledLabel>
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={onChangeRequestToggleClose}
environment={changeRequestDialogDetails?.environment}
onConfirm={onChangeRequestToggleConfirm}
messageComponent={
<UpdateEnabledMessage
enabled={changeRequestDialogDetails?.enabled!}
featureName={changeRequestDialogDetails?.featureName!}
environment={changeRequestDialogDetails.environment!}
/>
}
/>
</StyledContainer>
);
};

View File

@ -0,0 +1,94 @@
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
import { IFeatureToggle } from 'interfaces/featureToggle';
import { useState } from 'react';
import { FeatureOverviewSidePanelEnvironmentSwitch } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/FeatureOverviewSidePanelEnvironmentSwitch';
import { Link, styled } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
}));
const StyledLabel = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
}));
const StyledSubLabel = styled('p')(({ theme }) => ({
fontSize: theme.fontSizes.smallerBody,
color: theme.palette.text.secondary,
}));
const StyledLink = styled(Link<typeof RouterLink | 'a'>)(() => ({
'&:hover, &:focus': {
textDecoration: 'underline',
},
}));
interface IFeatureOverviewSidePanelEnvironmentSwitchesProps {
feature: IFeatureToggle;
header: React.ReactNode;
}
export const FeatureOverviewSidePanelEnvironmentSwitches = ({
feature,
header,
}: IFeatureOverviewSidePanelEnvironmentSwitchesProps) => {
const [showInfoBox, setShowInfoBox] = useState(false);
const [environmentName, setEnvironmentName] = useState('');
return (
<>
{header}
{feature.environments.map(environment => {
const strategiesLabel =
environment.strategies.length === 1
? '1 strategy'
: `${environment.strategies.length} strategies`;
const variants = environment.variants ?? [];
const variantsLink = variants.length > 0 && (
<>
{' - '}
<StyledLink
component={RouterLink}
to={`/projects/${feature.project}/features/${feature.name}/variants`}
underline="hover"
>
{variants.length === 1
? '1 variant'
: `${variants.length} variants`}
</StyledLink>
</>
);
return (
<FeatureOverviewSidePanelEnvironmentSwitch
key={environment.name}
env={environment}
showInfoBox={() => {
setEnvironmentName(environment.name);
setShowInfoBox(true);
}}
>
<StyledContainer>
<StyledLabel>{environment.name}</StyledLabel>
<StyledSubLabel>
{strategiesLabel}
{variantsLink}
</StyledSubLabel>
</StyledContainer>
</FeatureOverviewSidePanelEnvironmentSwitch>
);
})}
<EnvironmentStrategyDialog
open={showInfoBox}
onClose={() => setShowInfoBox(false)}
projectId={feature.project}
featureId={feature.name}
environmentName={environmentName}
/>
</>
);
};