mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	Fix/cr should activate disabled lag free (#3826)
<!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> - Adds change request option to activate disabled strategies UI - Fixes Disable strategy bug (onSuggestDisable) ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes # <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
		
							parent
							
								
									6b21a8b809
								
							
						
					
					
						commit
						8aadbc8ae9
					
				@ -120,8 +120,8 @@ describe('imports', () => {
 | 
				
			|||||||
        cy.get(
 | 
					        cy.get(
 | 
				
			||||||
            "[data-testid='feature-toggle-status'] input[type='checkbox']:checked"
 | 
					            "[data-testid='feature-toggle-status'] input[type='checkbox']:checked"
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
            .closest('div')
 | 
					            .invoke('attr', 'aria-label')
 | 
				
			||||||
            .contains('development');
 | 
					            .should('eq', 'development');
 | 
				
			||||||
        cy.contains('50%');
 | 
					        cy.contains('50%');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
@ -86,8 +86,6 @@ export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
 | 
				
			|||||||
                    <Truncated>
 | 
					                    <Truncated>
 | 
				
			||||||
                        <Typography component="s" color="text.secondary">
 | 
					                        <Typography component="s" color="text.secondary">
 | 
				
			||||||
                            {previousTitle}
 | 
					                            {previousTitle}
 | 
				
			||||||
                            PREVIOUS consectetur adipiscing elit, sed do eiusmod
 | 
					 | 
				
			||||||
                            tempor incididunt ut labore et dolore magna aliqua.
 | 
					 | 
				
			||||||
                        </Typography>{' '}
 | 
					                        </Typography>{' '}
 | 
				
			||||||
                    </Truncated>
 | 
					                    </Truncated>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
@ -103,9 +101,6 @@ export const StrategyTooltipLink: FC<IStrategyTooltipLinkProps> = ({
 | 
				
			|||||||
                    <Typography component="span">
 | 
					                    <Typography component="span">
 | 
				
			||||||
                        {change.payload.title ||
 | 
					                        {change.payload.title ||
 | 
				
			||||||
                            formatStrategyName(change.payload.name)}
 | 
					                            formatStrategyName(change.payload.name)}
 | 
				
			||||||
                        lorem ipsum dolor sit amet, consectetur adipiscing elit,
 | 
					 | 
				
			||||||
                        sed do eiusmod tempor incididunt ut labore et dolore
 | 
					 | 
				
			||||||
                        magna aliqua.
 | 
					 | 
				
			||||||
                    </Typography>
 | 
					                    </Typography>
 | 
				
			||||||
                </TooltipLink>
 | 
					                </TooltipLink>
 | 
				
			||||||
            </Truncated>
 | 
					            </Truncated>
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					import { screen } from '@testing-library/react';
 | 
				
			||||||
 | 
					import { render } from 'utils/testRenderer';
 | 
				
			||||||
 | 
					import { testServerRoute, testServerSetup } from 'utils/testServer';
 | 
				
			||||||
 | 
					import { DisableEnableStrategyDialog } from './DisableEnableStrategyDialog';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const server = testServerSetup();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const defaultProps = {
 | 
				
			||||||
 | 
					    isOpen: true,
 | 
				
			||||||
 | 
					    onClose: () => {},
 | 
				
			||||||
 | 
					    onConfirm: () => {},
 | 
				
			||||||
 | 
					    projectId: 'project1',
 | 
				
			||||||
 | 
					    featureId: 'feature1',
 | 
				
			||||||
 | 
					    environmentId: 'env1',
 | 
				
			||||||
 | 
					    strategy: {
 | 
				
			||||||
 | 
					        id: 'some-id',
 | 
				
			||||||
 | 
					        name: 'flexibleRollout',
 | 
				
			||||||
 | 
					        constraints: [],
 | 
				
			||||||
 | 
					        parameters: {
 | 
				
			||||||
 | 
					            rollout: '50%',
 | 
				
			||||||
 | 
					            stickiness: 'default',
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should render disable dialog in regular mode', async () => {
 | 
				
			||||||
 | 
					    testServerRoute(server, '/api/admin/ui-config', {});
 | 
				
			||||||
 | 
					    testServerRoute(server, '/api/admin/projects/project1', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render(<DisableEnableStrategyDialog {...defaultProps} />);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        screen.queryByText('Change requests are enabled for this environment.')
 | 
				
			||||||
 | 
					    ).not.toBeInTheDocument();
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        screen.getByText('Are you sure you want to disable this strategy?')
 | 
				
			||||||
 | 
					    ).toBeInTheDocument();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					test('should render enable dialog in regular mode', async () => {
 | 
				
			||||||
 | 
					    testServerRoute(server, '/api/admin/ui-config', {});
 | 
				
			||||||
 | 
					    testServerRoute(server, '/api/admin/projects/project1', {});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const props = {
 | 
				
			||||||
 | 
					        ...defaultProps,
 | 
				
			||||||
 | 
					        strategy: { ...defaultProps.strategy, disabled: true },
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    render(<DisableEnableStrategyDialog {...props} />);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(
 | 
				
			||||||
 | 
					        screen.queryByText('Change requests are enabled for this environment.')
 | 
				
			||||||
 | 
					    ).not.toBeInTheDocument();
 | 
				
			||||||
 | 
					    expect(screen.getByText('Enable strategy')).toBeInTheDocument();
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
@ -18,7 +18,9 @@ export const DisableEnableStrategyDialog = ({
 | 
				
			|||||||
    const { projectId, environmentId } = props;
 | 
					    const { projectId, environmentId } = props;
 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
				
			||||||
    const isChangeRequest = isChangeRequestConfigured(environmentId);
 | 
					    const isChangeRequest = isChangeRequestConfigured(environmentId);
 | 
				
			||||||
    const { onSuggestEnable } = useSuggestEnableDisable({ ...props });
 | 
					    const { onSuggestEnable, onSuggestDisable } = useSuggestEnableDisable({
 | 
				
			||||||
 | 
					        ...props,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
    const { onEnable, onDisable } = useEnableDisable({ ...props });
 | 
					    const { onEnable, onDisable } = useEnableDisable({ ...props });
 | 
				
			||||||
    const disabled = Boolean(props.strategy?.disabled);
 | 
					    const disabled = Boolean(props.strategy?.disabled);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -28,7 +30,7 @@ export const DisableEnableStrategyDialog = ({
 | 
				
			|||||||
            if (disabled) {
 | 
					            if (disabled) {
 | 
				
			||||||
                onSuggestEnable();
 | 
					                onSuggestEnable();
 | 
				
			||||||
            } else {
 | 
					            } else {
 | 
				
			||||||
                onSuggestEnable();
 | 
					                onSuggestDisable();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
            if (disabled) {
 | 
					            if (disabled) {
 | 
				
			||||||
@ -70,8 +72,8 @@ export const DisableEnableStrategyDialog = ({
 | 
				
			|||||||
                }
 | 
					                }
 | 
				
			||||||
                elseShow={
 | 
					                elseShow={
 | 
				
			||||||
                    <Alert severity="error">
 | 
					                    <Alert severity="error">
 | 
				
			||||||
                        Enabling the strategy will change which users receive
 | 
					                        {disabled ? 'Enabling' : 'Disabling'} the strategy will
 | 
				
			||||||
                        access to the feature.
 | 
					                        change which users receive access to the feature.
 | 
				
			||||||
                    </Alert>
 | 
					                    </Alert>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
 | 
				
			|||||||
@ -20,7 +20,10 @@ import {
 | 
				
			|||||||
    UPDATE_FEATURE_STRATEGY,
 | 
					    UPDATE_FEATURE_STRATEGY,
 | 
				
			||||||
} from '@server/types/permissions';
 | 
					} from '@server/types/permissions';
 | 
				
			||||||
import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
 | 
					import { useHasProjectEnvironmentAccess } from 'hooks/useHasAccess';
 | 
				
			||||||
import { STRATEGY_FORM_REMOVE_ID } from 'utils/testIds';
 | 
					import {
 | 
				
			||||||
 | 
					    STRATEGY_FORM_REMOVE_ID,
 | 
				
			||||||
 | 
					    STRATEGY_REMOVE_MENU_BTN,
 | 
				
			||||||
 | 
					} from 'utils/testIds';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IRemoveStrategyMenuProps {
 | 
					export interface IRemoveStrategyMenuProps {
 | 
				
			||||||
    projectId: string;
 | 
					    projectId: string;
 | 
				
			||||||
@ -74,6 +77,7 @@ const MenuStrategyRemove = ({
 | 
				
			|||||||
                        aria-controls={open ? 'actions-menu' : undefined}
 | 
					                        aria-controls={open ? 'actions-menu' : undefined}
 | 
				
			||||||
                        aria-haspopup="true"
 | 
					                        aria-haspopup="true"
 | 
				
			||||||
                        aria-expanded={open ? 'true' : undefined}
 | 
					                        aria-expanded={open ? 'true' : undefined}
 | 
				
			||||||
 | 
					                        data-testid={STRATEGY_REMOVE_MENU_BTN}
 | 
				
			||||||
                    >
 | 
					                    >
 | 
				
			||||||
                        <MoreVertIcon sx={{ width: 32, height: 32 }} />
 | 
					                        <MoreVertIcon sx={{ width: 32, height: 32 }} />
 | 
				
			||||||
                    </IconButton>
 | 
					                    </IconButton>
 | 
				
			||||||
 | 
				
			|||||||
@ -1,21 +1,10 @@
 | 
				
			|||||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
 | 
					 | 
				
			||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
 | 
					 | 
				
			||||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
					import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
 | 
				
			||||||
import useToast from 'hooks/useToast';
 | 
					 | 
				
			||||||
import { IFeatureEnvironment } from 'interfaces/featureToggle';
 | 
					import { IFeatureEnvironment } from 'interfaces/featureToggle';
 | 
				
			||||||
import PermissionSwitch from 'component/common/PermissionSwitch/PermissionSwitch';
 | 
					 | 
				
			||||||
import { UPDATE_FEATURE_ENVIRONMENT } from 'component/providers/AccessProvider/permissions';
 | 
					 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					 | 
				
			||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
					import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
 | 
				
			||||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
 | 
					 | 
				
			||||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
 | 
					 | 
				
			||||||
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
 | 
					 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					 | 
				
			||||||
import { styled } from '@mui/material';
 | 
					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 { useState } from 'react';
 | 
					import { FeatureToggleSwitch } from 'component/project/Project/ProjectFeatureToggles/FeatureToggleSwitch/FeatureToggleSwitch';
 | 
				
			||||||
import { EnableEnvironmentDialog } from './EnableEnvironmentDialog';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledContainer = styled('div')(({ theme }) => ({
 | 
					const StyledContainer = styled('div')(({ theme }) => ({
 | 
				
			||||||
    marginLeft: theme.spacing(-1.5),
 | 
					    marginLeft: theme.spacing(-1.5),
 | 
				
			||||||
@ -53,98 +42,7 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const projectId = useRequiredPathParam('projectId');
 | 
					    const projectId = useRequiredPathParam('projectId');
 | 
				
			||||||
    const featureId = useRequiredPathParam('featureId');
 | 
					    const featureId = useRequiredPathParam('featureId');
 | 
				
			||||||
    const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
 | 
					 | 
				
			||||||
        useFeatureApi();
 | 
					 | 
				
			||||||
    const { feature, refetchFeature } = useFeature(projectId, featureId);
 | 
					    const { feature, refetchFeature } = useFeature(projectId, featureId);
 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					 | 
				
			||||||
    const {
 | 
					 | 
				
			||||||
        onChangeRequestToggle,
 | 
					 | 
				
			||||||
        onChangeRequestToggleClose,
 | 
					 | 
				
			||||||
        onChangeRequestToggleConfirm,
 | 
					 | 
				
			||||||
        changeRequestDialogDetails,
 | 
					 | 
				
			||||||
    } = useChangeRequestToggle(projectId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const [showEnabledDialog, setShowEnabledDialog] = useState(false);
 | 
					 | 
				
			||||||
    const disabledStrategiesCount = environment.strategies.filter(
 | 
					 | 
				
			||||||
        strategy => strategy.disabled
 | 
					 | 
				
			||||||
    ).length;
 | 
					 | 
				
			||||||
    const handleToggleEnvironmentOn = async (
 | 
					 | 
				
			||||||
        shouldActivateDisabled = false
 | 
					 | 
				
			||||||
    ) => {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            await toggleFeatureEnvironmentOn(
 | 
					 | 
				
			||||||
                projectId,
 | 
					 | 
				
			||||||
                featureId,
 | 
					 | 
				
			||||||
                name,
 | 
					 | 
				
			||||||
                shouldActivateDisabled
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
            setToastData({
 | 
					 | 
				
			||||||
                type: 'success',
 | 
					 | 
				
			||||||
                title: `Available in ${name}`,
 | 
					 | 
				
			||||||
                text: `${featureId} is now available in ${name} based on its defined strategies.`,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            refetchFeature();
 | 
					 | 
				
			||||||
            if (callback) {
 | 
					 | 
				
			||||||
                callback();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (error: unknown) {
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
                error instanceof Error &&
 | 
					 | 
				
			||||||
                error.message === ENVIRONMENT_STRATEGY_ERROR
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
                showInfoBox();
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
                setToastApiError(formatUnknownError(error));
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const handleToggleEnvironmentOff = async () => {
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            await toggleFeatureEnvironmentOff(projectId, featureId, name);
 | 
					 | 
				
			||||||
            setToastData({
 | 
					 | 
				
			||||||
                type: 'success',
 | 
					 | 
				
			||||||
                title: `Unavailable in ${name}`,
 | 
					 | 
				
			||||||
                text: `${featureId} is unavailable in ${name} and its strategies will no longer have any effect.`,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            refetchFeature();
 | 
					 | 
				
			||||||
            if (callback) {
 | 
					 | 
				
			||||||
                callback();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (error: unknown) {
 | 
					 | 
				
			||||||
            setToastApiError(formatUnknownError(error));
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const toggleEnvironment = async (e: React.ChangeEvent) => {
 | 
					 | 
				
			||||||
        if (isChangeRequestConfigured(name)) {
 | 
					 | 
				
			||||||
            e.preventDefault();
 | 
					 | 
				
			||||||
            onChangeRequestToggle(featureId, name, !enabled);
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (enabled) {
 | 
					 | 
				
			||||||
            await handleToggleEnvironmentOff();
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (featureHasOnlyDisabledStrategies()) {
 | 
					 | 
				
			||||||
            setShowEnabledDialog(true);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
            await handleToggleEnvironmentOn();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const featureHasOnlyDisabledStrategies = () => {
 | 
					 | 
				
			||||||
        const featureEnvironment = feature?.environments?.find(
 | 
					 | 
				
			||||||
            env => env.name === name
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
            featureEnvironment?.strategies &&
 | 
					 | 
				
			||||||
            featureEnvironment?.strategies?.length > 0 &&
 | 
					 | 
				
			||||||
            featureEnvironment?.strategies?.every(strategy => strategy.disabled)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const defaultContent = (
 | 
					    const defaultContent = (
 | 
				
			||||||
        <>
 | 
					        <>
 | 
				
			||||||
@ -155,30 +53,21 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
 | 
				
			|||||||
        </>
 | 
					        </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onActivateStrategies = async () => {
 | 
					    const handleToggle = () => {
 | 
				
			||||||
        await handleToggleEnvironmentOn(true);
 | 
					        refetchFeature();
 | 
				
			||||||
        setShowEnabledDialog(false);
 | 
					        if (callback) callback();
 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const onAddDefaultStrategy = async () => {
 | 
					 | 
				
			||||||
        await handleToggleEnvironmentOn();
 | 
					 | 
				
			||||||
        setShowEnabledDialog(false);
 | 
					 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <StyledContainer>
 | 
					        <StyledContainer>
 | 
				
			||||||
            <StyledLabel>
 | 
					            <StyledLabel>
 | 
				
			||||||
                <PermissionSwitch
 | 
					                <FeatureToggleSwitch
 | 
				
			||||||
                    tooltip={
 | 
					                    featureId={feature.name}
 | 
				
			||||||
                        enabled
 | 
					 | 
				
			||||||
                            ? `Disable feature in ${name}`
 | 
					 | 
				
			||||||
                            : `Enable feature in ${name}`
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    permission={UPDATE_FEATURE_ENVIRONMENT}
 | 
					 | 
				
			||||||
                    projectId={projectId}
 | 
					                    projectId={projectId}
 | 
				
			||||||
                    checked={enabled}
 | 
					                    environmentName={environment.name}
 | 
				
			||||||
                    onChange={toggleEnvironment}
 | 
					                    onToggle={handleToggle}
 | 
				
			||||||
                    environmentId={name}
 | 
					                    onError={showInfoBox}
 | 
				
			||||||
 | 
					                    value={enabled}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
                {children ?? defaultContent}
 | 
					                {children ?? defaultContent}
 | 
				
			||||||
            </StyledLabel>
 | 
					            </StyledLabel>
 | 
				
			||||||
@ -187,27 +76,6 @@ export const FeatureOverviewSidePanelEnvironmentSwitch = ({
 | 
				
			|||||||
                hiddenEnvironments={hiddenEnvironments}
 | 
					                hiddenEnvironments={hiddenEnvironments}
 | 
				
			||||||
                setHiddenEnvironments={setHiddenEnvironments}
 | 
					                setHiddenEnvironments={setHiddenEnvironments}
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
            <ChangeRequestDialogue
 | 
					 | 
				
			||||||
                isOpen={changeRequestDialogDetails.isOpen}
 | 
					 | 
				
			||||||
                onClose={onChangeRequestToggleClose}
 | 
					 | 
				
			||||||
                environment={changeRequestDialogDetails?.environment}
 | 
					 | 
				
			||||||
                onConfirm={onChangeRequestToggleConfirm}
 | 
					 | 
				
			||||||
                messageComponent={
 | 
					 | 
				
			||||||
                    <UpdateEnabledMessage
 | 
					 | 
				
			||||||
                        enabled={changeRequestDialogDetails?.enabled!}
 | 
					 | 
				
			||||||
                        featureName={changeRequestDialogDetails?.featureName!}
 | 
					 | 
				
			||||||
                        environment={changeRequestDialogDetails.environment!}
 | 
					 | 
				
			||||||
                    />
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
            <EnableEnvironmentDialog
 | 
					 | 
				
			||||||
                isOpen={showEnabledDialog}
 | 
					 | 
				
			||||||
                onClose={() => setShowEnabledDialog(false)}
 | 
					 | 
				
			||||||
                environment={name}
 | 
					 | 
				
			||||||
                disabledStrategiesCount={disabledStrategiesCount}
 | 
					 | 
				
			||||||
                onActivateDisabledStrategies={onActivateStrategies}
 | 
					 | 
				
			||||||
                onAddDefaultStrategy={onAddDefaultStrategy}
 | 
					 | 
				
			||||||
            />
 | 
					 | 
				
			||||||
        </StyledContainer>
 | 
					        </StyledContainer>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -1,9 +1,21 @@
 | 
				
			|||||||
import { VFC } from 'react';
 | 
					import { useState, 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 { 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 useUiConfig from '../../../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
				
			||||||
 | 
					import { usePlausibleTracker } from '../../../../../hooks/usePlausibleTracker';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
 | 
					const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
 | 
				
			||||||
    mx: 'auto',
 | 
					    mx: 'auto',
 | 
				
			||||||
@ -11,44 +23,189 @@ const StyledBoxContainer = styled(Box)<{ 'data-testid': string }>(() => ({
 | 
				
			|||||||
}));
 | 
					}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IFeatureToggleSwitchProps {
 | 
					interface IFeatureToggleSwitchProps {
 | 
				
			||||||
    featureName: string;
 | 
					    featureId: string;
 | 
				
			||||||
    environmentName: string;
 | 
					    environmentName: string;
 | 
				
			||||||
    projectId: string;
 | 
					    projectId: string;
 | 
				
			||||||
    value: boolean;
 | 
					    value: boolean;
 | 
				
			||||||
    onToggle: (
 | 
					    onError?: () => void;
 | 
				
			||||||
 | 
					    onToggle?: (
 | 
				
			||||||
        projectId: string,
 | 
					        projectId: string,
 | 
				
			||||||
        feature: string,
 | 
					        feature: string,
 | 
				
			||||||
        env: string,
 | 
					        env: string,
 | 
				
			||||||
        state: boolean
 | 
					        state: boolean
 | 
				
			||||||
    ) => Promise<void>;
 | 
					    ) => void;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
 | 
					export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
 | 
				
			||||||
    projectId,
 | 
					    projectId,
 | 
				
			||||||
    featureName,
 | 
					    featureId,
 | 
				
			||||||
    environmentName,
 | 
					    environmentName,
 | 
				
			||||||
    value,
 | 
					    value,
 | 
				
			||||||
    onToggle,
 | 
					    onToggle,
 | 
				
			||||||
 | 
					    onError,
 | 
				
			||||||
}) => {
 | 
					}) => {
 | 
				
			||||||
 | 
					    const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
 | 
				
			||||||
 | 
					        useFeatureApi();
 | 
				
			||||||
 | 
					    const { setToastData, setToastApiError } = useToast();
 | 
				
			||||||
 | 
					    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
				
			||||||
 | 
					    const {
 | 
				
			||||||
 | 
					        onChangeRequestToggle,
 | 
				
			||||||
 | 
					        onChangeRequestToggleClose,
 | 
				
			||||||
 | 
					        onChangeRequestToggleConfirm,
 | 
				
			||||||
 | 
					        changeRequestDialogDetails,
 | 
				
			||||||
 | 
					    } = useChangeRequestToggle(projectId);
 | 
				
			||||||
    const [isChecked, setIsChecked, rollbackIsChecked] =
 | 
					    const [isChecked, setIsChecked, rollbackIsChecked] =
 | 
				
			||||||
        useOptimisticUpdate<boolean>(value);
 | 
					        useOptimisticUpdate<boolean>(value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onClick = () => {
 | 
					    const [showEnabledDialog, setShowEnabledDialog] = useState(false);
 | 
				
			||||||
 | 
					    const { feature } = useFeature(projectId, featureId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const disabledStrategiesCount =
 | 
				
			||||||
 | 
					        feature?.environments
 | 
				
			||||||
 | 
					            .find(env => env.name === environmentName)
 | 
				
			||||||
 | 
					            ?.strategies.filter(strategy => strategy.disabled).length ?? 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { uiConfig } = useUiConfig();
 | 
				
			||||||
 | 
					    const showStrategyImprovements = Boolean(
 | 
				
			||||||
 | 
					        uiConfig.flags.strategyImprovements
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const { trackEvent } = usePlausibleTracker();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const handleToggleEnvironmentOn = async (
 | 
				
			||||||
 | 
					        shouldActivateDisabled = false
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
            setIsChecked(!isChecked);
 | 
					            setIsChecked(!isChecked);
 | 
				
			||||||
        onToggle(projectId, featureName, environmentName, !isChecked).catch(
 | 
					            await toggleFeatureEnvironmentOn(
 | 
				
			||||||
            rollbackIsChecked
 | 
					                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 onClick = async (e: React.MouseEvent) => {
 | 
				
			||||||
 | 
					        if (isChangeRequestConfigured(environmentName)) {
 | 
				
			||||||
 | 
					            e.preventDefault();
 | 
				
			||||||
 | 
					            if (
 | 
				
			||||||
 | 
					                featureHasOnlyDisabledStrategies() &&
 | 
				
			||||||
 | 
					                showStrategyImprovements
 | 
				
			||||||
 | 
					            ) {
 | 
				
			||||||
 | 
					                setShowEnabledDialog(true);
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                onChangeRequestToggle(
 | 
				
			||||||
 | 
					                    feature.name,
 | 
				
			||||||
 | 
					                    environmentName,
 | 
				
			||||||
 | 
					                    !value,
 | 
				
			||||||
 | 
					                    false
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if (value) {
 | 
				
			||||||
 | 
					            await handleToggleEnvironmentOff();
 | 
				
			||||||
 | 
					            return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (featureHasOnlyDisabledStrategies() && showStrategyImprovements) {
 | 
				
			||||||
 | 
					            setShowEnabledDialog(true);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            await handleToggleEnvironmentOn();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onActivateStrategies = async () => {
 | 
				
			||||||
 | 
					        await trackEvent('strategyImprovements', {
 | 
				
			||||||
 | 
					            props: {
 | 
				
			||||||
 | 
					                eventType: 'activate disabled strategies',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (isChangeRequestConfigured(environmentName)) {
 | 
				
			||||||
 | 
					            onChangeRequestToggle(feature.name, environmentName, !value, true);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            await handleToggleEnvironmentOn(true);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        setShowEnabledDialog(false);
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const onAddDefaultStrategy = async () => {
 | 
				
			||||||
 | 
					        await trackEvent('strategyImprovements', {
 | 
				
			||||||
 | 
					            props: {
 | 
				
			||||||
 | 
					                eventType: 'add default strategy',
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					        if (isChangeRequestConfigured(environmentName)) {
 | 
				
			||||||
 | 
					            onChangeRequestToggle(feature.name, environmentName, !value, 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 = `${featureName}-${environmentName}`;
 | 
					    const key = `${feature.name}-${environmentName}`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return (
 | 
					    return (
 | 
				
			||||||
 | 
					        <>
 | 
				
			||||||
            <StyledBoxContainer
 | 
					            <StyledBoxContainer
 | 
				
			||||||
                key={key} // Prevent animation when archiving rows
 | 
					                key={key} // Prevent animation when archiving rows
 | 
				
			||||||
                data-testid={`TOGGLE-${key}`}
 | 
					                data-testid={`TOGGLE-${key}`}
 | 
				
			||||||
            >
 | 
					            >
 | 
				
			||||||
                <PermissionSwitch
 | 
					                <PermissionSwitch
 | 
				
			||||||
                checked={value}
 | 
					                    tooltip={
 | 
				
			||||||
 | 
					                        value
 | 
				
			||||||
 | 
					                            ? `Disable feature in ${environmentName}`
 | 
				
			||||||
 | 
					                            : `Enable feature in ${environmentName}`
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    checked={isChecked}
 | 
				
			||||||
                    environmentId={environmentName}
 | 
					                    environmentId={environmentName}
 | 
				
			||||||
                    projectId={projectId}
 | 
					                    projectId={projectId}
 | 
				
			||||||
                    permission={UPDATE_FEATURE_ENVIRONMENT}
 | 
					                    permission={UPDATE_FEATURE_ENVIRONMENT}
 | 
				
			||||||
@ -57,5 +214,29 @@ export const FeatureToggleSwitch: VFC<IFeatureToggleSwitchProps> = ({
 | 
				
			|||||||
                    disabled={isChecked !== value}
 | 
					                    disabled={isChecked !== value}
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
            </StyledBoxContainer>
 | 
					            </StyledBoxContainer>
 | 
				
			||||||
 | 
					            {showStrategyImprovements && (
 | 
				
			||||||
 | 
					                <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!}
 | 
				
			||||||
 | 
					                    />
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            />
 | 
				
			||||||
 | 
					        </>
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
@ -12,8 +12,8 @@ import { useNavigate, useSearchParams } from 'react-router-dom';
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
    SortingRule,
 | 
					    SortingRule,
 | 
				
			||||||
    useFlexLayout,
 | 
					    useFlexLayout,
 | 
				
			||||||
    useSortBy,
 | 
					 | 
				
			||||||
    useRowSelect,
 | 
					    useRowSelect,
 | 
				
			||||||
 | 
					    useSortBy,
 | 
				
			||||||
    useTable,
 | 
					    useTable,
 | 
				
			||||||
} from 'react-table';
 | 
					} from 'react-table';
 | 
				
			||||||
import type { FeatureSchema } from 'openapi';
 | 
					import type { FeatureSchema } from 'openapi';
 | 
				
			||||||
@ -28,14 +28,11 @@ import { DateCell } from 'component/common/Table/cells/DateCell/DateCell';
 | 
				
			|||||||
import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
 | 
					import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
 | 
				
			||||||
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
 | 
					import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
 | 
				
			||||||
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
 | 
					import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
 | 
				
			||||||
import { formatUnknownError } from 'utils/formatUnknownError';
 | 
					 | 
				
			||||||
import { IProject } from 'interfaces/project';
 | 
					import { IProject } from 'interfaces/project';
 | 
				
			||||||
import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
 | 
					import { TablePlaceholder, VirtualizedTable } from 'component/common/Table';
 | 
				
			||||||
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
					import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
 | 
				
			||||||
import useProject from 'hooks/api/getters/useProject/useProject';
 | 
					import useProject from 'hooks/api/getters/useProject/useProject';
 | 
				
			||||||
import { createLocalStorage } from 'utils/createLocalStorage';
 | 
					import { createLocalStorage } from 'utils/createLocalStorage';
 | 
				
			||||||
import useToast from 'hooks/useToast';
 | 
					 | 
				
			||||||
import { ENVIRONMENT_STRATEGY_ERROR } from 'constants/apiErrors';
 | 
					 | 
				
			||||||
import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
 | 
					import EnvironmentStrategyDialog from 'component/common/EnvironmentStrategiesDialog/EnvironmentStrategyDialog';
 | 
				
			||||||
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
 | 
					import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
 | 
				
			||||||
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
 | 
					import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
 | 
				
			||||||
@ -44,7 +41,6 @@ import { Search } from 'component/common/Search/Search';
 | 
				
			|||||||
import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
 | 
					import { useChangeRequestToggle } from 'hooks/useChangeRequestToggle';
 | 
				
			||||||
import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
 | 
					import { ChangeRequestDialogue } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestConfirmDialog';
 | 
				
			||||||
import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
 | 
					import { UpdateEnabledMessage } from 'component/changeRequest/ChangeRequestConfirmDialog/ChangeRequestMessages/UpdateEnabledMessage';
 | 
				
			||||||
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
 | 
					 | 
				
			||||||
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';
 | 
				
			||||||
@ -52,7 +48,6 @@ import {
 | 
				
			|||||||
    ProjectEnvironmentType,
 | 
					    ProjectEnvironmentType,
 | 
				
			||||||
    useEnvironmentsRef,
 | 
					    useEnvironmentsRef,
 | 
				
			||||||
} from './hooks/useEnvironmentsRef';
 | 
					} from './hooks/useEnvironmentsRef';
 | 
				
			||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
 | 
					 | 
				
			||||||
import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
 | 
					import { FeatureToggleSwitch } from './FeatureToggleSwitch/FeatureToggleSwitch';
 | 
				
			||||||
import { ActionsCell } from './ActionsCell/ActionsCell';
 | 
					import { ActionsCell } from './ActionsCell/ActionsCell';
 | 
				
			||||||
import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
 | 
					import { ColumnsMenu } from './ColumnsMenu/ColumnsMenu';
 | 
				
			||||||
@ -146,25 +141,20 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
        useGlobalLocalStorage();
 | 
					        useGlobalLocalStorage();
 | 
				
			||||||
    const navigate = useNavigate();
 | 
					    const navigate = useNavigate();
 | 
				
			||||||
    const [searchParams, setSearchParams] = useSearchParams();
 | 
					    const [searchParams, setSearchParams] = useSearchParams();
 | 
				
			||||||
    const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
 | 
					 | 
				
			||||||
    const environments = useEnvironmentsRef(
 | 
					    const environments = useEnvironmentsRef(
 | 
				
			||||||
        loading
 | 
					        loading
 | 
				
			||||||
            ? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
 | 
					            ? [{ environment: 'a' }, { environment: 'b' }, { environment: 'c' }]
 | 
				
			||||||
            : newEnvironments
 | 
					            : newEnvironments
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    const { refetch } = useProject(projectId);
 | 
					    const { refetch } = useProject(projectId);
 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					 | 
				
			||||||
    const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
 | 
					    const { isFavoritesPinned, sortTypes, onChangeIsFavoritePinned } =
 | 
				
			||||||
        usePinnedFavorites(
 | 
					        usePinnedFavorites(
 | 
				
			||||||
            searchParams.has('favorites')
 | 
					            searchParams.has('favorites')
 | 
				
			||||||
                ? searchParams.get('favorites') === 'true'
 | 
					                ? searchParams.get('favorites') === 'true'
 | 
				
			||||||
                : globalStore.favorites
 | 
					                : globalStore.favorites
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
    const { toggleFeatureEnvironmentOn, toggleFeatureEnvironmentOff } =
 | 
					 | 
				
			||||||
        useFeatureApi();
 | 
					 | 
				
			||||||
    const { favorite, unfavorite } = useFavoriteFeaturesApi();
 | 
					    const { favorite, unfavorite } = useFavoriteFeaturesApi();
 | 
				
			||||||
    const {
 | 
					    const {
 | 
				
			||||||
        onChangeRequestToggle,
 | 
					 | 
				
			||||||
        onChangeRequestToggleClose,
 | 
					        onChangeRequestToggleClose,
 | 
				
			||||||
        onChangeRequestToggleConfirm,
 | 
					        onChangeRequestToggleConfirm,
 | 
				
			||||||
        changeRequestDialogDetails,
 | 
					        changeRequestDialogDetails,
 | 
				
			||||||
@ -172,60 +162,6 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
    const [showExportDialog, setShowExportDialog] = useState(false);
 | 
					    const [showExportDialog, setShowExportDialog] = useState(false);
 | 
				
			||||||
    const { uiConfig } = useUiConfig();
 | 
					    const { uiConfig } = useUiConfig();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onToggle = useCallback(
 | 
					 | 
				
			||||||
        async (
 | 
					 | 
				
			||||||
            projectId: string,
 | 
					 | 
				
			||||||
            featureName: string,
 | 
					 | 
				
			||||||
            environment: string,
 | 
					 | 
				
			||||||
            enabled: boolean
 | 
					 | 
				
			||||||
        ) => {
 | 
					 | 
				
			||||||
            if (isChangeRequestConfigured(environment)) {
 | 
					 | 
				
			||||||
                onChangeRequestToggle(featureName, environment, enabled);
 | 
					 | 
				
			||||||
                throw new Error('Additional approval required');
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            try {
 | 
					 | 
				
			||||||
                if (enabled) {
 | 
					 | 
				
			||||||
                    await toggleFeatureEnvironmentOn(
 | 
					 | 
				
			||||||
                        projectId,
 | 
					 | 
				
			||||||
                        featureName,
 | 
					 | 
				
			||||||
                        environment
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    await toggleFeatureEnvironmentOff(
 | 
					 | 
				
			||||||
                        projectId,
 | 
					 | 
				
			||||||
                        featureName,
 | 
					 | 
				
			||||||
                        environment
 | 
					 | 
				
			||||||
                    );
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                refetch();
 | 
					 | 
				
			||||||
            } catch (error) {
 | 
					 | 
				
			||||||
                const message = formatUnknownError(error);
 | 
					 | 
				
			||||||
                if (message === ENVIRONMENT_STRATEGY_ERROR) {
 | 
					 | 
				
			||||||
                    setStrategiesDialogState({
 | 
					 | 
				
			||||||
                        open: true,
 | 
					 | 
				
			||||||
                        featureId: featureName,
 | 
					 | 
				
			||||||
                        environmentName: environment,
 | 
					 | 
				
			||||||
                    });
 | 
					 | 
				
			||||||
                } else {
 | 
					 | 
				
			||||||
                    setToastApiError(message);
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
                throw error; // caught when reverting optimistic update
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            setToastData({
 | 
					 | 
				
			||||||
                type: 'success',
 | 
					 | 
				
			||||||
                title: 'Updated toggle status',
 | 
					 | 
				
			||||||
                text: 'Successfully updated toggle status.',
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
            refetch();
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        [
 | 
					 | 
				
			||||||
            toggleFeatureEnvironmentOff,
 | 
					 | 
				
			||||||
            toggleFeatureEnvironmentOn,
 | 
					 | 
				
			||||||
            isChangeRequestConfigured,
 | 
					 | 
				
			||||||
        ]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const onFavorite = useCallback(
 | 
					    const onFavorite = useCallback(
 | 
				
			||||||
        async (feature: IFeatureToggleListItem) => {
 | 
					        async (feature: IFeatureToggleListItem) => {
 | 
				
			||||||
            if (feature?.favorite) {
 | 
					            if (feature?.favorite) {
 | 
				
			||||||
@ -355,9 +291,8 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
                                <FeatureToggleSwitch
 | 
					                                <FeatureToggleSwitch
 | 
				
			||||||
                                    value={value}
 | 
					                                    value={value}
 | 
				
			||||||
                                    projectId={projectId}
 | 
					                                    projectId={projectId}
 | 
				
			||||||
                                    featureName={feature?.name}
 | 
					                                    featureId={feature.name}
 | 
				
			||||||
                                    environmentName={name}
 | 
					                                    environmentName={name}
 | 
				
			||||||
                                    onToggle={onToggle}
 | 
					 | 
				
			||||||
                                />
 | 
					                                />
 | 
				
			||||||
                                <ConditionallyRender
 | 
					                                <ConditionallyRender
 | 
				
			||||||
                                    condition={hasWarning}
 | 
					                                    condition={hasWarning}
 | 
				
			||||||
@ -389,7 +324,7 @@ export const ProjectFeatureToggles = ({
 | 
				
			|||||||
                hideInMenu: true,
 | 
					                hideInMenu: true,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        [projectId, environments, loading, onToggle]
 | 
					        [projectId, environments, loading]
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [searchValue, setSearchValue] = useState(
 | 
					    const [searchValue, setSearchValue] = useState(
 | 
				
			||||||
 | 
				
			|||||||
@ -20,22 +20,23 @@ import { ProjectDefaultStrategyForm } from './ProjectDefaultStrategyForm';
 | 
				
			|||||||
import { CreateFeatureStrategySchema } from 'openapi';
 | 
					import { CreateFeatureStrategySchema } from 'openapi';
 | 
				
			||||||
import useProject from 'hooks/api/getters/useProject/useProject';
 | 
					import useProject from 'hooks/api/getters/useProject/useProject';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface EditDefaultStrategyProps {
 | 
					const EditDefaultStrategy = () => {
 | 
				
			||||||
    strategy: CreateFeatureStrategySchema;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const EditDefaultStrategy = ({ strategy }: EditDefaultStrategyProps) => {
 | 
					 | 
				
			||||||
    const projectId = useRequiredPathParam('projectId');
 | 
					    const projectId = useRequiredPathParam('projectId');
 | 
				
			||||||
    const environmentId = useRequiredQueryParam('environmentId');
 | 
					    const environmentId = useRequiredQueryParam('environmentId');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const { refetch: refetchProject } = useProject(projectId);
 | 
					    const { project, refetch: refetchProject } = useProject(projectId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [defaultStrategy, setDefaultStrategy] =
 | 
					    const strategy = project.environments.find(
 | 
				
			||||||
        useState<CreateFeatureStrategySchema>(strategy);
 | 
					        env => env.environment === environmentId
 | 
				
			||||||
 | 
					    )?.defaultStrategy;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const [defaultStrategy, setDefaultStrategy] = useState<
 | 
				
			||||||
 | 
					        CreateFeatureStrategySchema | undefined
 | 
				
			||||||
 | 
					    >(strategy);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const [segments, setSegments] = useState<ISegment[]>([]);
 | 
					    const [segments, setSegments] = useState<ISegment[]>([]);
 | 
				
			||||||
    const { updateDefaultStrategy, loading } = useProjectApi();
 | 
					    const { updateDefaultStrategy, loading } = useProjectApi();
 | 
				
			||||||
    const { strategyDefinition } = useStrategy(strategy.name);
 | 
					    const { strategyDefinition } = useStrategy(strategy?.name);
 | 
				
			||||||
    const { setToastData, setToastApiError } = useToast();
 | 
					    const { setToastData, setToastApiError } = useToast();
 | 
				
			||||||
    const errors = useFormErrors();
 | 
					    const errors = useFormErrors();
 | 
				
			||||||
    const { uiConfig } = useUiConfig();
 | 
					    const { uiConfig } = useUiConfig();
 | 
				
			||||||
@ -60,7 +61,7 @@ const EditDefaultStrategy = ({ strategy }: EditDefaultStrategyProps) => {
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            setSegments(temp);
 | 
					            setSegments(temp);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }, [JSON.stringify(allSegments), JSON.stringify(strategy.segments)]);
 | 
					    }, [JSON.stringify(allSegments), JSON.stringify(strategy?.segments)]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const segmentsToSubmit = uiConfig?.flags.SE ? segments : [];
 | 
					    const segmentsToSubmit = uiConfig?.flags.SE ? segments : [];
 | 
				
			||||||
    const payload = createStrategyPayload(
 | 
					    const payload = createStrategyPayload(
 | 
				
			||||||
@ -108,7 +109,7 @@ const EditDefaultStrategy = ({ strategy }: EditDefaultStrategyProps) => {
 | 
				
			|||||||
    return (
 | 
					    return (
 | 
				
			||||||
        <FormTemplate
 | 
					        <FormTemplate
 | 
				
			||||||
            modal
 | 
					            modal
 | 
				
			||||||
            title={formatStrategyName(strategy.name ?? '')}
 | 
					            title={formatStrategyName(strategy?.name ?? '')}
 | 
				
			||||||
            description={projectDefaultStrategyHelp}
 | 
					            description={projectDefaultStrategyHelp}
 | 
				
			||||||
            documentationLink={projectDefaultStrategyDocsLink}
 | 
					            documentationLink={projectDefaultStrategyDocsLink}
 | 
				
			||||||
            documentationLinkLabel={projectDefaultStrategyDocsLinkLabel}
 | 
					            documentationLinkLabel={projectDefaultStrategyDocsLinkLabel}
 | 
				
			||||||
 | 
				
			|||||||
@ -74,7 +74,7 @@ const ProjectEnvironmentDefaultStrategy = ({
 | 
				
			|||||||
                            tooltipProps={{
 | 
					                            tooltipProps={{
 | 
				
			||||||
                                title: `Edit default strategy for "${environmentId}"`,
 | 
					                                title: `Edit default strategy for "${environmentId}"`,
 | 
				
			||||||
                            }}
 | 
					                            }}
 | 
				
			||||||
                            data-testid={`STRATEGY_EDIT-${strategy.name}`}
 | 
					                            data-testid={`STRATEGY_EDIT-${strategy?.name}`}
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <Edit />
 | 
					                            <Edit />
 | 
				
			||||||
                        </PermissionIconButton>
 | 
					                        </PermissionIconButton>
 | 
				
			||||||
@ -92,7 +92,7 @@ const ProjectEnvironmentDefaultStrategy = ({
 | 
				
			|||||||
                            onClose={onSidebarClose}
 | 
					                            onClose={onSidebarClose}
 | 
				
			||||||
                            open
 | 
					                            open
 | 
				
			||||||
                        >
 | 
					                        >
 | 
				
			||||||
                            <EditDefaultStrategy strategy={strategy} />
 | 
					                            <EditDefaultStrategy />
 | 
				
			||||||
                        </SidebarModal>
 | 
					                        </SidebarModal>
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                />
 | 
					                />
 | 
				
			||||||
 | 
				
			|||||||
@ -13,17 +13,24 @@ export const useChangeRequestToggle = (project: string) => {
 | 
				
			|||||||
    const [changeRequestDialogDetails, setChangeRequestDialogDetails] =
 | 
					    const [changeRequestDialogDetails, setChangeRequestDialogDetails] =
 | 
				
			||||||
        useState<{
 | 
					        useState<{
 | 
				
			||||||
            enabled?: boolean;
 | 
					            enabled?: boolean;
 | 
				
			||||||
 | 
					            shouldActivateDisabledStrategies?: boolean;
 | 
				
			||||||
            featureName?: string;
 | 
					            featureName?: string;
 | 
				
			||||||
            environment?: string;
 | 
					            environment?: string;
 | 
				
			||||||
            isOpen: boolean;
 | 
					            isOpen: boolean;
 | 
				
			||||||
        }>({ isOpen: false });
 | 
					        }>({ isOpen: false });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const onChangeRequestToggle = useCallback(
 | 
					    const onChangeRequestToggle = useCallback(
 | 
				
			||||||
        (featureName: string, environment: string, enabled: boolean) => {
 | 
					        (
 | 
				
			||||||
 | 
					            featureName: string,
 | 
				
			||||||
 | 
					            environment: string,
 | 
				
			||||||
 | 
					            enabled: boolean,
 | 
				
			||||||
 | 
					            shouldActivateDisabledStrategies: boolean
 | 
				
			||||||
 | 
					        ) => {
 | 
				
			||||||
            setChangeRequestDialogDetails({
 | 
					            setChangeRequestDialogDetails({
 | 
				
			||||||
                featureName,
 | 
					                featureName,
 | 
				
			||||||
                environment,
 | 
					                environment,
 | 
				
			||||||
                enabled,
 | 
					                enabled,
 | 
				
			||||||
 | 
					                shouldActivateDisabledStrategies,
 | 
				
			||||||
                isOpen: true,
 | 
					                isOpen: true,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
@ -41,6 +48,9 @@ export const useChangeRequestToggle = (project: string) => {
 | 
				
			|||||||
                action: 'updateEnabled',
 | 
					                action: 'updateEnabled',
 | 
				
			||||||
                payload: {
 | 
					                payload: {
 | 
				
			||||||
                    enabled: Boolean(changeRequestDialogDetails.enabled),
 | 
					                    enabled: Boolean(changeRequestDialogDetails.enabled),
 | 
				
			||||||
 | 
					                    shouldActivateDisabledStrategies: Boolean(
 | 
				
			||||||
 | 
					                        changeRequestDialogDetails.shouldActivateDisabledStrategies
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
            refetchChangeRequests();
 | 
					            refetchChangeRequests();
 | 
				
			||||||
 | 
				
			|||||||
@ -25,6 +25,7 @@ export type CustomEvents =
 | 
				
			|||||||
    | 'notifications'
 | 
					    | 'notifications'
 | 
				
			||||||
    | 'batch_operations'
 | 
					    | 'batch_operations'
 | 
				
			||||||
    | 'strategyTitle'
 | 
					    | 'strategyTitle'
 | 
				
			||||||
 | 
					    | 'strategyImprovements'
 | 
				
			||||||
    | 'default_strategy'
 | 
					    | 'default_strategy'
 | 
				
			||||||
    | 'demo'
 | 
					    | 'demo'
 | 
				
			||||||
    | 'demo-start'
 | 
					    | 'demo-start'
 | 
				
			||||||
 | 
				
			|||||||
@ -63,6 +63,7 @@ export const ADD_TO_STRATEGY_INPUT_LIST = 'ADD_TO_STRATEGY_INPUT_LIST';
 | 
				
			|||||||
export const STRATEGY_FORM_SUBMIT_ID = 'STRATEGY_FORM_SUBMIT_ID';
 | 
					export const STRATEGY_FORM_SUBMIT_ID = 'STRATEGY_FORM_SUBMIT_ID';
 | 
				
			||||||
export const STRATEGY_FORM_REMOVE_ID = 'STRATEGY_FORM_REMOVE_ID';
 | 
					export const STRATEGY_FORM_REMOVE_ID = 'STRATEGY_FORM_REMOVE_ID';
 | 
				
			||||||
export const STRATEGY_FORM_COPY_ID = 'STRATEGY_FORM_COPY_ID';
 | 
					export const STRATEGY_FORM_COPY_ID = 'STRATEGY_FORM_COPY_ID';
 | 
				
			||||||
 | 
					export const STRATEGY_REMOVE_MENU_BTN = 'STRATEGY_REMOVE_MENU_BTN';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/* SPLASH */
 | 
					/* SPLASH */
 | 
				
			||||||
export const CLOSE_SPLASH = 'CLOSE_SPLASH';
 | 
					export const CLOSE_SPLASH = 'CLOSE_SPLASH';
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user