1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-18 13:48:58 +02:00

refactor: split new feature switch into multiple files

This commit is contained in:
Tymoteusz Czech 2023-10-18 14:18:30 +02:00
parent 6e270192a0
commit a748c0f987
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
6 changed files with 348 additions and 339 deletions

View File

@ -64,9 +64,17 @@ export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
>
<ConditionallyRender
condition={disabledStrategiesCount !== undefined}
show={<>The feature toggle has {disabledStrategiesCount} disabled
{disabledStrategiesCount === 1 ? ' strategy' : ' strategies'}.</>}
elseShow={"The feature toggle has disabled strategies."}
show={
<>
The feature toggle has {disabledStrategiesCount}{' '}
disabled
{disabledStrategiesCount === 1
? ' strategy'
: ' strategies'}
.
</>
}
elseShow={'The feature toggle has disabled strategies.'}
/>
</Typography>
<Typography variant='body1' color='text.primary'>

View File

@ -1,48 +1,15 @@
import {
ComponentProps,
useCallback,
useMemo,
useState,
type VFC,
} from 'react';
import { type 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 { formatUnknownError } from 'utils/formatUnknownError';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from 'hooks/useToast';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import {
FeatureStrategyProdGuard,
isProdGuardEnabled,
} from 'component/feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnvironmentDialog';
import { ListItemType } from '../ProjectFeatureToggles.types';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
mx: 'auto',
...flexRow,
}));
type OnFeatureToggleSwitchArgs = {
featureId: string;
projectId: string;
environmentName: string;
environmentType?: string;
hasStrategies?: boolean;
hasEnabledStrategies?: boolean;
isChangeRequestEnabled?: boolean;
changeRequestToggle?: ReturnType<typeof useChangeRequestToggle>;
onRollback?: () => void;
onSuccess?: () => void;
};
type FeatureToggleSwitchProps = {
featureId: string;
projectId: string;
@ -51,225 +18,6 @@ type FeatureToggleSwitchProps = {
onToggle: (newState: boolean, onRollback: () => void) => void;
};
type Middleware = (next: () => void) => void;
const composeAndRunMiddlewares = (middlewares: Middleware[]) => {
const runMiddleware = (currentIndex: number) => {
if (currentIndex < middlewares.length) {
middlewares[currentIndex](() => runMiddleware(currentIndex + 1));
}
};
runMiddleware(0);
};
export const useFeatureToggleSwitch = (projectId: string) => {
const { loading, toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi();
const { setToastData, setToastApiError } = useToast();
const [prodGuardModalState, setProdGuardModalState] = useState<
ComponentProps<typeof FeatureStrategyProdGuard>
>({
open: false,
label: '',
loading: false,
onClose: () => {},
onClick: () => {},
});
const [enableEnvironmentDialogState, setEnableEnvironmentDialogState] =
useState<ComponentProps<typeof EnableEnvironmentDialog>>({
isOpen: false,
environment: '',
onClose: () => {},
onActivateDisabledStrategies: () => {},
onAddDefaultStrategy: () => {},
});
const {
onChangeRequestToggle,
onChangeRequestToggleClose,
onChangeRequestToggleConfirm,
changeRequestDialogDetails,
} = useChangeRequestToggle(projectId);
const [changeRequestDialogCallback, setChangeRequestDialogCallback] =
useState<() => void>();
const onToggle = useCallback(
async (newState: boolean, config: OnFeatureToggleSwitchArgs) => {
let shouldActivateDisabledStrategies = false;
const confirmProductionChanges: Middleware = (next) => {
if (config.isChangeRequestEnabled) {
// skip if change requests are enabled
return next();
}
if (isProdGuardEnabled(config.environmentType || '')) {
setProdGuardModalState({
open: true,
label: `${
!newState ? 'Disable' : 'Enable'
} Environment`,
loading: false,
onClose: () => {
setProdGuardModalState((prev) => ({
...prev,
open: false,
}));
config.onRollback?.();
},
onClick: () => {
setProdGuardModalState((prev) => ({
...prev,
open: false,
loading: true,
}));
next();
},
});
}
return next();
};
const ensureActiveStrategies: Middleware = (next) => {
if (!config.hasStrategies || config.hasEnabledStrategies) {
return next();
}
setEnableEnvironmentDialogState({
isOpen: true,
environment: config.environmentName,
onClose: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
config.onRollback?.();
},
onActivateDisabledStrategies: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
shouldActivateDisabledStrategies = true;
next();
},
onAddDefaultStrategy: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
next();
},
});
};
const addToChangeRequest: Middleware = (next) => {
if (!config.isChangeRequestEnabled) {
return next();
}
setChangeRequestDialogCallback(() => {
setChangeRequestDialogCallback(undefined);
// always reset to previous state when using change requests
config.onRollback?.();
});
onChangeRequestToggle(
config.featureId,
config.environmentName,
newState,
shouldActivateDisabledStrategies,
);
};
const handleToggleEnvironmentOn: Middleware = async (next) => {
if (newState !== true) {
return next();
}
try {
await toggleFeatureEnvironmentOn(
config.projectId,
config.featureId,
config.environmentName,
shouldActivateDisabledStrategies,
);
setToastData({
type: 'success',
title: `Available in ${config.environmentName}`,
text: `${config.featureId} is now available in ${config.environmentName} based on its defined strategies.`,
});
config.onSuccess?.();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
config.onRollback?.();
}
};
const handleToggleEnvironmentOff: Middleware = async (next) => {
if (newState !== false) {
return next();
}
try {
await toggleFeatureEnvironmentOff(
config.projectId,
config.featureId,
config.environmentName,
);
setToastData({
type: 'success',
title: `Unavailable in ${config.environmentName}`,
text: `${config.featureId} is unavailable in ${config.environmentName} and its strategies will no longer have any effect.`,
});
config.onSuccess?.();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
config.onRollback?.();
}
};
return composeAndRunMiddlewares([
confirmProductionChanges,
ensureActiveStrategies,
addToChangeRequest,
handleToggleEnvironmentOff,
handleToggleEnvironmentOn,
]);
},
[setProdGuardModalState],
);
const modals = (
<>
<FeatureStrategyProdGuard {...prodGuardModalState} />
<EnableEnvironmentDialog {...enableEnvironmentDialogState} />
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={() => {
changeRequestDialogCallback?.();
onChangeRequestToggleClose();
}}
environment={changeRequestDialogDetails?.environment}
onConfirm={() => {
changeRequestDialogCallback?.();
onChangeRequestToggleConfirm();
}}
messageComponent={
<UpdateEnabledMessage
enabled={changeRequestDialogDetails?.enabled!}
featureName={changeRequestDialogDetails?.featureName!}
environment={changeRequestDialogDetails.environment!}
/>
}
/>
</>
);
return { onToggle, modals };
};
export const FeatureToggleSwitch: VFC<FeatureToggleSwitchProps> = ({
projectId,
featureId,
@ -315,76 +63,3 @@ export const FeatureToggleSwitch: VFC<FeatureToggleSwitchProps> = ({
</>
);
};
const StyledSwitchContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'hasWarning',
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
flexGrow: 0,
...flexRow,
justifyContent: 'center',
...(hasWarning && {
'::before': {
content: '""',
display: 'block',
width: theme.spacing(2),
},
}),
}));
export const createFeatureToggleCell =
(
projectId: string,
environmentName: string,
isChangeRequestEnabled: boolean,
refetch: () => void,
onFeatureToggleSwitch: ReturnType<
typeof useFeatureToggleSwitch
>['onToggle'],
) =>
({
value,
row: { original: feature },
}: {
value: boolean;
row: { original: ListItemType };
}) => {
const environment = feature.environments[environmentName];
const hasWarning = useMemo(
() =>
feature.someEnabledEnvironmentHasVariants &&
environment.variantCount === 0 &&
environment.enabled,
[feature, environment],
);
const onToggle = (newState: boolean, onRollback: () => void) => {
onFeatureToggleSwitch(newState, {
projectId,
featureId: feature.name,
environmentName,
environmentType: environment?.type,
hasStrategies: environment?.hasStrategies,
hasEnabledStrategies: environment?.hasEnabledStrategies,
isChangeRequestEnabled,
onRollback,
onSuccess: refetch,
});
};
return (
<StyledSwitchContainer hasWarning={hasWarning}>
<FeatureToggleSwitch
projectId={projectId}
value={value}
featureId={feature.name}
environmentName={environmentName}
onToggle={onToggle}
/>
<ConditionallyRender
condition={hasWarning}
show={<VariantsWarningTooltip />}
/>
</StyledSwitchContainer>
);
};

View File

@ -0,0 +1,18 @@
import { ReactNode } from 'react';
export type OnFeatureToggleSwitchArgs = {
featureId: string;
projectId: string;
environmentName: string;
environmentType?: string;
hasStrategies?: boolean;
hasEnabledStrategies?: boolean;
isChangeRequestEnabled?: boolean;
onRollback?: () => void;
onSuccess?: () => void;
};
export type UseFeatureToggleSwitchType = (projectId: string) => {
modals: ReactNode;
onToggle: (newState: boolean, config: OnFeatureToggleSwitchArgs) => void;
};

View File

@ -0,0 +1,79 @@
import { useMemo } from 'react';
import { styled } from '@mui/material';
import { flexRow } from 'themes/themeStyles';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
import { FeatureToggleSwitch } from './NewFeatureToggleSwitch';
import type { ListItemType } from '../ProjectFeatureToggles.types';
import type { UseFeatureToggleSwitchType } from './NewFeatureToggleSwitch.types';
const StyledSwitchContainer = styled('div', {
shouldForwardProp: (prop) => prop !== 'hasWarning',
})<{ hasWarning?: boolean }>(({ theme, hasWarning }) => ({
flexGrow: 0,
...flexRow,
justifyContent: 'center',
...(hasWarning && {
'::before': {
content: '""',
display: 'block',
width: theme.spacing(2),
},
}),
}));
export const createFeatureToggleCell =
(
projectId: string,
environmentName: string,
isChangeRequestEnabled: boolean,
refetch: () => void,
onFeatureToggleSwitch: ReturnType<UseFeatureToggleSwitchType>['onToggle'],
) =>
({
value,
row: { original: feature },
}: {
value: boolean;
row: { original: ListItemType };
}) => {
const environment = feature.environments[environmentName];
const hasWarning = useMemo(
() =>
feature.someEnabledEnvironmentHasVariants &&
environment.variantCount === 0 &&
environment.enabled,
[feature, environment],
);
const onToggle = (newState: boolean, onRollback: () => void) => {
onFeatureToggleSwitch(newState, {
projectId,
featureId: feature.name,
environmentName,
environmentType: environment?.type,
hasStrategies: environment?.hasStrategies,
hasEnabledStrategies: environment?.hasEnabledStrategies,
isChangeRequestEnabled,
onRollback,
onSuccess: refetch,
});
};
return (
<StyledSwitchContainer hasWarning={hasWarning}>
<FeatureToggleSwitch
projectId={projectId}
value={value}
featureId={feature.name}
environmentName={environmentName}
onToggle={onToggle}
/>
<ConditionallyRender
condition={hasWarning}
show={<VariantsWarningTooltip />}
/>
</StyledSwitchContainer>
);
};

View File

@ -0,0 +1,237 @@
import { ComponentProps, useCallback, useState } from 'react';
import { formatUnknownError } from 'utils/formatUnknownError';
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
import useToast from 'hooks/useToast';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import {
FeatureStrategyProdGuard,
isProdGuardEnabled,
} from 'component/feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
import { EnableEnvironmentDialog } from './EnableEnvironmentDialog/EnableEnvironmentDialog';
import {
OnFeatureToggleSwitchArgs,
UseFeatureToggleSwitchType,
} from './NewFeatureToggleSwitch.types';
type Middleware = (next: () => void) => void;
const composeAndRunMiddlewares = (middlewares: Middleware[]) => {
const runMiddleware = (currentIndex: number) => {
if (currentIndex < middlewares.length) {
middlewares[currentIndex](() => runMiddleware(currentIndex + 1));
}
};
runMiddleware(0);
};
export const useFeatureToggleSwitch: UseFeatureToggleSwitchType = (
projectId: string,
) => {
const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
useFeatureApi();
const { setToastData, setToastApiError } = useToast();
const [prodGuardModalState, setProdGuardModalState] = useState<
ComponentProps<typeof FeatureStrategyProdGuard>
>({
open: false,
label: '',
loading: false,
onClose: () => {},
onClick: () => {},
});
const [enableEnvironmentDialogState, setEnableEnvironmentDialogState] =
useState<ComponentProps<typeof EnableEnvironmentDialog>>({
isOpen: false,
environment: '',
onClose: () => {},
onActivateDisabledStrategies: () => {},
onAddDefaultStrategy: () => {},
});
const {
onChangeRequestToggle,
onChangeRequestToggleClose,
onChangeRequestToggleConfirm,
changeRequestDialogDetails,
} = useChangeRequestToggle(projectId);
const [changeRequestDialogCallback, setChangeRequestDialogCallback] =
useState<() => void>();
const onToggle = useCallback(
async (newState: boolean, config: OnFeatureToggleSwitchArgs) => {
let shouldActivateDisabledStrategies = false;
const confirmProductionChanges: Middleware = (next) => {
if (config.isChangeRequestEnabled) {
// skip if change requests are enabled
return next();
}
if (isProdGuardEnabled(config.environmentType || '')) {
setProdGuardModalState({
open: true,
label: `${
!newState ? 'Disable' : 'Enable'
} Environment`,
loading: false,
onClose: () => {
setProdGuardModalState((prev) => ({
...prev,
open: false,
}));
config.onRollback?.();
},
onClick: () => {
setProdGuardModalState((prev) => ({
...prev,
open: false,
loading: true,
}));
next();
},
});
}
return next();
};
const ensureActiveStrategies: Middleware = (next) => {
if (!config.hasStrategies || config.hasEnabledStrategies) {
return next();
}
setEnableEnvironmentDialogState({
isOpen: true,
environment: config.environmentName,
onClose: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
config.onRollback?.();
},
onActivateDisabledStrategies: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
shouldActivateDisabledStrategies = true;
next();
},
onAddDefaultStrategy: () => {
setEnableEnvironmentDialogState((prev) => ({
...prev,
isOpen: false,
}));
next();
},
});
};
const addToChangeRequest: Middleware = (next) => {
if (!config.isChangeRequestEnabled) {
return next();
}
setChangeRequestDialogCallback(() => {
setChangeRequestDialogCallback(undefined);
// always reset to previous state when using change requests
config.onRollback?.();
});
onChangeRequestToggle(
config.featureId,
config.environmentName,
newState,
shouldActivateDisabledStrategies,
);
};
const handleToggleEnvironmentOn: Middleware = async (next) => {
if (newState !== true) {
return next();
}
try {
await toggleFeatureEnvironmentOn(
config.projectId,
config.featureId,
config.environmentName,
shouldActivateDisabledStrategies,
);
setToastData({
type: 'success',
title: `Enabled in ${config.environmentName}`,
text: `${config.featureId} is now available in ${config.environmentName} based on its defined strategies.`,
});
config.onSuccess?.();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
config.onRollback?.();
}
};
const handleToggleEnvironmentOff: Middleware = async (next) => {
if (newState !== false) {
return next();
}
try {
await toggleFeatureEnvironmentOff(
config.projectId,
config.featureId,
config.environmentName,
);
setToastData({
type: 'success',
title: `Disabled in ${config.environmentName}`,
text: `${config.featureId} is unavailable in ${config.environmentName} and its strategies will no longer have any effect.`,
});
config.onSuccess?.();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
config.onRollback?.();
}
};
return composeAndRunMiddlewares([
confirmProductionChanges,
ensureActiveStrategies,
addToChangeRequest,
handleToggleEnvironmentOff,
handleToggleEnvironmentOn,
]);
},
[setProdGuardModalState],
);
const modals = (
<>
<FeatureStrategyProdGuard {...prodGuardModalState} />
<EnableEnvironmentDialog {...enableEnvironmentDialogState} />
<ChangeRequestDialogue
isOpen={changeRequestDialogDetails.isOpen}
onClose={() => {
changeRequestDialogCallback?.();
onChangeRequestToggleClose();
}}
environment={changeRequestDialogDetails?.environment}
onConfirm={() => {
changeRequestDialogCallback?.();
onChangeRequestToggleConfirm();
}}
messageComponent={
<UpdateEnabledMessage
enabled={changeRequestDialogDetails?.enabled!}
featureName={changeRequestDialogDetails?.featureName!}
environment={changeRequestDialogDetails.environment!}
/>
}
/>
</>
);
return { onToggle, modals };
};

View File

@ -38,9 +38,6 @@ import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureS
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
import { Search } from 'component/common/Search/Search';
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
import { IFeatureToggleListItem } from 'interfaces/featureToggle';
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
@ -48,11 +45,6 @@ import {
ProjectEnvironmentType,
useEnvironmentsRef,
} from './hooks/useEnvironmentsRef';
import {
FeatureToggleSwitch,
createFeatureToggleCell,
useFeatureToggleSwitch,
} from './FeatureToggleSwitch/NewFeatureToggleSwitch';
import { ActionsCell } from './ActionsCell/ActionsCell';
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
import { useStyles } from './ProjectFeatureToggles.styles';
@ -60,8 +52,6 @@ import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
import { flexRow } from 'themes/themeStyles';
import VariantsWarningTooltip from 'component/feature/FeatureView/FeatureVariants/VariantsTooltipWarning';
import FileDownload from '@mui/icons-material/FileDownload';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
@ -71,6 +61,8 @@ import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/Proje
import { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { ListItemType } from './ProjectFeatureToggles.types';
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap',