mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-04 00:18:01 +01: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}>
|
||||
API Command{' '}
|
||||
<IconButton
|
||||
className={styles.iconButton}
|
||||
onClick={copyCommand}
|
||||
>
|
||||
<FileCopy className={styles.icon} />
|
||||
|
@ -11,6 +11,15 @@ Array [
|
||||
"title": "Create",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
"menu": Object {},
|
||||
"parent": "/projects",
|
||||
"path": "/projects/:id/edit",
|
||||
"title": ":id",
|
||||
"type": "protected",
|
||||
},
|
||||
Object {
|
||||
"component": [Function],
|
||||
"layout": "main",
|
||||
|
@ -13,7 +13,6 @@ import ApplicationView from '../../page/applications/view';
|
||||
import ContextFields from '../../page/context';
|
||||
import CreateContextField from '../../page/context/create';
|
||||
import EditContextField from '../../page/context/edit';
|
||||
import CreateProject from '../../page/project/create';
|
||||
import ListTagTypes from '../../page/tag-types';
|
||||
import Addons from '../../page/addons';
|
||||
import AddonsCreate from '../../page/addons/create';
|
||||
@ -46,6 +45,8 @@ import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
||||
|
||||
import EditTagType from '../tagTypes/EditTagType/EditTagType';
|
||||
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
|
||||
import EditProject from '../project/Project/EditProject/EditProject';
|
||||
import CreateProject from '../project/Project/CreateProject/CreateProject';
|
||||
|
||||
export const routes = [
|
||||
// Project
|
||||
@ -59,6 +60,15 @@ export const routes = [
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/edit',
|
||||
parent: '/projects',
|
||||
title: ':id',
|
||||
component: EditProject,
|
||||
type: 'protected',
|
||||
layout: 'main',
|
||||
menu: {},
|
||||
},
|
||||
{
|
||||
path: '/projects/:id/archived',
|
||||
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 TabPanel from '../../common/TabNav/TabPanel';
|
||||
import ProjectAccess from '../access-container';
|
||||
import EditProject from '../edit-project-container';
|
||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
|
||||
import ProjectOverview from './ProjectOverview';
|
||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
|
||||
@ -58,19 +57,6 @@ const Project = () => {
|
||||
path: `${basePath}/environments`,
|
||||
name: 'environments',
|
||||
},
|
||||
{
|
||||
title: 'Settings',
|
||||
// @ts-ignore (fix later)
|
||||
component: (
|
||||
<EditProject
|
||||
projectId={id}
|
||||
history={history}
|
||||
title="Edit project"
|
||||
/>
|
||||
),
|
||||
path: `${basePath}/settings`,
|
||||
name: 'settings',
|
||||
},
|
||||
];
|
||||
|
||||
useEffect(() => {
|
||||
@ -101,15 +87,6 @@ const Project = () => {
|
||||
/* 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 = () => {
|
||||
return tabData.map((tab, index) => {
|
||||
return (
|
||||
@ -150,9 +127,9 @@ const Project = () => {
|
||||
Project: {project?.name}{' '}
|
||||
<PermissionIconButton
|
||||
permission={UPDATE_PROJECT}
|
||||
tooltip={'Edit description'}
|
||||
tooltip="Edit"
|
||||
projectId={project?.id}
|
||||
onClick={() => goToTabWithName('settings')}
|
||||
onClick={() => history.push(`/projects/${id}/edit`)}
|
||||
data-loading
|
||||
>
|
||||
<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}
|
||||
className={permissionButtonClass}
|
||||
data-loading
|
||||
to={`/projects/${id}/settings`}
|
||||
to={`/projects/${id}/edit`}
|
||||
>
|
||||
<Edit />
|
||||
</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';
|
||||
|
||||
interface ICreatePayload {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const useProjectApi = () => {
|
||||
const { makeRequest, createRequest, errors } = useAPI({
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
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 path = `api/admin/projects/${projectId}`;
|
||||
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 req = createRequest(path, {
|
||||
method: 'POST',
|
||||
@ -32,9 +89,12 @@ const useProjectApi = () => {
|
||||
} catch (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 req = createRequest(path, { method: 'DELETE' });
|
||||
|
||||
@ -45,9 +105,18 @@ const useProjectApi = () => {
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, errors };
|
||||
return {
|
||||
createProject,
|
||||
validateId,
|
||||
editProject,
|
||||
deleteProject,
|
||||
addEnvironmentToProject,
|
||||
removeEnvironmentFromProject,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useProjectApi;
|
||||
|
@ -15,6 +15,8 @@ interface IToastOptions {
|
||||
type: string;
|
||||
persist?: boolean;
|
||||
confetti?: boolean;
|
||||
autoHideDuration?: number;
|
||||
show?: boolean;
|
||||
}
|
||||
|
||||
const useToast = () => {
|
||||
|
Loading…
Reference in New Issue
Block a user