1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-02-04 00:18:01 +01:00

feat: archive toggles in change request UI (#4563)

This commit is contained in:
Mateusz Kwasniewski 2023-08-25 13:38:57 +02:00 committed by GitHub
parent 63e052bafe
commit cc62db46fb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 289 additions and 45 deletions

View File

@ -0,0 +1,22 @@
import { FC, ReactNode } from 'react';
import { Box, styled } from '@mui/material';
import { ChangeItemWrapper } from './StrategyChange';
const ArchiveBox = styled(Box)(({ theme }) => ({
display: 'flex',
alignItems: 'center',
color: theme.palette.error.main,
}));
interface IArchiveFeatureChange {
actions?: ReactNode;
}
export const ArchiveFeatureChange: FC<IArchiveFeatureChange> = ({
actions,
}) => (
<ChangeItemWrapper>
<ArchiveBox>Archiving feature</ArchiveBox>
{actions}
</ChangeItemWrapper>
);

View File

@ -11,6 +11,7 @@ import { ToggleStatusChange } from './ToggleStatusChange';
import { StrategyChange } from './StrategyChange';
import { VariantPatch } from './VariantPatch/VariantPatch';
import { EnvironmentStrategyExecutionOrder } from './EnvironmentStrategyExecutionOrder/EnvironmentStrategyExecutionOrder';
import { ArchiveFeatureChange } from './ArchiveFeatureChange';
const StyledSingleChangeBox = styled(Box, {
shouldForwardProp: (prop: string) => !prop.startsWith('$'),
@ -90,6 +91,9 @@ export const FeatureChange: FC<{
actions={actions}
/>
)}
{change.action === 'archiveFeature' && (
<ArchiveFeatureChange actions={actions} />
)}
{change.action === 'addStrategy' ||
change.action === 'deleteStrategy' ||

View File

@ -78,7 +78,8 @@ type ChangeRequestPayload =
| ChangeRequestVariantPatch
| IChangeRequestUpdateSegment
| IChangeRequestDeleteSegment
| SetStrategySortOrderSchema;
| SetStrategySortOrderSchema
| IChangeRequestArchiveFeature;
export interface IChangeRequestAddStrategy extends IChangeRequestChangeBase {
action: 'addStrategy';
@ -105,6 +106,10 @@ export interface IChangeRequestPatchVariant extends IChangeRequestChangeBase {
payload: ChangeRequestVariantPatch;
}
export interface IChangeRequestArchiveFeature extends IChangeRequestChangeBase {
action: 'archiveFeature';
}
export interface IChangeRequestReorderStrategy
extends IChangeRequestChangeBase {
action: 'reorderStrategy';
@ -144,7 +149,8 @@ export type IFeatureChange =
| IChangeRequestUpdateStrategy
| IChangeRequestEnabled
| IChangeRequestPatchVariant
| IChangeRequestReorderStrategy;
| IChangeRequestReorderStrategy
| IChangeRequestArchiveFeature;
export type ISegmentChange =
| IChangeRequestUpdateSegment
@ -178,4 +184,5 @@ export type ChangeRequestAction =
| 'patchVariant'
| 'reorderStrategy'
| 'updateSegment'
| 'deleteSegment';
| 'deleteSegment'
| 'archiveFeature';

View File

@ -0,0 +1,119 @@
import { vi } from 'vitest';
import React from 'react';
import { screen, waitFor } from '@testing-library/react';
import { render } from 'utils/testRenderer';
import { testServerRoute, testServerSetup } from 'utils/testServer';
import { FeatureArchiveDialog } from './FeatureArchiveDialog';
import { UIProviderContainer } from 'component/providers/UIProvider/UIProviderContainer';
const server = testServerSetup();
const setupHappyPathForChangeRequest = () => {
testServerRoute(
server,
'/api/admin/projects/projectId/environments/development/change-requests',
{},
'post'
);
testServerRoute(
server,
'/api/admin/projects/projectId/change-requests/config',
[
{
environment: 'development',
type: 'development',
requiredApprovals: 1,
changeRequestEnabled: true,
},
]
);
testServerRoute(server, '/api/admin/ui-config', {
versionInfo: {
current: { oss: 'version', enterprise: 'version' },
},
});
};
test('Add single archive feature change to change request', async () => {
const onClose = vi.fn();
const onConfirm = vi.fn();
setupHappyPathForChangeRequest();
render(
<UIProviderContainer>
<FeatureArchiveDialog
featureIds={['featureA']}
projectId={'projectId'}
isOpen={true}
onClose={onClose}
onConfirm={onConfirm}
featuresWithUsage={[]}
/>
</UIProviderContainer>
);
expect(screen.getByText('Archive feature toggle')).toBeInTheDocument();
const button = await screen.findByText('Add change to draft');
button.click();
await waitFor(() => {
expect(onConfirm).toBeCalledTimes(1);
});
expect(onClose).toBeCalledTimes(1);
});
test('Add multiple archive feature changes to change request', async () => {
const onClose = vi.fn();
const onConfirm = vi.fn();
setupHappyPathForChangeRequest();
render(
<UIProviderContainer>
<FeatureArchiveDialog
featureIds={['featureA', 'featureB']}
projectId={'projectId'}
isOpen={true}
onClose={onClose}
onConfirm={onConfirm}
featuresWithUsage={[]}
/>
</UIProviderContainer>
);
await screen.findByText('Archive feature toggles');
const button = await screen.findByText('Add to change request');
button.click();
await waitFor(() => {
expect(onConfirm).toBeCalledTimes(1);
});
expect(onClose).toBeCalledTimes(1);
});
test('Skip change request', async () => {
const onClose = vi.fn();
const onConfirm = vi.fn();
setupHappyPathForChangeRequest();
render(
<UIProviderContainer>
<FeatureArchiveDialog
featureIds={['featureA', 'featureB']}
projectId={'projectId'}
isOpen={true}
onClose={onClose}
onConfirm={onConfirm}
featuresWithUsage={[]}
/>
</UIProviderContainer>,
{ permissions: [{ permission: 'SKIP_CHANGE_REQUEST' }] }
);
await screen.findByText('Archive feature toggles');
const button = await screen.findByText('Archive toggles');
button.click();
await waitFor(() => {
expect(onClose).toBeCalledTimes(1);
});
expect(onConfirm).toBeCalledTimes(0); // we didn't setup non Change Request flow so failure
});

View File

@ -7,7 +7,11 @@ import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender'
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import { Alert, Typography } from '@mui/material';
import { Link } from 'react-router-dom';
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled';
import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi';
import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests';
import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment';
interface IFeatureArchiveDialogProps {
isOpen: boolean;
@ -58,6 +62,111 @@ const UsageWarning = ({
return null;
};
const useActionButtonText = (projectId: string, isBulkArchive: boolean) => {
const getHighestEnvironment =
useHighestPermissionChangeRequestEnvironment(projectId);
const environment = getHighestEnvironment();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
if (
environment &&
isChangeRequestConfigured(environment) &&
isBulkArchive
) {
return 'Add to change request';
}
if (environment && isChangeRequestConfigured(environment)) {
return 'Add change to draft';
}
if (isBulkArchive) {
return 'Archive toggles';
}
return 'Archive toggle';
};
const useArchiveAction = ({
projectId,
featureIds,
onSuccess,
onError,
}: {
projectId: string;
featureIds: string[];
onSuccess: () => void;
onError: () => void;
}) => {
const { setToastData, setToastApiError } = useToast();
const { archiveFeatureToggle } = useFeatureApi();
const { archiveFeatures } = useProjectApi();
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { addChange } = useChangeRequestApi();
const { refetch: refetchChangeRequests } =
usePendingChangeRequests(projectId);
const getHighestEnvironment =
useHighestPermissionChangeRequestEnvironment(projectId);
const isBulkArchive = featureIds?.length > 1;
const environment = getHighestEnvironment();
const addArchiveToggleToChangeRequest = async () => {
if (!environment) {
console.error('No change request environment');
return;
}
await addChange(
projectId,
environment,
featureIds.map(feature => ({
action: 'archiveFeature',
feature: feature,
payload: undefined,
}))
);
refetchChangeRequests();
setToastData({
text: isBulkArchive
? 'Your archive feature toggles changes have been added to change request'
: 'Your archive feature toggle change has been added to change request',
type: 'success',
title: isBulkArchive
? 'Changes added to a draft'
: 'Change added to a draft',
});
};
const archiveToggle = async () => {
await archiveFeatureToggle(projectId, featureIds[0]);
setToastData({
text: 'Your feature toggle has been archived',
type: 'success',
title: 'Feature archived',
});
};
const archiveToggles = async () => {
await archiveFeatures(projectId, featureIds);
setToastData({
text: 'Selected feature toggles have been archived',
type: 'success',
title: 'Features archived',
});
};
return async () => {
try {
if (environment && isChangeRequestConfigured(environment)) {
await addArchiveToggleToChangeRequest();
} else if (isBulkArchive) {
await archiveToggles();
} else {
await archiveToggle();
}
onSuccess();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
onError();
}
};
};
export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
isOpen,
onClose,
@ -66,58 +175,36 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
featureIds,
featuresWithUsage,
}) => {
const { archiveFeatureToggle } = useFeatureApi();
const { archiveFeatures } = useProjectApi();
const { setToastData, setToastApiError } = useToast();
const { uiConfig } = useUiConfig();
const isBulkArchive = featureIds?.length > 1;
const archiveToggle = async () => {
try {
await archiveFeatureToggle(projectId, featureIds[0]);
setToastData({
text: 'Your feature toggle has been archived',
type: 'success',
title: 'Feature archived',
});
onConfirm();
onClose();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
onClose();
}
};
const buttonText = useActionButtonText(projectId, isBulkArchive);
const archiveToggles = async () => {
try {
await archiveFeatures(projectId, featureIds);
setToastData({
text: 'Selected feature toggles have been archived',
type: 'success',
title: 'Feature toggles archived',
});
const dialogTitle = isBulkArchive
? 'Archive feature toggles'
: 'Archive feature toggle';
const archiveAction = useArchiveAction({
projectId,
featureIds,
onSuccess() {
onConfirm();
onClose();
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
},
onError() {
onClose();
}
};
},
});
return (
<Dialogue
onClick={isBulkArchive ? archiveToggles : archiveToggle}
onClick={archiveAction}
open={isOpen}
onClose={onClose}
primaryButtonText={
isBulkArchive ? 'Archive toggles' : 'Archive toggle'
}
primaryButtonText={buttonText}
secondaryButtonText="Cancel"
title={
isBulkArchive
? 'Archive feature toggles'
: 'Archive feature toggle'
}
title={dialogTitle}
>
<ConditionallyRender
condition={isBulkArchive}
@ -155,7 +242,11 @@ export const FeatureArchiveDialog: VFC<IFeatureArchiveDialogProps> = ({
}
elseShow={
<p>
Are you sure you want to archive these feature toggles?
Are you sure you want to archive{' '}
{isBulkArchive
? 'these feature toggles'
: 'this feature toggle'}
?
</p>
}
/>

View File

@ -10,8 +10,9 @@ export interface IChangeSchema {
| 'deleteStrategy'
| 'patchVariant'
| 'reorderStrategy'
| 'archiveFeature'
| 'updateSegment';
payload: string | boolean | object | number;
payload: string | boolean | object | number | undefined;
}
export interface IChangeRequestConfig {