mirror of
https://github.com/Unleash/unleash.git
synced 2025-09-05 17:53:12 +02:00
chore: remove create/edit feature components
This commit is contained in:
parent
341703978a
commit
4a43d2d956
@ -46,8 +46,6 @@ const BreadcrumbNav = () => {
|
||||
item !== 'copy' &&
|
||||
item !== 'features' &&
|
||||
item !== 'features2' &&
|
||||
// TODO: this can be removed after new create flag flow goes live
|
||||
item !== 'create-toggle' &&
|
||||
item !== 'settings' &&
|
||||
item !== 'profile' &&
|
||||
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,114 +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 * as jsonpatch from 'fast-json-patch';
|
||||
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import { useFeature } from 'hooks/api/getters/useFeature/useFeature';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { GO_BACK } from 'constants/navigate';
|
||||
|
||||
const EditFeature = () => {
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
const { uiConfig } = useUiConfig();
|
||||
const navigate = useNavigate();
|
||||
const { patchFeatureToggle: patchFeatureFlag, loading } = useFeatureApi();
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
|
||||
const {
|
||||
type,
|
||||
setType,
|
||||
name,
|
||||
setName,
|
||||
project,
|
||||
setProject,
|
||||
description,
|
||||
setDescription,
|
||||
impressionData,
|
||||
setImpressionData,
|
||||
clearErrors,
|
||||
errors,
|
||||
} = useFeatureForm(
|
||||
feature?.name,
|
||||
feature?.type,
|
||||
feature?.project,
|
||||
feature?.description,
|
||||
feature?.impressionData,
|
||||
);
|
||||
|
||||
const createPatch = () => {
|
||||
const comparison = { ...feature, type, description, impressionData };
|
||||
const patch = jsonpatch.compare(feature, comparison);
|
||||
return patch;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: Event) => {
|
||||
e.preventDefault();
|
||||
clearErrors();
|
||||
const patch = createPatch();
|
||||
try {
|
||||
await patchFeatureFlag(project, featureId, patch);
|
||||
navigate(`/projects/${project}/features/${name}`);
|
||||
setToastData({
|
||||
title: 'Flag updated successfully',
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
setToastApiError(formatUnknownError(error));
|
||||
}
|
||||
};
|
||||
|
||||
const formatApiCode = () => {
|
||||
return `curl --location --request PATCH '${
|
||||
uiConfig.unleashUrl
|
||||
}/api/admin/projects/${projectId}/features/${featureId}' \\
|
||||
--header 'Authorization: INSERT_API_KEY' \\
|
||||
--header 'Content-Type: application/json' \\
|
||||
--data-raw '${JSON.stringify(createPatch(), undefined, 2)}'`;
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
navigate(GO_BACK);
|
||||
};
|
||||
|
||||
return (
|
||||
<FormTemplate
|
||||
loading={loading}
|
||||
title='Edit 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}
|
||||
>
|
||||
<FeatureForm
|
||||
type={type}
|
||||
name={name}
|
||||
project={project}
|
||||
description={description}
|
||||
setType={setType}
|
||||
setName={setName}
|
||||
setProject={setProject}
|
||||
setDescription={setDescription}
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
handleCancel={handleCancel}
|
||||
impressionData={impressionData}
|
||||
setImpressionData={setImpressionData}
|
||||
mode='Edit'
|
||||
clearErrors={clearErrors}
|
||||
>
|
||||
<UpdateButton permission={UPDATE_FEATURE} projectId={project} />
|
||||
</FeatureForm>
|
||||
</FormTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditFeature;
|
@ -1,279 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
styled,
|
||||
Switch,
|
||||
type Theme,
|
||||
Typography,
|
||||
Link,
|
||||
Box,
|
||||
} from '@mui/material';
|
||||
import FeatureTypeSelect from '../FeatureView/FeatureSettings/FeatureSettingsMetadata/FeatureTypeSelect/FeatureTypeSelect';
|
||||
import { CF_DESC_ID, CF_NAME_ID, CF_TYPE_ID } from 'utils/testIds';
|
||||
import useFeatureTypes from 'hooks/api/getters/useFeatureTypes/useFeatureTypes';
|
||||
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 { CREATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
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';
|
||||
|
||||
interface IFeatureToggleForm {
|
||||
type: CreateFeatureSchemaType;
|
||||
name: string;
|
||||
description: string;
|
||||
project: string;
|
||||
impressionData: boolean;
|
||||
setType: React.Dispatch<React.SetStateAction<CreateFeatureSchemaType>>;
|
||||
setName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setDescription: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProject: React.Dispatch<React.SetStateAction<string>>;
|
||||
setImpressionData: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
featureNaming?: FeatureNamingType;
|
||||
validateToggleName?: () => void;
|
||||
handleSubmit: (e: any) => void;
|
||||
handleCancel: () => void;
|
||||
errors: { [key: string]: string };
|
||||
mode: 'Create' | 'Edit';
|
||||
clearErrors: () => void;
|
||||
children?: React.ReactNode;
|
||||
Limit?: React.ReactNode;
|
||||
}
|
||||
|
||||
const StyledForm = styled('form')({
|
||||
height: '100%',
|
||||
});
|
||||
|
||||
const StyledInputDescription = styled('p')(({ theme }) => ({
|
||||
marginBottom: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledInput = styled(Input)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const StyledFormControl = styled(FormControl)(({ theme }) => ({
|
||||
width: '100%',
|
||||
marginBottom: theme.spacing(2),
|
||||
}));
|
||||
|
||||
const styledSelectInput = (theme: Theme) => ({
|
||||
marginBottom: theme.spacing(2),
|
||||
minWidth: '400px',
|
||||
[theme.breakpoints.down('sm')]: {
|
||||
minWidth: '379px',
|
||||
},
|
||||
});
|
||||
|
||||
const StyledTypeDescription = styled('p')(({ theme }) => ({
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
top: '-13px',
|
||||
position: 'relative',
|
||||
}));
|
||||
|
||||
const StyledButtonContainer = styled('div')({
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
});
|
||||
|
||||
const StyledRow = styled('div')(({ theme }) => ({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: theme.spacing(1),
|
||||
}));
|
||||
|
||||
const StyledCancelButton = styled(Button)(({ theme }) => ({
|
||||
marginLeft: theme.spacing(3),
|
||||
}));
|
||||
|
||||
const styledTypography = (theme: Theme) => ({
|
||||
margin: theme.spacing(1, 0),
|
||||
});
|
||||
|
||||
const LimitContainer = styled(Box)(({ theme }) => ({
|
||||
'&:has(*)': {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const FeatureForm: React.FC<IFeatureToggleForm> = ({
|
||||
children,
|
||||
type,
|
||||
name,
|
||||
description,
|
||||
project,
|
||||
setType,
|
||||
setName,
|
||||
setDescription,
|
||||
setProject,
|
||||
validateToggleName,
|
||||
featureNaming,
|
||||
setImpressionData,
|
||||
impressionData,
|
||||
handleSubmit,
|
||||
handleCancel,
|
||||
errors,
|
||||
mode,
|
||||
clearErrors,
|
||||
Limit,
|
||||
}) => {
|
||||
const { featureTypes } = useFeatureTypes();
|
||||
const navigate = useNavigate();
|
||||
const { permissions } = useAuthPermissions();
|
||||
const editable = mode !== 'Edit';
|
||||
|
||||
const renderToggleDescription = () => {
|
||||
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 (
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
<StyledInputDescription>
|
||||
What would you like to call your flag?
|
||||
</StyledInputDescription>
|
||||
<ConditionallyRender
|
||||
condition={displayFeatureNamingInfo}
|
||||
show={
|
||||
<FeatureNamingPatternInfo featureNaming={featureNaming!} />
|
||||
}
|
||||
/>
|
||||
<StyledInput
|
||||
autoFocus
|
||||
disabled={mode === 'Edit'}
|
||||
label='Name'
|
||||
aria-details={
|
||||
displayFeatureNamingInfo
|
||||
? 'feature-naming-pattern-info'
|
||||
: undefined
|
||||
}
|
||||
id='feature-flag-name'
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => clearErrors()}
|
||||
value={name}
|
||||
onChange={(e) => setName(trim(e.target.value))}
|
||||
data-testid={CF_NAME_ID}
|
||||
onBlur={validateToggleName}
|
||||
/>
|
||||
<StyledInputDescription>
|
||||
What kind of feature flag do you want?
|
||||
</StyledInputDescription>
|
||||
<FeatureTypeSelect
|
||||
sx={styledSelectInput}
|
||||
value={type}
|
||||
onChange={setType}
|
||||
label={'Flag type'}
|
||||
id='feature-type-select'
|
||||
editable
|
||||
data-testid={CF_TYPE_ID}
|
||||
IconComponent={KeyboardArrowDownOutlined}
|
||||
/>
|
||||
<StyledTypeDescription>
|
||||
{renderToggleDescription()}
|
||||
</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>
|
||||
How would you describe your feature flag?
|
||||
</StyledInputDescription>
|
||||
<StyledInput
|
||||
multiline
|
||||
rows={4}
|
||||
label='Description'
|
||||
placeholder='A short description of the feature flag'
|
||||
value={description}
|
||||
data-testid={CF_DESC_ID}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<StyledFormControl>
|
||||
<Typography
|
||||
variant='subtitle1'
|
||||
sx={styledTypography}
|
||||
data-loading
|
||||
component='h2'
|
||||
>
|
||||
Impression Data
|
||||
</Typography>
|
||||
<p>
|
||||
When you enable impression data for a feature flag, your
|
||||
client SDKs will emit events you can listen for every time
|
||||
this flag gets triggered. Learn more in{' '}
|
||||
<Link
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
href='https://docs.getunleash.io/advanced/impression_data'
|
||||
>
|
||||
the impression data documentation
|
||||
</Link>
|
||||
</p>
|
||||
<StyledRow>
|
||||
<FormControlLabel
|
||||
labelPlacement='start'
|
||||
style={{ marginLeft: 0 }}
|
||||
control={
|
||||
<Switch
|
||||
name='impressionData'
|
||||
onChange={() =>
|
||||
setImpressionData(!impressionData)
|
||||
}
|
||||
checked={impressionData}
|
||||
/>
|
||||
}
|
||||
label='Enable impression data'
|
||||
/>
|
||||
</StyledRow>
|
||||
</StyledFormControl>
|
||||
<LimitContainer>{Limit}</LimitContainer>
|
||||
<StyledButtonContainer>
|
||||
{children}
|
||||
<StyledCancelButton onClick={handleCancel}>
|
||||
Cancel
|
||||
</StyledCancelButton>
|
||||
</StyledButtonContainer>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
||||
|
||||
export default FeatureForm;
|
@ -16,8 +16,6 @@ import EditEnvironment from 'component/environments/EditEnvironment/EditEnvironm
|
||||
import { EditContext } from 'component/context/EditContext/EditContext';
|
||||
import EditTagType from 'component/tags/EditTagType/EditTagType';
|
||||
import CreateTagType from 'component/tags/CreateTagType/CreateTagType';
|
||||
import CreateFeature from 'component/feature/CreateFeature/CreateFeature';
|
||||
import EditFeature from 'component/feature/EditFeature/EditFeature';
|
||||
import ContextList from 'component/context/ContextList/ContextList/ContextList';
|
||||
import { CreateIntegration } from 'component/integrations/CreateIntegration/CreateIntegration';
|
||||
import { EditIntegration } from 'component/integrations/EditIntegration/EditIntegration';
|
||||
@ -86,14 +84,6 @@ export const routes: IRoute[] = [
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/features/:featureId/edit',
|
||||
parent: '/projects',
|
||||
title: 'Edit feature',
|
||||
component: EditFeature,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/features/:featureId/*',
|
||||
parent: '/projects',
|
||||
@ -102,14 +92,6 @@ export const routes: IRoute[] = [
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/create-toggle',
|
||||
parent: '/projects/:projectId/features',
|
||||
title: 'Create feature flag',
|
||||
component: CreateFeature,
|
||||
type: 'protected',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:projectId/*',
|
||||
parent: '/projects',
|
||||
|
@ -21,7 +21,6 @@ import useFeatureForm from 'component/feature/hooks/useFeatureForm';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import FlagIcon from '@mui/icons-material/Flag';
|
||||
import ImpressionDataIcon from '@mui/icons-material/AltRoute';
|
||||
import { useFlagLimits } from 'component/feature/CreateFeature/CreateFeature';
|
||||
import { useGlobalFeatureSearch } from 'component/feature/FeatureToggleList/useGlobalFeatureSearch';
|
||||
import useProjectOverview, {
|
||||
featuresCount,
|
||||
@ -37,6 +36,7 @@ import { ProjectIcon } from 'component/common/ProjectIcon/ProjectIcon';
|
||||
import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton';
|
||||
import type { ITag } from 'interfaces/tags';
|
||||
import { ToggleConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/ToggleConfigButton';
|
||||
import { useFlagLimits } from './useFlagLimits';
|
||||
|
||||
interface ICreateFeatureDialogProps {
|
||||
open: boolean;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { isProjectFeatureLimitReached } from './CreateFeature';
|
||||
import { isProjectFeatureLimitReached } from './useFlagLimits';
|
||||
|
||||
test('isFeatureLimitReached should return false when featureLimit is null', async () => {
|
||||
expect(isProjectFeatureLimitReached(null, 5)).toBe(false);
|
@ -1,6 +1,6 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useFlagLimits } from './CreateFeature';
|
||||
import { vi } from 'vitest';
|
||||
import { useFlagLimits } from './useFlagLimits';
|
||||
|
||||
vi.mock('hooks/useUiFlag', async (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,
|
||||
query?: Record<string, string>,
|
||||
) => {
|
||||
const path = `/projects/${projectId}/create-toggle`;
|
||||
const path = `/projects/${projectId}?create=true`;
|
||||
|
||||
let queryString: string | undefined;
|
||||
if (query) {
|
||||
@ -16,12 +16,8 @@ export const getCreateTogglePath = (
|
||||
}
|
||||
|
||||
if (queryString) {
|
||||
return `${path}?${queryString}`;
|
||||
return `${path}&${queryString}`;
|
||||
}
|
||||
|
||||
return path;
|
||||
};
|
||||
|
||||
export const getProjectEditPath = (projectId: string) => {
|
||||
return `/projects/${projectId}/settings`;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user