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.intercept('POST', `/api/admin/projects`).as('createProject');
|
||||||
|
|
||||||
cy.get("[data-testid='PROJECT_ID_INPUT']").type(projectName);
|
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']")
|
cy.get("[id='stickiness-select']")
|
||||||
.first()
|
.first()
|
||||||
.click()
|
.click()
|
||||||
.get(`[data-testid=SELECT_ITEM_ID-${defaultStickiness}`)
|
.get(`[data-testid=SELECT_ITEM_ID-${defaultStickiness}`)
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
cy.get("[data-testid='CREATE_PROJECT_BTN']").click();
|
cy.get("[data-testid='FORM_CREATE_BTN']").click();
|
||||||
cy.wait('@createProject');
|
cy.wait('@createProject');
|
||||||
return cy.visit(`/projects/${projectName}`);
|
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 Input from 'component/common/Input/Input';
|
||||||
import { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
|
|
||||||
|
|
||||||
export const StyledForm = styled('form')(({ theme }) => ({
|
export const StyledForm = styled('form')(({ theme }) => ({
|
||||||
background: theme.palette.background.default,
|
background: theme.palette.background.default,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledFormSection = styled('div')(({ theme }) => ({
|
const StyledFormSection = styled('div')(({ theme }) => ({
|
||||||
'& + *': {
|
'& + *': {
|
||||||
borderBlockStart: `1px solid ${theme.palette.divider}`,
|
borderBlockStart: `1px solid ${theme.palette.divider}`,
|
||||||
},
|
},
|
||||||
@ -16,16 +15,19 @@ export const StyledFormSection = styled('div')(({ theme }) => ({
|
|||||||
|
|
||||||
export const TopGrid = styled(StyledFormSection)(({ theme }) => ({
|
export const TopGrid = styled(StyledFormSection)(({ theme }) => ({
|
||||||
display: 'grid',
|
display: 'grid',
|
||||||
gridTemplateAreas:
|
gridTemplateAreas: `
|
||||||
'"icon header" "icon project-name" "icon project-description"',
|
"icon header"
|
||||||
|
". project-name"
|
||||||
|
". project-description"`,
|
||||||
gridTemplateColumns: 'auto 1fr',
|
gridTemplateColumns: 'auto 1fr',
|
||||||
gap: theme.spacing(4),
|
gap: theme.spacing(4),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledIcon = styled(ProjectIcon)(({ theme }) => ({
|
export const styleIcon = (Icon: React.ComponentType) =>
|
||||||
fill: theme.palette.primary.main,
|
styled(Icon)(({ theme }) => ({
|
||||||
stroke: theme.palette.primary.main,
|
fill: theme.palette.primary.main,
|
||||||
}));
|
stroke: theme.palette.primary.main,
|
||||||
|
}));
|
||||||
|
|
||||||
export const StyledHeader = styled(Typography)({
|
export const StyledHeader = styled(Typography)({
|
||||||
gridArea: 'header',
|
gridArea: 'header',
|
||||||
@ -46,7 +48,7 @@ export const StyledInput = styled(Input)({
|
|||||||
fieldset: { border: 'none' },
|
fieldset: { border: 'none' },
|
||||||
});
|
});
|
||||||
|
|
||||||
export const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
|
export const ConfigButtons = styled(StyledFormSection)(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexFlow: 'row wrap',
|
flexFlow: 'row wrap',
|
||||||
gap: theme.spacing(2),
|
gap: theme.spacing(2),
|
||||||
@ -78,15 +80,8 @@ export const FormActions = styled(StyledFormSection)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const StyledDefinitionList = styled('dl')(({ theme }) => ({
|
export const LimitContainer = styled(Box)(({ theme }) => ({
|
||||||
dt: {
|
'&:has(*)': {
|
||||||
fontWeight: 'bold',
|
padding: theme.spacing(4, 6, 0, 6),
|
||||||
'&:after': {
|
|
||||||
content: '":"',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
'dd + dt': {
|
|
||||||
marginBlockStart: theme.spacing(1),
|
|
||||||
},
|
},
|
||||||
}));
|
}));
|
@ -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 { styled } from '@mui/material';
|
||||||
import { StyledDropdownSearch } from './shared.styles';
|
import { StyledDropdownSearch } from 'component/common/DialogFormTemplate/ConfigButtons/shared.styles';
|
||||||
|
|
||||||
export const TableSearchInput = styled(StyledDropdownSearch)({
|
export const TableSearchInput = styled(StyledDropdownSearch)({
|
||||||
maxWidth: '30ch',
|
maxWidth: '30ch',
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { type FC, useState, useMemo } from 'react';
|
import { type FC, useState, useMemo } from 'react';
|
||||||
import { ConfigButton, type ConfigButtonProps } from './ConfigButton';
|
|
||||||
import { InputAdornment } from '@mui/material';
|
import { InputAdornment } from '@mui/material';
|
||||||
import Search from '@mui/icons-material/Search';
|
import Search from '@mui/icons-material/Search';
|
||||||
import { ChangeRequestTable } from './ChangeRequestTable';
|
import { ChangeRequestTable } from './ChangeRequestTable';
|
||||||
@ -7,6 +6,10 @@ import {
|
|||||||
ScrollContainer,
|
ScrollContainer,
|
||||||
TableSearchInput,
|
TableSearchInput,
|
||||||
} from './ChangeRequestTableConfigButton.styles';
|
} from './ChangeRequestTableConfigButton.styles';
|
||||||
|
import {
|
||||||
|
ConfigButton,
|
||||||
|
type ConfigButtonProps,
|
||||||
|
} from 'component/common/DialogFormTemplate/ConfigButtons/ConfigButton';
|
||||||
|
|
||||||
type ChangeRequestTableConfigButtonProps = Pick<
|
type ChangeRequestTableConfigButtonProps = Pick<
|
||||||
ConfigButtonProps,
|
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 }],
|
permissions: [{ permission: CREATE_PROJECT }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const button = await screen.findByText('Create project');
|
const button = await screen.findByRole('button', {
|
||||||
|
name: 'Create project',
|
||||||
|
});
|
||||||
expect(button).toBeDisabled();
|
expect(button).toBeDisabled();
|
||||||
|
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
const button = await screen.findByText('Create project');
|
const button = await screen.findByRole('button', {
|
||||||
|
name: 'Create project',
|
||||||
|
});
|
||||||
expect(button).not.toBeDisabled();
|
expect(button).not.toBeDisabled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -49,6 +53,8 @@ test('Project limit reached', async () => {
|
|||||||
|
|
||||||
await screen.findByText('You have reached the limit for projects');
|
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();
|
expect(button).toBeDisabled();
|
||||||
});
|
});
|
||||||
|
@ -1,24 +1,33 @@
|
|||||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
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 useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
import useToast from 'hooks/useToast';
|
import useToast from 'hooks/useToast';
|
||||||
import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
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 { CREATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||||
import useProjectForm, {
|
import useProjectForm, {
|
||||||
DEFAULT_PROJECT_STICKINESS,
|
DEFAULT_PROJECT_STICKINESS,
|
||||||
} from '../../hooks/useProjectForm';
|
} from '../../hooks/useProjectForm';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
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 { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import { useNavigate } from 'react-router-dom';
|
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 { ReactComponent as ProjectIcon } from 'assets/icons/projectIconSmall.svg';
|
||||||
import { useUiFlag } from 'hooks/useUiFlag';
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
import useProjects from 'hooks/api/getters/useProjects/useProjects';
|
||||||
import { Limit } from 'component/common/Limit/Limit';
|
import { Limit } from 'component/common/Limit/Limit';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
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 {
|
interface ICreateProjectDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@ -38,13 +47,59 @@ const StyledDialog = styled(Dialog)(({ theme }) => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
|
||||||
|
|
||||||
const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
|
const StyledProjectIcon = styled(ProjectIcon)(({ theme }) => ({
|
||||||
fill: theme.palette.common.white,
|
fill: theme.palette.common.white,
|
||||||
stroke: 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 useProjectLimit = () => {
|
||||||
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
const resourceLimitsEnabled = useUiFlag('resourceLimits');
|
||||||
const { projects, loading: loadingProjects } = useProjects();
|
const { projects, loading: loadingProjects } = useProjects();
|
||||||
@ -122,7 +177,7 @@ export const CreateProjectDialog = ({
|
|||||||
--data-raw '${JSON.stringify(projectPayload, undefined, 2)}'`;
|
--data-raw '${JSON.stringify(projectPayload, undefined, 2)}'`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
clearErrors();
|
clearErrors();
|
||||||
const validName = validateName();
|
const validName = validateName();
|
||||||
@ -159,6 +214,29 @@ export const CreateProjectDialog = ({
|
|||||||
loading: loadingLimit,
|
loading: loadingLimit,
|
||||||
} = useProjectLimit();
|
} = 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 (
|
return (
|
||||||
<StyledDialog open={open} onClose={onClose}>
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
@ -171,28 +249,13 @@ export const CreateProjectDialog = ({
|
|||||||
formatApiCode={formatApiCode}
|
formatApiCode={formatApiCode}
|
||||||
useFixedSidebar
|
useFixedSidebar
|
||||||
>
|
>
|
||||||
<NewProjectForm
|
<DialogFormTemplate
|
||||||
errors={errors}
|
resource='project'
|
||||||
handleSubmit={handleSubmit}
|
createButtonProps={{
|
||||||
projectId={projectId}
|
permission: CREATE_PROJECT,
|
||||||
projectEnvironments={projectEnvironments}
|
disabled:
|
||||||
setProjectEnvironments={setProjectEnvironments}
|
creatingProject || limitReached || loadingLimit,
|
||||||
projectName={projectName}
|
}}
|
||||||
projectStickiness={projectStickiness}
|
|
||||||
projectChangeRequestConfiguration={
|
|
||||||
projectChangeRequestConfiguration
|
|
||||||
}
|
|
||||||
updateProjectChangeRequestConfig={
|
|
||||||
updateProjectChangeRequestConfig
|
|
||||||
}
|
|
||||||
projectMode={projectMode}
|
|
||||||
setProjectMode={setProjectMode}
|
|
||||||
setProjectStickiness={setProjectStickiness}
|
|
||||||
setProjectName={setProjectName}
|
|
||||||
projectDesc={projectDesc}
|
|
||||||
setProjectDesc={setProjectDesc}
|
|
||||||
overrideDocumentation={setDocumentation}
|
|
||||||
clearDocumentationOverride={clearDocumentationOverride}
|
|
||||||
Limit={
|
Limit={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={resourceLimitsEnabled}
|
condition={resourceLimitsEnabled}
|
||||||
@ -205,17 +268,154 @@ export const CreateProjectDialog = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
>
|
handleSubmit={handleSubmit}
|
||||||
<Button onClick={onClose}>Cancel</Button>
|
name={projectName}
|
||||||
<CreateButton
|
setName={setProjectName}
|
||||||
name='project'
|
description={projectDesc}
|
||||||
permission={CREATE_PROJECT}
|
setDescription={setProjectDesc}
|
||||||
disabled={
|
errors={errors}
|
||||||
creatingProject || limitReached || loadingLimit
|
icon={StyledProjectIcon}
|
||||||
}
|
onClose={onClose}
|
||||||
data-testid={CREATE_PROJECT_BTN}
|
configButtons={
|
||||||
/>
|
<>
|
||||||
</NewProjectForm>
|
<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}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</FormTemplate>
|
</FormTemplate>
|
||||||
</StyledDialog>
|
</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