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:
parent
6e270192a0
commit
a748c0f987
@ -64,9 +64,17 @@ export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
|
|||||||
>
|
>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={disabledStrategiesCount !== undefined}
|
condition={disabledStrategiesCount !== undefined}
|
||||||
show={<>The feature toggle has {disabledStrategiesCount} disabled
|
show={
|
||||||
{disabledStrategiesCount === 1 ? ' strategy' : ' strategies'}.</>}
|
<>
|
||||||
elseShow={"The feature toggle has disabled strategies."}
|
The feature toggle has {disabledStrategiesCount}{' '}
|
||||||
|
disabled
|
||||||
|
{disabledStrategiesCount === 1
|
||||||
|
? ' strategy'
|
||||||
|
: ' strategies'}
|
||||||
|
.
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
elseShow={'The feature toggle has disabled strategies.'}
|
||||||
/>
|
/>
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant='body1' color='text.primary'>
|
<Typography variant='body1' color='text.primary'>
|
||||||
|
@ -1,48 +1,15 @@
|
|||||||
import {
|
import { type VFC } from 'react';
|
||||||
ComponentProps,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
useState,
|
|
||||||
type VFC,
|
|
||||||
} from 'react';
|
|
||||||
import { Box, styled } from '@mui/material';
|
import { Box, styled } from '@mui/material';
|
||||||
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
|
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
|
||||||
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
|
||||||
import { useOptimisticUpdate } from './hooks/useOptimisticUpdate';
|
import { useOptimisticUpdate } from './hooks/useOptimisticUpdate';
|
||||||
import { flexRow } from 'themes/themeStyles';
|
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 }>(() => ({
|
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
|
||||||
mx: 'auto',
|
mx: 'auto',
|
||||||
...flexRow,
|
...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 = {
|
type FeatureToggleSwitchProps = {
|
||||||
featureId: string;
|
featureId: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -51,225 +18,6 @@ type FeatureToggleSwitchProps = {
|
|||||||
onToggle: (newState: boolean, onRollback: () => void) => void;
|
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> = ({
|
export const FeatureToggleSwitch: VFC<FeatureToggleSwitchProps> = ({
|
||||||
projectId,
|
projectId,
|
||||||
featureId,
|
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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@ -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;
|
||||||
|
};
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
@ -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 };
|
||||||
|
};
|
@ -38,9 +38,6 @@ import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureS
|
|||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
|
||||||
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
import { getColumnValues, includesFilter, useSearch } from 'hooks/useSearch';
|
||||||
import { Search } from 'component/common/Search/Search';
|
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 { IFeatureToggleListItem } from 'interfaces/featureToggle';
|
||||||
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
import { FavoriteIconHeader } from 'component/common/Table/FavoriteIconHeader/FavoriteIconHeader';
|
||||||
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
import { FavoriteIconCell } from 'component/common/Table/cells/FavoriteIconCell/FavoriteIconCell';
|
||||||
@ -48,11 +45,6 @@ import {
|
|||||||
ProjectEnvironmentType,
|
ProjectEnvironmentType,
|
||||||
useEnvironmentsRef,
|
useEnvironmentsRef,
|
||||||
} from './hooks/useEnvironmentsRef';
|
} from './hooks/useEnvironmentsRef';
|
||||||
import {
|
|
||||||
FeatureToggleSwitch,
|
|
||||||
createFeatureToggleCell,
|
|
||||||
useFeatureToggleSwitch,
|
|
||||||
} from './FeatureToggleSwitch/NewFeatureToggleSwitch';
|
|
||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
|
import { ActionsCell } from './ActionsCell/ActionsCell';
|
||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
|
||||||
import { useStyles } from './ProjectFeatureToggles.styles';
|
import { useStyles } from './ProjectFeatureToggles.styles';
|
||||||
@ -60,8 +52,6 @@ import { usePinnedFavorites } from 'hooks/usePinnedFavorites';
|
|||||||
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
import { useFavoriteFeaturesApi } from 'hooks/api/actions/useFavoriteFeaturesApi/useFavoriteFeaturesApi';
|
||||||
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
import { FeatureTagCell } from 'component/common/Table/cells/FeatureTagCell/FeatureTagCell';
|
||||||
import { useGlobalLocalStorage } from 'hooks/useGlobalLocalStorage';
|
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 FileDownload from '@mui/icons-material/FileDownload';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
|
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 { FeatureEnvironmentSeenCell } from '../../../common/Table/cells/FeatureSeenCell/FeatureEnvironmentSeenCell';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { ListItemType } from './ProjectFeatureToggles.types';
|
import { ListItemType } from './ProjectFeatureToggles.types';
|
||||||
|
import { createFeatureToggleCell } from './FeatureToggleSwitch/createFeatureToggleCell';
|
||||||
|
import { useFeatureToggleSwitch } from './FeatureToggleSwitch/useFeatureToggleSwitch';
|
||||||
|
|
||||||
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
|
||||||
whiteSpace: 'nowrap',
|
whiteSpace: 'nowrap',
|
||||||
|
Loading…
Reference in New Issue
Block a user