1
0
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:
Tymoteusz Czech 2023-10-12 14:45:12 +02:00
parent 16575c4f4b
commit 576e907d7c
No known key found for this signature in database
GPG Key ID: 133555230D88D75F
5 changed files with 72 additions and 55 deletions

View File

@ -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.

View File

@ -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

View File

@ -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>

View File

@ -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}
/> />

View File

@ -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}