mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
feat: add create and edit screen for tag-types (NEW) (#603)
* feat: add create and edit screen for tag-types * feat: update Edit and create component with permissions * refactor: add TagForm type to react FC * fix: routes * fix: add edit button * fix: update snapshot * fix: update permission * fix: permission Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
parent
80e80805f7
commit
7baf8400ca
@ -263,15 +263,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",
|
||||||
|
@ -15,10 +15,6 @@ 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 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 +44,8 @@ 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';
|
||||||
|
|
||||||
export const routes = [
|
export const routes = [
|
||||||
// Project
|
// Project
|
||||||
@ -304,24 +302,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
|
||||||
{
|
{
|
||||||
|
@ -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,11 +14,13 @@ 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}>
|
||||||
|
<UIProvider>
|
||||||
<AccessProvider
|
<AccessProvider
|
||||||
store={createFakeStore([{ permission: ADMIN }])}
|
store={createFakeStore([{ permission: ADMIN }])}
|
||||||
>
|
>
|
||||||
@ -29,6 +31,7 @@ test('renders an empty list correctly', () => {
|
|||||||
history={{}}
|
history={{}}
|
||||||
/>
|
/>
|
||||||
</AccessProvider>
|
</AccessProvider>
|
||||||
|
</UIProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
);
|
);
|
||||||
@ -39,6 +42,7 @@ test('renders a list with elements correctly', () => {
|
|||||||
const tree = renderer.create(
|
const tree = renderer.create(
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<MemoryRouter>
|
<MemoryRouter>
|
||||||
|
<UIProvider>
|
||||||
<AccessProvider
|
<AccessProvider
|
||||||
store={createFakeStore([
|
store={createFakeStore([
|
||||||
{ permission: CREATE_TAG_TYPE },
|
{ permission: CREATE_TAG_TYPE },
|
||||||
@ -59,6 +63,7 @@ test('renders a list with elements correctly', () => {
|
|||||||
history={{}}
|
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
|
||||||
|
@ -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;
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user