mirror of
https://github.com/Unleash/unleash.git
synced 2025-08-18 13:48:58 +02:00
refactor: frontend for feature toggle switch
This commit is contained in:
parent
16575c4f4b
commit
576e907d7c
@ -62,20 +62,24 @@ export const FeatureStrategyProdGuard = ({
|
|||||||
|
|
||||||
// Check if the prod guard dialog should be enabled.
|
// Check if the prod guard dialog should be enabled.
|
||||||
export const useFeatureStrategyProdGuard = (
|
export const useFeatureStrategyProdGuard = (
|
||||||
feature: IFeatureToggle,
|
featureOrType: string | IFeatureToggle,
|
||||||
environmentId: string,
|
environmentId?: string,
|
||||||
): boolean => {
|
): boolean => {
|
||||||
const [settings] = useFeatureStrategyProdGuardSettings();
|
const [settings] = useFeatureStrategyProdGuardSettings();
|
||||||
|
|
||||||
const environment = feature.environments.find((environment) => {
|
|
||||||
return environment.name === environmentId;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (settings.hide) {
|
if (settings.hide) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return environment?.type === PRODUCTION;
|
if (typeof featureOrType === 'string') {
|
||||||
|
return featureOrType === PRODUCTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
return featureOrType?.environments?.some(
|
||||||
|
(environment) =>
|
||||||
|
environment.name === environmentId ||
|
||||||
|
environment.type === PRODUCTION,
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Store the "always hide" prod guard dialog setting in localStorage.
|
// Store the "always hide" prod guard dialog setting in localStorage.
|
||||||
|
@ -4,6 +4,7 @@ import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
|||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
|
||||||
interface IEnableEnvironmentDialogProps {
|
interface IEnableEnvironmentDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -12,7 +13,7 @@ interface IEnableEnvironmentDialogProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
environment?: string;
|
environment?: string;
|
||||||
showBanner?: boolean;
|
showBanner?: boolean;
|
||||||
disabledStrategiesCount: number;
|
disabledStrategiesCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
|
export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
|
||||||
@ -21,7 +22,7 @@ export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
|
|||||||
onActivateDisabledStrategies,
|
onActivateDisabledStrategies,
|
||||||
onClose,
|
onClose,
|
||||||
environment,
|
environment,
|
||||||
disabledStrategiesCount = 0,
|
disabledStrategiesCount,
|
||||||
}) => {
|
}) => {
|
||||||
const projectId = useRequiredPathParam('projectId');
|
const projectId = useRequiredPathParam('projectId');
|
||||||
|
|
||||||
@ -61,8 +62,12 @@ export const EnableEnvironmentDialog: FC<IEnableEnvironmentDialogProps> = ({
|
|||||||
color='text.primary'
|
color='text.primary'
|
||||||
sx={{ mb: (theme) => theme.spacing(2) }}
|
sx={{ mb: (theme) => theme.spacing(2) }}
|
||||||
>
|
>
|
||||||
The feature toggle has {disabledStrategiesCount} disabled
|
<ConditionallyRender
|
||||||
{disabledStrategiesCount === 1 ? ' strategy' : ' strategies'}.
|
condition={disabledStrategiesCount !== undefined}
|
||||||
|
show={<>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'>
|
||||||
You can choose to enable all the disabled strategies or you can
|
You can choose to enable all the disabled strategies or you can
|
||||||
|
@ -5,6 +5,7 @@ import { styled } from '@mui/material';
|
|||||||
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
import StringTruncator from 'component/common/StringTruncator/StringTruncator';
|
||||||
import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider';
|
import { FeatureOverviewSidePanelEnvironmentHider } from './FeatureOverviewSidePanelEnvironmentHider';
|
||||||
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
marginLeft: theme.spacing(-1.5),
|
marginLeft: theme.spacing(-1.5),
|
||||||
@ -58,6 +59,20 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
|||||||
if (callback) callback();
|
if (callback) callback();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const featureEnvironment = feature?.environments?.find(
|
||||||
|
(env) => env.name === environment.name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasStrategies =
|
||||||
|
featureEnvironment?.strategies &&
|
||||||
|
featureEnvironment?.strategies?.length > 0;
|
||||||
|
|
||||||
|
const hasEnabledStrategies =
|
||||||
|
hasStrategies &&
|
||||||
|
featureEnvironment?.strategies?.some(
|
||||||
|
(strategy) => strategy.disabled !== true,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
<StyledLabel>
|
<StyledLabel>
|
||||||
@ -65,9 +80,12 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
|
|||||||
featureId={feature.name}
|
featureId={feature.name}
|
||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
environmentName={environment.name}
|
environmentName={environment.name}
|
||||||
|
type={featureEnvironment?.type}
|
||||||
onToggle={handleToggle}
|
onToggle={handleToggle}
|
||||||
onError={showInfoBox}
|
onError={showInfoBox}
|
||||||
value={enabled}
|
value={enabled}
|
||||||
|
hasStrategies={hasStrategies}
|
||||||
|
hasEnabledStrategies={hasEnabledStrategies}
|
||||||
/>
|
/>
|
||||||
{children ?? defaultContent}
|
{children ?? defaultContent}
|
||||||
</StyledLabel>
|
</StyledLabel>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { useState, VFC } from 'react';
|
import { 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';
|
||||||
@ -7,7 +7,6 @@ import { flexRow } from 'themes/themeStyles';
|
|||||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
|
||||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
|
||||||
@ -17,7 +16,7 @@ import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConf
|
|||||||
import {
|
import {
|
||||||
FeatureStrategyProdGuard,
|
FeatureStrategyProdGuard,
|
||||||
useFeatureStrategyProdGuard,
|
useFeatureStrategyProdGuard,
|
||||||
} from '../../../../feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
|
} from 'component/feature/FeatureStrategy/FeatureStrategyProdGuard/FeatureStrategyProdGuard';
|
||||||
|
|
||||||
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
|
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
|
||||||
mx: 'auto',
|
mx: 'auto',
|
||||||
@ -29,6 +28,9 @@ interface IFeatureToggleSwitchProps {
|
|||||||
environmentName: string;
|
environmentName: string;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
value: boolean;
|
value: boolean;
|
||||||
|
type: string;
|
||||||
|
hasStrategies?: boolean;
|
||||||
|
hasEnabledStrategies?: boolean;
|
||||||
onError?: () => void;
|
onError?: () => void;
|
||||||
onToggle?: (
|
onToggle?: (
|
||||||
projectId: string,
|
projectId: string,
|
||||||
@ -43,6 +45,9 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
featureId,
|
featureId,
|
||||||
environmentName,
|
environmentName,
|
||||||
value,
|
value,
|
||||||
|
type,
|
||||||
|
hasStrategies,
|
||||||
|
hasEnabledStrategies,
|
||||||
onToggle,
|
onToggle,
|
||||||
onError,
|
onError,
|
||||||
}) => {
|
}) => {
|
||||||
@ -60,17 +65,10 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
useOptimisticUpdate<boolean>(value);
|
useOptimisticUpdate<boolean>(value);
|
||||||
|
|
||||||
const [showEnabledDialog, setShowEnabledDialog] = useState(false);
|
const [showEnabledDialog, setShowEnabledDialog] = useState(false);
|
||||||
const { feature } = useFeature(projectId, featureId);
|
const enableProdGuard = useFeatureStrategyProdGuard(type, environmentName);
|
||||||
const enableProdGuard = useFeatureStrategyProdGuard(
|
|
||||||
feature,
|
|
||||||
environmentName,
|
|
||||||
);
|
|
||||||
const [showProdGuard, setShowProdGuard] = useState(false);
|
const [showProdGuard, setShowProdGuard] = useState(false);
|
||||||
|
const featureHasOnlyDisabledStrategies =
|
||||||
const disabledStrategiesCount =
|
hasStrategies && !hasEnabledStrategies;
|
||||||
feature?.environments
|
|
||||||
.find((env) => env.name === environmentName)
|
|
||||||
?.strategies.filter((strategy) => strategy.disabled).length ?? 0;
|
|
||||||
|
|
||||||
const handleToggleEnvironmentOn = async (
|
const handleToggleEnvironmentOn = async (
|
||||||
shouldActivateDisabled = false,
|
shouldActivateDisabled = false,
|
||||||
@ -79,16 +77,16 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
setIsChecked(!isChecked);
|
setIsChecked(!isChecked);
|
||||||
await toggleFeatureEnvironmentOn(
|
await toggleFeatureEnvironmentOn(
|
||||||
projectId,
|
projectId,
|
||||||
feature.name,
|
featureId,
|
||||||
environmentName,
|
environmentName,
|
||||||
shouldActivateDisabled,
|
shouldActivateDisabled,
|
||||||
);
|
);
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `Available in ${environmentName}`,
|
title: `Available in ${environmentName}`,
|
||||||
text: `${feature.name} is now available in ${environmentName} based on its defined strategies.`,
|
text: `${featureId} is now available in ${environmentName} based on its defined strategies.`,
|
||||||
});
|
});
|
||||||
onToggle?.(projectId, feature.name, environmentName, !isChecked);
|
onToggle?.(projectId, featureId, environmentName, !isChecked);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
if (
|
if (
|
||||||
error instanceof Error &&
|
error instanceof Error &&
|
||||||
@ -107,15 +105,15 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
setIsChecked(!isChecked);
|
setIsChecked(!isChecked);
|
||||||
await toggleFeatureEnvironmentOff(
|
await toggleFeatureEnvironmentOff(
|
||||||
projectId,
|
projectId,
|
||||||
feature.name,
|
featureId,
|
||||||
environmentName,
|
environmentName,
|
||||||
);
|
);
|
||||||
setToastData({
|
setToastData({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
title: `Unavailable in ${environmentName}`,
|
title: `Unavailable in ${environmentName}`,
|
||||||
text: `${feature.name} is unavailable in ${environmentName} and its strategies will no longer have any effect.`,
|
text: `${featureId} is unavailable in ${environmentName} and its strategies will no longer have any effect.`,
|
||||||
});
|
});
|
||||||
onToggle?.(projectId, feature.name, environmentName, !isChecked);
|
onToggle?.(projectId, featureId, environmentName, !isChecked);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
setToastApiError(formatUnknownError(error));
|
setToastApiError(formatUnknownError(error));
|
||||||
rollbackIsChecked();
|
rollbackIsChecked();
|
||||||
@ -125,11 +123,11 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
setShowProdGuard(false);
|
setShowProdGuard(false);
|
||||||
if (isChangeRequestConfigured(environmentName)) {
|
if (isChangeRequestConfigured(environmentName)) {
|
||||||
if (featureHasOnlyDisabledStrategies()) {
|
if (featureHasOnlyDisabledStrategies) {
|
||||||
setShowEnabledDialog(true);
|
setShowEnabledDialog(true);
|
||||||
} else {
|
} else {
|
||||||
onChangeRequestToggle(
|
onChangeRequestToggle(
|
||||||
feature.name,
|
featureId,
|
||||||
environmentName,
|
environmentName,
|
||||||
!isChecked,
|
!isChecked,
|
||||||
false,
|
false,
|
||||||
@ -142,7 +140,7 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (featureHasOnlyDisabledStrategies()) {
|
if (featureHasOnlyDisabledStrategies) {
|
||||||
setShowEnabledDialog(true);
|
setShowEnabledDialog(true);
|
||||||
} else {
|
} else {
|
||||||
await handleToggleEnvironmentOn();
|
await handleToggleEnvironmentOn();
|
||||||
@ -159,12 +157,7 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
|
|
||||||
const onActivateStrategies = async () => {
|
const onActivateStrategies = async () => {
|
||||||
if (isChangeRequestConfigured(environmentName)) {
|
if (isChangeRequestConfigured(environmentName)) {
|
||||||
onChangeRequestToggle(
|
onChangeRequestToggle(featureId, environmentName, !isChecked, true);
|
||||||
feature.name,
|
|
||||||
environmentName,
|
|
||||||
!isChecked,
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
await handleToggleEnvironmentOn(true);
|
await handleToggleEnvironmentOn(true);
|
||||||
}
|
}
|
||||||
@ -174,7 +167,7 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
const onAddDefaultStrategy = async () => {
|
const onAddDefaultStrategy = async () => {
|
||||||
if (isChangeRequestConfigured(environmentName)) {
|
if (isChangeRequestConfigured(environmentName)) {
|
||||||
onChangeRequestToggle(
|
onChangeRequestToggle(
|
||||||
feature.name,
|
featureId,
|
||||||
environmentName,
|
environmentName,
|
||||||
!isChecked,
|
!isChecked,
|
||||||
false,
|
false,
|
||||||
@ -185,20 +178,7 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
setShowEnabledDialog(false);
|
setShowEnabledDialog(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const featureHasOnlyDisabledStrategies = () => {
|
const key = `${featureId}-${environmentName}`;
|
||||||
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -225,7 +205,6 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
|
|||||||
isOpen={showEnabledDialog}
|
isOpen={showEnabledDialog}
|
||||||
onClose={() => setShowEnabledDialog(false)}
|
onClose={() => setShowEnabledDialog(false)}
|
||||||
environment={environmentName}
|
environment={environmentName}
|
||||||
disabledStrategiesCount={disabledStrategiesCount}
|
|
||||||
onActivateDisabledStrategies={onActivateStrategies}
|
onActivateDisabledStrategies={onActivateStrategies}
|
||||||
onAddDefaultStrategy={onAddDefaultStrategy}
|
onAddDefaultStrategy={onAddDefaultStrategy}
|
||||||
/>
|
/>
|
||||||
|
@ -100,6 +100,9 @@ type ListItemType = Pick<
|
|||||||
name: string;
|
name: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
variantCount: number;
|
variantCount: number;
|
||||||
|
type: string;
|
||||||
|
hasStrategies: boolean;
|
||||||
|
hasEnabledStrategies: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
someEnabledEnvironmentHasVariants: boolean;
|
someEnabledEnvironmentHasVariants: boolean;
|
||||||
@ -317,6 +320,14 @@ export const ProjectFeatureToggles = ({
|
|||||||
projectId={projectId}
|
projectId={projectId}
|
||||||
featureId={feature.name}
|
featureId={feature.name}
|
||||||
environmentName={name}
|
environmentName={name}
|
||||||
|
type={feature.environments[name].type}
|
||||||
|
hasStrategies={
|
||||||
|
feature.environments[name].hasStrategies
|
||||||
|
}
|
||||||
|
hasEnabledStrategies={
|
||||||
|
feature.environments[name]
|
||||||
|
.hasEnabledStrategies
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasWarning}
|
condition={hasWarning}
|
||||||
|
Loading…
Reference in New Issue
Block a user