1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-04-06 01:15:28 +02:00
unleash.unleash/frontend/src/component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch.tsx
andreas-unleash e7b1e7979e
Feat: add prod guard when toggling envs (#4774)
Adds the prod guard dialog when enabling/disabling production
environment

Closes # [1-1386]
(https://linear.app/unleash/issue/1-1386/production-guard-modal-for-enablingdisabling-environment-is-gone)



https://github.com/Unleash/unleash/assets/104830839/0041bfc8-872b-455c-b4fa-e03cc160c3e9

---------

Signed-off-by: andreas-unleash <andreas@getunleash.ai>
2023-09-26 12:16:26 +03:00

257 lines
9.2 KiB
TypeScript

import React, { useState, VFC } from 'react';
import { Box, styled } from '@mui/material';
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
import { useOptimisticUpdate } from './hooks/useOptimisticUpdate';
import { flexRow } from 'themes/themeStyles';
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
import { formatUnknownError } from 'utils/formatUnknownError';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
import useToast from 'hooks/useToast';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { EnableEnvironmentDialog } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitch/EnableEnvironmentDialog';
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import {
FeatureStrategyProdGuard,
useFeatureStrategyProdGuard,
} from '../../../../feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
mx: 'auto',
...flexRow,
}));
interface IFeatureToggleSwitchProps {
featureId: string;
environmentName: string;
projectId: string;
value: boolean;
onError?: () => void;
onToggle?: (
projectId: string,
feature: string,
env: string,
state: boolean
) => void;
}
export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
projectId,
featureId,
environmentName,
value,
onToggle,
onError,
}) => {
const { loading, toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi();
const { setToastData, setToastApiError } = useToast();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const {
onChangeRequestToggle,
onChangeRequestToggleClose,
onChangeRequestToggleConfirm,
changeRequestDialogDetails,
} = useChangeRequestToggle(projectId);
const [isChecked, setIsChecked, rollbackIsChecked] =
useOptimisticUpdate<boolean>(value);
const [showEnabledDialog, setShowEnabledDialog] = useState(false);
const { feature } = useFeature(projectId, featureId);
const enableProdGuard = useFeatureStrategyProdGuard(
feature,
environmentName
);
const [showProdGuard, setShowProdGuard] = useState(false);
const disabledStrategiesCount =
feature?.environments
.find(env => env.name === environmentName)
?.strategies.filter(strategy => strategy.disabled).length ?? 0;
const handleToggleEnvironmentOn = async (
shouldActivateDisabled = false
) => {
try {
setIsChecked(!isChecked);
await toggleFeatureEnvironmentOn(
projectId,
feature.name,
environmentName,
shouldActivateDisabled
);
setToastData({
type: 'success',
title: `Available in ${environmentName}`,
text: `${feature.name} is now available in ${environmentName} based on its defined strategies.`,
});
onToggle?.(projectId, feature.name, environmentName, !isChecked);
} catch (error: unknown) {
if (
error instanceof Error &&
error.message === ENVIRONMENT_STRATEGY_ERROR
) {
onError?.();
} else {
setToastApiError(formatUnknownError(error));
}
rollbackIsChecked();
}
};
const handleToggleEnvironmentOff = async () => {
try {
setIsChecked(!isChecked);
await toggleFeatureEnvironmentOff(
projectId,
feature.name,
environmentName
);
setToastData({
type: 'success',
title: `Unavailable in ${environmentName}`,
text: `${feature.name} is unavailable in ${environmentName} and its strategies will no longer have any effect.`,
});
onToggle?.(projectId, feature.name, environmentName, !isChecked);
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
rollbackIsChecked();
}
};
const handleClick = async () => {
setShowProdGuard(false);
if (isChangeRequestConfigured(environmentName)) {
if (featureHasOnlyDisabledStrategies()) {
setShowEnabledDialog(true);
} else {
onChangeRequestToggle(
feature.name,
environmentName,
!isChecked,
false
);
}
return;
}
if (isChecked) {
await handleToggleEnvironmentOff();
return;
}
if (featureHasOnlyDisabledStrategies()) {
setShowEnabledDialog(true);
} else {
await handleToggleEnvironmentOn();
}
};
const onClick = async () => {
console.log('*********');
console.log(enableProdGuard);
console.log(isChangeRequestConfigured(environmentName));
console.log('*********');
if (enableProdGuard && !isChangeRequestConfigured(environmentName)) {
setShowProdGuard(true);
} else {
await handleClick();
}
};
const onActivateStrategies = async () => {
if (isChangeRequestConfigured(environmentName)) {
onChangeRequestToggle(
feature.name,
environmentName,
!isChecked,
true
);
} else {
await handleToggleEnvironmentOn(true);
}
setShowEnabledDialog(false);
};
const onAddDefaultStrategy = async () => {
if (isChangeRequestConfigured(environmentName)) {
onChangeRequestToggle(
feature.name,
environmentName,
!isChecked,
false
);
} else {
await handleToggleEnvironmentOn();
}
setShowEnabledDialog(false);
};
const featureHasOnlyDisabledStrategies = () => {
const featureEnvironment = feature?.environments?.find(
env => env.name === environmentName
);
return (
featureEnvironment?.strategies &&
featureEnvironment?.strategies?.length > 0 &&
featureEnvironment?.strategies?.every(strategy => strategy.disabled)
);
};
const key = `${feature.name}-${environmentName}`;
return (
<>
<StyledBoxContainer
key={key} // Prevent animation when archiving rows
data-testid={`TOGGLE-${key}`}
>
<PermissionSwitch
tooltip={
isChecked
? `Disable feature in ${environmentName}`
: `Enable feature in ${environmentName}`
}
checked={isChecked}
environmentId={environmentName}
projectId={projectId}
permission={UPDATE_FEATURE_ENVIRONMENT}
inputProps={{ 'aria-label': environmentName }}
onClick={onClick}
data-testId={'permission-switch'}
/>
</StyledBoxContainer>
<EnableEnvironmentDialog
isOpen={showEnabledDialog}
onClose={() => setShowEnabledDialog(false)}
environment={environmentName}
disabledStrategiesCount={disabledStrategiesCount}
onActivateDisabledStrategies={onActivateStrategies}
onAddDefaultStrategy={onAddDefaultStrategy}
/>
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={onChangeRequestToggleClose}
environment={changeRequestDialogDetails?.environment}
onConfirm={onChangeRequestToggleConfirm}
messageComponent={
<UpdateEnabledMessage
enabled={changeRequestDialogDetails?.enabled!}
featureName={changeRequestDialogDetails?.featureName!}
environment={changeRequestDialogDetails.environment!}
/>
}
/>
<FeatureStrategyProdGuard
open={showProdGuard}
onClose={() => setShowProdGuard(false)}
onClick={handleClick}
loading={loading}
label={`${isChecked ? 'Disable' : 'Enable'} Environment`}
/>
</>
);
};