mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
  Object {
 | 
			
		||||
    "component": [Function],
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
    "menu": Object {},
 | 
			
		||||
    "parent": "/tags",
 | 
			
		||||
    "path": "/tags/create",
 | 
			
		||||
    "title": "Create",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
  Object {
 | 
			
		||||
    "component": [Function],
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,6 @@ import CreateContextField from '../../page/context/create';
 | 
			
		||||
import EditContextField from '../../page/context/edit';
 | 
			
		||||
import CreateProject from '../../page/project/create';
 | 
			
		||||
import ListTagTypes from '../../page/tag-types';
 | 
			
		||||
import 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 AddonsCreate from '../../page/addons/create';
 | 
			
		||||
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 EditEnvironment from '../environments/EditEnvironment/EditEnvironment';
 | 
			
		||||
 | 
			
		||||
import EditTagType from '../tagTypes/EditTagType/EditTagType';
 | 
			
		||||
import CreateTagType from '../tagTypes/CreateTagType/CreateTagType';
 | 
			
		||||
 | 
			
		||||
export const routes = [
 | 
			
		||||
    // Project
 | 
			
		||||
@ -304,24 +302,6 @@ export const routes = [
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        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
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -13,7 +13,6 @@ export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD';
 | 
			
		||||
export const CREATE_PROJECT = 'CREATE_PROJECT';
 | 
			
		||||
export const UPDATE_PROJECT = 'UPDATE_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 UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE';
 | 
			
		||||
export const CREATE_TAG = 'CREATE_TAG';
 | 
			
		||||
 | 
			
		||||
@ -2,6 +2,10 @@
 | 
			
		||||
    min-width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.icon {
 | 
			
		||||
    fill: #757575;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.textfield {
 | 
			
		||||
    margin-left: 15px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { useContext, useState } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { Link, useHistory } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
    List,
 | 
			
		||||
    ListItem,
 | 
			
		||||
@ -11,38 +10,53 @@ import {
 | 
			
		||||
    Button,
 | 
			
		||||
    Tooltip,
 | 
			
		||||
} 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 PageContent from '../../common/PageContent/PageContent';
 | 
			
		||||
import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender';
 | 
			
		||||
import {
 | 
			
		||||
    CREATE_TAG_TYPE,
 | 
			
		||||
    DELETE_TAG_TYPE,
 | 
			
		||||
    UPDATE_TAG_TYPE,
 | 
			
		||||
} from '../../providers/AccessProvider/permissions';
 | 
			
		||||
import Dialogue from '../../common/Dialogue/Dialogue';
 | 
			
		||||
import useMediaQuery from '@material-ui/core/useMediaQuery';
 | 
			
		||||
 | 
			
		||||
import styles from '../TagType.module.scss';
 | 
			
		||||
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 [deletion, setDeletion] = useState({ open: false });
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const smallScreen = useMediaQuery('(max-width:700px)');
 | 
			
		||||
    const { deleteTagType } = useTagTypesApi();
 | 
			
		||||
    const { tagTypes, refetch } = useTagTypes();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        fetchTagTypes();
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, []);
 | 
			
		||||
    const deleteTag = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            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 = (
 | 
			
		||||
        <HeaderTitle
 | 
			
		||||
            title="Tag Types"
 | 
			
		||||
            actions={
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={hasAccess(CREATE_TAG_TYPE)}
 | 
			
		||||
                    condition={hasAccess(UPDATE_TAG_TYPE)}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <ConditionallyRender
 | 
			
		||||
                            condition={smallScreen}
 | 
			
		||||
@ -96,6 +110,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
 | 
			
		||||
                </IconButton>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return (
 | 
			
		||||
            <ListItem
 | 
			
		||||
                key={`${tagType.name}`}
 | 
			
		||||
@ -105,6 +120,13 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
 | 
			
		||||
                    <Label />
 | 
			
		||||
                </ListItemIcon>
 | 
			
		||||
                <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
 | 
			
		||||
                    condition={hasAccess(DELETE_TAG_TYPE)}
 | 
			
		||||
                    show={deleteButton}
 | 
			
		||||
@ -124,10 +146,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => {
 | 
			
		||||
            <Dialogue
 | 
			
		||||
                title="Really delete Tag type?"
 | 
			
		||||
                open={deletion.open}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                    removeTagType(deletion.name);
 | 
			
		||||
                    setDeletion({ open: false });
 | 
			
		||||
                }}
 | 
			
		||||
                onClick={deleteTag}
 | 
			
		||||
                onClose={() => {
 | 
			
		||||
                    setDeletion({ open: false });
 | 
			
		||||
                }}
 | 
			
		||||
 | 
			
		||||
@ -38,44 +38,10 @@ exports[`renders a list with elements correctly 1`] = `
 | 
			
		||||
      className="MuiList-root MuiList-padding"
 | 
			
		||||
    >
 | 
			
		||||
      <li
 | 
			
		||||
        className="MuiListItem-root tagListItem MuiListItem-gutters"
 | 
			
		||||
        className="MuiListItem-root MuiListItem-gutters"
 | 
			
		||||
        disabled={false}
 | 
			
		||||
      >
 | 
			
		||||
        <div
 | 
			
		||||
          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>
 | 
			
		||||
        No entries
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
@ -14,21 +14,24 @@ import {
 | 
			
		||||
    UPDATE_TAG_TYPE,
 | 
			
		||||
    DELETE_TAG_TYPE,
 | 
			
		||||
} from '../../providers/AccessProvider/permissions';
 | 
			
		||||
import UIProvider from '../../providers/UIProvider/UIProvider';
 | 
			
		||||
 | 
			
		||||
test('renders an empty list correctly', () => {
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <MemoryRouter>
 | 
			
		||||
            <ThemeProvider theme={theme}>
 | 
			
		||||
                <AccessProvider
 | 
			
		||||
                    store={createFakeStore([{ permission: ADMIN }])}
 | 
			
		||||
                >
 | 
			
		||||
                    <TagTypesList
 | 
			
		||||
                        tagTypes={[]}
 | 
			
		||||
                        fetchTagTypes={jest.fn()}
 | 
			
		||||
                        removeTagType={jest.fn()}
 | 
			
		||||
                        history={{}}
 | 
			
		||||
                    />
 | 
			
		||||
                </AccessProvider>
 | 
			
		||||
                <UIProvider>
 | 
			
		||||
                    <AccessProvider
 | 
			
		||||
                        store={createFakeStore([{ permission: ADMIN }])}
 | 
			
		||||
                    >
 | 
			
		||||
                        <TagTypesList
 | 
			
		||||
                            tagTypes={[]}
 | 
			
		||||
                            fetchTagTypes={jest.fn()}
 | 
			
		||||
                            removeTagType={jest.fn()}
 | 
			
		||||
                            history={{}}
 | 
			
		||||
                        />
 | 
			
		||||
                    </AccessProvider>
 | 
			
		||||
                </UIProvider>
 | 
			
		||||
            </ThemeProvider>
 | 
			
		||||
        </MemoryRouter>
 | 
			
		||||
    );
 | 
			
		||||
@ -39,26 +42,28 @@ test('renders a list with elements correctly', () => {
 | 
			
		||||
    const tree = renderer.create(
 | 
			
		||||
        <ThemeProvider theme={theme}>
 | 
			
		||||
            <MemoryRouter>
 | 
			
		||||
                <AccessProvider
 | 
			
		||||
                    store={createFakeStore([
 | 
			
		||||
                        { permission: CREATE_TAG_TYPE },
 | 
			
		||||
                        { permission: UPDATE_TAG_TYPE },
 | 
			
		||||
                        { permission: DELETE_TAG_TYPE },
 | 
			
		||||
                    ])}
 | 
			
		||||
                >
 | 
			
		||||
                    <TagTypesList
 | 
			
		||||
                        tagTypes={[
 | 
			
		||||
                            {
 | 
			
		||||
                                name: 'simple',
 | 
			
		||||
                                description: 'Some simple description',
 | 
			
		||||
                                icon: '#',
 | 
			
		||||
                            },
 | 
			
		||||
                        ]}
 | 
			
		||||
                        fetchTagTypes={jest.fn()}
 | 
			
		||||
                        removeTagType={jest.fn()}
 | 
			
		||||
                        history={{}}
 | 
			
		||||
                    />
 | 
			
		||||
                </AccessProvider>
 | 
			
		||||
                <UIProvider>
 | 
			
		||||
                    <AccessProvider
 | 
			
		||||
                        store={createFakeStore([
 | 
			
		||||
                            { permission: CREATE_TAG_TYPE },
 | 
			
		||||
                            { permission: UPDATE_TAG_TYPE },
 | 
			
		||||
                            { permission: DELETE_TAG_TYPE },
 | 
			
		||||
                        ])}
 | 
			
		||||
                    >
 | 
			
		||||
                        <TagTypesList
 | 
			
		||||
                            tagTypes={[
 | 
			
		||||
                                {
 | 
			
		||||
                                    name: 'simple',
 | 
			
		||||
                                    description: 'Some simple description',
 | 
			
		||||
                                    icon: '#',
 | 
			
		||||
                                },
 | 
			
		||||
                            ]}
 | 
			
		||||
                            fetchTagTypes={jest.fn()}
 | 
			
		||||
                            removeTagType={jest.fn()}
 | 
			
		||||
                            history={{}}
 | 
			
		||||
                        />
 | 
			
		||||
                    </AccessProvider>
 | 
			
		||||
                </UIProvider>
 | 
			
		||||
            </MemoryRouter>
 | 
			
		||||
        </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 />
 | 
			
		||||
            </ListItemIcon>
 | 
			
		||||
            <ListItemText primary={tag.value} secondary={tag.type} />
 | 
			
		||||
 | 
			
		||||
            <ConditionallyRender
 | 
			
		||||
                condition={hasAccess(DELETE_TAG)}
 | 
			
		||||
                show={<DeleteButton tagType={tag.type} tagValue={tag.value} />}
 | 
			
		||||
@ -81,7 +82,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
 | 
			
		||||
                    show={
 | 
			
		||||
                        <IconButton
 | 
			
		||||
                            aria-label="add tag"
 | 
			
		||||
                            onClick={() => history.push('/tags/create')}
 | 
			
		||||
                            onClick={() => history.push('/tag-types/create')}
 | 
			
		||||
                        >
 | 
			
		||||
                            <Add />
 | 
			
		||||
                        </IconButton>
 | 
			
		||||
@ -91,7 +92,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => {
 | 
			
		||||
                            <Button
 | 
			
		||||
                                color="primary"
 | 
			
		||||
                                startIcon={<Add />}
 | 
			
		||||
                                onClick={() => history.push('/tags/create')}
 | 
			
		||||
                                onClick={() =>
 | 
			
		||||
                                    history.push('/tag-types/create')
 | 
			
		||||
                                }
 | 
			
		||||
                                variant="contained"
 | 
			
		||||
                            >
 | 
			
		||||
                                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;
 | 
			
		||||
    icon: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ITagPayload {
 | 
			
		||||
    name: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "esModuleInterop": true,
 | 
			
		||||
    "allowSyntheticDefaultImports": true,
 | 
			
		||||
    "strict": true,
 | 
			
		||||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "noFallthroughCasesInSwitch": true,
 | 
			
		||||
    "module": "esnext",
 | 
			
		||||
@ -18,7 +17,8 @@
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "isolatedModules": true,
 | 
			
		||||
    "noEmit": true,
 | 
			
		||||
    "jsx": "react-jsx"
 | 
			
		||||
    "jsx": "react-jsx",
 | 
			
		||||
    "strict": true
 | 
			
		||||
  },
 | 
			
		||||
  "include": [
 | 
			
		||||
    "src"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user