diff --git a/frontend/cypress/support/UI.ts b/frontend/cypress/support/UI.ts index aad2b6fb0f..e4cdad5ef1 100644 --- a/frontend/cypress/support/UI.ts +++ b/frontend/cypress/support/UI.ts @@ -84,14 +84,14 @@ export const createProject_UI = ( cy.intercept('POST', `/api/admin/projects`).as('createProject'); cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName); - cy.get("[data-testid='PROJECT_NAME_INPUT']").type(projectName); + cy.get("[data-testid='FORM_NAME_INPUT']").type(projectName); cy.get("[id='stickiness-select']") .first() .click() .get(`[data-testid=SELECT_ITEM_ID-${defaultStickiness}`) .first() .click(); - cy.get("[data-testid='CREATE_PROJECT_BTN']").click(); + cy.get("[data-testid='FORM_CREATE_BTN']").click(); cy.wait('@createProject'); return cy.visit(`/projects/${projectName}`); }; diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ConfigButton.styles.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/ConfigButton.styles.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ConfigButton.styles.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/ConfigButton.styles.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ConfigButton.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/ConfigButton.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ConfigButton.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/ConfigButton.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/DropdownList.styles.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/DropdownList.styles.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/DropdownList.styles.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/DropdownList.styles.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/DropdownList.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/DropdownList.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/DropdownList.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/DropdownList.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/MultiSelectConfigButton.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/MultiSelectConfigButton.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/SingleSelectConfigButton.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/SingleSelectConfigButton.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/shared.styles.tsx b/frontend/src/component/common/DialogFormTemplate/ConfigButtons/shared.styles.tsx similarity index 100% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/shared.styles.tsx rename to frontend/src/component/common/DialogFormTemplate/ConfigButtons/shared.styles.tsx diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.styles.tsx b/frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.styles.tsx similarity index 68% rename from frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.styles.tsx rename to frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.styles.tsx index 610b51922a..9026f8017e 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.styles.tsx +++ b/frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.styles.tsx @@ -1,12 +1,11 @@ -import { Typography, styled } from '@mui/material'; +import { Box, Typography, styled } from '@mui/material'; import Input from 'component/common/Input/Input'; -import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; export const StyledForm = styled('form')(({ theme }) => ({ background: theme.palette.background.default, })); -export const StyledFormSection = styled('div')(({ theme }) => ({ +const StyledFormSection = styled('div')(({ theme }) => ({ '& + *': { borderBlockStart: `1px solid ${theme.palette.divider}`, }, @@ -16,16 +15,19 @@ export const StyledFormSection = styled('div')(({ theme }) => ({ export const TopGrid = styled(StyledFormSection)(({ theme }) => ({ display: 'grid', - gridTemplateAreas: - '"icon header" "icon project-name" "icon project-description"', + gridTemplateAreas: ` + "icon header" + ". project-name" + ". project-description"`, gridTemplateColumns: 'auto 1fr', gap: theme.spacing(4), })); -export const StyledIcon = styled(ProjectIcon)(({ theme }) => ({ - fill: theme.palette.primary.main, - stroke: theme.palette.primary.main, -})); +export const styleIcon = (Icon: React.ComponentType) => + styled(Icon)(({ theme }) => ({ + fill: theme.palette.primary.main, + stroke: theme.palette.primary.main, + })); export const StyledHeader = styled(Typography)({ gridArea: 'header', @@ -46,7 +48,7 @@ export const StyledInput = styled(Input)({ fieldset: { border: 'none' }, }); -export const OptionButtons = styled(StyledFormSection)(({ theme }) => ({ +export const ConfigButtons = styled(StyledFormSection)(({ theme }) => ({ display: 'flex', flexFlow: 'row wrap', gap: theme.spacing(2), @@ -78,15 +80,8 @@ export const FormActions = styled(StyledFormSection)(({ theme }) => ({ }, })); -export const StyledDefinitionList = styled('dl')(({ theme }) => ({ - dt: { - fontWeight: 'bold', - '&:after': { - content: '":"', - }, - }, - - 'dd + dt': { - marginBlockStart: theme.spacing(1), +export const LimitContainer = styled(Box)(({ theme }) => ({ + '&:has(*)': { + padding: theme.spacing(4, 6, 0, 6), }, })); diff --git a/frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.tsx b/frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.tsx new file mode 100644 index 0000000000..8b0bbed770 --- /dev/null +++ b/frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.tsx @@ -0,0 +1,113 @@ +import type { FormEventHandler } from 'react'; +import theme from 'themes/theme'; +import { + ConfigButtons, + ProjectDescriptionContainer, + ProjectNameContainer, + StyledForm, + StyledHeader, + StyledInput, + TopGrid, + LimitContainer, + FormActions, + styleIcon, +} from './DialogFormTemplate.styles'; +import { Button } from '@mui/material'; +import { CreateButton } from 'component/common/CreateButton/CreateButton'; +import type { IPermissionButtonProps } from 'component/common/PermissionButton/PermissionButton'; + +type FormProps = { + createButtonProps: IPermissionButtonProps; + description: string; + errors: { [key: string]: string }; + handleSubmit: FormEventHandler; + icon: React.ComponentType; + Limit?: React.ReactNode; + name: string; + onClose: () => void; + configButtons: React.ReactNode; + resource: string; + setDescription: (newDescription: string) => void; + setName: (newName: string) => void; + validateName?: () => void; +}; + +export const DialogFormTemplate: React.FC = ({ + Limit, + handleSubmit, + name, + setName, + description, + setDescription, + errors, + icon, + resource, + onClose, + configButtons, + createButtonProps, + validateName = () => {}, +}) => { + const StyledIcon = styleIcon(icon); + + return ( + + + + + {configButtons} + + {Limit} + + + + + + ); +}; diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.styles.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.styles.tsx index f38aabb57b..795b9360df 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.styles.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.styles.tsx @@ -1,5 +1,5 @@ import { styled } from '@mui/material'; -import { StyledDropdownSearch } from './shared.styles'; +import { StyledDropdownSearch } from 'component/common/DialogFormTemplate/ConfigButtons/shared.styles'; export const TableSearchInput = styled(StyledDropdownSearch)({ maxWidth: '30ch', diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.tsx index 4637521dc9..13b5f125e9 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/ConfigButtons/ChangeRequestTableConfigButton.tsx @@ -1,5 +1,4 @@ import { type FC, useState, useMemo } from 'react'; -import { ConfigButton, type ConfigButtonProps } from './ConfigButton'; import { InputAdornment } from '@mui/material'; import Search from '@mui/icons-material/Search'; import { ChangeRequestTable } from './ChangeRequestTable'; @@ -7,6 +6,10 @@ import { ScrollContainer, TableSearchInput, } from './ChangeRequestTableConfigButton.styles'; +import { + ConfigButton, + type ConfigButtonProps, +} from 'component/common/DialogFormTemplate/ConfigButtons/ConfigButton'; type ChangeRequestTableConfigButtonProps = Pick< ConfigButtonProps, diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.styles.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.styles.tsx new file mode 100644 index 0000000000..2080c3650d --- /dev/null +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.styles.tsx @@ -0,0 +1,14 @@ +import { styled } from '@mui/material'; + +export const StyledDefinitionList = styled('dl')(({ theme }) => ({ + dt: { + fontWeight: 'bold', + '&:after': { + content: '":"', + }, + }, + + 'dd + dt': { + marginBlockStart: theme.spacing(1), + }, +})); diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx index 41f641cce5..2bff470d44 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.test.tsx @@ -32,11 +32,15 @@ test('Enabled new project button when limits, version and permission allow for i permissions: [{ permission: CREATE_PROJECT }], }); - const button = await screen.findByText('Create project'); + const button = await screen.findByRole('button', { + name: 'Create project', + }); expect(button).toBeDisabled(); await waitFor(async () => { - const button = await screen.findByText('Create project'); + const button = await screen.findByRole('button', { + name: 'Create project', + }); expect(button).not.toBeDisabled(); }); }); @@ -49,6 +53,8 @@ test('Project limit reached', async () => { await screen.findByText('You have reached the limit for projects'); - const button = await screen.findByText('Create project'); + const button = await screen.findByRole('button', { + name: 'Create project', + }); expect(button).toBeDisabled(); }); diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx index 8a8eb48ebe..08f5bb389a 100644 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx +++ b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/CreateProjectDialog.tsx @@ -1,24 +1,33 @@ import { formatUnknownError } from 'utils/formatUnknownError'; +import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg'; +import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; +import StickinessIcon from '@mui/icons-material/FormatPaint'; +import ProjectModeIcon from '@mui/icons-material/Adjust'; import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi'; import useToast from 'hooks/useToast'; import FormTemplate from 'component/common/FormTemplate/FormTemplate'; -import { NewProjectForm } from './NewProjectForm'; -import { CreateButton } from 'component/common/CreateButton/CreateButton'; import { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions'; import useProjectForm, { DEFAULT_PROJECT_STICKINESS, } from '../../hooks/useProjectForm'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; -import { type ReactNode, useState } from 'react'; +import { type ReactNode, useState, type FormEvent } from 'react'; import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useNavigate } from 'react-router-dom'; -import { Button, Dialog, styled } from '@mui/material'; +import { Dialog, styled } from '@mui/material'; import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg'; import { useUiFlag } from 'hooks/useUiFlag'; import useProjects from 'hooks/api/getters/useProjects/useProjects'; import { Limit } from 'component/common/Limit/Limit'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { DialogFormTemplate } from 'component/common/DialogFormTemplate/DialogFormTemplate'; +import { MultiSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/MultiSelectConfigButton'; +import { SingleSelectConfigButton } from 'component/common/DialogFormTemplate/ConfigButtons/SingleSelectConfigButton'; +import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; +import { useStickinessOptions } from 'hooks/useStickinessOptions'; +import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton'; +import { StyledDefinitionList } from './CreateProjectDialog.styles'; interface ICreateProjectDialogProps { open: boolean; @@ -38,13 +47,59 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({ }, })); -const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN'; - const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({ fill: theme.palette.common.white, stroke: theme.palette.common.white, })); +const projectModeOptions = [ + { value: 'open', label: 'open' }, + { value: 'protected', label: 'protected' }, + { value: 'private', label: 'private' }, +]; + +const configButtonData = { + environments: { + icon: , + text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`, + }, + stickiness: { + icon: , + text: 'Stickiness is used to guarantee that your users see the same result when using a gradual rollout. Default stickiness allows you to choose which field is used by default in this project.', + }, + mode: { + icon: , + text: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.", + additionalTooltipContent: ( + <> +

The modes and their functions are:

+ +
Open
+
+ Anyone can see the project and anyone can create change + requests. +
+
Protected
+
+ Anyone can see the project, but only admins and project + members can submit change requests. +
+
Private
+
+ Hides the project from users with the "viewer" root role + who are not members of the project. Only project members + and admins can submit change requests. +
+
+ + ), + }, + changeRequests: { + icon: , + text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.', + }, +}; + const useProjectLimit = () => { const resourceLimitsEnabled = useUiFlag('resourceLimits'); const { projects, loading: loadingProjects } = useProjects(); @@ -122,7 +177,7 @@ export const CreateProjectDialog = ({ --data-raw '${JSON.stringify(projectPayload, undefined, 2)}'`; }; - const handleSubmit = async (e: Event) => { + const handleSubmit = async (e: FormEvent) => { e.preventDefault(); clearErrors(); const validName = validateName(); @@ -159,6 +214,29 @@ export const CreateProjectDialog = ({ loading: loadingLimit, } = useProjectLimit(); + const { isEnterprise } = useUiConfig(); + const { environments: allEnvironments } = useEnvironments(); + const activeEnvironments = allEnvironments.filter((env) => env.enabled); + const stickinessOptions = useStickinessOptions(projectStickiness); + + const numberOfConfiguredChangeRequestEnvironments = Object.keys( + projectChangeRequestConfiguration, + ).length; + const changeRequestSelectorLabel = + numberOfConfiguredChangeRequestEnvironments > 1 + ? `${numberOfConfiguredChangeRequestEnvironments} environments configured` + : numberOfConfiguredChangeRequestEnvironments === 1 + ? `1 environment configured` + : 'Configure change requests'; + + const availableChangeRequestEnvironments = ( + projectEnvironments.size === 0 + ? activeEnvironments + : activeEnvironments.filter((env) => + projectEnvironments.has(env.name), + ) + ).map(({ name, type }) => ({ name, type })); + return ( - } - > - - - + handleSubmit={handleSubmit} + name={projectName} + setName={setProjectName} + description={projectDesc} + setDescription={setProjectDesc} + errors={errors} + icon={StyledProjectIcon} + onClose={onClose} + configButtons={ + <> + ({ + label: env.name, + value: env.name, + }))} + onChange={setProjectEnvironments} + button={{ + label: + projectEnvironments.size > 0 + ? `${projectEnvironments.size} selected` + : 'All environments', + labelWidth: `${'all environments'.length}ch`, + icon: , + }} + search={{ + label: 'Filter project environments', + placeholder: 'Select project environments', + }} + onOpen={() => + setDocumentation( + configButtonData.environments, + ) + } + onClose={clearDocumentationOverride} + /> + + ({ + value: key, + ...rest, + }), + )} + onChange={(value: any) => { + setProjectStickiness(value); + }} + button={{ + label: projectStickiness, + icon: , + labelWidth: '12ch', + }} + search={{ + label: 'Filter stickiness options', + placeholder: 'Select default stickiness', + }} + onOpen={() => + setDocumentation( + configButtonData.stickiness, + ) + } + onClose={clearDocumentationOverride} + /> + + { + setProjectMode(value); + }} + button={{ + label: projectMode, + icon: , + labelWidth: `${`protected`.length}ch`, + }} + search={{ + label: 'Filter project mode options', + placeholder: 'Select project mode', + }} + onOpen={() => + setDocumentation( + configButtonData.mode, + ) + } + onClose={clearDocumentationOverride} + /> + } + /> + , + labelWidth: `${ + 'nn environments configured' + .length + }ch`, + }} + search={{ + label: 'Filter environments', + placeholder: 'Filter environments', + }} + projectChangeRequestConfiguration={ + projectChangeRequestConfiguration + } + onOpen={() => + setDocumentation( + configButtonData.changeRequests, + ) + } + onClose={clearDocumentationOverride} + /> + } + /> + + } + /> ); diff --git a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx b/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx deleted file mode 100644 index f022b4eac4..0000000000 --- a/frontend/src/component/project/Project/CreateProject/NewCreateProjectForm/NewProjectForm.tsx +++ /dev/null @@ -1,340 +0,0 @@ -import type { ProjectMode } from '../../hooks/useProjectEnterpriseSettingsForm'; -import { useEnvironments } from 'hooks/api/getters/useEnvironments/useEnvironments'; -import StickinessIcon from '@mui/icons-material/FormatPaint'; -import ProjectModeIcon from '@mui/icons-material/Adjust'; -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import EnvironmentsIcon from '@mui/icons-material/CloudCircle'; -import { useStickinessOptions } from 'hooks/useStickinessOptions'; -import { ReactComponent as ChangeRequestIcon } from 'assets/icons/merge.svg'; -import type React from 'react'; -import type { ReactNode } from 'react'; -import theme from 'themes/theme'; -import { - FormActions, - OptionButtons, - ProjectDescriptionContainer, - ProjectNameContainer, - StyledDefinitionList, - StyledForm, - StyledHeader, - StyledIcon, - StyledInput, - TopGrid, -} from './NewProjectForm.styles'; -import { MultiSelectConfigButton } from './ConfigButtons/MultiSelectConfigButton'; -import { SingleSelectConfigButton } from './ConfigButtons/SingleSelectConfigButton'; -import { ChangeRequestTableConfigButton } from './ConfigButtons/ChangeRequestTableConfigButton'; -import { Box, styled } from '@mui/material'; - -type FormProps = { - projectId: string; - projectName: string; - projectDesc: string; - projectStickiness: string; - projectMode: string; - projectEnvironments: Set; - projectChangeRequestConfiguration: Record< - string, - { requiredApprovals: number } - >; - setProjectStickiness: React.Dispatch>; - setProjectEnvironments: (envs: Set) => void; - setProjectName: React.Dispatch>; - setProjectDesc: React.Dispatch>; - setProjectMode: React.Dispatch>; - updateProjectChangeRequestConfig: { - disableChangeRequests: (env: string) => void; - enableChangeRequests: (env: string, requiredApprovals: number) => void; - }; - handleSubmit: (e: any) => void; - errors: { [key: string]: string }; - overrideDocumentation: (args: { text: string; icon: ReactNode }) => void; - clearDocumentationOverride: () => void; - children?: React.ReactNode; - Limit?: React.ReactNode; -}; - -const PROJECT_NAME_INPUT = 'PROJECT_NAME_INPUT'; -const PROJECT_DESCRIPTION_INPUT = 'PROJECT_DESCRIPTION_INPUT'; - -const projectModeOptions = [ - { value: 'open', label: 'open' }, - { value: 'protected', label: 'protected' }, - { value: 'private', label: 'private' }, -]; - -const configButtonData = { - environments: { - icon: , - text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`, - }, - stickiness: { - icon: , - text: 'Stickiness is used to guarantee that your users see the same result when using a gradual rollout. Default stickiness allows you to choose which field is used by default in this project.', - }, - mode: { - icon: , - text: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.", - additionalTooltipContent: ( - <> -

The modes and their functions are:

- -
Open
-
- Anyone can see the project and anyone can create change - requests. -
-
Protected
-
- Anyone can see the project, but only admins and project - members can submit change requests. -
-
Private
-
- Hides the project from users with the "viewer" root role - who are not members of the project. Only project members - and admins can submit change requests. -
-
- - ), - }, - changeRequests: { - icon: , - text: 'Change requests can be configured per environment and require changes to go through an approval process before being applied.', - }, -}; - -const LimitContainer = styled(Box)(({ theme }) => ({ - '&:has(*)': { - padding: theme.spacing(4, 6, 0, 6), - }, -})); - -export const NewProjectForm: React.FC = ({ - children, - Limit, - handleSubmit, - projectName, - projectDesc, - projectStickiness, - projectEnvironments, - projectChangeRequestConfiguration, - projectMode, - setProjectMode, - setProjectEnvironments, - setProjectName, - setProjectDesc, - setProjectStickiness, - updateProjectChangeRequestConfig, - errors, - overrideDocumentation, - clearDocumentationOverride, -}) => { - const { isEnterprise } = useUiConfig(); - const { environments: allEnvironments } = useEnvironments(); - const activeEnvironments = allEnvironments.filter((env) => env.enabled); - const stickinessOptions = useStickinessOptions(projectStickiness); - - const handleProjectNameUpdate = ( - e: React.ChangeEvent, - ) => { - const input = e.target.value; - setProjectName(input); - }; - - const numberOfConfiguredChangeRequestEnvironments = Object.keys( - projectChangeRequestConfiguration, - ).length; - const changeRequestSelectorLabel = - numberOfConfiguredChangeRequestEnvironments > 1 - ? `${numberOfConfiguredChangeRequestEnvironments} environments configured` - : numberOfConfiguredChangeRequestEnvironments === 1 - ? `1 environment configured` - : 'Configure change requests'; - - const availableChangeRequestEnvironments = ( - projectEnvironments.size === 0 - ? activeEnvironments - : activeEnvironments.filter((env) => - projectEnvironments.has(env.name), - ) - ).map(({ name, type }) => ({ name, type })); - - return ( - { - handleSubmit(submitEvent); - }} - > - - - - - ({ - label: env.name, - value: env.name, - }))} - onChange={setProjectEnvironments} - button={{ - label: - projectEnvironments.size > 0 - ? `${projectEnvironments.size} selected` - : 'All environments', - labelWidth: `${'all environments'.length}ch`, - icon: , - }} - search={{ - label: 'Filter project environments', - placeholder: 'Select project environments', - }} - onOpen={() => - overrideDocumentation(configButtonData.environments) - } - onClose={clearDocumentationOverride} - /> - - ({ - value: key, - ...rest, - }))} - onChange={(value: any) => { - setProjectStickiness(value); - }} - button={{ - label: projectStickiness, - icon: , - labelWidth: '12ch', - }} - search={{ - label: 'Filter stickiness options', - placeholder: 'Select default stickiness', - }} - onOpen={() => - overrideDocumentation(configButtonData.stickiness) - } - onClose={clearDocumentationOverride} - /> - - { - setProjectMode(value); - }} - button={{ - label: projectMode, - icon: , - labelWidth: `${`protected`.length}ch`, - }} - search={{ - label: 'Filter project mode options', - placeholder: 'Select project mode', - }} - onOpen={() => - overrideDocumentation(configButtonData.mode) - } - onClose={clearDocumentationOverride} - /> - } - /> - , - labelWidth: `${ - 'nn environments configured'.length - }ch`, - }} - search={{ - label: 'Filter environments', - placeholder: 'Filter environments', - }} - projectChangeRequestConfiguration={ - projectChangeRequestConfiguration - } - onOpen={() => - overrideDocumentation( - configButtonData.changeRequests, - ) - } - onClose={clearDocumentationOverride} - /> - } - /> - - {Limit} - {children} - - ); -};