1
0
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:
Jaanus Sellin 2024-08-22 11:30:41 +03:00 committed by GitHub
parent e5cca661d9
commit 4a4dafcc3f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 67 additions and 452 deletions

View File

@ -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',

View File

@ -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');
});
});

View File

@ -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;

View File

@ -1,6 +1,6 @@
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import { useNavigate } from 'react-router-dom';
import FeatureForm from '../FeatureForm/FeatureForm';
import EditFeatureForm from '../FeatureForm/EditFeatureForm';
import useFeatureForm from '../hooks/useFeatureForm';
import * as jsonpatch from 'fast-json-patch';
import { UpdateButton } from 'component/common/UpdateButton/UpdateButton';
@ -26,15 +26,12 @@ const EditFeature = () => {
type,
setType,
name,
setName,
project,
setProject,
description,
setDescription,
impressionData,
setImpressionData,
clearErrors,
errors,
} = useFeatureForm(
feature?.name,
feature?.type,
@ -88,25 +85,19 @@ const EditFeature = () => {
documentationLinkLabel='Feature flag types documentation'
formatApiCode={formatApiCode}
>
<FeatureForm
<EditFeatureForm
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>
</EditFeatureForm>
</FormTemplate>
);
};

View File

@ -10,40 +10,23 @@ import {
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 { CF_DESC_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 React from 'react';
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;
}
@ -106,73 +89,37 @@ const LimitContainer = styled(Box)(({ theme }) => ({
},
}));
const FeatureForm: React.FC<IFeatureToggleForm> = ({
const EditFeatureForm: 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'}
disabled={true}
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}
onChange={() => {}}
/>
<StyledInputDescription>
What kind of feature flag do you want?
@ -190,31 +137,6 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
<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>
@ -276,4 +198,4 @@ const FeatureForm: React.FC<IFeatureToggleForm> = ({
);
};
export default FeatureForm;
export default EditFeatureForm;

View File

@ -65,14 +65,6 @@ exports[`returns all baseRoutes 1`] = `
"title": "FeatureView",
"type": "protected",
},
{
"component": [Function],
"menu": {},
"parent": "/projects/:projectId/features",
"path": "/projects/:projectId/create-toggle",
"title": "Create feature flag",
"type": "protected",
},
{
"component": {
"$$typeof": Symbol(react.lazy),

View File

@ -16,7 +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';
@ -102,14 +101,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',

View File

@ -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;

View File

@ -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);

View File

@ -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();

View File

@ -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
);
};

View File

@ -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`;
};