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:
parent
97372cf48c
commit
3dca3d53f9
@ -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 />
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
|
||||
<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>
|
||||
);
|
||||
};
|
@ -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}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user