From b021e7cf851bfc05b5b73b7a16d768bf090f16bd Mon Sep 17 00:00:00 2001
From: Thomas Heartman
Date: Mon, 27 Nov 2023 11:34:34 +0100
Subject: [PATCH] feat: show strategies used by segments (#5407)
This PR displays change request usage of segments when such usage is
returned from the API. It expects at least #5406 to have been merged
before it can be merged.
![image](https://github.com/Unleash/unleash/assets/17786332/c74bb1c9-07f9-4bca-95bb-4ca020398444)
---
.../segments/SegmentDelete/SegmentDelete.tsx | 8 +-
.../SegmentDeleteUsedSegment.test.tsx | 63 ++++++++++++
.../SegmentDeleteUsedSegment.tsx | 99 +++++++++++++++----
.../useStrategiesBySegment.ts | 19 ++++
4 files changed, 168 insertions(+), 21 deletions(-)
create mode 100644 frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.test.tsx
diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx
index 64d804bbfa3e..e3944c57fede 100644
--- a/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx
+++ b/frontend/src/component/segments/SegmentDelete/SegmentDelete.tsx
@@ -18,9 +18,10 @@ export const SegmentDelete = ({
onClose,
onRemove,
}: ISegmentDeleteProps) => {
- const { strategies, loading } = useStrategiesBySegment(segment.id);
- const canDeleteSegment = strategies?.length === 0;
-
+ const { strategies, changeRequestStrategies, loading } =
+ useStrategiesBySegment(segment.id);
+ const canDeleteSegment =
+ strategies?.length === 0 && changeRequestStrategies?.length === 0;
if (loading) {
return null;
}
@@ -42,6 +43,7 @@ export const SegmentDelete = ({
open={open}
onClose={onClose}
strategies={strategies}
+ changeRequestStrategies={changeRequestStrategies}
/>
}
/>
diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.test.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.test.tsx
new file mode 100644
index 000000000000..c146d6eda601
--- /dev/null
+++ b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.test.tsx
@@ -0,0 +1,63 @@
+import React from 'react';
+import { render } from 'utils/testRenderer';
+import { screen } from '@testing-library/react';
+import { SegmentDeleteUsedSegment } from './SegmentDeleteUsedSegment';
+
+describe('SegmentDeleteUsedSegment', () => {
+ it('should link to change requests for change request strategies', async () => {
+ const projectId = 'project1';
+
+ const crStrategies = [
+ {
+ projectId,
+ featureName: 'feature1',
+ strategyName: 'flexible rollout',
+ environment: 'default',
+ changeRequest: { id: 1, title: null },
+ },
+ {
+ projectId,
+ featureName: 'feature1',
+ strategyName: 'flexible rollout',
+ environment: 'default',
+ changeRequest: { id: 2, title: 'My cool CR' },
+ },
+ ];
+
+ render(
+ {}}
+ />,
+ );
+
+ const links = await screen.findAllByRole('link');
+ expect(links).toHaveLength(crStrategies.length);
+
+ const [link1, link2] = links;
+
+ expect(link1).toHaveTextContent('#1');
+ expect(link1).toHaveAccessibleDescription('Change request 1');
+ expect(link1).toHaveAttribute(
+ 'href',
+ `/projects/${projectId}/change-requests/1`,
+ );
+ expect(link2).toHaveTextContent('#2 (My cool CR)');
+ expect(link2).toHaveAccessibleDescription('Change request 2');
+ expect(link2).toHaveAttribute(
+ 'href',
+ `/projects/${projectId}/change-requests/2`,
+ );
+ });
+});
diff --git a/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx
index 580bba9193f7..67ea0ce33bbf 100644
--- a/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx
+++ b/frontend/src/component/segments/SegmentDelete/SegmentDeleteUsedSegment/SegmentDeleteUsedSegment.tsx
@@ -5,6 +5,12 @@ import { Link } from 'react-router-dom';
import { formatEditStrategyPath } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit';
import { formatStrategyName } from 'utils/strategyNames';
import { styled } from '@mui/material';
+import {
+ ChangeRequestNewStrategy,
+ ChangeRequestStrategy,
+ ChangeRequestUpdatedStrategy,
+} from 'hooks/api/getters/useStrategiesBySegment/useStrategiesBySegment';
+import { sortStrategiesByFeature } from './sort-strategies';
const StyledUl = styled('ul')({
marginBottom: 0,
@@ -21,14 +27,29 @@ interface ISegmentDeleteUsedSegmentProps {
open: boolean;
onClose: () => void;
strategies: IFeatureStrategy[] | undefined;
+ changeRequestStrategies: ChangeRequestStrategy[] | undefined;
}
+export const formatChangeRequestPath = (
+ projectId: string,
+ changeRequestId: number,
+): string => {
+ return `/projects/${projectId}/change-requests/${changeRequestId}`;
+};
+
export const SegmentDeleteUsedSegment = ({
segment,
open,
onClose,
strategies,
+ changeRequestStrategies,
}: ISegmentDeleteUsedSegmentProps) => {
+ const sortedStrategies = sortStrategiesByFeature<
+ IFeatureStrategy,
+ ChangeRequestUpdatedStrategy,
+ ChangeRequestNewStrategy
+ >(strategies ?? [], changeRequestStrategies ?? []);
+
return (
{segment.name} segment for their strategies:
- {strategies?.map((strategy) => (
-
-
- {strategy.featureName!}{' '}
- {formatStrategyNameParens(strategy)}
-
-
- ))}
+ {sortedStrategies.map((strategy, index) =>
+ strategyListItem(strategy, index),
+ )}
);
};
-const formatStrategyNameParens = (strategy: IFeatureStrategy): string => {
+const formatStrategyNameParens = (strategy: {
+ strategyName?: string;
+}): string => {
if (!strategy.strategyName) {
return '';
}
return `(${formatStrategyName(strategy.strategyName)})`;
};
+
+const strategyListItem = (
+ strategy:
+ | IFeatureStrategy
+ | ChangeRequestUpdatedStrategy
+ | ChangeRequestNewStrategy,
+ index: number,
+) => {
+ const isChangeRequest = (
+ strategy: IFeatureStrategy | ChangeRequestStrategy,
+ ): strategy is ChangeRequestStrategy => 'changeRequest' in strategy;
+
+ if (isChangeRequest(strategy)) {
+ const { id, title } = strategy.changeRequest;
+
+ const text = title ? `#${id} (${title})` : `#${id}`;
+ return (
+
+
+ {strategy.featureName}{' '}
+ {`${formatStrategyNameParens(
+ strategy,
+ )} — in change request `}
+
+
+ {text}
+
+
+
+ );
+ } else {
+ return (
+
+
+ {strategy.featureName!} {formatStrategyNameParens(strategy)}
+
+
+ );
+ }
+};
diff --git a/frontend/src/hooks/api/getters/useStrategiesBySegment/useStrategiesBySegment.ts b/frontend/src/hooks/api/getters/useStrategiesBySegment/useStrategiesBySegment.ts
index 8ffe12dbfa28..bfb9fa92aa8a 100644
--- a/frontend/src/hooks/api/getters/useStrategiesBySegment/useStrategiesBySegment.ts
+++ b/frontend/src/hooks/api/getters/useStrategiesBySegment/useStrategiesBySegment.ts
@@ -5,8 +5,26 @@ import handleErrorResponses from '../httpErrorResponseHandler';
import { IFeatureStrategy } from 'interfaces/strategy';
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
+export type ChangeRequestInfo = { id: number; title: string | null };
+export type ChangeRequestNewStrategy = {
+ projectId: string;
+ featureName: string;
+ strategyName: string;
+ environment: string;
+ changeRequest: ChangeRequestInfo;
+};
+
+export type ChangeRequestUpdatedStrategy = ChangeRequestNewStrategy & {
+ id: string;
+};
+
+export type ChangeRequestStrategy =
+ | ChangeRequestNewStrategy
+ | ChangeRequestUpdatedStrategy;
+
export interface IUseStrategiesBySegmentOutput {
strategies: IFeatureStrategy[];
+ changeRequestStrategies: ChangeRequestStrategy[];
refetchUsedSegments: () => void;
loading: boolean;
error?: Error;
@@ -26,6 +44,7 @@ export const useStrategiesBySegment = (
return {
strategies: data?.strategies || [],
+ changeRequestStrategies: data?.changeRequestStrategies || [],
refetchUsedSegments,
loading: !error && !data,
error,