mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
chore: create shared dialog form template (#7663)
This PR extracts the dialog form that we created for the new project form into a shared component in the `common` folder. Most of the code has been lifted and shifted, but there's been some minor adjustments along the way. The main file is `frontend/src/component/common/DialogFormTemplate/DialogFormTemplate.tsx`. Everything else is just cleanup.
This commit is contained in:
parent
1e3c690185
commit
eb7208025f
@ -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}`);
|
||||
};
|
||||
|
@ -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 }) => ({
|
||||
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),
|
||||
},
|
||||
}));
|
@ -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<HTMLFormElement>;
|
||||
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<FormProps> = ({
|
||||
Limit,
|
||||
handleSubmit,
|
||||
name,
|
||||
setName,
|
||||
description,
|
||||
setDescription,
|
||||
errors,
|
||||
icon,
|
||||
resource,
|
||||
onClose,
|
||||
configButtons,
|
||||
createButtonProps,
|
||||
validateName = () => {},
|
||||
}) => {
|
||||
const StyledIcon = styleIcon(icon);
|
||||
|
||||
return (
|
||||
<StyledForm onSubmit={handleSubmit}>
|
||||
<TopGrid>
|
||||
<StyledIcon aria-hidden='true' />
|
||||
<StyledHeader variant='h2'>Create {resource}</StyledHeader>
|
||||
<ProjectNameContainer>
|
||||
<StyledInput
|
||||
label={`${resource} name`}
|
||||
aria-required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onBlur={validateName}
|
||||
onFocus={() => {
|
||||
delete errors.name;
|
||||
}}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
style: { fontSize: theme.typography.h1.fontSize },
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: { fontSize: theme.typography.h1.fontSize },
|
||||
}}
|
||||
data-testid='FORM_NAME_INPUT'
|
||||
size='medium'
|
||||
/>
|
||||
</ProjectNameContainer>
|
||||
<ProjectDescriptionContainer>
|
||||
<StyledInput
|
||||
size='medium'
|
||||
className='description'
|
||||
label='Description (optional)'
|
||||
multiline
|
||||
maxRows={3}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
InputProps={{
|
||||
style: { fontSize: theme.typography.h2.fontSize },
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: { fontSize: theme.typography.h2.fontSize },
|
||||
}}
|
||||
data-testid='FORM_DESCRIPTION_INPUT'
|
||||
/>
|
||||
</ProjectDescriptionContainer>
|
||||
</TopGrid>
|
||||
|
||||
<ConfigButtons>{configButtons}</ConfigButtons>
|
||||
|
||||
<LimitContainer>{Limit}</LimitContainer>
|
||||
<FormActions>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<CreateButton
|
||||
data-testid='FORM_CREATE_BUTTON'
|
||||
name={resource}
|
||||
{...createButtonProps}
|
||||
/>
|
||||
</FormActions>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
@ -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',
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
},
|
||||
}));
|
@ -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();
|
||||
});
|
||||
|
@ -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: <EnvironmentsIcon />,
|
||||
text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`,
|
||||
},
|
||||
stickiness: {
|
||||
icon: <StickinessIcon />,
|
||||
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: <ProjectModeIcon />,
|
||||
text: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.",
|
||||
additionalTooltipContent: (
|
||||
<>
|
||||
<p>The modes and their functions are:</p>
|
||||
<StyledDefinitionList>
|
||||
<dt>Open</dt>
|
||||
<dd>
|
||||
Anyone can see the project and anyone can create change
|
||||
requests.
|
||||
</dd>
|
||||
<dt>Protected</dt>
|
||||
<dd>
|
||||
Anyone can see the project, but only admins and project
|
||||
members can submit change requests.
|
||||
</dd>
|
||||
<dt>Private</dt>
|
||||
<dd>
|
||||
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.
|
||||
</dd>
|
||||
</StyledDefinitionList>
|
||||
</>
|
||||
),
|
||||
},
|
||||
changeRequests: {
|
||||
icon: <ChangeRequestIcon />,
|
||||
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<HTMLFormElement>) => {
|
||||
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 (
|
||||
<StyledDialog open={open} onClose={onClose}>
|
||||
<FormTemplate
|
||||
@ -171,28 +249,13 @@ export const CreateProjectDialog = ({
|
||||
formatApiCode={formatApiCode}
|
||||
useFixedSidebar
|
||||
>
|
||||
<NewProjectForm
|
||||
errors={errors}
|
||||
handleSubmit={handleSubmit}
|
||||
projectId={projectId}
|
||||
projectEnvironments={projectEnvironments}
|
||||
setProjectEnvironments={setProjectEnvironments}
|
||||
projectName={projectName}
|
||||
projectStickiness={projectStickiness}
|
||||
projectChangeRequestConfiguration={
|
||||
projectChangeRequestConfiguration
|
||||
}
|
||||
updateProjectChangeRequestConfig={
|
||||
updateProjectChangeRequestConfig
|
||||
}
|
||||
projectMode={projectMode}
|
||||
setProjectMode={setProjectMode}
|
||||
setProjectStickiness={setProjectStickiness}
|
||||
setProjectName={setProjectName}
|
||||
projectDesc={projectDesc}
|
||||
setProjectDesc={setProjectDesc}
|
||||
overrideDocumentation={setDocumentation}
|
||||
clearDocumentationOverride={clearDocumentationOverride}
|
||||
<DialogFormTemplate
|
||||
resource='project'
|
||||
createButtonProps={{
|
||||
permission: CREATE_PROJECT,
|
||||
disabled:
|
||||
creatingProject || limitReached || loadingLimit,
|
||||
}}
|
||||
Limit={
|
||||
<ConditionallyRender
|
||||
condition={resourceLimitsEnabled}
|
||||
@ -205,17 +268,154 @@ export const CreateProjectDialog = ({
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
<CreateButton
|
||||
name='project'
|
||||
permission={CREATE_PROJECT}
|
||||
disabled={
|
||||
creatingProject || limitReached || loadingLimit
|
||||
handleSubmit={handleSubmit}
|
||||
name={projectName}
|
||||
setName={setProjectName}
|
||||
description={projectDesc}
|
||||
setDescription={setProjectDesc}
|
||||
errors={errors}
|
||||
icon={StyledProjectIcon}
|
||||
onClose={onClose}
|
||||
configButtons={
|
||||
<>
|
||||
<MultiSelectConfigButton
|
||||
tooltip={{
|
||||
header: 'Select project environments',
|
||||
}}
|
||||
description={configButtonData.environments.text}
|
||||
selectedOptions={projectEnvironments}
|
||||
options={activeEnvironments.map((env) => ({
|
||||
label: env.name,
|
||||
value: env.name,
|
||||
}))}
|
||||
onChange={setProjectEnvironments}
|
||||
button={{
|
||||
label:
|
||||
projectEnvironments.size > 0
|
||||
? `${projectEnvironments.size} selected`
|
||||
: 'All environments',
|
||||
labelWidth: `${'all environments'.length}ch`,
|
||||
icon: <EnvironmentsIcon />,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter project environments',
|
||||
placeholder: 'Select project environments',
|
||||
}}
|
||||
onOpen={() =>
|
||||
setDocumentation(
|
||||
configButtonData.environments,
|
||||
)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
|
||||
<SingleSelectConfigButton
|
||||
tooltip={{
|
||||
header: 'Set default project stickiness',
|
||||
}}
|
||||
description={configButtonData.stickiness.text}
|
||||
options={stickinessOptions.map(
|
||||
({ key, ...rest }) => ({
|
||||
value: key,
|
||||
...rest,
|
||||
}),
|
||||
)}
|
||||
onChange={(value: any) => {
|
||||
setProjectStickiness(value);
|
||||
}}
|
||||
button={{
|
||||
label: projectStickiness,
|
||||
icon: <StickinessIcon />,
|
||||
labelWidth: '12ch',
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter stickiness options',
|
||||
placeholder: 'Select default stickiness',
|
||||
}}
|
||||
onOpen={() =>
|
||||
setDocumentation(
|
||||
configButtonData.stickiness,
|
||||
)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<SingleSelectConfigButton
|
||||
tooltip={{
|
||||
header: 'Set project collaboration mode',
|
||||
additionalContent:
|
||||
configButtonData.mode
|
||||
.additionalTooltipContent,
|
||||
}}
|
||||
description={configButtonData.mode.text}
|
||||
options={projectModeOptions}
|
||||
onChange={(value: any) => {
|
||||
setProjectMode(value);
|
||||
}}
|
||||
button={{
|
||||
label: projectMode,
|
||||
icon: <ProjectModeIcon />,
|
||||
labelWidth: `${`protected`.length}ch`,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter project mode options',
|
||||
placeholder: 'Select project mode',
|
||||
}}
|
||||
onOpen={() =>
|
||||
setDocumentation(
|
||||
configButtonData.mode,
|
||||
)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<ChangeRequestTableConfigButton
|
||||
tooltip={{
|
||||
header: 'Configure change requests',
|
||||
}}
|
||||
description={
|
||||
configButtonData.changeRequests.text
|
||||
}
|
||||
activeEnvironments={
|
||||
availableChangeRequestEnvironments
|
||||
}
|
||||
updateProjectChangeRequestConfiguration={
|
||||
updateProjectChangeRequestConfig
|
||||
}
|
||||
button={{
|
||||
label: changeRequestSelectorLabel,
|
||||
icon: <ChangeRequestIcon />,
|
||||
labelWidth: `${
|
||||
'nn environments configured'
|
||||
.length
|
||||
}ch`,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter environments',
|
||||
placeholder: 'Filter environments',
|
||||
}}
|
||||
projectChangeRequestConfiguration={
|
||||
projectChangeRequestConfiguration
|
||||
}
|
||||
onOpen={() =>
|
||||
setDocumentation(
|
||||
configButtonData.changeRequests,
|
||||
)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
data-testid={CREATE_PROJECT_BTN}
|
||||
/>
|
||||
</NewProjectForm>
|
||||
</FormTemplate>
|
||||
</StyledDialog>
|
||||
);
|
||||
|
@ -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<string>;
|
||||
projectChangeRequestConfiguration: Record<
|
||||
string,
|
||||
{ requiredApprovals: number }
|
||||
>;
|
||||
setProjectStickiness: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectEnvironments: (envs: Set<string>) => void;
|
||||
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||
setProjectMode: React.Dispatch<React.SetStateAction<ProjectMode>>;
|
||||
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: <EnvironmentsIcon />,
|
||||
text: `Each feature flag can have a separate configuration per environment. This setting configures which environments your project should start with.`,
|
||||
},
|
||||
stickiness: {
|
||||
icon: <StickinessIcon />,
|
||||
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: <ProjectModeIcon />,
|
||||
text: "A project's collaboration mode defines who should be allowed see your project and create change requests in it.",
|
||||
additionalTooltipContent: (
|
||||
<>
|
||||
<p>The modes and their functions are:</p>
|
||||
<StyledDefinitionList>
|
||||
<dt>Open</dt>
|
||||
<dd>
|
||||
Anyone can see the project and anyone can create change
|
||||
requests.
|
||||
</dd>
|
||||
<dt>Protected</dt>
|
||||
<dd>
|
||||
Anyone can see the project, but only admins and project
|
||||
members can submit change requests.
|
||||
</dd>
|
||||
<dt>Private</dt>
|
||||
<dd>
|
||||
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.
|
||||
</dd>
|
||||
</StyledDefinitionList>
|
||||
</>
|
||||
),
|
||||
},
|
||||
changeRequests: {
|
||||
icon: <ChangeRequestIcon />,
|
||||
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<FormProps> = ({
|
||||
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<HTMLInputElement>,
|
||||
) => {
|
||||
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 (
|
||||
<StyledForm
|
||||
onSubmit={(submitEvent) => {
|
||||
handleSubmit(submitEvent);
|
||||
}}
|
||||
>
|
||||
<TopGrid>
|
||||
<StyledIcon aria-hidden='true' />
|
||||
<StyledHeader variant='h2'>New project</StyledHeader>
|
||||
<ProjectNameContainer>
|
||||
<StyledInput
|
||||
label='Project name'
|
||||
aria-required
|
||||
value={projectName}
|
||||
onChange={handleProjectNameUpdate}
|
||||
error={Boolean(errors.name)}
|
||||
errorText={errors.name}
|
||||
onFocus={() => {
|
||||
delete errors.name;
|
||||
}}
|
||||
data-testid={PROJECT_NAME_INPUT}
|
||||
autoFocus
|
||||
InputProps={{
|
||||
style: { fontSize: theme.typography.h1.fontSize },
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: { fontSize: theme.typography.h1.fontSize },
|
||||
}}
|
||||
size='medium'
|
||||
/>
|
||||
</ProjectNameContainer>
|
||||
<ProjectDescriptionContainer>
|
||||
<StyledInput
|
||||
size='medium'
|
||||
className='description'
|
||||
label='Description (optional)'
|
||||
multiline
|
||||
maxRows={3}
|
||||
value={projectDesc}
|
||||
onChange={(e) => setProjectDesc(e.target.value)}
|
||||
data-testid={PROJECT_DESCRIPTION_INPUT}
|
||||
InputProps={{
|
||||
style: { fontSize: theme.typography.h2.fontSize },
|
||||
}}
|
||||
InputLabelProps={{
|
||||
style: { fontSize: theme.typography.h2.fontSize },
|
||||
}}
|
||||
/>
|
||||
</ProjectDescriptionContainer>
|
||||
</TopGrid>
|
||||
|
||||
<OptionButtons>
|
||||
<MultiSelectConfigButton
|
||||
tooltip={{ header: 'Select project environments' }}
|
||||
description={configButtonData.environments.text}
|
||||
selectedOptions={projectEnvironments}
|
||||
options={activeEnvironments.map((env) => ({
|
||||
label: env.name,
|
||||
value: env.name,
|
||||
}))}
|
||||
onChange={setProjectEnvironments}
|
||||
button={{
|
||||
label:
|
||||
projectEnvironments.size > 0
|
||||
? `${projectEnvironments.size} selected`
|
||||
: 'All environments',
|
||||
labelWidth: `${'all environments'.length}ch`,
|
||||
icon: <EnvironmentsIcon />,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter project environments',
|
||||
placeholder: 'Select project environments',
|
||||
}}
|
||||
onOpen={() =>
|
||||
overrideDocumentation(configButtonData.environments)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
|
||||
<SingleSelectConfigButton
|
||||
tooltip={{ header: 'Set default project stickiness' }}
|
||||
description={configButtonData.stickiness.text}
|
||||
options={stickinessOptions.map(({ key, ...rest }) => ({
|
||||
value: key,
|
||||
...rest,
|
||||
}))}
|
||||
onChange={(value: any) => {
|
||||
setProjectStickiness(value);
|
||||
}}
|
||||
button={{
|
||||
label: projectStickiness,
|
||||
icon: <StickinessIcon />,
|
||||
labelWidth: '12ch',
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter stickiness options',
|
||||
placeholder: 'Select default stickiness',
|
||||
}}
|
||||
onOpen={() =>
|
||||
overrideDocumentation(configButtonData.stickiness)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
|
||||
<ConditionallyRender
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<SingleSelectConfigButton
|
||||
tooltip={{
|
||||
header: 'Set project collaboration mode',
|
||||
additionalContent:
|
||||
configButtonData.mode
|
||||
.additionalTooltipContent,
|
||||
}}
|
||||
description={configButtonData.mode.text}
|
||||
options={projectModeOptions}
|
||||
onChange={(value: any) => {
|
||||
setProjectMode(value);
|
||||
}}
|
||||
button={{
|
||||
label: projectMode,
|
||||
icon: <ProjectModeIcon />,
|
||||
labelWidth: `${`protected`.length}ch`,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter project mode options',
|
||||
placeholder: 'Select project mode',
|
||||
}}
|
||||
onOpen={() =>
|
||||
overrideDocumentation(configButtonData.mode)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<ConditionallyRender
|
||||
condition={isEnterprise()}
|
||||
show={
|
||||
<ChangeRequestTableConfigButton
|
||||
tooltip={{ header: 'Configure change requests' }}
|
||||
description={configButtonData.changeRequests.text}
|
||||
activeEnvironments={
|
||||
availableChangeRequestEnvironments
|
||||
}
|
||||
updateProjectChangeRequestConfiguration={
|
||||
updateProjectChangeRequestConfig
|
||||
}
|
||||
button={{
|
||||
label: changeRequestSelectorLabel,
|
||||
icon: <ChangeRequestIcon />,
|
||||
labelWidth: `${
|
||||
'nn environments configured'.length
|
||||
}ch`,
|
||||
}}
|
||||
search={{
|
||||
label: 'Filter environments',
|
||||
placeholder: 'Filter environments',
|
||||
}}
|
||||
projectChangeRequestConfiguration={
|
||||
projectChangeRequestConfiguration
|
||||
}
|
||||
onOpen={() =>
|
||||
overrideDocumentation(
|
||||
configButtonData.changeRequests,
|
||||
)
|
||||
}
|
||||
onClose={clearDocumentationOverride}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</OptionButtons>
|
||||
<LimitContainer>{Limit}</LimitContainer>
|
||||
<FormActions>{children}</FormActions>
|
||||
</StyledForm>
|
||||
);
|
||||
};
|
Loading…
Reference in New Issue
Block a user