diff --git a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx index f022381f6cce..937dd38814b7 100644 --- a/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx +++ b/frontend/src/component/common/BreadcrumbNav/BreadcrumbNav.tsx @@ -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', diff --git a/frontend/src/component/feature/CreateFeature/CreateFeature.test.tsx b/frontend/src/component/feature/CreateFeature/CreateFeature.test.tsx deleted file mode 100644 index 6215809f4dd9..000000000000 --- a/frontend/src/component/feature/CreateFeature/CreateFeature.test.tsx +++ /dev/null @@ -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( - - } - /> - , - { - 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( - - } - /> - , - { - 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( - - } - /> - , - { - route: '/projects/default/create-toggle', - permissions: [{ permission: CREATE_FEATURE }], - }, - ); - - await screen.findByText('You are nearing the limit for feature flags'); - }); -}); diff --git a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx b/frontend/src/component/feature/CreateFeature/CreateFeature.tsx deleted file mode 100644 index bb059bf26ff8..000000000000 --- a/frontend/src/component/feature/CreateFeature/CreateFeature.tsx +++ /dev/null @@ -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 ( - - - Feature flag project limit reached. To - be able to create more feature flags in this project - please increase the feature flag upper limit in the - project settings. - - } - /> - - - } - /> - } - > - - - - ); -}; - -export default CreateFeature; diff --git a/frontend/src/component/feature/EditFeature/EditFeature.tsx b/frontend/src/component/feature/EditFeature/EditFeature.tsx index 333f1d7e9f40..77066c8190d6 100644 --- a/frontend/src/component/feature/EditFeature/EditFeature.tsx +++ b/frontend/src/component/feature/EditFeature/EditFeature.tsx @@ -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} > - - + ); }; diff --git a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx b/frontend/src/component/feature/FeatureForm/EditFeatureForm.tsx similarity index 63% rename from frontend/src/component/feature/FeatureForm/FeatureForm.tsx rename to frontend/src/component/feature/FeatureForm/EditFeatureForm.tsx index 57ae8124f775..793942cccf1d 100644 --- a/frontend/src/component/feature/FeatureForm/FeatureForm.tsx +++ b/frontend/src/component/feature/FeatureForm/EditFeatureForm.tsx @@ -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>; - setName: React.Dispatch>; setDescription: React.Dispatch>; - setProject: React.Dispatch>; setImpressionData: React.Dispatch>; - 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 = ({ +const EditFeatureForm: React.FC = ({ 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 ( What would you like to call your flag? - - } - /> clearErrors()} value={name} - onChange={(e) => setName(trim(e.target.value))} - data-testid={CF_NAME_ID} - onBlur={validateToggleName} + onChange={() => {}} /> What kind of feature flag do you want? @@ -190,31 +137,6 @@ const FeatureForm: React.FC = ({ {renderToggleDescription()} - - In which project do you want to save the flag? - - } - /> - {/* TODO: this can be removed after new create flag flow goes live */} - { - setProject(projectId); - navigate(`/projects/${projectId}/create-toggle`, { - replace: true, - }); - }} - enabled={editable} - filter={projectFilterGenerator( - permissions || [], - CREATE_FEATURE, - )} - IconComponent={KeyboardArrowDownOutlined} - sx={styledSelectInput} - /> How would you describe your feature flag? @@ -276,4 +198,4 @@ const FeatureForm: React.FC = ({ ); }; -export default FeatureForm; +export default EditFeatureForm; diff --git a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap index 9f3ab5c999c2..e347aed92c26 100644 --- a/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap +++ b/frontend/src/component/menu/__tests__/__snapshots__/routes.test.tsx.snap @@ -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), diff --git a/frontend/src/component/menu/routes.ts b/frontend/src/component/menu/routes.ts index b7901e015719..d6373d94e8a9 100644 --- a/frontend/src/component/menu/routes.ts +++ b/frontend/src/component/menu/routes.ts @@ -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', diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx index 616434619f87..66dd34060d81 100644 --- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/CreateFeatureDialog.tsx @@ -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; diff --git a/frontend/src/component/feature/CreateFeature/isProjectFeatureLimitReached.test.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/isProjectFeatureLimitReached.test.ts similarity index 93% rename from frontend/src/component/feature/CreateFeature/isProjectFeatureLimitReached.test.ts rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/isProjectFeatureLimitReached.test.ts index 77a520f9792e..d3d764f1fc58 100644 --- a/frontend/src/component/feature/CreateFeature/isProjectFeatureLimitReached.test.ts +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/isProjectFeatureLimitReached.test.ts @@ -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); diff --git a/frontend/src/component/feature/CreateFeature/useFlagLimits.test.ts b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.test.ts similarity index 97% rename from frontend/src/component/feature/CreateFeature/useFlagLimits.test.ts rename to frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.test.ts index b8abb2cf312a..a785ec26e366 100644 --- a/frontend/src/component/feature/CreateFeature/useFlagLimits.test.ts +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.test.ts @@ -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(); diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.tsx new file mode 100644 index 000000000000..c41d26a70b6b --- /dev/null +++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/ProjectFeatureTogglesHeader/useFlagLimits.tsx @@ -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 + ); +}; diff --git a/frontend/src/utils/routePathHelpers.ts b/frontend/src/utils/routePathHelpers.ts index bab9a6c391d0..177937ac914f 100644 --- a/frontend/src/utils/routePathHelpers.ts +++ b/frontend/src/utils/routePathHelpers.ts @@ -6,7 +6,7 @@ export const getCreateTogglePath = ( projectId: string, query?: Record, ) => { - 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`; -};