mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
chore: remove create feature component (#7959)
After we implemented new feature flag creation flow, this are not used anymore. Creation is now handled by **CreateFeatureDialog**. Also edit component can be minified, because it does not need so many fields anymore.
This commit is contained in:
parent
e5cca661d9
commit
4a4dafcc3f
@ -46,8 +46,6 @@ const BreadcrumbNav = () => {
|
|||||||
item !== 'copy' &&
|
item !== 'copy' &&
|
||||||
item !== 'features' &&
|
item !== 'features' &&
|
||||||
item !== 'features2' &&
|
item !== 'features2' &&
|
||||||
// TODO: this can be removed after new create flag flow goes live
|
|
||||||
item !== 'create-toggle' &&
|
|
||||||
item !== 'settings' &&
|
item !== 'settings' &&
|
||||||
item !== 'profile' &&
|
item !== 'profile' &&
|
||||||
item !== 'insights',
|
item !== 'insights',
|
||||||
|
@ -1,93 +0,0 @@
|
|||||||
import { screen, waitFor } from '@testing-library/react';
|
|
||||||
import { render } from 'utils/testRenderer';
|
|
||||||
import { testServerRoute, testServerSetup } from 'utils/testServer';
|
|
||||||
import CreateFeature from './CreateFeature';
|
|
||||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
const server = testServerSetup();
|
|
||||||
|
|
||||||
const setupApi = ({
|
|
||||||
flagCount,
|
|
||||||
flagLimit,
|
|
||||||
}: { flagCount: number; flagLimit: number }) => {
|
|
||||||
testServerRoute(server, '/api/admin/ui-config', {
|
|
||||||
flags: {
|
|
||||||
resourceLimits: true,
|
|
||||||
},
|
|
||||||
resourceLimits: {
|
|
||||||
featureFlags: flagLimit,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
testServerRoute(server, '/api/admin/search/features', {
|
|
||||||
total: flagCount,
|
|
||||||
features: Array.from({ length: flagCount }).map((_, i) => ({
|
|
||||||
name: `flag-${i}`,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('button states', () => {
|
|
||||||
test("should allow you to create feature flags when you're below the global limit", async () => {
|
|
||||||
setupApi({ flagLimit: 3, flagCount: 2 });
|
|
||||||
|
|
||||||
render(
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path='/projects/:projectId/create-toggle'
|
|
||||||
element={<CreateFeature />}
|
|
||||||
/>
|
|
||||||
</Routes>,
|
|
||||||
{
|
|
||||||
route: '/projects/default/create-toggle',
|
|
||||||
permissions: [{ permission: CREATE_FEATURE }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const button = await screen.findByRole('button', {
|
|
||||||
name: /create feature flag/i,
|
|
||||||
});
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(button).not.toBeDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('limit component', () => {
|
|
||||||
test('should show limit reached info', async () => {
|
|
||||||
setupApi({ flagLimit: 1, flagCount: 1 });
|
|
||||||
render(
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path='/projects/:projectId/create-toggle'
|
|
||||||
element={<CreateFeature />}
|
|
||||||
/>
|
|
||||||
</Routes>,
|
|
||||||
{
|
|
||||||
route: '/projects/default/create-toggle',
|
|
||||||
permissions: [{ permission: CREATE_FEATURE }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await screen.findByText('You have reached the limit for feature flags');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('should show approaching limit info', async () => {
|
|
||||||
setupApi({ flagLimit: 10, flagCount: 9 });
|
|
||||||
render(
|
|
||||||
<Routes>
|
|
||||||
<Route
|
|
||||||
path='/projects/:projectId/create-toggle'
|
|
||||||
element={<CreateFeature />}
|
|
||||||
/>
|
|
||||||
</Routes>,
|
|
||||||
{
|
|
||||||
route: '/projects/default/create-toggle',
|
|
||||||
permissions: [{ permission: CREATE_FEATURE }],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await screen.findByText('You are nearing the limit for feature flags');
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,235 +0,0 @@
|
|||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import FeatureForm from '../FeatureForm/FeatureForm';
|
|
||||||
import useFeatureForm from '../hooks/useFeatureForm';
|
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import useToast from 'hooks/useToast';
|
|
||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
|
||||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
|
||||||
import { useContext } from 'react';
|
|
||||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
|
||||||
import UIContext from 'contexts/UIContext';
|
|
||||||
import { CF_CREATE_BTN_ID } from 'utils/testIds';
|
|
||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
|
||||||
import { GO_BACK } from 'constants/navigate';
|
|
||||||
import { Alert, styled } from '@mui/material';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import useProjectOverview, {
|
|
||||||
featuresCount,
|
|
||||||
} from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
|
||||||
import { useGlobalFeatureSearch } from '../FeatureToggleList/useGlobalFeatureSearch';
|
|
||||||
import { Limit } from 'component/common/Limit/Limit';
|
|
||||||
|
|
||||||
const StyledAlert = styled(Alert)(({ theme }) => ({
|
|
||||||
marginBottom: theme.spacing(2),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const isProjectFeatureLimitReached = (
|
|
||||||
featureLimit: number | null | undefined,
|
|
||||||
currentFeatureCount: number,
|
|
||||||
): boolean => {
|
|
||||||
return (
|
|
||||||
featureLimit !== null &&
|
|
||||||
featureLimit !== undefined &&
|
|
||||||
featureLimit <= currentFeatureCount
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const useGlobalFlagLimit = (flagLimit: number, flagCount: number) => {
|
|
||||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
|
||||||
const limitReached = resourceLimitsEnabled && flagCount >= flagLimit;
|
|
||||||
|
|
||||||
return {
|
|
||||||
limitReached,
|
|
||||||
limitMessage: limitReached
|
|
||||||
? `You have reached the instance-wide limit of ${flagLimit} feature flags.`
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
type FlagLimitsProps = {
|
|
||||||
global: { limit: number; count: number };
|
|
||||||
project: { limit?: number; count: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const useFlagLimits = ({ global, project }: FlagLimitsProps) => {
|
|
||||||
const {
|
|
||||||
limitReached: globalFlagLimitReached,
|
|
||||||
limitMessage: globalLimitMessage,
|
|
||||||
} = useGlobalFlagLimit(global.limit, global.count);
|
|
||||||
|
|
||||||
const projectFlagLimitReached = isProjectFeatureLimitReached(
|
|
||||||
project.limit,
|
|
||||||
project.count,
|
|
||||||
);
|
|
||||||
|
|
||||||
const limitMessage = globalFlagLimitReached
|
|
||||||
? globalLimitMessage
|
|
||||||
: projectFlagLimitReached
|
|
||||||
? `You have reached the project limit of ${project.limit} feature flags.`
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
limitMessage,
|
|
||||||
globalFlagLimitReached,
|
|
||||||
projectFlagLimitReached,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const CreateFeature = () => {
|
|
||||||
const { setToastData, setToastApiError } = useToast();
|
|
||||||
const { setShowFeedback } = useContext(UIContext);
|
|
||||||
const { uiConfig } = useUiConfig();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const {
|
|
||||||
type,
|
|
||||||
setType,
|
|
||||||
name,
|
|
||||||
setName,
|
|
||||||
project,
|
|
||||||
setProject,
|
|
||||||
description,
|
|
||||||
setDescription,
|
|
||||||
validateToggleName,
|
|
||||||
impressionData,
|
|
||||||
setImpressionData,
|
|
||||||
getTogglePayload,
|
|
||||||
clearErrors,
|
|
||||||
errors,
|
|
||||||
} = useFeatureForm();
|
|
||||||
|
|
||||||
const { project: projectInfo } = useProjectOverview(project);
|
|
||||||
|
|
||||||
const { createFeatureToggle, loading } = useFeatureApi();
|
|
||||||
|
|
||||||
const { total: totalFlags, loading: loadingTotalFlagCount } =
|
|
||||||
useGlobalFeatureSearch();
|
|
||||||
|
|
||||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
|
||||||
|
|
||||||
const { globalFlagLimitReached, projectFlagLimitReached, limitMessage } =
|
|
||||||
useFlagLimits({
|
|
||||||
global: {
|
|
||||||
limit: uiConfig.resourceLimits.featureFlags,
|
|
||||||
count: totalFlags ?? 0,
|
|
||||||
},
|
|
||||||
project: {
|
|
||||||
limit: projectInfo.featureLimit,
|
|
||||||
count: featuresCount(projectInfo),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
|
||||||
e.preventDefault();
|
|
||||||
clearErrors();
|
|
||||||
const validToggleName = await validateToggleName();
|
|
||||||
|
|
||||||
if (validToggleName) {
|
|
||||||
const payload = getTogglePayload();
|
|
||||||
try {
|
|
||||||
await createFeatureToggle(project, payload);
|
|
||||||
navigate(`/projects/${project}/features/${name}`, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
setToastData({
|
|
||||||
title: 'Flag created successfully',
|
|
||||||
text: 'Now you can start using your flag.',
|
|
||||||
confetti: true,
|
|
||||||
type: 'success',
|
|
||||||
});
|
|
||||||
setShowFeedback(true);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
setToastApiError(formatUnknownError(error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatApiCode = () => {
|
|
||||||
return `curl --location --request POST '${
|
|
||||||
uiConfig.unleashUrl
|
|
||||||
}/api/admin/projects/${project}/features' \\
|
|
||||||
--header 'Authorization: INSERT_API_KEY' \\
|
|
||||||
--header 'Content-Type: application/json' \\
|
|
||||||
--data-raw '${JSON.stringify(getTogglePayload(), undefined, 2)}'`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
navigate(GO_BACK);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormTemplate
|
|
||||||
loading={loading}
|
|
||||||
title='Create feature flag'
|
|
||||||
description='Feature flags support different use cases, each with their own specific needs such as simple static routing or more complex routing.
|
|
||||||
The feature flag is disabled when created and you decide when to enable'
|
|
||||||
documentationLink='https://docs.getunleash.io/reference/feature-toggle-types'
|
|
||||||
documentationLinkLabel='Feature flag types documentation'
|
|
||||||
formatApiCode={formatApiCode}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={projectFlagLimitReached}
|
|
||||||
show={
|
|
||||||
<StyledAlert severity='error'>
|
|
||||||
<strong>Feature flag project limit reached. </strong> To
|
|
||||||
be able to create more feature flags in this project
|
|
||||||
please increase the feature flag upper limit in the
|
|
||||||
project settings.
|
|
||||||
</StyledAlert>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FeatureForm
|
|
||||||
type={type}
|
|
||||||
name={name}
|
|
||||||
project={project}
|
|
||||||
description={description}
|
|
||||||
setType={setType}
|
|
||||||
setName={setName}
|
|
||||||
setProject={setProject}
|
|
||||||
setDescription={setDescription}
|
|
||||||
validateToggleName={validateToggleName}
|
|
||||||
setImpressionData={setImpressionData}
|
|
||||||
impressionData={impressionData}
|
|
||||||
errors={errors}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
handleCancel={handleCancel}
|
|
||||||
mode='Create'
|
|
||||||
clearErrors={clearErrors}
|
|
||||||
featureNaming={projectInfo.featureNaming}
|
|
||||||
Limit={
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={resourceLimitsEnabled}
|
|
||||||
show={
|
|
||||||
<Limit
|
|
||||||
name='feature flags'
|
|
||||||
limit={uiConfig.resourceLimits.featureFlags}
|
|
||||||
currentValue={totalFlags ?? 0}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CreateButton
|
|
||||||
name='feature flag'
|
|
||||||
disabled={
|
|
||||||
loadingTotalFlagCount ||
|
|
||||||
globalFlagLimitReached ||
|
|
||||||
projectFlagLimitReached
|
|
||||||
}
|
|
||||||
permission={CREATE_FEATURE}
|
|
||||||
projectId={project}
|
|
||||||
data-testid={CF_CREATE_BTN_ID}
|
|
||||||
tooltipProps={{
|
|
||||||
title: limitMessage,
|
|
||||||
arrow: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</FeatureForm>
|
|
||||||
</FormTemplate>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CreateFeature;
|
|
@ -1,6 +1,6 @@
|
|||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import FeatureForm from '../FeatureForm/FeatureForm';
|
import EditFeatureForm from '../FeatureForm/EditFeatureForm';
|
||||||
import useFeatureForm from '../hooks/useFeatureForm';
|
import useFeatureForm from '../hooks/useFeatureForm';
|
||||||
import * as jsonpatch from 'fast-json-patch';
|
import * as jsonpatch from 'fast-json-patch';
|
||||||
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
||||||
@ -26,15 +26,12 @@ const EditFeature = () => {
|
|||||||
type,
|
type,
|
||||||
setType,
|
setType,
|
||||||
name,
|
name,
|
||||||
setName,
|
|
||||||
project,
|
project,
|
||||||
setProject,
|
|
||||||
description,
|
description,
|
||||||
setDescription,
|
setDescription,
|
||||||
impressionData,
|
impressionData,
|
||||||
setImpressionData,
|
setImpressionData,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
|
||||||
} = useFeatureForm(
|
} = useFeatureForm(
|
||||||
feature?.name,
|
feature?.name,
|
||||||
feature?.type,
|
feature?.type,
|
||||||
@ -88,25 +85,19 @@ const EditFeature = () => {
|
|||||||
documentationLinkLabel='Feature flag types documentation'
|
documentationLinkLabel='Feature flag types documentation'
|
||||||
formatApiCode={formatApiCode}
|
formatApiCode={formatApiCode}
|
||||||
>
|
>
|
||||||
<FeatureForm
|
<EditFeatureForm
|
||||||
type={type}
|
type={type}
|
||||||
name={name}
|
name={name}
|
||||||
project={project}
|
|
||||||
description={description}
|
description={description}
|
||||||
setType={setType}
|
setType={setType}
|
||||||
setName={setName}
|
|
||||||
setProject={setProject}
|
|
||||||
setDescription={setDescription}
|
setDescription={setDescription}
|
||||||
errors={errors}
|
|
||||||
handleSubmit={handleSubmit}
|
handleSubmit={handleSubmit}
|
||||||
handleCancel={handleCancel}
|
handleCancel={handleCancel}
|
||||||
impressionData={impressionData}
|
impressionData={impressionData}
|
||||||
setImpressionData={setImpressionData}
|
setImpressionData={setImpressionData}
|
||||||
mode='Edit'
|
|
||||||
clearErrors={clearErrors}
|
|
||||||
>
|
>
|
||||||
<UpdateButton permission={UPDATE_FEATURE} projectId={project} />
|
<UpdateButton permission={UPDATE_FEATURE} projectId={project} />
|
||||||
</FeatureForm>
|
</EditFeatureForm>
|
||||||
</FormTemplate>
|
</FormTemplate>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -10,40 +10,23 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
||||||
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds';
|
import { CF_DESC_ID, CF_TYPE_ID } from 'utils/testIds';
|
||||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||||
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
|
import KeyboardArrowDownOutlined from '@mui/icons-material/KeyboardArrowDownOutlined';
|
||||||
import { projectFilterGenerator } from 'utils/projectFilterGenerator';
|
|
||||||
import FeatureProjectSelect from '../FeatureView/FeatureSettings/FeatureSettingsProject/FeatureProjectSelect/FeatureProjectSelect';
|
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
|
||||||
import { trim } from 'component/common/util';
|
|
||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
import type React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
import React from 'react';
|
|
||||||
import { useAuthPermissions } from 'hooks/api/getters/useAuth/useAuthPermissions';
|
|
||||||
import type { FeatureNamingType } from 'interfaces/project';
|
|
||||||
import { FeatureNamingPatternInfo } from '../FeatureNamingPatternInfo/FeatureNamingPatternInfo';
|
|
||||||
import type { CreateFeatureSchemaType } from 'openapi';
|
import type { CreateFeatureSchemaType } from 'openapi';
|
||||||
|
|
||||||
interface IFeatureToggleForm {
|
interface IFeatureToggleForm {
|
||||||
type: CreateFeatureSchemaType;
|
type: CreateFeatureSchemaType;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
project: string;
|
|
||||||
impressionData: boolean;
|
impressionData: boolean;
|
||||||
setType: React.Dispatch<React.SetStateAction<CreateFeatureSchemaType>>;
|
setType: React.Dispatch<React.SetStateAction<CreateFeatureSchemaType>>;
|
||||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||||
setProject: React.Dispatch<React.SetStateAction<string>>;
|
|
||||||
setImpressionData: React.Dispatch<React.SetStateAction<boolean>>;
|
setImpressionData: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
featureNaming?: FeatureNamingType;
|
|
||||||
validateToggleName?: () => void;
|
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
handleCancel: () => void;
|
handleCancel: () => void;
|
||||||
errors: { [key: string]: string };
|
|
||||||
mode: 'Create' | 'Edit';
|
|
||||||
clearErrors: () => void;
|
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
Limit?: React.ReactNode;
|
Limit?: React.ReactNode;
|
||||||
}
|
}
|
||||||
@ -106,73 +89,37 @@ const LimitContainer = styled(Box)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
const EditFeatureForm: React.FC<IFeatureToggleForm> = ({
|
||||||
children,
|
children,
|
||||||
type,
|
type,
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
project,
|
|
||||||
setType,
|
setType,
|
||||||
setName,
|
|
||||||
setDescription,
|
setDescription,
|
||||||
setProject,
|
|
||||||
validateToggleName,
|
|
||||||
featureNaming,
|
|
||||||
setImpressionData,
|
setImpressionData,
|
||||||
impressionData,
|
impressionData,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
handleCancel,
|
handleCancel,
|
||||||
errors,
|
|
||||||
mode,
|
|
||||||
clearErrors,
|
|
||||||
Limit,
|
Limit,
|
||||||
}) => {
|
}) => {
|
||||||
const { featureTypes } = useFeatureTypes();
|
const { featureTypes } = useFeatureTypes();
|
||||||
const navigate = useNavigate();
|
|
||||||
const { permissions } = useAuthPermissions();
|
|
||||||
const editable = mode !== 'Edit';
|
|
||||||
|
|
||||||
const renderToggleDescription = () => {
|
const renderToggleDescription = () => {
|
||||||
return featureTypes.find((flag) => flag.id === type)?.description;
|
return featureTypes.find((flag) => flag.id === type)?.description;
|
||||||
};
|
};
|
||||||
|
|
||||||
const displayFeatureNamingInfo = Boolean(featureNaming?.pattern);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (featureNaming?.pattern && validateToggleName && name) {
|
|
||||||
clearErrors();
|
|
||||||
validateToggleName();
|
|
||||||
}
|
|
||||||
}, [featureNaming?.pattern]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
What would you like to call your flag?
|
What would you like to call your flag?
|
||||||
</StyledInputDescription>
|
</StyledInputDescription>
|
||||||
<ConditionallyRender
|
|
||||||
condition={displayFeatureNamingInfo}
|
|
||||||
show={
|
|
||||||
<FeatureNamingPatternInfo featureNaming={featureNaming!} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<StyledInput
|
<StyledInput
|
||||||
autoFocus
|
autoFocus
|
||||||
disabled={mode === 'Edit'}
|
disabled={true}
|
||||||
label='Name'
|
label='Name'
|
||||||
aria-details={
|
|
||||||
displayFeatureNamingInfo
|
|
||||||
? 'feature-naming-pattern-info'
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
id='feature-flag-name'
|
id='feature-flag-name'
|
||||||
error={Boolean(errors.name)}
|
|
||||||
errorText={errors.name}
|
|
||||||
onFocus={() => clearErrors()}
|
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(trim(e.target.value))}
|
onChange={() => {}}
|
||||||
data-testid={CF_NAME_ID}
|
|
||||||
onBlur={validateToggleName}
|
|
||||||
/>
|
/>
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
What kind of feature flag do you want?
|
What kind of feature flag do you want?
|
||||||
@ -190,31 +137,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
|||||||
<StyledTypeDescription>
|
<StyledTypeDescription>
|
||||||
{renderToggleDescription()}
|
{renderToggleDescription()}
|
||||||
</StyledTypeDescription>
|
</StyledTypeDescription>
|
||||||
<ConditionallyRender
|
|
||||||
condition={editable}
|
|
||||||
show={
|
|
||||||
<StyledInputDescription>
|
|
||||||
In which project do you want to save the flag?
|
|
||||||
</StyledInputDescription>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{/* TODO: this can be removed after new create flag flow goes live */}
|
|
||||||
<FeatureProjectSelect
|
|
||||||
value={project}
|
|
||||||
onChange={(projectId) => {
|
|
||||||
setProject(projectId);
|
|
||||||
navigate(`/projects/${projectId}/create-toggle`, {
|
|
||||||
replace: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
enabled={editable}
|
|
||||||
filter={projectFilterGenerator(
|
|
||||||
permissions || [],
|
|
||||||
CREATE_FEATURE,
|
|
||||||
)}
|
|
||||||
IconComponent={KeyboardArrowDownOutlined}
|
|
||||||
sx={styledSelectInput}
|
|
||||||
/>
|
|
||||||
<StyledInputDescription>
|
<StyledInputDescription>
|
||||||
How would you describe your feature flag?
|
How would you describe your feature flag?
|
||||||
</StyledInputDescription>
|
</StyledInputDescription>
|
||||||
@ -276,4 +198,4 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default FeatureForm;
|
export default EditFeatureForm;
|
@ -65,14 +65,6 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "FeatureView",
|
"title": "FeatureView",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"component": [Function],
|
|
||||||
"menu": {},
|
|
||||||
"parent": "/projects/:projectId/features",
|
|
||||||
"path": "/projects/:projectId/create-toggle",
|
|
||||||
"title": "Create feature flag",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"component": {
|
"component": {
|
||||||
"$$typeof": Symbol(react.lazy),
|
"$$typeof": Symbol(react.lazy),
|
||||||
|
@ -16,7 +16,6 @@ import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironm
|
|||||||
import { EditContext } from 'component/context/EditContext/EditContext';
|
import { EditContext } from 'component/context/EditContext/EditContext';
|
||||||
import EditTagType from 'component/tags/EditTagType/EditTagType';
|
import EditTagType from 'component/tags/EditTagType/EditTagType';
|
||||||
import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
|
import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
|
||||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
|
||||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||||
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
||||||
@ -102,14 +101,6 @@ export const routes: IRoute[] = [
|
|||||||
type: 'protected',
|
type: 'protected',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/projects/:projectId/create-toggle',
|
|
||||||
parent: '/projects/:projectId/features',
|
|
||||||
title: 'Create feature flag',
|
|
||||||
component: CreateFeature,
|
|
||||||
type: 'protected',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/projects/:projectId/*',
|
path: '/projects/:projectId/*',
|
||||||
parent: '/projects',
|
parent: '/projects',
|
||||||
|
@ -21,7 +21,6 @@ import useFeatureForm from 'component/feature/hooks/useFeatureForm';
|
|||||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||||
import FlagIcon from '@mui/icons-material/Flag';
|
import FlagIcon from '@mui/icons-material/Flag';
|
||||||
import ImpressionDataIcon from '@mui/icons-material/AltRoute';
|
import ImpressionDataIcon from '@mui/icons-material/AltRoute';
|
||||||
import { useFlagLimits } from 'component/feature/CreateFeature/CreateFeature';
|
|
||||||
import { useGlobalFeatureSearch } from 'component/feature/FeatureToggleList/useGlobalFeatureSearch';
|
import { useGlobalFeatureSearch } from 'component/feature/FeatureToggleList/useGlobalFeatureSearch';
|
||||||
import useProjectOverview, {
|
import useProjectOverview, {
|
||||||
featuresCount,
|
featuresCount,
|
||||||
@ -37,6 +36,7 @@ import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
|||||||
import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton';
|
import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton';
|
||||||
import type { ITag } from 'interfaces/tags';
|
import type { ITag } from 'interfaces/tags';
|
||||||
import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton';
|
import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton';
|
||||||
|
import { useFlagLimits } from './useFlagLimits';
|
||||||
|
|
||||||
interface ICreateFeatureDialogProps {
|
interface ICreateFeatureDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { isProjectFeatureLimitReached } from './CreateFeature';
|
import { isProjectFeatureLimitReached } from './useFlagLimits';
|
||||||
|
|
||||||
test('isFeatureLimitReached should return false when featureLimit is null', async () => {
|
test('isFeatureLimitReached should return false when featureLimit is null', async () => {
|
||||||
expect(isProjectFeatureLimitReached(null, 5)).toBe(false);
|
expect(isProjectFeatureLimitReached(null, 5)).toBe(false);
|
@ -1,6 +1,6 @@
|
|||||||
import { renderHook } from '@testing-library/react';
|
import { renderHook } from '@testing-library/react';
|
||||||
import { useFlagLimits } from './CreateFeature';
|
|
||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
|
import { useFlagLimits } from './useFlagLimits';
|
||||||
|
|
||||||
vi.mock('hooks/useUiFlag', async (importOriginal) => {
|
vi.mock('hooks/useUiFlag', async (importOriginal) => {
|
||||||
const actual = await importOriginal();
|
const actual = await importOriginal();
|
@ -0,0 +1,53 @@
|
|||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
|
||||||
|
type FlagLimitsProps = {
|
||||||
|
global: { limit: number; count: number };
|
||||||
|
project: { limit?: number; count: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useFlagLimits = ({ global, project }: FlagLimitsProps) => {
|
||||||
|
const {
|
||||||
|
limitReached: globalFlagLimitReached,
|
||||||
|
limitMessage: globalLimitMessage,
|
||||||
|
} = useGlobalFlagLimit(global.limit, global.count);
|
||||||
|
|
||||||
|
const projectFlagLimitReached = isProjectFeatureLimitReached(
|
||||||
|
project.limit,
|
||||||
|
project.count,
|
||||||
|
);
|
||||||
|
|
||||||
|
const limitMessage = globalFlagLimitReached
|
||||||
|
? globalLimitMessage
|
||||||
|
: projectFlagLimitReached
|
||||||
|
? `You have reached the project limit of ${project.limit} feature flags.`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
limitMessage,
|
||||||
|
globalFlagLimitReached,
|
||||||
|
projectFlagLimitReached,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const useGlobalFlagLimit = (flagLimit: number, flagCount: number) => {
|
||||||
|
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
||||||
|
const limitReached = resourceLimitsEnabled && flagCount >= flagLimit;
|
||||||
|
|
||||||
|
return {
|
||||||
|
limitReached,
|
||||||
|
limitMessage: limitReached
|
||||||
|
? `You have reached the instance-wide limit of ${flagLimit} feature flags.`
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isProjectFeatureLimitReached = (
|
||||||
|
featureLimit: number | null | undefined,
|
||||||
|
currentFeatureCount: number,
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
featureLimit !== null &&
|
||||||
|
featureLimit !== undefined &&
|
||||||
|
featureLimit <= currentFeatureCount
|
||||||
|
);
|
||||||
|
};
|
@ -6,7 +6,7 @@ export const getCreateTogglePath = (
|
|||||||
projectId: string,
|
projectId: string,
|
||||||
query?: Record<string, string>,
|
query?: Record<string, string>,
|
||||||
) => {
|
) => {
|
||||||
const path = `/projects/${projectId}/create-toggle`;
|
const path = `/projects/${projectId}?create=true`;
|
||||||
|
|
||||||
let queryString: string | undefined;
|
let queryString: string | undefined;
|
||||||
if (query) {
|
if (query) {
|
||||||
@ -16,12 +16,8 @@ export const getCreateTogglePath = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (queryString) {
|
if (queryString) {
|
||||||
return `${path}?${queryString}`;
|
return `${path}&${queryString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return path;
|
return path;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProjectEditPath = (projectId: string) => {
|
|
||||||
return `/projects/${projectId}/settings`;
|
|
||||||
};
|
|
||||||
|
Loading…
Reference in New Issue
Block a user