mirror of
https://github.com/Unleash/unleash.git
synced 2025-05-03 01:18:43 +02:00
feat: add create and edit project screen (NEW) (#602)
* feat: add create and edit project screen * feat: fix correct permission and validate projectId * feat: remove unused variable and logs * feat: remove unused import * fix: delete unused project components * fix: add unique validation * fix: add unused import * fix: project header Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
416d67da34
commit
39b5adb950
@ -73,7 +73,6 @@ const FormTemplate: React.FC<ICreateProps> = ({
|
|||||||
<h3 className={styles.subtitle}>
|
<h3 className={styles.subtitle}>
|
||||||
API Command{' '}
|
API Command{' '}
|
||||||
<IconButton
|
<IconButton
|
||||||
className={styles.iconButton}
|
|
||||||
onClick={copyCommand}
|
onClick={copyCommand}
|
||||||
>
|
>
|
||||||
<FileCopy className={styles.icon} />
|
<FileCopy className={styles.icon} />
|
||||||
|
@ -11,6 +11,15 @@ Array [
|
|||||||
"title": "Create",
|
"title": "Create",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
Object {
|
||||||
|
"component": [Function],
|
||||||
|
"layout": "main",
|
||||||
|
"menu": Object {},
|
||||||
|
"parent": "/projects",
|
||||||
|
"path": "/projects/:id/edit",
|
||||||
|
"title": ":id",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
|
@ -13,7 +13,6 @@ import ApplicationView from '../../page/applications/view';
|
|||||||
import ContextFields from '../../page/context';
|
import ContextFields from '../../page/context';
|
||||||
import CreateContextField from '../../page/context/create';
|
import CreateContextField from '../../page/context/create';
|
||||||
import EditContextField from '../../page/context/edit';
|
import EditContextField from '../../page/context/edit';
|
||||||
import CreateProject from '../../page/project/create';
|
|
||||||
import ListTagTypes from '../../page/tag-types';
|
import ListTagTypes from '../../page/tag-types';
|
||||||
import Addons from '../../page/addons';
|
import Addons from '../../page/addons';
|
||||||
import AddonsCreate from '../../page/addons/create';
|
import AddonsCreate from '../../page/addons/create';
|
||||||
@ -46,6 +45,8 @@ import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
|||||||
|
|
||||||
import EditTagType from '../tagTypes/EditTagType/EditTagType';
|
import EditTagType from '../tagTypes/EditTagType/EditTagType';
|
||||||
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
|
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
|
||||||
|
import EditProject from '../project/Project/EditProject/EditProject';
|
||||||
|
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -59,6 +60,15 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: {},
|
menu: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/projects/:id/edit',
|
||||||
|
parent: '/projects',
|
||||||
|
title: ':id',
|
||||||
|
component: EditProject,
|
||||||
|
type: 'protected',
|
||||||
|
layout: 'main',
|
||||||
|
menu: {},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/projects/:id/archived',
|
path: '/projects/:id/archived',
|
||||||
title: ':name',
|
title: ':name',
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
.header {
|
|
||||||
padding: var(--card-header-padding);
|
|
||||||
margin-bottom: var(--card-margin-y);
|
|
||||||
word-break: break-all;
|
|
||||||
border-bottom: var(--default-border);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header h1 {
|
|
||||||
font-size: var(--h1-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
.supporting {
|
|
||||||
font-size: var(--caption-size);
|
|
||||||
max-width: 450px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
padding: var(--card-padding);
|
|
||||||
}
|
|
||||||
|
|
||||||
.formButtons {
|
|
||||||
padding-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContainer {
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
max-width: 350px;
|
|
||||||
}
|
|
@ -0,0 +1,94 @@
|
|||||||
|
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||||
|
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import ProjectForm from '../ProjectForm/ProjectForm';
|
||||||
|
import useProjectForm from '../hooks/useProjectForm';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
import useUser from '../../../../hooks/api/getters/useUser/useUser';
|
||||||
|
|
||||||
|
const CreateProject = () => {
|
||||||
|
/* @ts-ignore */
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { refetch } = useUser();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectDesc,
|
||||||
|
setProjectId,
|
||||||
|
setProjectName,
|
||||||
|
setProjectDesc,
|
||||||
|
getProjectPayload,
|
||||||
|
clearErrors,
|
||||||
|
validateIdUniqueness,
|
||||||
|
validateName,
|
||||||
|
errors,
|
||||||
|
} = useProjectForm();
|
||||||
|
|
||||||
|
const { createProject, loading } = useProjectApi();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrors();
|
||||||
|
const validName = validateName();
|
||||||
|
const validId = await validateIdUniqueness();
|
||||||
|
if (validName && validId) {
|
||||||
|
const payload = getProjectPayload();
|
||||||
|
try {
|
||||||
|
await createProject(payload);
|
||||||
|
refetch();
|
||||||
|
history.push(`/projects/${projectId}`);
|
||||||
|
setToastData({
|
||||||
|
title: 'Project created',
|
||||||
|
text: 'Now you can add toggles to this project',
|
||||||
|
confetti: true,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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(getProjectPayload(), undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.push('/projects');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Create project"
|
||||||
|
description="Projects allows you to group feature toggles together in the management UI."
|
||||||
|
documentationLink="https://docs.getunleash.io/user_guide/projects"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<ProjectForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
projectId={projectId}
|
||||||
|
setProjectId={setProjectId}
|
||||||
|
projectName={projectName}
|
||||||
|
setProjectName={setProjectName}
|
||||||
|
projectDesc={projectDesc}
|
||||||
|
setProjectDesc={setProjectDesc}
|
||||||
|
submitButtonText="Create"
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
validateIdUniqueness={validateIdUniqueness}
|
||||||
|
/>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateProject;
|
@ -0,0 +1,93 @@
|
|||||||
|
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
|
||||||
|
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import ProjectForm from '../ProjectForm/ProjectForm';
|
||||||
|
import useProjectForm from '../hooks/useProjectForm';
|
||||||
|
import useProject from '../../../../hooks/api/getters/useProject/useProject';
|
||||||
|
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useToast from '../../../../hooks/useToast';
|
||||||
|
|
||||||
|
const EditProject = () => {
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const { project } = useProject(id);
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectDesc,
|
||||||
|
setProjectId,
|
||||||
|
setProjectName,
|
||||||
|
setProjectDesc,
|
||||||
|
getProjectPayload,
|
||||||
|
clearErrors,
|
||||||
|
validateIdUniqueness,
|
||||||
|
validateName,
|
||||||
|
errors,
|
||||||
|
} = useProjectForm(id, project.name, project.description);
|
||||||
|
|
||||||
|
const formatApiCode = () => {
|
||||||
|
return `curl --location --request PUT '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/projects/${id}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const { refetch } = useProject(id);
|
||||||
|
const { editProject, loading } = useProjectApi();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const payload = getProjectPayload();
|
||||||
|
|
||||||
|
const validName = validateName();
|
||||||
|
|
||||||
|
if (validName) {
|
||||||
|
try {
|
||||||
|
await editProject(id, payload);
|
||||||
|
refetch();
|
||||||
|
history.push(`/projects/${id}`);
|
||||||
|
setToastData({
|
||||||
|
title: 'Project information updated',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Edit project"
|
||||||
|
description="Projects allows you to group feature toggles together in the management UI."
|
||||||
|
documentationLink="https://docs.getunleash.io/user_guide/projects"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<ProjectForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
projectId={projectId}
|
||||||
|
setProjectId={setProjectId}
|
||||||
|
projectName={projectName}
|
||||||
|
setProjectName={setProjectName}
|
||||||
|
projectDesc={projectDesc}
|
||||||
|
setProjectDesc={setProjectDesc}
|
||||||
|
submitButtonText="Edit"
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
validateIdUniqueness={validateIdUniqueness}
|
||||||
|
/>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditProject;
|
@ -13,7 +13,6 @@ import { useEffect } from 'react';
|
|||||||
import useTabs from '../../../hooks/useTabs';
|
import useTabs from '../../../hooks/useTabs';
|
||||||
import TabPanel from '../../common/TabNav/TabPanel';
|
import TabPanel from '../../common/TabNav/TabPanel';
|
||||||
import ProjectAccess from '../access-container';
|
import ProjectAccess from '../access-container';
|
||||||
import EditProject from '../edit-project-container';
|
|
||||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
||||||
import ProjectOverview from './ProjectOverview';
|
import ProjectOverview from './ProjectOverview';
|
||||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
||||||
@ -58,19 +57,6 @@ const Project = () => {
|
|||||||
path: `${basePath}/environments`,
|
path: `${basePath}/environments`,
|
||||||
name: 'environments',
|
name: 'environments',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Settings',
|
|
||||||
// @ts-ignore (fix later)
|
|
||||||
component: (
|
|
||||||
<EditProject
|
|
||||||
projectId={id}
|
|
||||||
history={history}
|
|
||||||
title="Edit project"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
path: `${basePath}/settings`,
|
|
||||||
name: 'settings',
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -101,15 +87,6 @@ const Project = () => {
|
|||||||
/* eslint-disable-next-line */
|
/* eslint-disable-next-line */
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const goToTabWithName = (name: string) => {
|
|
||||||
const index = tabData.findIndex(t => t.name === name);
|
|
||||||
if (index >= 0) {
|
|
||||||
const tab = tabData[index];
|
|
||||||
history.push(tab.path);
|
|
||||||
setActiveTab(index);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderTabs = () => {
|
const renderTabs = () => {
|
||||||
return tabData.map((tab, index) => {
|
return tabData.map((tab, index) => {
|
||||||
return (
|
return (
|
||||||
@ -150,9 +127,9 @@ const Project = () => {
|
|||||||
Project: {project?.name}{' '}
|
Project: {project?.name}{' '}
|
||||||
<PermissionIconButton
|
<PermissionIconButton
|
||||||
permission={UPDATE_PROJECT}
|
permission={UPDATE_PROJECT}
|
||||||
tooltip={'Edit description'}
|
tooltip="Edit"
|
||||||
projectId={project?.id}
|
projectId={project?.id}
|
||||||
onClick={() => goToTabWithName('settings')}
|
onClick={() => history.push(`/projects/${id}/edit`)}
|
||||||
data-loading
|
data-loading
|
||||||
>
|
>
|
||||||
<Edit />
|
<Edit />
|
||||||
|
@ -0,0 +1,47 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
form: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
container: {
|
||||||
|
maxWidth: '400px',
|
||||||
|
},
|
||||||
|
input: { width: '100%', marginBottom: '1rem' },
|
||||||
|
label: {
|
||||||
|
minWidth: '300px',
|
||||||
|
[theme.breakpoints.down(600)]: {
|
||||||
|
minWidth: 'auto',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
buttonContainer: {
|
||||||
|
marginTop: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
},
|
||||||
|
cancelButton: {
|
||||||
|
marginRight: '1.5rem',
|
||||||
|
},
|
||||||
|
inputDescription: {
|
||||||
|
marginBottom: '0.5rem',
|
||||||
|
},
|
||||||
|
formHeader: {
|
||||||
|
fontWeight: 'normal',
|
||||||
|
marginTop: '0',
|
||||||
|
},
|
||||||
|
header: {
|
||||||
|
fontWeight: 'normal',
|
||||||
|
},
|
||||||
|
permissionErrorContainer: {
|
||||||
|
position: 'relative',
|
||||||
|
},
|
||||||
|
errorMessage: {
|
||||||
|
//@ts-ignore
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
color: theme.palette.error.main,
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-8px',
|
||||||
|
},
|
||||||
|
}));
|
@ -0,0 +1,103 @@
|
|||||||
|
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
|
||||||
|
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
|
||||||
|
import Input from '../../../common/Input/Input';
|
||||||
|
import { TextField, Button } from '@material-ui/core';
|
||||||
|
import { useStyles } from './ProjectForm.style';
|
||||||
|
import React from 'react';
|
||||||
|
import { trim } from '../../../common/util';
|
||||||
|
|
||||||
|
interface IProjectForm {
|
||||||
|
projectId: string;
|
||||||
|
projectName: string;
|
||||||
|
projectDesc: string;
|
||||||
|
setProjectId: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setProjectName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
handleSubmit: (e: any) => void;
|
||||||
|
handleCancel: () => void;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
submitButtonText: string;
|
||||||
|
clearErrors: () => void;
|
||||||
|
validateIdUniqueness: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProjectForm = ({
|
||||||
|
handleSubmit,
|
||||||
|
handleCancel,
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectDesc,
|
||||||
|
setProjectId,
|
||||||
|
setProjectName,
|
||||||
|
setProjectDesc,
|
||||||
|
errors,
|
||||||
|
submitButtonText,
|
||||||
|
validateIdUniqueness,
|
||||||
|
clearErrors,
|
||||||
|
}: IProjectForm) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
|
<h3 className={styles.formHeader}>Project Information</h3>
|
||||||
|
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
|
What is your project Id?
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
label="Project Id"
|
||||||
|
value={projectId}
|
||||||
|
onChange={e => setProjectId(trim(e.target.value))}
|
||||||
|
error={Boolean(errors.id)}
|
||||||
|
errorText={errors.id}
|
||||||
|
onFocus={() => clearErrors()}
|
||||||
|
onBlur={validateIdUniqueness}
|
||||||
|
disabled={submitButtonText === 'Edit'}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
|
What is your project name?
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
label="Project name"
|
||||||
|
value={projectName}
|
||||||
|
onChange={e => setProjectName(e.target.value)}
|
||||||
|
error={Boolean(errors.name)}
|
||||||
|
errorText={errors.name}
|
||||||
|
onFocus={() => clearErrors()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
|
What is your project description?
|
||||||
|
</p>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
label="Project description"
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
maxRows={4}
|
||||||
|
value={projectDesc}
|
||||||
|
onChange={e => setProjectDesc(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button onClick={handleCancel} className={styles.cancelButton}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
permission={CREATE_PROJECT}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
{submitButtonText} project
|
||||||
|
</PermissionButton>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ProjectForm;
|
@ -55,7 +55,7 @@ const ProjectInfo = ({
|
|||||||
component={Link}
|
component={Link}
|
||||||
className={permissionButtonClass}
|
className={permissionButtonClass}
|
||||||
data-loading
|
data-loading
|
||||||
to={`/projects/${id}/settings`}
|
to={`/projects/${id}/edit`}
|
||||||
>
|
>
|
||||||
<Edit />
|
<Edit />
|
||||||
</PermissionIconButton>
|
</PermissionIconButton>
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
|
import { IPermission } from '../../../../interfaces/project';
|
||||||
|
|
||||||
|
export interface ICheckedPermission {
|
||||||
|
[key: string]: IPermission;
|
||||||
|
}
|
||||||
|
|
||||||
|
const useProjectForm = (
|
||||||
|
initialProjectId = '',
|
||||||
|
initialProjectName = '',
|
||||||
|
initialProjectDesc = ''
|
||||||
|
) => {
|
||||||
|
const [projectId, setProjectId] = useState(initialProjectId);
|
||||||
|
const [projectName, setProjectName] = useState(initialProjectName);
|
||||||
|
const [projectDesc, setProjectDesc] = useState(initialProjectDesc);
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
const { validateId } = useProjectApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProjectId(initialProjectId);
|
||||||
|
}, [initialProjectId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProjectName(initialProjectName);
|
||||||
|
}, [initialProjectName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setProjectDesc(initialProjectDesc);
|
||||||
|
}, [initialProjectDesc]);
|
||||||
|
|
||||||
|
const getProjectPayload = () => {
|
||||||
|
return {
|
||||||
|
id: projectId,
|
||||||
|
name: projectName,
|
||||||
|
description: projectDesc,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const NAME_EXISTS_ERROR = 'Error: A project with this id already exists.';
|
||||||
|
|
||||||
|
const validateIdUniqueness = async () => {
|
||||||
|
try {
|
||||||
|
await validateId(getProjectPayload());
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
id: 'A project with this id already exists',
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateName = () => {
|
||||||
|
if (projectName.length === 0) {
|
||||||
|
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
projectId,
|
||||||
|
projectName,
|
||||||
|
projectDesc,
|
||||||
|
setProjectId,
|
||||||
|
setProjectName,
|
||||||
|
setProjectDesc,
|
||||||
|
getProjectPayload,
|
||||||
|
validateName,
|
||||||
|
validateIdUniqueness,
|
||||||
|
clearErrors,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useProjectForm;
|
@ -1,21 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import ProjectComponent from './form-project-component';
|
|
||||||
import { createProject, validateId } from './../../store/project/actions';
|
|
||||||
import { fetchUser } from './../../store/user/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = () => ({
|
|
||||||
project: { id: '', name: '', description: '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
validateId,
|
|
||||||
submit: async project => {
|
|
||||||
await createProject(project)(dispatch);
|
|
||||||
fetchUser()(dispatch);
|
|
||||||
},
|
|
||||||
editMode: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectComponent);
|
|
||||||
|
|
||||||
export default FormAddContainer;
|
|
@ -1,23 +0,0 @@
|
|||||||
import { connect } from 'react-redux';
|
|
||||||
import Component from './form-project-component';
|
|
||||||
import { updateProject, validateId } from './../../store/project/actions';
|
|
||||||
|
|
||||||
const mapStateToProps = (state, props) => {
|
|
||||||
const projectBase = { id: '', name: '', description: '' };
|
|
||||||
const realProject = state.projects.toJS().find(n => n.id === props.projectId);
|
|
||||||
const project = Object.assign(projectBase, realProject);
|
|
||||||
|
|
||||||
return {
|
|
||||||
project,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapDispatchToProps = dispatch => ({
|
|
||||||
validateId,
|
|
||||||
submit: project => updateProject(project)(dispatch),
|
|
||||||
editMode: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
|
|
||||||
|
|
||||||
export default FormAddContainer;
|
|
@ -1,239 +0,0 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { TextField, Typography } from '@material-ui/core';
|
|
||||||
|
|
||||||
import styles from './Project.module.scss';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { FormButtons, styles as commonStyles } from '../common';
|
|
||||||
import { trim } from '../common/util';
|
|
||||||
import PageContent from '../common/PageContent/PageContent';
|
|
||||||
import AccessContext from '../../contexts/AccessContext';
|
|
||||||
import ConditionallyRender from '../common/ConditionallyRender';
|
|
||||||
import { CREATE_PROJECT } from '../providers/AccessProvider/permissions';
|
|
||||||
import HeaderTitle from '../common/HeaderTitle';
|
|
||||||
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
|
|
||||||
import { Alert } from '@material-ui/lab';
|
|
||||||
import { FormEvent } from 'react-router/node_modules/@types/react';
|
|
||||||
import useLoading from '../../hooks/useLoading';
|
|
||||||
import PermissionButton from '../common/PermissionButton/PermissionButton';
|
|
||||||
import { UPDATE_PROJECT } from '../../store/project/actions';
|
|
||||||
import useUser from '../../hooks/api/getters/useUser/useUser';
|
|
||||||
|
|
||||||
interface ProjectFormComponentProps {
|
|
||||||
editMode: boolean;
|
|
||||||
project: any;
|
|
||||||
validateId: (id: string) => Promise<void>;
|
|
||||||
history: any;
|
|
||||||
submit: (project: any) => Promise<void>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ProjectFormComponent = (props: ProjectFormComponentProps) => {
|
|
||||||
const { editMode } = props;
|
|
||||||
const { refetch } = useUser();
|
|
||||||
const { hasAccess } = useContext(AccessContext);
|
|
||||||
const [project, setProject] = useState(props.project || {});
|
|
||||||
const [errors, setErrors] = useState<any>({});
|
|
||||||
const { isOss, loading } = useUiConfig();
|
|
||||||
const ref = useLoading(loading);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!project.id && props.project.id) {
|
|
||||||
setProject(props.project);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [props.project]);
|
|
||||||
|
|
||||||
const setValue = (field: string, value: string) => {
|
|
||||||
const p = { ...project };
|
|
||||||
p[field] = value;
|
|
||||||
setProject(p);
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateId = async (id: string) => {
|
|
||||||
if (editMode) return true;
|
|
||||||
|
|
||||||
const e = { ...errors };
|
|
||||||
try {
|
|
||||||
await props.validateId(id);
|
|
||||||
e.id = undefined;
|
|
||||||
} catch (err: any) {
|
|
||||||
e.id = err.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
setErrors(e);
|
|
||||||
if (e.id) return false;
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validateName = () => {
|
|
||||||
if (project.name.length === 0) {
|
|
||||||
setErrors({ ...errors, name: 'Name can not be empty.' });
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const validate = async (id: string) => {
|
|
||||||
const validId = await validateId(id);
|
|
||||||
const validName = validateName();
|
|
||||||
|
|
||||||
return validId && validName;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onCancel = (evt: Event) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
if (editMode) {
|
|
||||||
props.history.push(`/projects/${project.id}`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.history.push(`/projects/`);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onSubmit = async (evt: FormEvent<HTMLFormElement>) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
|
|
||||||
const valid = await validate(project.id);
|
|
||||||
|
|
||||||
if (valid) {
|
|
||||||
const query = editMode ? 'edited=true' : 'created=true';
|
|
||||||
await props.submit(project);
|
|
||||||
refetch();
|
|
||||||
props.history.push(`/projects/${project.id}?${query}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitText = editMode ? 'Update' : 'Create';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div ref={ref}>
|
|
||||||
<PageContent
|
|
||||||
headerContent={
|
|
||||||
<HeaderTitle
|
|
||||||
title={`${submitText} ${props.project?.name} project`}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={isOss()}
|
|
||||||
show={
|
|
||||||
<Alert data-loading severity="error">
|
|
||||||
{submitText} project requires a paid version of
|
|
||||||
Unleash. Check out{' '}
|
|
||||||
<a
|
|
||||||
href="https://www.getunleash.io"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
getunleash.io
|
|
||||||
</a>{' '}
|
|
||||||
to learn more.
|
|
||||||
</Alert>
|
|
||||||
}
|
|
||||||
elseShow={
|
|
||||||
<>
|
|
||||||
<Typography
|
|
||||||
variant="subtitle1"
|
|
||||||
style={{ marginBottom: '0.5rem' }}
|
|
||||||
>
|
|
||||||
Projects allows you to group feature toggles
|
|
||||||
together in the management UI.
|
|
||||||
</Typography>
|
|
||||||
<form
|
|
||||||
data-loading
|
|
||||||
onSubmit={onSubmit}
|
|
||||||
className={classnames(
|
|
||||||
commonStyles.contentSpacing,
|
|
||||||
styles.formContainer
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TextField
|
|
||||||
label="Project Id"
|
|
||||||
name="id"
|
|
||||||
placeholder="A-unique-key"
|
|
||||||
value={project.id}
|
|
||||||
error={!!errors.id}
|
|
||||||
helperText={errors.id}
|
|
||||||
disabled={editMode}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
onBlur={v => validateId(v.target.value)}
|
|
||||||
onChange={v =>
|
|
||||||
setValue('id', trim(v.target.value))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<TextField
|
|
||||||
label="Name"
|
|
||||||
name="name"
|
|
||||||
placeholder="Project name"
|
|
||||||
value={project.name}
|
|
||||||
error={!!errors.name}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
helperText={errors.name}
|
|
||||||
onChange={v =>
|
|
||||||
setValue('name', v.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<TextField
|
|
||||||
className={commonStyles.fullwidth}
|
|
||||||
placeholder="A short description"
|
|
||||||
maxRows={2}
|
|
||||||
label="Description"
|
|
||||||
error={!!errors.description}
|
|
||||||
helperText={errors.description}
|
|
||||||
variant="outlined"
|
|
||||||
size="small"
|
|
||||||
multiline
|
|
||||||
value={project.description}
|
|
||||||
onChange={v =>
|
|
||||||
setValue('description', v.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={
|
|
||||||
hasAccess(CREATE_PROJECT) && !editMode
|
|
||||||
}
|
|
||||||
show={
|
|
||||||
<div className={styles.formButtons}>
|
|
||||||
<FormButtons
|
|
||||||
submitText={submitText}
|
|
||||||
onCancel={onCancel}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<ConditionallyRender
|
|
||||||
condition={editMode}
|
|
||||||
show={
|
|
||||||
<PermissionButton
|
|
||||||
permission={UPDATE_PROJECT}
|
|
||||||
projectId={props.project.id}
|
|
||||||
type="submit"
|
|
||||||
style={{ marginTop: '1rem' }}
|
|
||||||
>
|
|
||||||
Update project
|
|
||||||
</PermissionButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageContent>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ProjectFormComponent.propTypes = {
|
|
||||||
project: PropTypes.object.isRequired,
|
|
||||||
validateId: PropTypes.func.isRequired,
|
|
||||||
submit: PropTypes.func.isRequired,
|
|
||||||
history: PropTypes.object.isRequired,
|
|
||||||
editMode: PropTypes.bool.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProjectFormComponent;
|
|
@ -1,10 +1,64 @@
|
|||||||
import useAPI from '../useApi/useApi';
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
interface ICreatePayload {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
const useProjectApi = () => {
|
const useProjectApi = () => {
|
||||||
const { makeRequest, createRequest, errors } = useAPI({
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
propagateErrors: true,
|
propagateErrors: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const createProject = async (payload: ICreatePayload) => {
|
||||||
|
const path = `api/admin/projects`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateId = async (payload: ICreatePayload) => {
|
||||||
|
const path = `api/admin/projects/validate`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const editProject = async (id: string, payload: ICreatePayload) => {
|
||||||
|
const path = `api/admin/projects/${id}`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const deleteProject = async (projectId: string) => {
|
const deleteProject = async (projectId: string) => {
|
||||||
const path = `api/admin/projects/${projectId}`;
|
const path = `api/admin/projects/${projectId}`;
|
||||||
const req = createRequest(path, { method: 'DELETE' });
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
@ -18,7 +72,10 @@ const useProjectApi = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addEnvironmentToProject = async (projectId: string, environment: string) => {
|
const addEnvironmentToProject = async (
|
||||||
|
projectId: string,
|
||||||
|
environment: string
|
||||||
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/environments`;
|
const path = `api/admin/projects/${projectId}/environments`;
|
||||||
const req = createRequest(path, {
|
const req = createRequest(path, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -32,9 +89,12 @@ const useProjectApi = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
const removeEnvironmentFromProject = async (projectId: string, environment: string) => {
|
const removeEnvironmentFromProject = async (
|
||||||
|
projectId: string,
|
||||||
|
environment: string
|
||||||
|
) => {
|
||||||
const path = `api/admin/projects/${projectId}/environments/${environment}`;
|
const path = `api/admin/projects/${projectId}/environments/${environment}`;
|
||||||
const req = createRequest(path, { method: 'DELETE' });
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
@ -45,9 +105,18 @@ const useProjectApi = () => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
return { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, errors };
|
return {
|
||||||
|
createProject,
|
||||||
|
validateId,
|
||||||
|
editProject,
|
||||||
|
deleteProject,
|
||||||
|
addEnvironmentToProject,
|
||||||
|
removeEnvironmentFromProject,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export default useProjectApi;
|
export default useProjectApi;
|
||||||
|
@ -15,6 +15,8 @@ interface IToastOptions {
|
|||||||
type: string;
|
type: string;
|
||||||
persist?: boolean;
|
persist?: boolean;
|
||||||
confetti?: boolean;
|
confetti?: boolean;
|
||||||
|
autoHideDuration?: number;
|
||||||
|
show?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const useToast = () => {
|
const useToast = () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user