diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.test.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.test.tsx new file mode 100644 index 000000000000..90c7f8a4565f --- /dev/null +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.test.tsx @@ -0,0 +1,197 @@ +import { render } from 'utils/testRenderer'; +import { StrategyChange } from './StrategyChange'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +const server = testServerSetup(); + +const feature = 'my_feature'; +const projectId = 'default'; +const environmentName = 'production'; +const snapshotRollout = '70'; +const currentRollout = '80'; +const changeRequestRollout = '90'; +const strategy = { + name: 'flexibleRollout', + constraints: [], + variants: [], + parameters: { + groupId: 'child_1', + stickiness: 'default', + }, + segments: [], + id: '8e25e369-6424-4dad-b17f-5d32cceb2fbe', + disabled: false, +}; + +const setupApi = () => { + testServerRoute( + server, + `/api/admin/projects/${projectId}/features/${feature}`, + { + environments: [ + { + name: environmentName, + strategies: [ + { + ...strategy, + title: 'current_title', + parameters: { + ...strategy.parameters, + rollout: currentRollout, + }, + }, + ], + }, + ], + }, + ); +}; + +beforeEach(setupApi); + +test('Editing strategy before change request is applied diffs against current strategy', async () => { + render( + , + ); + + await screen.findByText('Editing strategy:'); + await screen.findByText('change_request_title'); + await screen.findByText('current_title'); + expect(screen.queryByText('snapshot_title')).not.toBeInTheDocument(); + + const viewDiff = await screen.findByText('View Diff'); + await userEvent.hover(viewDiff); + await screen.findByText(`- parameters.rollout: "${currentRollout}"`); + await screen.findByText(`+ parameters.rollout: "${changeRequestRollout}"`); +}); + +test('Editing strategy after change request is applied diffs against the snapshot', async () => { + render( + , + ); + + await screen.findByText('Editing strategy:'); + await screen.findByText('change_request_title'); + await screen.findByText('snapshot_title'); + expect(screen.queryByText('current_title')).not.toBeInTheDocument(); + + const viewDiff = await screen.findByText('View Diff'); + await userEvent.hover(viewDiff); + await screen.findByText(`- parameters.rollout: "${snapshotRollout}"`); + await screen.findByText(`+ parameters.rollout: "${changeRequestRollout}"`); +}); + +test('Deleting strategy before change request is applied diffs against current strategy', async () => { + render( + , + ); + + await screen.findByText('- Deleting strategy:'); + await screen.findByText('Gradual rollout'); + await screen.findByText('current_title'); + + const viewDiff = await screen.findByText('View Diff'); + await userEvent.hover(viewDiff); + await screen.findByText('- constraints (deleted)'); +}); + +test('Deleting strategy after change request is applied diffs against the snapshot', async () => { + render( + , + ); + + await screen.findByText('- Deleting strategy:'); + await screen.findByText('Gradual rollout'); + await screen.findByText('snapshot_title'); + expect(screen.queryByText('current_title')).not.toBeInTheDocument(); + + const viewDiff = await screen.findByText('View Diff'); + await userEvent.hover(viewDiff); + await screen.findByText('- constraints (deleted)'); +}); diff --git a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.tsx b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.tsx index 03321f4f4984..fd03701a168b 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/Changes/Change/StrategyChange.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import type { VFC, FC, ReactNode } from 'react'; +import type { FC, ReactNode } from 'react'; import { Box, styled, Tooltip, Typography } from '@mui/material'; import BlockIcon from '@mui/icons-material/Block'; import TrackChangesIcon from '@mui/icons-material/TrackChanges'; @@ -20,6 +20,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit import { flexRow } from 'themes/themeStyles'; import { EnvironmentVariantsTable } from 'component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsTable/EnvironmentVariantsTable'; import { ChangeOverwriteWarning } from './ChangeOverwriteWarning/ChangeOverwriteWarning'; +import type { IFeatureStrategy } from 'interfaces/strategy'; export const ChangeItemWrapper = styled(Box)({ display: 'flex', @@ -60,10 +61,7 @@ const StyledTypography: FC<{ children?: React.ReactNode }> = styled(Typography)( }), ); -const hasNameField = (payload: unknown): payload is { name: string } => - typeof payload === 'object' && payload !== null && 'name' in payload; - -const DisabledEnabledState: VFC<{ show?: boolean; disabled: boolean }> = ({ +const DisabledEnabledState: FC<{ show?: boolean; disabled: boolean }> = ({ show = true, disabled, }) => { @@ -98,7 +96,7 @@ const DisabledEnabledState: VFC<{ show?: boolean; disabled: boolean }> = ({ ); }; -const EditHeader: VFC<{ +const EditHeader: FC<{ wasDisabled?: boolean; willBeDisabled?: boolean; }> = ({ wasDisabled = false, willBeDisabled = false }) => { @@ -119,7 +117,157 @@ const EditHeader: VFC<{ return Editing strategy:; }; -export const StrategyChange: VFC<{ +const hasDiff = (object: unknown, objectToCompare: unknown) => + JSON.stringify(object) !== JSON.stringify(objectToCompare); + +const DeleteStrategy: FC<{ + change: IChangeRequestDeleteStrategy; + changeRequestState: ChangeRequestState; + currentStrategy: IFeatureStrategy | undefined; + actions?: ReactNode; +}> = ({ change, changeRequestState, currentStrategy, actions }) => { + const name = + changeRequestState === 'Applied' + ? change.payload?.snapshot?.name + : currentStrategy?.name; + const title = + changeRequestState === 'Applied' + ? change.payload?.snapshot?.title + : currentStrategy?.title; + const referenceStrategy = + changeRequestState === 'Applied' + ? change.payload.snapshot + : currentStrategy; + + return ( + <> + + + ({ + color: theme.palette.error.main, + })} + > + - Deleting strategy: + + + + + +
{actions}
+
+ {referenceStrategy && ( + + )} + + + Deleting strategy variants: + + + + ) + } + /> + + ); +}; + +const UpdateStrategy: FC<{ + change: IChangeRequestUpdateStrategy; + changeRequestState: ChangeRequestState; + currentStrategy: IFeatureStrategy | undefined; + actions?: ReactNode; +}> = ({ change, changeRequestState, currentStrategy, actions }) => { + const hasVariantDiff = hasDiff( + currentStrategy?.variants || [], + change.payload.variants || [], + ); + const previousTitle = + changeRequestState === 'Applied' + ? change.payload.snapshot?.title + : currentStrategy?.title; + const referenceStrategy = + changeRequestState === 'Applied' + ? change.payload.snapshot + : currentStrategy; + + return ( + <> + + + + + + + + +
{actions}
+
+ theme.spacing(2), + marginBottom: (theme) => theme.spacing(2), + ...flexRow, + gap: (theme) => theme.spacing(1), + }} + > + This strategy will be{' '} + + + } + /> + + + + Updating feature variants to: + + + + } + /> + + ); +}; + +export const StrategyChange: FC<{ actions?: ReactNode; change: | IChangeRequestAddStrategy @@ -144,16 +292,6 @@ export const StrategyChange: VFC<{ environmentName, ); - const hasDiff = (object: unknown, objectToCompare: unknown) => - JSON.stringify(object) !== JSON.stringify(objectToCompare); - - const isStrategyAction = - change.action === 'addStrategy' || change.action === 'updateStrategy'; - - const hasVariantDiff = - isStrategyAction && - hasDiff(currentStrategy?.variants || [], change.payload.variants || []); - return ( <> {change.action === 'addStrategy' && ( @@ -169,7 +307,10 @@ export const StrategyChange: VFC<{ > + Adding strategy: - + {actions} - - - Updating feature variants to: - - - - ) - } - /> - - )} - {change.action === 'deleteStrategy' && ( - <> - - - ({ - color: theme.palette.error.main, - })} - > - - Deleting strategy: - - {hasNameField(change.payload) && ( - - - - )} - -
{actions}
-
- - { - - } - - } - /> - - - Deleting strategy variants: - - - - ) - } - /> - - )} - {change.action === 'updateStrategy' && ( - <> - - - - - - - - -
{actions}
-
- theme.spacing(2), - marginBottom: (theme) => theme.spacing(2), - ...flexRow, - gap: (theme) => theme.spacing(1), - }} - > - This strategy will be{' '} - - - } - /> - - 0 && ( Updating feature variants to: - } - /> + )} )} + {change.action === 'deleteStrategy' && ( + + )} + {change.action === 'updateStrategy' && ( + + )} ); }; diff --git a/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx b/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx index 7295dcf614e2..8719d91e2470 100644 --- a/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx +++ b/frontend/src/component/changeRequest/ChangeRequest/StrategyTooltipLink/StrategyTooltipLink.tsx @@ -51,10 +51,8 @@ export const StrategyDiff: FC<{ }; interface IStrategyTooltipLinkProps { - change: - | IChangeRequestAddStrategy - | IChangeRequestUpdateStrategy - | IChangeRequestDeleteStrategy; + name: string; + title?: string; previousTitle?: string; children?: React.ReactNode; } @@ -80,29 +78,32 @@ const Truncated = styled('div')(() => ({ })); export const StrategyTooltipLink: FC = ({ - change, + name, + title, previousTitle, children, -}) => ( - - - - - {formatStrategyName(change.payload.name)} - - - View Diff - - - - -); +}) => { + return ( + + + + + {formatStrategyName(name)} + + + View Diff + + + + + ); +}; diff --git a/frontend/src/component/changeRequest/changeRequest.types.ts b/frontend/src/component/changeRequest/changeRequest.types.ts index 61ccaeeecd64..e2e742b8027a 100644 --- a/frontend/src/component/changeRequest/changeRequest.types.ts +++ b/frontend/src/component/changeRequest/changeRequest.types.ts @@ -241,14 +241,15 @@ export type ChangeRequestAddStrategy = Pick< export type ChangeRequestEditStrategy = ChangeRequestAddStrategy & { id: string; - snapshot?: Omit & { title?: string | null }; + snapshot?: IFeatureStrategy; }; type ChangeRequestDeleteStrategy = { id: string; - name: string; + name?: string; title?: string; disabled?: boolean; + snapshot?: IFeatureStrategy; }; export type ChangeRequestAction = diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx index 9f7bf3dbe801..38dbc9bca533 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewEnvironments/FeatureOverviewEnvironment/EnvironmentAccordionBody/StrategyDraggableItem/StrategyItem/StrategyExecution/StrategyExecution.tsx @@ -1,4 +1,4 @@ -import { Fragment, useMemo, type VFC } from 'react'; +import { type FC, Fragment, useMemo } from 'react'; import { Alert, Box, Chip, Link, styled } from '@mui/material'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import PercentageCircle from 'component/common/PercentageCircle/PercentageCircle'; @@ -56,7 +56,7 @@ const CustomStrategyDeprecationWarning = () => ( ); -const NoItems: VFC = () => ( +const NoItems: FC = () => ( This strategy does not have constraints or parameters. @@ -73,7 +73,7 @@ const StyledValueSeparator = styled('span')(({ theme }) => ({ color: theme.palette.neutral.main, })); -export const StrategyExecution: VFC = ({ +export const StrategyExecution: FC = ({ strategy, }) => { const { parameters, constraints = [] } = strategy;