mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-28 00:06:53 +01:00
feat: create project dialog (#7012)
Adds a redirect to `/projects?create=true` to keep the `projects/create` url still usable. Loads the form in a modal Closes [1-2296](https://linear.app/unleash/issue/1-2296/project-creation-move-to-modal) <img width="1537" alt="Screenshot 2024-05-09 at 14 06 50" src="https://github.com/Unleash/unleash/assets/104830839/c108c7ee-5751-4380-9766-11368281a11a"> --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
1cc8ca901c
commit
9fbb041bfd
@ -35,7 +35,6 @@ import { SegmentTable } from '../segments/SegmentTable/SegmentTable';
|
|||||||
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
import { FeaturesArchiveTable } from '../archive/FeaturesArchiveTable';
|
||||||
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
|
import { LazyPlayground } from 'component/playground/Playground/LazyPlayground';
|
||||||
import { Profile } from 'component/user/Profile/Profile';
|
import { Profile } from 'component/user/Profile/Profile';
|
||||||
import { LazyCreateProject } from 'component/project/Project/CreateProject/LazyCreateProject';
|
|
||||||
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
import { LazyFeatureView } from 'component/feature/FeatureView/LazyFeatureView';
|
||||||
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
import { LazyAdmin } from 'component/admin/LazyAdmin';
|
||||||
import { LazyProject } from 'component/project/Project/LazyProject';
|
import { LazyProject } from 'component/project/Project/LazyProject';
|
||||||
@ -48,6 +47,7 @@ import { Insights } from '../insights/Insights';
|
|||||||
import { FeedbackList } from '../feedbackNew/FeedbackList';
|
import { FeedbackList } from '../feedbackNew/FeedbackList';
|
||||||
import { Application } from 'component/application/Application';
|
import { Application } from 'component/application/Application';
|
||||||
import { Signals } from 'component/signals/Signals';
|
import { Signals } from 'component/signals/Signals';
|
||||||
|
import { LazyCreateProject } from '../project/Project/CreateProject/LazyCreateProject';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { useNavigate } from 'react-router-dom';
|
import { Navigate, useNavigate } from 'react-router-dom';
|
||||||
import ProjectForm from '../ProjectForm/ProjectForm';
|
import ProjectForm from '../ProjectForm/ProjectForm';
|
||||||
import { NewProjectForm } from './NewProjectForm';
|
|
||||||
import useProjectForm, {
|
import useProjectForm, {
|
||||||
DEFAULT_PROJECT_STICKINESS,
|
DEFAULT_PROJECT_STICKINESS,
|
||||||
} from '../hooks/useProjectForm';
|
} from '../hooks/useProjectForm';
|
||||||
@ -28,6 +27,7 @@ const CreateProject = () => {
|
|||||||
const { setToastData, setToastApiError } = useToast();
|
const { setToastData, setToastApiError } = useToast();
|
||||||
const { refetchUser } = useAuthUser();
|
const { refetchUser } = useAuthUser();
|
||||||
const { uiConfig } = useUiConfig();
|
const { uiConfig } = useUiConfig();
|
||||||
|
const useNewProjectForm = useUiFlag('newCreateProjectUI');
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
const {
|
const {
|
||||||
@ -52,6 +52,10 @@ const CreateProject = () => {
|
|||||||
errors,
|
errors,
|
||||||
} = useProjectForm();
|
} = useProjectForm();
|
||||||
|
|
||||||
|
if (useNewProjectForm) {
|
||||||
|
return <Navigate to={`/projects?create=true`} replace />;
|
||||||
|
}
|
||||||
|
|
||||||
const generalDocumentation =
|
const generalDocumentation =
|
||||||
'Projects allows you to group feature toggles together in the management UI.';
|
'Projects allows you to group feature toggles together in the management UI.';
|
||||||
|
|
||||||
@ -60,8 +64,6 @@ const CreateProject = () => {
|
|||||||
const clearDocumentationOverride = () =>
|
const clearDocumentationOverride = () =>
|
||||||
setDocumentation(generalDocumentation);
|
setDocumentation(generalDocumentation);
|
||||||
|
|
||||||
const useNewProjectForm = useUiFlag('newCreateProjectUI');
|
|
||||||
|
|
||||||
const { createProject, loading } = useProjectApi();
|
const { createProject, loading } = useProjectApi();
|
||||||
|
|
||||||
const handleSubmit = async (e: Event) => {
|
const handleSubmit = async (e: Event) => {
|
||||||
@ -112,54 +114,6 @@ const CreateProject = () => {
|
|||||||
navigate(GO_BACK);
|
navigate(GO_BACK);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (useNewProjectForm) {
|
|
||||||
return (
|
|
||||||
<FormTemplate
|
|
||||||
disablePadding
|
|
||||||
loading={loading}
|
|
||||||
description={documentation}
|
|
||||||
documentationLink='https://docs.getunleash.io/reference/projects'
|
|
||||||
documentationLinkLabel='Projects documentation'
|
|
||||||
formatApiCode={formatApiCode}
|
|
||||||
>
|
|
||||||
<NewProjectForm
|
|
||||||
errors={errors}
|
|
||||||
handleSubmit={handleSubmit}
|
|
||||||
projectId={projectId}
|
|
||||||
projectEnvironments={projectEnvironments}
|
|
||||||
setProjectEnvironments={setProjectEnvironments}
|
|
||||||
setProjectId={setProjectId}
|
|
||||||
projectName={projectName}
|
|
||||||
projectStickiness={projectStickiness}
|
|
||||||
projectChangeRequestConfiguration={
|
|
||||||
projectChangeRequestConfiguration
|
|
||||||
}
|
|
||||||
updateProjectChangeRequestConfig={
|
|
||||||
updateProjectChangeRequestConfig
|
|
||||||
}
|
|
||||||
projectMode={projectMode}
|
|
||||||
setProjectMode={setProjectMode}
|
|
||||||
setProjectStickiness={setProjectStickiness}
|
|
||||||
setProjectName={setProjectName}
|
|
||||||
projectDesc={projectDesc}
|
|
||||||
setProjectDesc={setProjectDesc}
|
|
||||||
mode='Create'
|
|
||||||
clearErrors={clearErrors}
|
|
||||||
validateProjectId={validateProjectId}
|
|
||||||
overrideDocumentation={setDocumentation}
|
|
||||||
clearDocumentationOverride={clearDocumentationOverride}
|
|
||||||
>
|
|
||||||
<StyledButton onClick={handleCancel}>Cancel</StyledButton>
|
|
||||||
<CreateButton
|
|
||||||
name='project'
|
|
||||||
permission={CREATE_PROJECT}
|
|
||||||
data-testid={CREATE_PROJECT_BTN}
|
|
||||||
/>
|
|
||||||
</NewProjectForm>
|
|
||||||
</FormTemplate>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormTemplate
|
<FormTemplate
|
||||||
loading={loading}
|
loading={loading}
|
||||||
|
@ -0,0 +1,164 @@
|
|||||||
|
import type React from 'react';
|
||||||
|
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||||
|
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 { useState } 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';
|
||||||
|
|
||||||
|
interface ICreateProjectDialogueProps {
|
||||||
|
open: boolean;
|
||||||
|
onClose: (e: React.SyntheticEvent) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StyledDialog = styled(Dialog)(({ theme, maxWidth }) => ({
|
||||||
|
'& .MuiDialog-paper': {
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
maxWidth: theme.spacing(170),
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
},
|
||||||
|
padding: 0,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CREATE_PROJECT_BTN = 'CREATE_PROJECT_BTN';
|
||||||
|
|
||||||
|
const StyledButton = styled(Button)(({ theme }) => ({
|
||||||
|
marginLeft: theme.spacing(3),
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const CreateProjectDialogue = ({
|
||||||
|
open,
|
||||||
|
onClose,
|
||||||
|
}: ICreateProjectDialogueProps) => {
|
||||||
|
const { createProject, loading } = useProjectApi();
|
||||||
|
const { refetchUser } = useAuthUser();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectDesc,
|
||||||
|
projectMode,
|
||||||
|
projectEnvironments,
|
||||||
|
projectChangeRequestConfiguration,
|
||||||
|
setProjectMode,
|
||||||
|
setProjectId,
|
||||||
|
setProjectName,
|
||||||
|
setProjectDesc,
|
||||||
|
setProjectEnvironments,
|
||||||
|
updateProjectChangeRequestConfig,
|
||||||
|
getCreateProjectPayload,
|
||||||
|
clearErrors,
|
||||||
|
validateProjectId,
|
||||||
|
validateName,
|
||||||
|
setProjectStickiness,
|
||||||
|
projectStickiness,
|
||||||
|
errors,
|
||||||
|
} = useProjectForm();
|
||||||
|
|
||||||
|
const generalDocumentation =
|
||||||
|
'Projects allows you to group feature toggles together in the management UI.';
|
||||||
|
|
||||||
|
const [documentation, setDocumentation] = useState(generalDocumentation);
|
||||||
|
|
||||||
|
const clearDocumentationOverride = () =>
|
||||||
|
setDocumentation(generalDocumentation);
|
||||||
|
|
||||||
|
const formatApiCode = () => {
|
||||||
|
return `curl --location --request POST '${uiConfig.unleashUrl}/api/admin/projects' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getCreateProjectPayload(), undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrors();
|
||||||
|
const validName = validateName();
|
||||||
|
const validId = await validateProjectId();
|
||||||
|
|
||||||
|
if (validName && validId) {
|
||||||
|
const payload = getCreateProjectPayload();
|
||||||
|
try {
|
||||||
|
await createProject(payload);
|
||||||
|
refetchUser();
|
||||||
|
navigate(`/projects/${projectId}`, { replace: true });
|
||||||
|
setToastData({
|
||||||
|
title: 'Project created',
|
||||||
|
text: 'Now you can add toggles to this project',
|
||||||
|
confetti: true,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (projectStickiness !== DEFAULT_PROJECT_STICKINESS) {
|
||||||
|
trackEvent('project_stickiness_set');
|
||||||
|
}
|
||||||
|
trackEvent('project-mode', {
|
||||||
|
props: { mode: projectMode, action: 'added' },
|
||||||
|
});
|
||||||
|
} catch (error: unknown) {
|
||||||
|
setToastApiError(formatUnknownError(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledDialog open={open} onClose={onClose}>
|
||||||
|
<FormTemplate
|
||||||
|
disablePadding
|
||||||
|
loading={loading}
|
||||||
|
description={documentation}
|
||||||
|
documentationLink='https://docs.getunleash.io/reference/projects'
|
||||||
|
documentationLinkLabel='Projects documentation'
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<NewProjectForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
projectId={projectId}
|
||||||
|
projectEnvironments={projectEnvironments}
|
||||||
|
setProjectEnvironments={setProjectEnvironments}
|
||||||
|
setProjectId={setProjectId}
|
||||||
|
projectName={projectName}
|
||||||
|
projectStickiness={projectStickiness}
|
||||||
|
projectChangeRequestConfiguration={
|
||||||
|
projectChangeRequestConfiguration
|
||||||
|
}
|
||||||
|
updateProjectChangeRequestConfig={
|
||||||
|
updateProjectChangeRequestConfig
|
||||||
|
}
|
||||||
|
projectMode={projectMode}
|
||||||
|
setProjectMode={setProjectMode}
|
||||||
|
setProjectStickiness={setProjectStickiness}
|
||||||
|
setProjectName={setProjectName}
|
||||||
|
projectDesc={projectDesc}
|
||||||
|
setProjectDesc={setProjectDesc}
|
||||||
|
mode='Create'
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
validateProjectId={validateProjectId}
|
||||||
|
overrideDocumentation={setDocumentation}
|
||||||
|
clearDocumentationOverride={clearDocumentationOverride}
|
||||||
|
>
|
||||||
|
<StyledButton onClick={onClose}>Cancel</StyledButton>
|
||||||
|
<CreateButton
|
||||||
|
name='project'
|
||||||
|
permission={CREATE_PROJECT}
|
||||||
|
data-testid={CREATE_PROJECT_BTN}
|
||||||
|
/>
|
||||||
|
</NewProjectForm>
|
||||||
|
</FormTemplate>
|
||||||
|
</StyledDialog>
|
||||||
|
);
|
||||||
|
};
|
@ -25,7 +25,7 @@ const StyledFormSection = styled('div')(({ theme }) => ({
|
|||||||
borderBlockStart: `1px solid ${theme.palette.divider}`,
|
borderBlockStart: `1px solid ${theme.palette.divider}`,
|
||||||
},
|
},
|
||||||
|
|
||||||
padding: theme.spacing(7),
|
padding: theme.spacing(6),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const TopGrid = styled(StyledFormSection)(({ theme }) => ({
|
const TopGrid = styled(StyledFormSection)(({ theme }) => ({
|
||||||
@ -60,11 +60,11 @@ const StyledInput = styled(Input)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledProjectName = styled(StyledInput)(({ theme }) => ({
|
const StyledProjectName = styled(StyledInput)(({ theme }) => ({
|
||||||
'*': { fontSize: theme.typography.h1.fontSize },
|
'*': { fontSize: theme.typography.h2.fontSize },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const StyledProjectDescription = styled(StyledInput)(({ theme }) => ({
|
const StyledProjectDescription = styled(StyledInput)(({ theme }) => ({
|
||||||
'*': { fontSize: theme.typography.h2.fontSize },
|
'*': { fontSize: theme.typography.h3.fontSize },
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
|
const OptionButtons = styled(StyledFormSection)(({ theme }) => ({
|
||||||
|
@ -26,6 +26,7 @@ import { useUiFlag } from 'hooks/useUiFlag';
|
|||||||
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
|
import { useProfile } from 'hooks/api/getters/useProfile/useProfile';
|
||||||
import { groupProjects } from './group-projects';
|
import { groupProjects } from './group-projects';
|
||||||
import { ProjectGroup } from './ProjectGroup';
|
import { ProjectGroup } from './ProjectGroup';
|
||||||
|
import { CreateProjectDialogue } from '../Project/CreateProject/CreateProjectDialog/CreateProjectDialog';
|
||||||
|
|
||||||
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
const StyledApiError = styled(ApiError)(({ theme }) => ({
|
||||||
maxWidth: '500px',
|
maxWidth: '500px',
|
||||||
@ -104,6 +105,10 @@ export const ProjectListNew = () => {
|
|||||||
const splitProjectList = useUiFlag('projectListFilterMyProjects');
|
const splitProjectList = useUiFlag('projectListFilterMyProjects');
|
||||||
const myProjects = new Set(useProfile().profile?.projects || []);
|
const myProjects = new Set(useProfile().profile?.projects || []);
|
||||||
|
|
||||||
|
const showCreateDialog = Boolean(searchParams.get('create'));
|
||||||
|
const [openCreateDialog, setOpenCreateDialog] = useState(showCreateDialog);
|
||||||
|
const useNewProjectForm = useUiFlag('newCreateProjectUI');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tableState: PageQueryType = {};
|
const tableState: PageQueryType = {};
|
||||||
if (searchValue) {
|
if (searchValue) {
|
||||||
@ -173,6 +178,13 @@ export const ProjectListNew = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (useNewProjectForm) {
|
||||||
|
return setOpenCreateDialog(true);
|
||||||
|
}
|
||||||
|
navigate('/projects/create');
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContent
|
<PageContent
|
||||||
isLoading={loading}
|
isLoading={loading}
|
||||||
@ -196,7 +208,7 @@ export const ProjectListNew = () => {
|
|||||||
<ResponsiveButton
|
<ResponsiveButton
|
||||||
Icon={Add}
|
Icon={Add}
|
||||||
endIcon={createButtonData.endIcon}
|
endIcon={createButtonData.endIcon}
|
||||||
onClick={() => navigate('/projects/create')}
|
onClick={handleClick}
|
||||||
maxWidth='700px'
|
maxWidth='700px'
|
||||||
permission={CREATE_PROJECT}
|
permission={CREATE_PROJECT}
|
||||||
disabled={createButtonData.disabled}
|
disabled={createButtonData.disabled}
|
||||||
@ -250,6 +262,15 @@ export const ProjectListNew = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={useNewProjectForm}
|
||||||
|
show={
|
||||||
|
<CreateProjectDialogue
|
||||||
|
open={openCreateDialog}
|
||||||
|
onClose={() => setOpenCreateDialog(false)}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</PageContent>
|
</PageContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user