diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.test.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.test.tsx new file mode 100644 index 0000000000..8ab488802a --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.test.tsx @@ -0,0 +1,263 @@ +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { render } from 'utils/testRenderer'; +import { ADMIN } from 'component/providers/AccessProvider/permissions'; +import { screen } from '@testing-library/dom'; +import { Route, Routes } from 'react-router-dom'; +import { + ChangeRequestAction, + IChangeRequest, +} from 'component/changeRequest/changeRequest.types'; +import { EnvironmentVariantsCard } from './EnvironmentVariantsCard'; +import { IFeatureEnvironment } from 'interfaces/featureToggle'; + +const server = testServerSetup(); + +const strategy = { + name: 'flexibleRollout', + constraints: [], + variants: [], + parameters: { + groupId: 'CR-toggle', + rollout: '100', + stickiness: 'default', + }, + sortOrder: 0, + id: 'b6363cc8-ad8e-478a-b464-484bbd3b31f6', + title: '', + disabled: false, +}; + +const scheduledRequest = ( + action: Omit = 'updateStrategy', + createdBy = 1, +): IChangeRequest => { + return { + id: 71, + title: 'Change request #71', + environment: 'production', + minApprovals: 1, + project: 'dafault', + createdBy: { + id: createdBy, + username: 'admin', + imageUrl: + 'https://gravatar.com/avatar/8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918?s=42&d=retro&r=g', + }, + createdAt: new Date('2023-11-08T10:28:47.183Z'), + features: [ + { + name: 'feature1', + changes: [ + { + id: 84, + action: action as any, + payload: { + id: 'b6363cc8-ad8e-478a-b464-484bbd3b31f6', + name: 'flexibleRollout', + title: '', + disabled: false, + segments: [], + variants: [], + parameters: { + groupId: 'CR-toggle', + rollout: '15', + stickiness: 'default', + }, + constraints: [], + }, + createdAt: new Date('2023-11-08T10:28:47.183Z'), + createdBy: { + id: 1, + username: 'admin', + imageUrl: + 'https://gravatar.com/avatar/8c6976e5b5410415bde908bd4dee15dfb167a9c873fc4bb8a81f6f2ab448a918?s=42&d=retro&r=g', + }, + }, + ], + }, + ], + segments: [], + approvals: [], + rejections: [], + comments: [], + state: 'Scheduled', + schedule: { + scheduledAt: new Date().toISOString(), + status: 'pending', + }, + }; +}; + +const uiConfig = () => { + testServerRoute(server, '/api/admin/ui-config', { + versionInfo: { + current: { oss: 'version', enterprise: 'version' }, + }, + flags: { + scheduledConfigurationChanges: true, + }, + }); +}; + +const user = () => { + testServerRoute(server, '/api/admin/user', { + user: { + isAPI: false, + id: 1, + name: 'Some User', + email: 'user@example.com', + imageUrl: + 'https://gravatar.com/avatar/8aa1132e102345f8c79322340e15340?size=42&default=retro', + seenAt: '2022-11-28T14:55:18.982Z', + loginAttempts: 0, + createdAt: '2022-11-23T13:31:17.061Z', + }, + permissions: [{ permission: ADMIN }], + feedback: [], + splash: {}, + }); +}; +const changeRequestConfig = () => + testServerRoute( + server, + '/api/admin/projects/default/change-requests/config', + [ + { + environment: 'development', + type: 'development', + changeRequestEnabled: false, + }, + { + environment: 'production', + type: 'production', + changeRequestEnabled: true, + }, + ], + 'get', + ); + +const feature = () => { + testServerRoute(server, '/api/admin/projects/default/features/feature1', { + environments: [ + { + name: 'development', + lastSeenAt: null, + variants: [], + enabled: false, + type: 'development', + sortOrder: 2, + strategies: [], + }, + { + name: 'production', + lastSeenAt: null, + variants: [], + enabled: false, + type: 'production', + sortOrder: 3, + strategies: [ + { + name: 'flexibleRollout', + constraints: [], + variants: [], + parameters: { + groupId: 'CR-toggle', + rollout: '100', + stickiness: 'default', + }, + sortOrder: 0, + id: 'b6363cc8-ad8e-478a-b464-484bbd3b31f6', + title: '', + disabled: false, + }, + ], + }, + ], + name: 'feature1', + favorite: false, + impressionData: false, + description: null, + project: 'MyNewProject', + stale: false, + lastSeenAt: null, + createdAt: '2023-11-01T10:11:58.505Z', + type: 'release', + variants: [], + archived: false, + dependencies: [], + children: [], + }); +}; + +const setupOtherServerRoutes = () => { + uiConfig(); + changeRequestConfig(); + user(); + feature(); +}; + +beforeEach(() => { + setupOtherServerRoutes(); +}); + +const Component = () => { + return ( + <> + + + } + /> + + + ); +}; +describe('Change request badges for variants', () => { + test('should not render a badge if no changes', async () => { + testServerRoute( + server, + '/api/admin/projects/default/change-requests/pending/feature1/variants', + [], + ); + + render(, { + route: '/projects/default/features/feature1/variants', + permissions: [ + { + permission: ADMIN, + }, + ], + }); + + expect(screen.queryByText('Changes Scheduled')).toBe(null); + }); + + test('should render the badge when scheduled request with "patchVariant" action', async () => { + const changeRequest = scheduledRequest('patchVariant', 1); + + testServerRoute( + server, + '/api/admin/projects/default/change-requests/pending/feature1', + [changeRequest], + ); + + render(, { + route: '/projects/default/features/feature1/variants', + permissions: [ + { + permission: ADMIN, + }, + ], + }); + await screen.findByText('Changes Scheduled'); + }); +}); diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx index 5d192eacc0..9b7b296625 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/EnvironmentVariantsCard.tsx @@ -4,6 +4,10 @@ import { IFeatureEnvironment } from 'interfaces/featureToggle'; import { EnvironmentVariantsTable } from './EnvironmentVariantsTable/EnvironmentVariantsTable'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { Badge } from 'component/common/Badge/Badge'; +import { useRequiredPathParam } from '../../../../../../hooks/useRequiredPathParam'; +import { useVariantsFromScheduledRequests } from './useVariantsFromScheduledRequests'; +import { ChangesScheduledBadge } from '../../../../../changeRequest/ModifiedInChangeRequestStatusBadge/ChangesScheduledBadge'; +import { Box } from '@mui/system'; const StyledCard = styled('div')(({ theme }) => ({ padding: theme.spacing(3), @@ -70,6 +74,13 @@ export const EnvironmentVariantsCard = ({ searchValue, children, }: IEnvironmentVariantsCardProps) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const scheduledRequestIds = useVariantsFromScheduledRequests( + projectId, + featureId, + environment.name, + ); const variants = environment.variants ?? []; const stickiness = variants[0]?.stickiness || 'default'; @@ -81,6 +92,18 @@ export const EnvironmentVariantsCard = ({ {environment.name} + 0} + show={ + + + + } + /> {children} diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/useVariantsFromScheduledRequests.ts b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/useVariantsFromScheduledRequests.ts new file mode 100644 index 0000000000..325e088773 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/EnvironmentVariantsCard/useVariantsFromScheduledRequests.ts @@ -0,0 +1,39 @@ +import { usePendingChangeRequestsForFeature } from 'hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature'; + +export const useVariantsFromScheduledRequests = ( + projectId: string, + featureId: string, + environment: string, +): number[] => { + const { changeRequests } = usePendingChangeRequestsForFeature( + projectId, + featureId, + ); + + const scheduledEnvironmentRequests = + changeRequests?.filter( + (request) => + request.environment === environment && + request.state === 'Scheduled', + ) || []; + + const result: number[] = []; + if (scheduledEnvironmentRequests.length === 0) { + return result; + } + + scheduledEnvironmentRequests.forEach((scheduledRequest) => { + const feature = scheduledRequest?.features.find( + (feature) => feature.name === featureId, + ); + const change = feature?.changes.find((change) => { + return change.action === 'patchVariant'; + }); + + if (change) { + result.push(scheduledRequest.id); + } + }); + + return result; +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx index 26d501a3f0..8333b8a198 100644 --- a/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureVariants/FeatureEnvironmentVariants/FeatureEnvironmentVariants.tsx @@ -27,7 +27,6 @@ import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; import { Edit } from '@mui/icons-material'; -import { VariantInfoAlert } from 'component/common/VariantInfoAlert/VariantInfoAlert'; import { StrategyVariantsPreferredAlert } from 'component/common/StrategyVariantsUpgradeAlert/StrategyVariantsUpgradeAlert'; const StyledButtonContainer = styled('div')(({ theme }) => ({