mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-10 01:16:39 +02:00
Merge branch 'main' into refactor/create-token
This commit is contained in:
commit
05f395f638
@ -48,10 +48,10 @@
|
|||||||
"@types/enzyme": "3.10.11",
|
"@types/enzyme": "3.10.11",
|
||||||
"@types/enzyme-adapter-react-16": "1.0.6",
|
"@types/enzyme-adapter-react-16": "1.0.6",
|
||||||
"@types/jest": "27.4.0",
|
"@types/jest": "27.4.0",
|
||||||
"@types/node": "14.18.7",
|
"@types/node": "14.18.9",
|
||||||
"@types/react": "17.0.38",
|
"@types/react": "17.0.38",
|
||||||
"@types/react-dom": "17.0.11",
|
"@types/react-dom": "17.0.11",
|
||||||
"@types/react-router-dom": "5.3.2",
|
"@types/react-router-dom": "5.3.3",
|
||||||
"@types/react-timeago": "4.1.3",
|
"@types/react-timeago": "4.1.3",
|
||||||
"@welldone-software/why-did-you-render": "6.2.3",
|
"@welldone-software/why-did-you-render": "6.2.3",
|
||||||
"array-move": "3.0.1",
|
"array-move": "3.0.1",
|
||||||
@ -87,7 +87,7 @@
|
|||||||
"redux-devtools-extension": "2.13.9",
|
"redux-devtools-extension": "2.13.9",
|
||||||
"redux-mock-store": "1.5.4",
|
"redux-mock-store": "1.5.4",
|
||||||
"redux-thunk": "2.4.1",
|
"redux-thunk": "2.4.1",
|
||||||
"sass": "1.48.0",
|
"sass": "1.49.0",
|
||||||
"swr": "1.0.1",
|
"swr": "1.0.1",
|
||||||
"typescript": "4.5.4",
|
"typescript": "4.5.4",
|
||||||
"web-vitals": "2.1.3"
|
"web-vitals": "2.1.3"
|
||||||
|
@ -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",
|
||||||
@ -263,15 +272,6 @@ Array [
|
|||||||
"title": "Tag types",
|
"title": "Tag types",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
Object {
|
|
||||||
"component": [Function],
|
|
||||||
"layout": "main",
|
|
||||||
"menu": Object {},
|
|
||||||
"parent": "/tags",
|
|
||||||
"path": "/tags/create",
|
|
||||||
"title": "Create",
|
|
||||||
"type": "protected",
|
|
||||||
},
|
|
||||||
Object {
|
Object {
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"layout": "main",
|
"layout": "main",
|
||||||
|
@ -13,12 +13,7 @@ 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 CreateTagType from '../../page/tag-types/create';
|
|
||||||
import EditTagType from '../../page/tag-types/edit';
|
|
||||||
import ListTags from '../../page/tags';
|
|
||||||
import CreateTag from '../../page/tags/create';
|
|
||||||
import Addons from '../../page/addons';
|
import Addons from '../../page/addons';
|
||||||
import AddonsCreate from '../../page/addons/create';
|
import AddonsCreate from '../../page/addons/create';
|
||||||
import AddonsEdit from '../../page/addons/edit';
|
import AddonsEdit from '../../page/addons/edit';
|
||||||
@ -48,6 +43,10 @@ import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken';
|
|||||||
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment';
|
||||||
import EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
|
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 = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -61,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',
|
||||||
@ -304,24 +312,6 @@ export const routes = [
|
|||||||
layout: 'main',
|
layout: 'main',
|
||||||
menu: { mobile: true, advanced: true },
|
menu: { mobile: true, advanced: true },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/tags/create',
|
|
||||||
parent: '/tags',
|
|
||||||
title: 'Create',
|
|
||||||
component: CreateTag,
|
|
||||||
type: 'protected',
|
|
||||||
layout: 'main',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/tags',
|
|
||||||
title: 'Tags',
|
|
||||||
component: ListTags,
|
|
||||||
hidden: true,
|
|
||||||
type: 'protected',
|
|
||||||
layout: 'main',
|
|
||||||
menu: {},
|
|
||||||
},
|
|
||||||
|
|
||||||
// Addons
|
// Addons
|
||||||
{
|
{
|
||||||
|
@ -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;
|
|
@ -13,7 +13,6 @@ export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
|
|||||||
export const CREATE_PROJECT = 'CREATE_PROJECT';
|
export const CREATE_PROJECT = 'CREATE_PROJECT';
|
||||||
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
export const UPDATE_PROJECT = 'UPDATE_PROJECT';
|
||||||
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
export const DELETE_PROJECT = 'DELETE_PROJECT';
|
||||||
export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE';
|
|
||||||
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE';
|
||||||
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
|
||||||
export const CREATE_TAG = 'CREATE_TAG';
|
export const CREATE_TAG = 'CREATE_TAG';
|
||||||
|
@ -2,6 +2,10 @@
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
fill: #757575;
|
||||||
|
}
|
||||||
|
|
||||||
.textfield {
|
.textfield {
|
||||||
margin-left: 15px;
|
margin-left: 15px;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { useContext, useEffect, useState } from 'react';
|
import { useContext, useState } from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link, useHistory } from 'react-router-dom';
|
import { Link, useHistory } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
@ -11,38 +10,53 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
import { Add, Delete, Label } from '@material-ui/icons';
|
import { Add, Delete, Edit, Label } from '@material-ui/icons';
|
||||||
|
|
||||||
import HeaderTitle from '../../common/HeaderTitle';
|
import HeaderTitle from '../../common/HeaderTitle';
|
||||||
import PageContent from '../../common/PageContent/PageContent';
|
import PageContent from '../../common/PageContent/PageContent';
|
||||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
|
||||||
import {
|
import {
|
||||||
CREATE_TAG_TYPE,
|
|
||||||
DELETE_TAG_TYPE,
|
DELETE_TAG_TYPE,
|
||||||
|
UPDATE_TAG_TYPE,
|
||||||
} from '../../providers/AccessProvider/permissions';
|
} from '../../providers/AccessProvider/permissions';
|
||||||
import Dialogue from '../../common/Dialogue/Dialogue';
|
import Dialogue from '../../common/Dialogue/Dialogue';
|
||||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
import useMediaQuery from '@material-ui/core/useMediaQuery';
|
||||||
|
|
||||||
import styles from '../TagType.module.scss';
|
import styles from '../TagType.module.scss';
|
||||||
import AccessContext from '../../../contexts/AccessContext';
|
import AccessContext from '../../../contexts/AccessContext';
|
||||||
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
|
import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton';
|
||||||
|
|
||||||
const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
const TagTypeList = () => {
|
||||||
const { hasAccess } = useContext(AccessContext);
|
const { hasAccess } = useContext(AccessContext);
|
||||||
const [deletion, setDeletion] = useState({ open: false });
|
const [deletion, setDeletion] = useState({ open: false });
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const smallScreen = useMediaQuery('(max-width:700px)');
|
const smallScreen = useMediaQuery('(max-width:700px)');
|
||||||
|
const { deleteTagType } = useTagTypesApi();
|
||||||
|
const { tagTypes, refetch } = useTagTypes();
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
|
||||||
useEffect(() => {
|
const deleteTag = async () => {
|
||||||
fetchTagTypes();
|
try {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
await deleteTagType(deletion.name);
|
||||||
}, []);
|
refetch();
|
||||||
|
setDeletion({ open: false });
|
||||||
|
setToastData({
|
||||||
|
type: 'success',
|
||||||
|
show: true,
|
||||||
|
text: 'Successfully deleted tag type.',
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let header = (
|
let header = (
|
||||||
<HeaderTitle
|
<HeaderTitle
|
||||||
title="Tag Types"
|
title="Tag Types"
|
||||||
actions={
|
actions={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(CREATE_TAG_TYPE)}
|
condition={hasAccess(UPDATE_TAG_TYPE)}
|
||||||
show={
|
show={
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={smallScreen}
|
condition={smallScreen}
|
||||||
@ -96,6 +110,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
key={`${tagType.name}`}
|
key={`${tagType.name}`}
|
||||||
@ -105,6 +120,13 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
|||||||
<Label />
|
<Label />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={link} secondary={tagType.description} />
|
<ListItemText primary={link} secondary={tagType.description} />
|
||||||
|
<PermissionIconButton
|
||||||
|
permission={UPDATE_TAG_TYPE}
|
||||||
|
component={Link}
|
||||||
|
to={`/tag-types/edit/${tagType.name}`}
|
||||||
|
>
|
||||||
|
<Edit className={styles.icon} />
|
||||||
|
</PermissionIconButton>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(DELETE_TAG_TYPE)}
|
condition={hasAccess(DELETE_TAG_TYPE)}
|
||||||
show={deleteButton}
|
show={deleteButton}
|
||||||
@ -124,10 +146,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
|
|||||||
<Dialogue
|
<Dialogue
|
||||||
title="Really delete Tag type?"
|
title="Really delete Tag type?"
|
||||||
open={deletion.open}
|
open={deletion.open}
|
||||||
onClick={() => {
|
onClick={deleteTag}
|
||||||
removeTagType(deletion.name);
|
|
||||||
setDeletion({ open: false });
|
|
||||||
}}
|
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setDeletion({ open: false });
|
setDeletion({ open: false });
|
||||||
}}
|
}}
|
||||||
|
@ -38,44 +38,10 @@ exports[`renders a list with elements correctly 1`] = `
|
|||||||
className="MuiList-root MuiList-padding"
|
className="MuiList-root MuiList-padding"
|
||||||
>
|
>
|
||||||
<li
|
<li
|
||||||
className="MuiListItem-root tagListItem MuiListItem-gutters"
|
className="MuiListItem-root MuiListItem-gutters"
|
||||||
disabled={false}
|
disabled={false}
|
||||||
>
|
>
|
||||||
<div
|
No entries
|
||||||
className="MuiListItemIcon-root"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
aria-hidden={true}
|
|
||||||
className="MuiSvgIcon-root"
|
|
||||||
focusable="false"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className="MuiListItemText-root MuiListItemText-multiline"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/tag-types/edit/simple"
|
|
||||||
onClick={[Function]}
|
|
||||||
>
|
|
||||||
<strong>
|
|
||||||
simple
|
|
||||||
</strong>
|
|
||||||
</a>
|
|
||||||
</span>
|
|
||||||
<p
|
|
||||||
className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock"
|
|
||||||
>
|
|
||||||
Some simple description
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,21 +14,24 @@ import {
|
|||||||
UPDATE_TAG_TYPE,
|
UPDATE_TAG_TYPE,
|
||||||
DELETE_TAG_TYPE,
|
DELETE_TAG_TYPE,
|
||||||
} from '../../providers/AccessProvider/permissions';
|
} from '../../providers/AccessProvider/permissions';
|
||||||
|
import UIProvider from '../../providers/UIProvider/UIProvider';
|
||||||
|
|
||||||
test('renders an empty list correctly', () => {
|
test('renders an empty list correctly', () => {
|
||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<AccessProvider
|
<UIProvider>
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
<AccessProvider
|
||||||
>
|
store={createFakeStore([{ permission: ADMIN }])}
|
||||||
<TagTypesList
|
>
|
||||||
tagTypes={[]}
|
<TagTypesList
|
||||||
fetchTagTypes={jest.fn()}
|
tagTypes={[]}
|
||||||
removeTagType={jest.fn()}
|
fetchTagTypes={jest.fn()}
|
||||||
history={{}}
|
removeTagType={jest.fn()}
|
||||||
/>
|
history={{}}
|
||||||
</AccessProvider>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
|
</UIProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@ -39,26 +42,28 @@ test('renders a list with elements correctly', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
<AccessProvider
|
<UIProvider>
|
||||||
store={createFakeStore([
|
<AccessProvider
|
||||||
{ permission: CREATE_TAG_TYPE },
|
store={createFakeStore([
|
||||||
{ permission: UPDATE_TAG_TYPE },
|
{ permission: CREATE_TAG_TYPE },
|
||||||
{ permission: DELETE_TAG_TYPE },
|
{ permission: UPDATE_TAG_TYPE },
|
||||||
])}
|
{ permission: DELETE_TAG_TYPE },
|
||||||
>
|
])}
|
||||||
<TagTypesList
|
>
|
||||||
tagTypes={[
|
<TagTypesList
|
||||||
{
|
tagTypes={[
|
||||||
name: 'simple',
|
{
|
||||||
description: 'Some simple description',
|
name: 'simple',
|
||||||
icon: '#',
|
description: 'Some simple description',
|
||||||
},
|
icon: '#',
|
||||||
]}
|
},
|
||||||
fetchTagTypes={jest.fn()}
|
]}
|
||||||
removeTagType={jest.fn()}
|
fetchTagTypes={jest.fn()}
|
||||||
history={{}}
|
removeTagType={jest.fn()}
|
||||||
/>
|
history={{}}
|
||||||
</AccessProvider>
|
/>
|
||||||
|
</AccessProvider>
|
||||||
|
</UIProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
@ -0,0 +1,91 @@
|
|||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
|
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
||||||
|
import useTagForm from '../hooks/useTagForm';
|
||||||
|
import TagTypeForm from '../TagTypeForm/TagTypeForm';
|
||||||
|
|
||||||
|
const CreateTagType = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const history = useHistory();
|
||||||
|
const {
|
||||||
|
tagName,
|
||||||
|
tagDesc,
|
||||||
|
setTagName,
|
||||||
|
setTagDesc,
|
||||||
|
getTagPayload,
|
||||||
|
validateNameUniqueness,
|
||||||
|
errors,
|
||||||
|
clearErrors,
|
||||||
|
} = useTagForm();
|
||||||
|
const { createTag, loading } = useTagTypesApi();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrors();
|
||||||
|
const validName = await validateNameUniqueness();
|
||||||
|
if (validName) {
|
||||||
|
const payload = getTagPayload();
|
||||||
|
try {
|
||||||
|
await createTag(payload);
|
||||||
|
history.push('/tag-types');
|
||||||
|
setToastData({
|
||||||
|
title: 'Tag type created',
|
||||||
|
confetti: true,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatApiCode = () => {
|
||||||
|
return `curl --location --request POST '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/tag-types' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Create tag type"
|
||||||
|
description="Tag types allow you to group tags together in the management UI"
|
||||||
|
documentationLink="https://docs.getunleash.io/advanced/tags"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<TagTypeForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
tagName={tagName}
|
||||||
|
setTagName={setTagName}
|
||||||
|
tagDesc={tagDesc}
|
||||||
|
setTagDesc={setTagDesc}
|
||||||
|
mode="Create"
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
permission={UPDATE_TAG_TYPE}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Create type
|
||||||
|
</PermissionButton>
|
||||||
|
</TagTypeForm>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CreateTagType;
|
89
frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
Normal file
89
frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import { useHistory, useParams } from 'react-router-dom';
|
||||||
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
|
import useTagType from '../../../hooks/api/getters/useTagType/useTagType';
|
||||||
|
import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import useToast from '../../../hooks/useToast';
|
||||||
|
import FormTemplate from '../../common/FormTemplate/FormTemplate';
|
||||||
|
import PermissionButton from '../../common/PermissionButton/PermissionButton';
|
||||||
|
import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions';
|
||||||
|
import useTagForm from '../hooks/useTagForm';
|
||||||
|
import TagForm from '../TagTypeForm/TagTypeForm';
|
||||||
|
|
||||||
|
const EditTagType = () => {
|
||||||
|
const { setToastData, setToastApiError } = useToast();
|
||||||
|
const { uiConfig } = useUiConfig();
|
||||||
|
const history = useHistory();
|
||||||
|
const { name } = useParams<{ name: string }>();
|
||||||
|
const { tagType } = useTagType(name);
|
||||||
|
const {
|
||||||
|
tagName,
|
||||||
|
tagDesc,
|
||||||
|
setTagName,
|
||||||
|
setTagDesc,
|
||||||
|
getTagPayload,
|
||||||
|
errors,
|
||||||
|
clearErrors,
|
||||||
|
} = useTagForm(tagType?.name, tagType?.description);
|
||||||
|
const { updateTagType, loading } = useTagTypesApi();
|
||||||
|
|
||||||
|
const handleSubmit = async (e: Event) => {
|
||||||
|
e.preventDefault();
|
||||||
|
clearErrors();
|
||||||
|
const payload = getTagPayload();
|
||||||
|
try {
|
||||||
|
await updateTagType(tagName, payload);
|
||||||
|
history.push('/tag-types');
|
||||||
|
setToastData({
|
||||||
|
title: 'Tag type updated',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
} catch (e: any) {
|
||||||
|
setToastApiError(e.toString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatApiCode = () => {
|
||||||
|
return `curl --location --request PUT '${
|
||||||
|
uiConfig.unleashUrl
|
||||||
|
}/api/admin/tag-types/${name}' \\
|
||||||
|
--header 'Authorization: INSERT_API_KEY' \\
|
||||||
|
--header 'Content-Type: application/json' \\
|
||||||
|
--data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
history.goBack();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormTemplate
|
||||||
|
loading={loading}
|
||||||
|
title="Edit tag type"
|
||||||
|
description="Tag types allow you to group tags together in the management UI"
|
||||||
|
documentationLink="https://docs.getunleash.io/"
|
||||||
|
formatApiCode={formatApiCode}
|
||||||
|
>
|
||||||
|
<TagForm
|
||||||
|
errors={errors}
|
||||||
|
handleSubmit={handleSubmit}
|
||||||
|
handleCancel={handleCancel}
|
||||||
|
tagName={tagName}
|
||||||
|
setTagName={setTagName}
|
||||||
|
tagDesc={tagDesc}
|
||||||
|
setTagDesc={setTagDesc}
|
||||||
|
mode="Edit"
|
||||||
|
clearErrors={clearErrors}
|
||||||
|
>
|
||||||
|
<PermissionButton
|
||||||
|
onClick={handleSubmit}
|
||||||
|
permission={UPDATE_TAG_TYPE}
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
Edit type
|
||||||
|
</PermissionButton>
|
||||||
|
</TagForm>
|
||||||
|
</FormTemplate>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditTagType;
|
@ -0,0 +1,47 @@
|
|||||||
|
import { makeStyles } from '@material-ui/core/styles';
|
||||||
|
|
||||||
|
export const useStyles = makeStyles(theme => ({
|
||||||
|
container: {
|
||||||
|
maxWidth: '400px',
|
||||||
|
},
|
||||||
|
form: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
height: '100%',
|
||||||
|
},
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
}));
|
77
frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
Normal file
77
frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import Input from '../../common/Input/Input';
|
||||||
|
import { TextField, Button } from '@material-ui/core';
|
||||||
|
|
||||||
|
import { useStyles } from './TagTypeForm.styles';
|
||||||
|
import React from 'react';
|
||||||
|
import { trim } from '../../common/util';
|
||||||
|
import { EDIT } from '../../../constants/misc';
|
||||||
|
|
||||||
|
interface ITagTypeForm {
|
||||||
|
tagName: string;
|
||||||
|
tagDesc: string;
|
||||||
|
setTagName: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
setTagDesc: React.Dispatch<React.SetStateAction<string>>;
|
||||||
|
handleSubmit: (e: any) => void;
|
||||||
|
handleCancel: () => void;
|
||||||
|
errors: { [key: string]: string };
|
||||||
|
mode: string;
|
||||||
|
clearErrors: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const TagTypeForm: React.FC<ITagTypeForm> = ({
|
||||||
|
children,
|
||||||
|
handleSubmit,
|
||||||
|
handleCancel,
|
||||||
|
tagName,
|
||||||
|
tagDesc,
|
||||||
|
setTagName,
|
||||||
|
setTagDesc,
|
||||||
|
errors,
|
||||||
|
mode,
|
||||||
|
clearErrors,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
|
<h3 className={styles.formHeader}>Tag information</h3>
|
||||||
|
|
||||||
|
<div className={styles.container}>
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
|
What is your tag name?
|
||||||
|
</p>
|
||||||
|
<Input
|
||||||
|
className={styles.input}
|
||||||
|
label="Tag name"
|
||||||
|
value={tagName}
|
||||||
|
onChange={e => setTagName(trim(e.target.value))}
|
||||||
|
error={Boolean(errors.name)}
|
||||||
|
errorText={errors.name}
|
||||||
|
onFocus={() => clearErrors()}
|
||||||
|
disabled={mode === EDIT}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className={styles.inputDescription}>
|
||||||
|
What is this role for?
|
||||||
|
</p>
|
||||||
|
<TextField
|
||||||
|
className={styles.input}
|
||||||
|
label="Tag description"
|
||||||
|
variant="outlined"
|
||||||
|
multiline
|
||||||
|
maxRows={4}
|
||||||
|
value={tagDesc}
|
||||||
|
onChange={e => setTagDesc(e.target.value)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttonContainer}>
|
||||||
|
<Button onClick={handleCancel} className={styles.cancelButton}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TagTypeForm;
|
69
frontend/src/component/tagTypes/hooks/useTagForm.ts
Normal file
69
frontend/src/component/tagTypes/hooks/useTagForm.ts
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi';
|
||||||
|
|
||||||
|
const useTagForm = (initialTagName = '', initialTagDesc = '') => {
|
||||||
|
const [tagName, setTagName] = useState(initialTagName);
|
||||||
|
const [tagDesc, setTagDesc] = useState(initialTagDesc);
|
||||||
|
const [errors, setErrors] = useState({});
|
||||||
|
const { validateTagName } = useTagTypesApi();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTagName(initialTagName);
|
||||||
|
}, [initialTagName]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTagDesc(initialTagDesc);
|
||||||
|
}, [initialTagDesc]);
|
||||||
|
|
||||||
|
const getTagPayload = () => {
|
||||||
|
return {
|
||||||
|
name: tagName,
|
||||||
|
description: tagDesc,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const NAME_EXISTS_ERROR =
|
||||||
|
'There already exists a tag-type with the name simple';
|
||||||
|
const validateNameUniqueness = async () => {
|
||||||
|
if (tagName.length === 0) {
|
||||||
|
setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tagName.length < 2) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
name: 'Tag name length must be at least 2 characters long',
|
||||||
|
}));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await validateTagName(tagName);
|
||||||
|
return true;
|
||||||
|
} catch (e: any) {
|
||||||
|
if (e.toString().includes(NAME_EXISTS_ERROR)) {
|
||||||
|
setErrors(prev => ({
|
||||||
|
...prev,
|
||||||
|
name: NAME_EXISTS_ERROR,
|
||||||
|
}));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearErrors = () => {
|
||||||
|
setErrors({});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagName,
|
||||||
|
tagDesc,
|
||||||
|
setTagName,
|
||||||
|
setTagDesc,
|
||||||
|
getTagPayload,
|
||||||
|
clearErrors,
|
||||||
|
validateNameUniqueness,
|
||||||
|
errors,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTagForm;
|
@ -50,6 +50,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
|||||||
<Label />
|
<Label />
|
||||||
</ListItemIcon>
|
</ListItemIcon>
|
||||||
<ListItemText primary={tag.value} secondary={tag.type} />
|
<ListItemText primary={tag.value} secondary={tag.type} />
|
||||||
|
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={hasAccess(DELETE_TAG)}
|
condition={hasAccess(DELETE_TAG)}
|
||||||
show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
|
show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
|
||||||
@ -81,7 +82,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
|||||||
show={
|
show={
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-label="add tag"
|
aria-label="add tag"
|
||||||
onClick={() => history.push('/tags/create')}
|
onClick={() => history.push('/tag-types/create')}
|
||||||
>
|
>
|
||||||
<Add />
|
<Add />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@ -91,7 +92,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<Add />}
|
startIcon={<Add />}
|
||||||
onClick={() => history.push('/tags/create')}
|
onClick={() =>
|
||||||
|
history.push('/tag-types/create')
|
||||||
|
}
|
||||||
variant="contained"
|
variant="contained"
|
||||||
>
|
>
|
||||||
Add new tag
|
Add new tag
|
||||||
|
@ -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;
|
||||||
|
@ -0,0 +1,75 @@
|
|||||||
|
import { ITagPayload } from '../../../../interfaces/tags';
|
||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
const useTagTypesApi = () => {
|
||||||
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createTag = async (payload: ITagPayload) => {
|
||||||
|
const path = `api/admin/tag-types`;
|
||||||
|
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 validateTagName = async (name: string) => {
|
||||||
|
const path = `api/admin/tag-types/validate`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const updateTagType = async (tagName: string, payload: ITagPayload) => {
|
||||||
|
const path = `api/admin/tag-types/${tagName}`;
|
||||||
|
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 deleteTagType = async (tagName: string) => {
|
||||||
|
const path = `api/admin/tag-types/${tagName}`;
|
||||||
|
const req = createRequest(path, { method: 'DELETE' });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await makeRequest(req.caller, req.id);
|
||||||
|
return res;
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
createTag,
|
||||||
|
validateTagName,
|
||||||
|
updateTagType,
|
||||||
|
deleteTagType,
|
||||||
|
errors,
|
||||||
|
loading
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTagTypesApi;
|
41
frontend/src/hooks/api/getters/useTagType/useTagType.ts
Normal file
41
frontend/src/hooks/api/getters/useTagType/useTagType.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import useSWR, { mutate, SWRConfiguration } from 'swr';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { formatApiPath } from '../../../../utils/format-path';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
|
||||||
|
const useTagType = (name: string, options: SWRConfiguration = {}) => {
|
||||||
|
const fetcher = async () => {
|
||||||
|
const path = formatApiPath(`api/admin/tag-types/${name}`);
|
||||||
|
return fetch(path, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
.then(handleErrorResponses('Tag data'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
||||||
|
|
||||||
|
const FEATURE_CACHE_KEY = `api/admin/tag-types/${name}`;
|
||||||
|
|
||||||
|
const { data, error } = useSWR(FEATURE_CACHE_KEY, fetcher, {
|
||||||
|
...options,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(!error && !data);
|
||||||
|
|
||||||
|
const refetch = () => {
|
||||||
|
mutate(FEATURE_CACHE_KEY);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLoading(!error && !data);
|
||||||
|
}, [data, error]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
tagType: data?.tagType || { name: '', description: '' },
|
||||||
|
error,
|
||||||
|
loading,
|
||||||
|
refetch,
|
||||||
|
FEATURE_CACHE_KEY,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useTagType;
|
@ -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 = () => {
|
||||||
|
@ -8,3 +8,8 @@ export interface ITagType {
|
|||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ITagPayload {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
@ -18,7 +17,8 @@
|
|||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx"
|
"jsx": "react-jsx",
|
||||||
|
"strict": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"src"
|
"src"
|
||||||
|
@ -2037,6 +2037,11 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz"
|
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz"
|
||||||
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
|
integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA==
|
||||||
|
|
||||||
|
"@types/history@^4.7.11":
|
||||||
|
version "4.7.11"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
|
||||||
|
integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
|
||||||
|
|
||||||
"@types/hoist-non-react-statics@^3.3.0":
|
"@types/hoist-non-react-statics@^3.3.0":
|
||||||
version "3.3.1"
|
version "3.3.1"
|
||||||
resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
|
resolved "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz"
|
||||||
@ -2112,10 +2117,10 @@
|
|||||||
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
|
resolved "https://registry.npmjs.org/@types/node/-/node-14.14.37.tgz"
|
||||||
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
|
integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
|
||||||
|
|
||||||
"@types/node@14.18.7":
|
"@types/node@14.18.9":
|
||||||
version "14.18.7"
|
version "14.18.9"
|
||||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.7.tgz#bf973dbd8e156dbf860504a8811033cbd26967d1"
|
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.9.tgz#0e5944eefe2b287391279a19b407aa98bd14436d"
|
||||||
integrity sha512-UpLEO1iBG7esNPusSAjoZhWFK5Mfd8QfwWhHRrg5io13POn/stsBgTCba9suQaFflNA4tc0+6AFM3R6BZNng6A==
|
integrity sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==
|
||||||
|
|
||||||
"@types/node@^14.14.31":
|
"@types/node@^14.14.31":
|
||||||
version "14.17.19"
|
version "14.17.19"
|
||||||
@ -2164,12 +2169,12 @@
|
|||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
redux "^4.0.0"
|
redux "^4.0.0"
|
||||||
|
|
||||||
"@types/react-router-dom@5.3.2":
|
"@types/react-router-dom@5.3.3":
|
||||||
version "5.3.2"
|
version "5.3.3"
|
||||||
resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.2.tgz"
|
resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
|
||||||
integrity sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ==
|
integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/history" "*"
|
"@types/history" "^4.7.11"
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
"@types/react-router" "*"
|
"@types/react-router" "*"
|
||||||
|
|
||||||
@ -11265,10 +11270,10 @@ sass-loader@^10.0.5:
|
|||||||
schema-utils "^3.0.0"
|
schema-utils "^3.0.0"
|
||||||
semver "^7.3.2"
|
semver "^7.3.2"
|
||||||
|
|
||||||
sass@1.48.0:
|
sass@1.49.0:
|
||||||
version "1.48.0"
|
version "1.49.0"
|
||||||
resolved "https://registry.yarnpkg.com/sass/-/sass-1.48.0.tgz#b53cfccc1b8ab4be375cc54f306fda9d4711162c"
|
resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078"
|
||||||
integrity sha512-hQi5g4DcfjcipotoHZ80l7GNJHGqQS5LwMBjVYB/TaT0vcSSpbgM8Ad7cgfsB2M0MinbkEQQPO9+sjjSiwxqmw==
|
integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar ">=3.0.0 <4.0.0"
|
chokidar ">=3.0.0 <4.0.0"
|
||||||
immutable "^4.0.0"
|
immutable "^4.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user