diff --git a/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx b/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx
index 186520ea37..98a2cee76a 100644
--- a/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx
+++ b/frontend/src/component/addons/AddonForm/AddonMultiSelector/AddonMultiSelector.test.tsx
@@ -7,6 +7,7 @@ import {
IAddonMultiSelectorProps,
AddonMultiSelector,
} from './AddonMultiSelector';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
const onChange = vi.fn();
const onFocus = vi.fn();
@@ -24,10 +25,13 @@ const mockProps: IAddonMultiSelectorProps = {
entityName: 'project',
};
+const server = testServerSetup();
+
describe('AddonMultiSelector', () => {
beforeEach(() => {
onChange.mockClear();
onFocus.mockClear();
+ testServerRoute(server, '/api/admin/ui-config', {});
});
it('renders with default state', () => {
diff --git a/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx b/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx
index 97ccfa906c..f013194bb5 100644
--- a/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx
+++ b/frontend/src/component/admin/apiToken/ApiTokenForm/SelectProjectInput/SelectProjectInput.test.tsx
@@ -7,6 +7,7 @@ import {
ISelectProjectInputProps,
SelectProjectInput,
} from './SelectProjectInput';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
const onChange = vi.fn();
const onFocus = vi.fn();
@@ -22,10 +23,13 @@ const mockProps: ISelectProjectInputProps = {
onFocus,
};
+const server = testServerSetup();
+
describe('SelectProjectInput', () => {
beforeEach(() => {
onChange.mockClear();
onFocus.mockClear();
+ testServerRoute(server, '/api/admin/ui-config', {});
});
it('renders with default state', () => {
diff --git a/frontend/src/component/changeRequest/ChangeRequest.test.tsx b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
new file mode 100644
index 0000000000..5d878a3205
--- /dev/null
+++ b/frontend/src/component/changeRequest/ChangeRequest.test.tsx
@@ -0,0 +1,288 @@
+import {
+ render,
+ screen,
+ waitFor,
+ within,
+ getAllByRole,
+ fireEvent,
+} from '@testing-library/react';
+import { MemoryRouter, Routes, Route } from 'react-router-dom';
+import { FeatureView } from '../feature/FeatureView/FeatureView';
+import { ThemeProvider } from 'themes/ThemeProvider';
+import { AccessProvider } from '../providers/AccessProvider/AccessProvider';
+import { AnnouncerProvider } from '../common/Announcer/AnnouncerProvider/AnnouncerProvider';
+import { testServerRoute, testServerSetup } from '../../utils/testServer';
+import { UIProviderContainer } from '../providers/UIProvider/UIProviderContainer';
+import { FC } from 'react';
+
+const server = testServerSetup();
+
+const pendingChangeRequest = (featureName: string) =>
+ testServerRoute(
+ server,
+ 'api/admin/projects/default/change-requests/pending',
+ [
+ {
+ id: 156,
+ environment: 'production',
+ state: 'Draft',
+ minApprovals: 1,
+ project: 'default',
+ createdBy: {
+ id: 1,
+ username: 'admin',
+ imageUrl:
+ 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
+ },
+ createdAt: '2022-12-02T09:19:12.242Z',
+ features: [
+ {
+ name: featureName,
+ changes: [
+ {
+ id: 292,
+ action: 'addStrategy',
+ payload: {
+ name: 'default',
+ segments: [],
+ parameters: {},
+ constraints: [],
+ },
+ createdAt: '2022-12-02T09:19:12.245Z',
+ createdBy: {
+ id: 1,
+ username: 'admin',
+ imageUrl:
+ 'https://gravatar.com/avatar/21232f297a57a5a743894a0e4a801fc3?size=42&default=retro',
+ },
+ },
+ ],
+ },
+ ],
+ approvals: [],
+ comments: [],
+ },
+ ]
+ );
+
+const changeRequestsEnabledIn = (env: string) =>
+ testServerRoute(
+ server,
+ '/api/admin/projects/default/change-requests/config',
+ [
+ {
+ environment: 'development',
+ type: 'development',
+ changeRequestEnabled: env === 'development',
+ },
+ {
+ environment: 'production',
+ type: 'production',
+ changeRequestEnabled: env === 'production',
+ },
+ ]
+ );
+
+const uiConfigForEnterprise = () =>
+ testServerRoute(server, '/api/admin/ui-config', {
+ environment: 'Open Source',
+ flags: {
+ changeRequests: true,
+ },
+ slogan: 'getunleash.io - All rights reserved',
+ name: 'Unleash enterprise',
+ links: [
+ {
+ value: 'Documentation',
+ icon: 'library_books',
+ href: 'https://docs.getunleash.io/docs',
+ title: 'User documentation',
+ },
+ {
+ value: 'GitHub',
+ icon: 'c_github',
+ href: 'https://github.com/Unleash/unleash',
+ title: 'Source code on GitHub',
+ },
+ ],
+ version: '4.18.0-beta.5',
+ emailEnabled: false,
+ unleashUrl: 'http://localhost:4242',
+ baseUriPath: '',
+ authenticationType: 'enterprise',
+ segmentValuesLimit: 100,
+ strategySegmentsLimit: 5,
+ frontendApiOrigins: ['*'],
+ versionInfo: {
+ current: { oss: '4.18.0-beta.5', enterprise: '4.17.0-beta.1' },
+ latest: {},
+ isLatest: true,
+ instanceId: 'c7566052-15d7-4e09-9625-9c988e1f2be7',
+ },
+ disablePasswordAuth: false,
+ });
+
+const featureList = (featureName: string) =>
+ testServerRoute(server, '/api/admin/projects/default', {
+ name: 'Default',
+ description: 'Default project',
+ health: 100,
+ updatedAt: '2022-11-14T10:15:59.228Z',
+ environments: ['development', 'production'],
+ features: [
+ {
+ type: 'release',
+ name: featureName,
+ createdAt: '2022-11-14T08:16:33.338Z',
+ lastSeenAt: null,
+ stale: false,
+ environments: [
+ {
+ name: 'development',
+ enabled: false,
+ type: 'development',
+ sortOrder: 100,
+ },
+ {
+ name: 'production',
+ enabled: false,
+ type: 'production',
+ sortOrder: 200,
+ },
+ ],
+ },
+ ],
+ members: 0,
+ version: 1,
+ });
+
+const feature = ({ name, enabled }: { name: string; enabled: boolean }) =>
+ testServerRoute(server, `/api/admin/projects/default/features/${name}`, {
+ environments: [
+ {
+ name: 'development',
+ enabled: false,
+ type: 'development',
+ sortOrder: 100,
+ strategies: [],
+ },
+ {
+ name: 'production',
+ enabled,
+ type: 'production',
+ sortOrder: 200,
+ strategies: [],
+ },
+ ],
+ name,
+ impressionData: false,
+ description: '',
+ project: 'default',
+ stale: false,
+ variants: [],
+ createdAt: '2022-11-14T08:16:33.338Z',
+ lastSeenAt: null,
+ type: 'release',
+ archived: false,
+ });
+
+const otherRequests = (feature: string) => {
+ testServerRoute(server, `api/admin/client-metrics/features/${feature}`, {
+ version: 1,
+ maturity: 'stable',
+ featureName: feature,
+ lastHourUsage: [],
+ seenApplications: [],
+ });
+ testServerRoute(server, `api/admin/features/${feature}/tags`, {
+ version: 1,
+ tags: [],
+ });
+ testServerRoute(server, 'api/admin/user', {
+ user: {
+ isAPI: false,
+ id: 17,
+ 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 UnleashUiSetup: FC<{ path: string; pathTemplate: string }> = ({
+ children,
+ path,
+ pathTemplate,
+}) => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+const setupHttpRoutes = ({
+ featureName,
+ enabled,
+}: {
+ featureName: string;
+ enabled: boolean;
+}) => {
+ pendingChangeRequest(featureName);
+ changeRequestsEnabledIn('production');
+ uiConfigForEnterprise();
+ featureList(featureName);
+ feature({ name: featureName, enabled });
+ otherRequests(featureName);
+};
+
+const verifyBannerForPendingChangeRequest = async () => {
+ return screen.findByText('Change request mode', {}, { timeout: 5000 });
+};
+
+const changeToggle = async (environment: string) => {
+ const featureToggleStatusBox = screen.getByTestId('feature-toggle-status');
+ await within(featureToggleStatusBox).findByText(environment);
+ const toggle = screen.getAllByRole('checkbox')[1];
+ fireEvent.click(toggle);
+};
+
+const verifyChangeRequestDialog = async (bannerMainText: string) => {
+ await screen.findByText('Your suggestion:');
+ const message = screen.getByTestId('update-enabled-message').textContent;
+ expect(message).toBe(bannerMainText);
+};
+
+test('add toggle change to pending change request', async () => {
+ setupHttpRoutes({ featureName: 'test', enabled: false });
+
+ render(
+
+
+
+ );
+
+ await verifyBannerForPendingChangeRequest();
+
+ await changeToggle('production');
+
+ await verifyChangeRequestDialog('Enable feature toggle test in production');
+});
diff --git a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx
index a571beb130..778ba31460 100644
--- a/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx
+++ b/frontend/src/component/changeRequest/ChangeRequest/ChangeRequest.tsx
@@ -224,6 +224,7 @@ export const ChangeRequest: VFC = ({
>
{feature.changes.map((change, index) => (
(
-
+
{enabled ? 'Enable' : 'Disable'} feature toggle{' '}
{featureName} in {environment}
diff --git a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
index 8791740e68..d78c26248d 100644
--- a/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
+++ b/frontend/src/component/common/InstanceStatus/InstanceStatusBar.test.tsx
@@ -5,6 +5,13 @@ import { screen } from '@testing-library/react';
import { addDays, subDays } from 'date-fns';
import { INSTANCE_STATUS_BAR_ID } from 'utils/testIds';
import { UNKNOWN_INSTANCE_STATUS } from 'hooks/api/getters/useInstanceStatus/useInstanceStatus';
+import { testServerRoute, testServerSetup } from 'utils/testServer';
+
+const server = testServerSetup();
+
+beforeEach(() => {
+ testServerRoute(server, '/api/admin/ui-config', {});
+});
test('InstanceStatusBar should be hidden by default', async () => {
render();
diff --git a/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx b/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx
index 3d99a7af94..d0c91e65ee 100644
--- a/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx
+++ b/frontend/src/component/common/PermissionSwitch/PermissionSwitch.tsx
@@ -44,6 +44,7 @@ const PermissionSwitch = React.forwardRef<
{
};
return (
-
+
Feature toggle status
{
+ testServerRoute(server, '/api/admin/ui-config', {});
testServerRoute(server, '/api/admin/user', {});
testServerRoute(server, '/auth/reset/validate', {
name: INVALID_TOKEN_ERROR,
diff --git a/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts b/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts
index 83f8d1c147..6b351aa1db 100644
--- a/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts
+++ b/frontend/src/hooks/api/getters/useChangeRequestConfig/useChangeRequestConfig.ts
@@ -1,20 +1,21 @@
-import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IChangeRequestEnvironmentConfig } from 'component/changeRequest/changeRequest.types';
-import useUiConfig from '../useUiConfig/useUiConfig';
+import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';
export const useChangeRequestConfig = (projectId: string) => {
- const { isOss } = useUiConfig();
- const { data, error, mutate } = useSWR(
+ const { data, error, mutate } = useEnterpriseSWR<
+ IChangeRequestEnvironmentConfig[]
+ >(
formatApiPath(`api/admin/projects/${projectId}/change-requests/config`),
- (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path))
+ fetcher,
+ []
);
return {
data: data || [],
loading: !error && !data,
- refetchChangeRequestConfig: () => mutate(),
+ refetchChangeRequestConfig: mutate,
error,
};
};
diff --git a/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts b/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts
new file mode 100644
index 0000000000..c391bab7f5
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useEnterpriseSWR/useEnterpriseSWR.ts
@@ -0,0 +1,34 @@
+import useSWR, { BareFetcher, Key, SWRResponse } from 'swr';
+import { useEffect } from 'react';
+import useUiConfig from '../useUiConfig/useUiConfig';
+
+export const useConditionalSWR = (
+ key: Key,
+ fetcher: BareFetcher,
+ condition: T
+): SWRResponse => {
+ const result = useSWR(key, fetcher);
+
+ useEffect(() => {
+ result.mutate();
+ }, [condition]);
+
+ return result;
+};
+
+export const useEnterpriseSWR = (
+ key: Key,
+ fetcher: BareFetcher,
+ fallback: Data
+) => {
+ const { isEnterprise } = useUiConfig();
+
+ const result = useConditionalSWR(
+ key,
+ (path: string) =>
+ isEnterprise() ? fetcher(path) : Promise.resolve(fallback),
+ isEnterprise()
+ );
+
+ return result;
+};
diff --git a/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts b/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts
index 1a73d1653f..9f8e939062 100644
--- a/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts
+++ b/frontend/src/hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests.ts
@@ -1,8 +1,7 @@
-import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
-import useUiConfig from '../useUiConfig/useUiConfig';
+import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';
const fetcher = (path: string) => {
return fetch(path)
@@ -11,10 +10,10 @@ const fetcher = (path: string) => {
};
export const usePendingChangeRequests = (project: string) => {
- const { isOss } = useUiConfig();
- const { data, error, mutate } = useSWR(
+ const { data, error, mutate } = useEnterpriseSWR(
formatApiPath(`api/admin/projects/${project}/change-requests/pending`),
- (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path))
+ fetcher,
+ []
);
return {
diff --git a/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts b/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts
index 9f780c0181..5a196f1f70 100644
--- a/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts
+++ b/frontend/src/hooks/api/getters/usePendingChangeRequestsForFeature/usePendingChangeRequestsForFeature.ts
@@ -1,8 +1,7 @@
-import useSWR from 'swr';
import { formatApiPath } from 'utils/formatPath';
import handleErrorResponses from '../httpErrorResponseHandler';
import { IChangeRequest } from 'component/changeRequest/changeRequest.types';
-import useUiConfig from '../useUiConfig/useUiConfig';
+import { useEnterpriseSWR } from '../useEnterpriseSWR/useEnterpriseSWR';
const fetcher = (path: string) => {
return fetch(path)
@@ -14,12 +13,12 @@ export const usePendingChangeRequestsForFeature = (
project: string,
featureName: string
) => {
- const { isOss } = useUiConfig();
- const { data, error, mutate } = useSWR(
+ const { data, error, mutate } = useEnterpriseSWR(
formatApiPath(
`api/admin/projects/${project}/change-requests/pending/${featureName}`
),
- (path: string) => (isOss() ? Promise.resolve([]) : fetcher(path))
+ fetcher,
+ []
);
return {