mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01: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-adapter-react-16": "1.0.6",
 | 
			
		||||
    "@types/jest": "27.4.0",
 | 
			
		||||
    "@types/node": "14.18.7",
 | 
			
		||||
    "@types/node": "14.18.9",
 | 
			
		||||
    "@types/react": "17.0.38",
 | 
			
		||||
    "@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",
 | 
			
		||||
    "@welldone-software/why-did-you-render": "6.2.3",
 | 
			
		||||
    "array-move": "3.0.1",
 | 
			
		||||
@ -87,7 +87,7 @@
 | 
			
		||||
    "redux-devtools-extension": "2.13.9",
 | 
			
		||||
    "redux-mock-store": "1.5.4",
 | 
			
		||||
    "redux-thunk": "2.4.1",
 | 
			
		||||
    "sass": "1.48.0",
 | 
			
		||||
    "sass": "1.49.0",
 | 
			
		||||
    "swr": "1.0.1",
 | 
			
		||||
    "typescript": "4.5.4",
 | 
			
		||||
    "web-vitals": "2.1.3"
 | 
			
		||||
 | 
			
		||||
@ -73,7 +73,6 @@ const FormTemplate: React.FC<ICreateProps> = ({
 | 
			
		||||
                            <h3 className={styles.subtitle}>
 | 
			
		||||
                                API Command{' '}
 | 
			
		||||
                                <IconButton
 | 
			
		||||
                                    className={styles.iconButton}
 | 
			
		||||
                                    onClick={copyCommand}
 | 
			
		||||
                                >
 | 
			
		||||
                                    <FileCopy className={styles.icon} />
 | 
			
		||||
 | 
			
		||||
@ -11,6 +11,15 @@ Array [
 | 
			
		||||
    "title": "Create",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
  Object {
 | 
			
		||||
    "component": [Function],
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
    "menu": Object {},
 | 
			
		||||
    "parent": "/projects",
 | 
			
		||||
    "path": "/projects/:id/edit",
 | 
			
		||||
    "title": ":id",
 | 
			
		||||
    "type": "protected",
 | 
			
		||||
  },
 | 
			
		||||
  Object {
 | 
			
		||||
    "component": [Function],
 | 
			
		||||
    "layout": "main",
 | 
			
		||||
@ -263,15 +272,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",
 | 
			
		||||
 | 
			
		||||
@ -13,12 +13,7 @@ import ApplicationView from '../../page/applications/view';
 | 
			
		||||
import ContextFields from '../../page/context';
 | 
			
		||||
import CreateContextField from '../../page/context/create';
 | 
			
		||||
import EditContextField from '../../page/context/edit';
 | 
			
		||||
import CreateProject from '../../page/project/create';
 | 
			
		||||
import ListTagTypes from '../../page/tag-types';
 | 
			
		||||
import 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 +43,10 @@ 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';
 | 
			
		||||
import EditProject from '../project/Project/EditProject/EditProject';
 | 
			
		||||
import CreateProject from '../project/Project/CreateProject/CreateProject';
 | 
			
		||||
 | 
			
		||||
export const routes = [
 | 
			
		||||
    // Project
 | 
			
		||||
@ -61,6 +60,15 @@ export const routes = [
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: {},
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: '/projects/:id/edit',
 | 
			
		||||
        parent: '/projects',
 | 
			
		||||
        title: ':id',
 | 
			
		||||
        component: EditProject,
 | 
			
		||||
        type: 'protected',
 | 
			
		||||
        layout: 'main',
 | 
			
		||||
        menu: {},
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
        path: '/projects/:id/archived',
 | 
			
		||||
        title: ':name',
 | 
			
		||||
@ -304,24 +312,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
 | 
			
		||||
    {
 | 
			
		||||
 | 
			
		||||
@ -1,31 +0,0 @@
 | 
			
		||||
.header {
 | 
			
		||||
    padding: var(--card-header-padding);
 | 
			
		||||
    margin-bottom: var(--card-margin-y);
 | 
			
		||||
    word-break: break-all;
 | 
			
		||||
    border-bottom: var(--default-border);
 | 
			
		||||
    display: flex;
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: space-between;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header h1 {
 | 
			
		||||
    font-size: var(--h1-size);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.supporting {
 | 
			
		||||
    font-size: var(--caption-size);
 | 
			
		||||
    max-width: 450px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.container {
 | 
			
		||||
    padding: var(--card-padding);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formButtons {
 | 
			
		||||
    padding-top: 1rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.formContainer {
 | 
			
		||||
    margin-bottom: 1.5rem;
 | 
			
		||||
    max-width: 350px;
 | 
			
		||||
}
 | 
			
		||||
@ -0,0 +1,94 @@
 | 
			
		||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
 | 
			
		||||
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import ProjectForm from '../ProjectForm/ProjectForm';
 | 
			
		||||
import useProjectForm from '../hooks/useProjectForm';
 | 
			
		||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import useToast from '../../../../hooks/useToast';
 | 
			
		||||
import useUser from '../../../../hooks/api/getters/useUser/useUser';
 | 
			
		||||
 | 
			
		||||
const CreateProject = () => {
 | 
			
		||||
    /* @ts-ignore */
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { refetch } = useUser();
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const {
 | 
			
		||||
        projectId,
 | 
			
		||||
        projectName,
 | 
			
		||||
        projectDesc,
 | 
			
		||||
        setProjectId,
 | 
			
		||||
        setProjectName,
 | 
			
		||||
        setProjectDesc,
 | 
			
		||||
        getProjectPayload,
 | 
			
		||||
        clearErrors,
 | 
			
		||||
        validateIdUniqueness,
 | 
			
		||||
        validateName,
 | 
			
		||||
        errors,
 | 
			
		||||
    } = useProjectForm();
 | 
			
		||||
 | 
			
		||||
    const { createProject, loading } = useProjectApi();
 | 
			
		||||
 | 
			
		||||
    const handleSubmit = async (e: Event) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        clearErrors();
 | 
			
		||||
        const validName = validateName();
 | 
			
		||||
        const validId = await validateIdUniqueness();
 | 
			
		||||
        if (validName && validId) {
 | 
			
		||||
            const payload = getProjectPayload();
 | 
			
		||||
            try {
 | 
			
		||||
                await createProject(payload);
 | 
			
		||||
                refetch();
 | 
			
		||||
                history.push(`/projects/${projectId}`);
 | 
			
		||||
                setToastData({
 | 
			
		||||
                    title: 'Project created',
 | 
			
		||||
                    text: 'Now you can add toggles to this project',
 | 
			
		||||
                    confetti: true,
 | 
			
		||||
                    type: 'success',
 | 
			
		||||
                });
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                setToastApiError(e.toString());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const formatApiCode = () => {
 | 
			
		||||
        return `curl --location --request POST '${
 | 
			
		||||
            uiConfig.unleashUrl
 | 
			
		||||
        }/api/admin/projects' \\
 | 
			
		||||
--header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
--header 'Content-Type: application/json' \\
 | 
			
		||||
--data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleCancel = () => {
 | 
			
		||||
        history.push('/projects');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            loading={loading}
 | 
			
		||||
            title="Create project"
 | 
			
		||||
            description="Projects allows you to group feature toggles together in the management UI."
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/user_guide/projects"
 | 
			
		||||
            formatApiCode={formatApiCode}
 | 
			
		||||
        >
 | 
			
		||||
            <ProjectForm
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                handleSubmit={handleSubmit}
 | 
			
		||||
                handleCancel={handleCancel}
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                setProjectId={setProjectId}
 | 
			
		||||
                projectName={projectName}
 | 
			
		||||
                setProjectName={setProjectName}
 | 
			
		||||
                projectDesc={projectDesc}
 | 
			
		||||
                setProjectDesc={setProjectDesc}
 | 
			
		||||
                submitButtonText="Create"
 | 
			
		||||
                clearErrors={clearErrors}
 | 
			
		||||
                validateIdUniqueness={validateIdUniqueness}
 | 
			
		||||
            />
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default CreateProject;
 | 
			
		||||
@ -0,0 +1,93 @@
 | 
			
		||||
import FormTemplate from '../../../common/FormTemplate/FormTemplate';
 | 
			
		||||
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
 | 
			
		||||
import { useHistory, useParams } from 'react-router-dom';
 | 
			
		||||
import ProjectForm from '../ProjectForm/ProjectForm';
 | 
			
		||||
import useProjectForm from '../hooks/useProjectForm';
 | 
			
		||||
import useProject from '../../../../hooks/api/getters/useProject/useProject';
 | 
			
		||||
import useUiConfig from '../../../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import useToast from '../../../../hooks/useToast';
 | 
			
		||||
 | 
			
		||||
const EditProject = () => {
 | 
			
		||||
    const { uiConfig } = useUiConfig();
 | 
			
		||||
    const { setToastData, setToastApiError } = useToast();
 | 
			
		||||
    const { id } = useParams<{ id: string }>();
 | 
			
		||||
    const { project } = useProject(id);
 | 
			
		||||
    const history = useHistory();
 | 
			
		||||
    const {
 | 
			
		||||
        projectId,
 | 
			
		||||
        projectName,
 | 
			
		||||
        projectDesc,
 | 
			
		||||
        setProjectId,
 | 
			
		||||
        setProjectName,
 | 
			
		||||
        setProjectDesc,
 | 
			
		||||
        getProjectPayload,
 | 
			
		||||
        clearErrors,
 | 
			
		||||
        validateIdUniqueness,
 | 
			
		||||
        validateName,
 | 
			
		||||
        errors,
 | 
			
		||||
    } = useProjectForm(id, project.name, project.description);
 | 
			
		||||
 | 
			
		||||
    const formatApiCode = () => {
 | 
			
		||||
        return `curl --location --request PUT '${
 | 
			
		||||
            uiConfig.unleashUrl
 | 
			
		||||
        }/api/admin/projects/${id}' \\
 | 
			
		||||
--header 'Authorization: INSERT_API_KEY' \\
 | 
			
		||||
--header 'Content-Type: application/json' \\
 | 
			
		||||
--data-raw '${JSON.stringify(getProjectPayload(), undefined, 2)}'`;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const { refetch } = useProject(id);
 | 
			
		||||
    const { editProject, loading } = useProjectApi();
 | 
			
		||||
 | 
			
		||||
    const handleSubmit = async (e: Event) => {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        const payload = getProjectPayload();
 | 
			
		||||
 | 
			
		||||
        const validName = validateName();
 | 
			
		||||
 | 
			
		||||
        if (validName) {
 | 
			
		||||
            try {
 | 
			
		||||
                await editProject(id, payload);
 | 
			
		||||
                refetch();
 | 
			
		||||
                history.push(`/projects/${id}`);
 | 
			
		||||
                setToastData({
 | 
			
		||||
                    title: 'Project information updated',
 | 
			
		||||
                    type: 'success',
 | 
			
		||||
                });
 | 
			
		||||
            } catch (e: any) {
 | 
			
		||||
                setToastApiError(e.toString());
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const handleCancel = () => {
 | 
			
		||||
        history.goBack();
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <FormTemplate
 | 
			
		||||
            loading={loading}
 | 
			
		||||
            title="Edit project"
 | 
			
		||||
            description="Projects allows you to group feature toggles together in the management UI."
 | 
			
		||||
            documentationLink="https://docs.getunleash.io/user_guide/projects"
 | 
			
		||||
            formatApiCode={formatApiCode}
 | 
			
		||||
        >
 | 
			
		||||
            <ProjectForm
 | 
			
		||||
                errors={errors}
 | 
			
		||||
                handleSubmit={handleSubmit}
 | 
			
		||||
                handleCancel={handleCancel}
 | 
			
		||||
                projectId={projectId}
 | 
			
		||||
                setProjectId={setProjectId}
 | 
			
		||||
                projectName={projectName}
 | 
			
		||||
                setProjectName={setProjectName}
 | 
			
		||||
                projectDesc={projectDesc}
 | 
			
		||||
                setProjectDesc={setProjectDesc}
 | 
			
		||||
                submitButtonText="Edit"
 | 
			
		||||
                clearErrors={clearErrors}
 | 
			
		||||
                validateIdUniqueness={validateIdUniqueness}
 | 
			
		||||
            />
 | 
			
		||||
        </FormTemplate>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default EditProject;
 | 
			
		||||
@ -13,7 +13,6 @@ import { useEffect } from 'react';
 | 
			
		||||
import useTabs from '../../../hooks/useTabs';
 | 
			
		||||
import TabPanel from '../../common/TabNav/TabPanel';
 | 
			
		||||
import ProjectAccess from '../access-container';
 | 
			
		||||
import EditProject from '../edit-project-container';
 | 
			
		||||
import ProjectEnvironment from '../ProjectEnvironment/ProjectEnvironment';
 | 
			
		||||
import ProjectOverview from './ProjectOverview';
 | 
			
		||||
import ProjectHealth from './ProjectHealth/ProjectHealth';
 | 
			
		||||
@ -58,19 +57,6 @@ const Project = () => {
 | 
			
		||||
            path: `${basePath}/environments`,
 | 
			
		||||
            name: 'environments',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
            title: 'Settings',
 | 
			
		||||
            // @ts-ignore (fix later)
 | 
			
		||||
            component: (
 | 
			
		||||
                <EditProject
 | 
			
		||||
                    projectId={id}
 | 
			
		||||
                    history={history}
 | 
			
		||||
                    title="Edit project"
 | 
			
		||||
                />
 | 
			
		||||
            ),
 | 
			
		||||
            path: `${basePath}/settings`,
 | 
			
		||||
            name: 'settings',
 | 
			
		||||
        },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
@ -101,15 +87,6 @@ const Project = () => {
 | 
			
		||||
        /* eslint-disable-next-line */
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const goToTabWithName = (name: string) => {
 | 
			
		||||
        const index = tabData.findIndex(t => t.name === name);
 | 
			
		||||
        if (index >= 0) {
 | 
			
		||||
            const tab = tabData[index];
 | 
			
		||||
            history.push(tab.path);
 | 
			
		||||
            setActiveTab(index);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const renderTabs = () => {
 | 
			
		||||
        return tabData.map((tab, index) => {
 | 
			
		||||
            return (
 | 
			
		||||
@ -150,9 +127,9 @@ const Project = () => {
 | 
			
		||||
                        Project: {project?.name}{' '}
 | 
			
		||||
                        <PermissionIconButton
 | 
			
		||||
                            permission={UPDATE_PROJECT}
 | 
			
		||||
                            tooltip={'Edit description'}
 | 
			
		||||
                            tooltip="Edit"
 | 
			
		||||
                            projectId={project?.id}
 | 
			
		||||
                            onClick={() => goToTabWithName('settings')}
 | 
			
		||||
                            onClick={() => history.push(`/projects/${id}/edit`)}
 | 
			
		||||
                            data-loading
 | 
			
		||||
                        >
 | 
			
		||||
                            <Edit />
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
import { makeStyles } from '@material-ui/core/styles';
 | 
			
		||||
 | 
			
		||||
export const useStyles = makeStyles(theme => ({
 | 
			
		||||
    form: {
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        flexDirection: 'column',
 | 
			
		||||
        height: '100%',
 | 
			
		||||
    },
 | 
			
		||||
    container: {
 | 
			
		||||
        maxWidth: '400px',
 | 
			
		||||
    },
 | 
			
		||||
    input: { width: '100%', marginBottom: '1rem' },
 | 
			
		||||
    label: {
 | 
			
		||||
        minWidth: '300px',
 | 
			
		||||
        [theme.breakpoints.down(600)]: {
 | 
			
		||||
            minWidth: 'auto',
 | 
			
		||||
        },
 | 
			
		||||
    },
 | 
			
		||||
    buttonContainer: {
 | 
			
		||||
        marginTop: 'auto',
 | 
			
		||||
        display: 'flex',
 | 
			
		||||
        justifyContent: 'flex-end',
 | 
			
		||||
    },
 | 
			
		||||
    cancelButton: {
 | 
			
		||||
        marginRight: '1.5rem',
 | 
			
		||||
    },
 | 
			
		||||
    inputDescription: {
 | 
			
		||||
        marginBottom: '0.5rem',
 | 
			
		||||
    },
 | 
			
		||||
    formHeader: {
 | 
			
		||||
        fontWeight: 'normal',
 | 
			
		||||
        marginTop: '0',
 | 
			
		||||
    },
 | 
			
		||||
    header: {
 | 
			
		||||
        fontWeight: 'normal',
 | 
			
		||||
    },
 | 
			
		||||
    permissionErrorContainer: {
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
    },
 | 
			
		||||
    errorMessage: {
 | 
			
		||||
        //@ts-ignore
 | 
			
		||||
        fontSize: theme.fontSizes.smallBody,
 | 
			
		||||
        color: theme.palette.error.main,
 | 
			
		||||
        position: 'absolute',
 | 
			
		||||
        top: '-8px',
 | 
			
		||||
    },
 | 
			
		||||
}));
 | 
			
		||||
@ -0,0 +1,103 @@
 | 
			
		||||
import PermissionButton from '../../../common/PermissionButton/PermissionButton';
 | 
			
		||||
import { CREATE_PROJECT } from '../../../providers/AccessProvider/permissions';
 | 
			
		||||
import Input from '../../../common/Input/Input';
 | 
			
		||||
import { TextField, Button } from '@material-ui/core';
 | 
			
		||||
import { useStyles } from './ProjectForm.style';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { trim } from '../../../common/util';
 | 
			
		||||
 | 
			
		||||
interface IProjectForm {
 | 
			
		||||
    projectId: string;
 | 
			
		||||
    projectName: string;
 | 
			
		||||
    projectDesc: string;
 | 
			
		||||
    setProjectId: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectName: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    setProjectDesc: React.Dispatch<React.SetStateAction<string>>;
 | 
			
		||||
    handleSubmit: (e: any) => void;
 | 
			
		||||
    handleCancel: () => void;
 | 
			
		||||
    errors: { [key: string]: string };
 | 
			
		||||
    submitButtonText: string;
 | 
			
		||||
    clearErrors: () => void;
 | 
			
		||||
    validateIdUniqueness: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProjectForm = ({
 | 
			
		||||
    handleSubmit,
 | 
			
		||||
    handleCancel,
 | 
			
		||||
    projectId,
 | 
			
		||||
    projectName,
 | 
			
		||||
    projectDesc,
 | 
			
		||||
    setProjectId,
 | 
			
		||||
    setProjectName,
 | 
			
		||||
    setProjectDesc,
 | 
			
		||||
    errors,
 | 
			
		||||
    submitButtonText,
 | 
			
		||||
    validateIdUniqueness,
 | 
			
		||||
    clearErrors,
 | 
			
		||||
}: IProjectForm) => {
 | 
			
		||||
    const styles = useStyles();
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <form onSubmit={handleSubmit} className={styles.form}>
 | 
			
		||||
            <h3 className={styles.formHeader}>Project Information</h3>
 | 
			
		||||
 | 
			
		||||
            <div className={styles.container}>
 | 
			
		||||
                <p className={styles.inputDescription}>
 | 
			
		||||
                    What is your project Id?
 | 
			
		||||
                </p>
 | 
			
		||||
                <Input
 | 
			
		||||
                    className={styles.input}
 | 
			
		||||
                    label="Project Id"
 | 
			
		||||
                    value={projectId}
 | 
			
		||||
                    onChange={e => setProjectId(trim(e.target.value))}
 | 
			
		||||
                    error={Boolean(errors.id)}
 | 
			
		||||
                    errorText={errors.id}
 | 
			
		||||
                    onFocus={() => clearErrors()}
 | 
			
		||||
                    onBlur={validateIdUniqueness}
 | 
			
		||||
                    disabled={submitButtonText === 'Edit'}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <p className={styles.inputDescription}>
 | 
			
		||||
                    What is your project name?
 | 
			
		||||
                </p>
 | 
			
		||||
                <Input
 | 
			
		||||
                    className={styles.input}
 | 
			
		||||
                    label="Project name"
 | 
			
		||||
                    value={projectName}
 | 
			
		||||
                    onChange={e => setProjectName(e.target.value)}
 | 
			
		||||
                    error={Boolean(errors.name)}
 | 
			
		||||
                    errorText={errors.name}
 | 
			
		||||
                    onFocus={() => clearErrors()}
 | 
			
		||||
                />
 | 
			
		||||
 | 
			
		||||
                <p className={styles.inputDescription}>
 | 
			
		||||
                    What is your project description?
 | 
			
		||||
                </p>
 | 
			
		||||
                <TextField
 | 
			
		||||
                    className={styles.input}
 | 
			
		||||
                    label="Project description"
 | 
			
		||||
                    variant="outlined"
 | 
			
		||||
                    multiline
 | 
			
		||||
                    maxRows={4}
 | 
			
		||||
                    value={projectDesc}
 | 
			
		||||
                    onChange={e => setProjectDesc(e.target.value)}
 | 
			
		||||
                />
 | 
			
		||||
            </div>
 | 
			
		||||
 | 
			
		||||
            <div className={styles.buttonContainer}>
 | 
			
		||||
                <Button onClick={handleCancel} className={styles.cancelButton}>
 | 
			
		||||
                    Cancel
 | 
			
		||||
                </Button>
 | 
			
		||||
                <PermissionButton
 | 
			
		||||
                    onClick={handleSubmit}
 | 
			
		||||
                    permission={CREATE_PROJECT}
 | 
			
		||||
                    type="submit"
 | 
			
		||||
                >
 | 
			
		||||
                    {submitButtonText} project
 | 
			
		||||
                </PermissionButton>
 | 
			
		||||
            </div>
 | 
			
		||||
        </form>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProjectForm;
 | 
			
		||||
@ -55,7 +55,7 @@ const ProjectInfo = ({
 | 
			
		||||
            component={Link}
 | 
			
		||||
            className={permissionButtonClass}
 | 
			
		||||
            data-loading
 | 
			
		||||
            to={`/projects/${id}/settings`}
 | 
			
		||||
            to={`/projects/${id}/edit`}
 | 
			
		||||
        >
 | 
			
		||||
            <Edit />
 | 
			
		||||
        </PermissionIconButton>
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,83 @@
 | 
			
		||||
import { useEffect, useState } from 'react';
 | 
			
		||||
import useProjectApi from '../../../../hooks/api/actions/useProjectApi/useProjectApi';
 | 
			
		||||
import { IPermission } from '../../../../interfaces/project';
 | 
			
		||||
 | 
			
		||||
export interface ICheckedPermission {
 | 
			
		||||
    [key: string]: IPermission;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useProjectForm = (
 | 
			
		||||
    initialProjectId = '',
 | 
			
		||||
    initialProjectName = '',
 | 
			
		||||
    initialProjectDesc = ''
 | 
			
		||||
) => {
 | 
			
		||||
    const [projectId, setProjectId] = useState(initialProjectId);
 | 
			
		||||
    const [projectName, setProjectName] = useState(initialProjectName);
 | 
			
		||||
    const [projectDesc, setProjectDesc] = useState(initialProjectDesc);
 | 
			
		||||
    const [errors, setErrors] = useState({});
 | 
			
		||||
    const { validateId } = useProjectApi();
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setProjectId(initialProjectId);
 | 
			
		||||
    }, [initialProjectId]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setProjectName(initialProjectName);
 | 
			
		||||
    }, [initialProjectName]);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        setProjectDesc(initialProjectDesc);
 | 
			
		||||
    }, [initialProjectDesc]);
 | 
			
		||||
 | 
			
		||||
    const getProjectPayload = () => {
 | 
			
		||||
        return {
 | 
			
		||||
            id: projectId,
 | 
			
		||||
            name: projectName,
 | 
			
		||||
            description: projectDesc,
 | 
			
		||||
        };
 | 
			
		||||
    };
 | 
			
		||||
    const NAME_EXISTS_ERROR = 'Error: A project with this id already exists.';
 | 
			
		||||
 | 
			
		||||
    const validateIdUniqueness = async () => {
 | 
			
		||||
        try {
 | 
			
		||||
            await validateId(getProjectPayload());
 | 
			
		||||
            return true;
 | 
			
		||||
        } catch (e: any) {
 | 
			
		||||
            if (e.toString().includes(NAME_EXISTS_ERROR)) {
 | 
			
		||||
                setErrors(prev => ({
 | 
			
		||||
                    ...prev,
 | 
			
		||||
                    id: 'A project with this id already exists',
 | 
			
		||||
                }));
 | 
			
		||||
            }
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateName = () => {
 | 
			
		||||
        if (projectName.length === 0) {
 | 
			
		||||
            setErrors(prev => ({ ...prev, name: 'Name can not be empty.' }));
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const clearErrors = () => {
 | 
			
		||||
        setErrors({});
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        projectId,
 | 
			
		||||
        projectName,
 | 
			
		||||
        projectDesc,
 | 
			
		||||
        setProjectId,
 | 
			
		||||
        setProjectName,
 | 
			
		||||
        setProjectDesc,
 | 
			
		||||
        getProjectPayload,
 | 
			
		||||
        validateName,
 | 
			
		||||
        validateIdUniqueness,
 | 
			
		||||
        clearErrors,
 | 
			
		||||
        errors,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useProjectForm;
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import ProjectComponent from './form-project-component';
 | 
			
		||||
import { createProject, validateId } from './../../store/project/actions';
 | 
			
		||||
import { fetchUser } from './../../store/user/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = () => ({
 | 
			
		||||
    project: { id: '', name: '', description: '' },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
    validateId,
 | 
			
		||||
    submit: async project => {
 | 
			
		||||
        await createProject(project)(dispatch);
 | 
			
		||||
        fetchUser()(dispatch);
 | 
			
		||||
    },
 | 
			
		||||
    editMode: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(ProjectComponent);
 | 
			
		||||
 | 
			
		||||
export default FormAddContainer;
 | 
			
		||||
@ -1,23 +0,0 @@
 | 
			
		||||
import { connect } from 'react-redux';
 | 
			
		||||
import Component from './form-project-component';
 | 
			
		||||
import { updateProject, validateId } from './../../store/project/actions';
 | 
			
		||||
 | 
			
		||||
const mapStateToProps = (state, props) => {
 | 
			
		||||
    const projectBase = { id: '', name: '', description: '' };
 | 
			
		||||
    const realProject = state.projects.toJS().find(n => n.id === props.projectId);
 | 
			
		||||
    const project = Object.assign(projectBase, realProject);
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
        project,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mapDispatchToProps = dispatch => ({
 | 
			
		||||
    validateId,
 | 
			
		||||
    submit: project => updateProject(project)(dispatch),
 | 
			
		||||
    editMode: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const FormAddContainer = connect(mapStateToProps, mapDispatchToProps)(Component);
 | 
			
		||||
 | 
			
		||||
export default FormAddContainer;
 | 
			
		||||
@ -1,239 +0,0 @@
 | 
			
		||||
import { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { TextField, Typography } from '@material-ui/core';
 | 
			
		||||
 | 
			
		||||
import styles from './Project.module.scss';
 | 
			
		||||
import classnames from 'classnames';
 | 
			
		||||
import { FormButtons, styles as commonStyles } from '../common';
 | 
			
		||||
import { trim } from '../common/util';
 | 
			
		||||
import PageContent from '../common/PageContent/PageContent';
 | 
			
		||||
import AccessContext from '../../contexts/AccessContext';
 | 
			
		||||
import ConditionallyRender from '../common/ConditionallyRender';
 | 
			
		||||
import { CREATE_PROJECT } from '../providers/AccessProvider/permissions';
 | 
			
		||||
import HeaderTitle from '../common/HeaderTitle';
 | 
			
		||||
import useUiConfig from '../../hooks/api/getters/useUiConfig/useUiConfig';
 | 
			
		||||
import { Alert } from '@material-ui/lab';
 | 
			
		||||
import { FormEvent } from 'react-router/node_modules/@types/react';
 | 
			
		||||
import useLoading from '../../hooks/useLoading';
 | 
			
		||||
import PermissionButton from '../common/PermissionButton/PermissionButton';
 | 
			
		||||
import { UPDATE_PROJECT } from '../../store/project/actions';
 | 
			
		||||
import useUser from '../../hooks/api/getters/useUser/useUser';
 | 
			
		||||
 | 
			
		||||
interface ProjectFormComponentProps {
 | 
			
		||||
    editMode: boolean;
 | 
			
		||||
    project: any;
 | 
			
		||||
    validateId: (id: string) => Promise<void>;
 | 
			
		||||
    history: any;
 | 
			
		||||
    submit: (project: any) => Promise<void>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ProjectFormComponent = (props: ProjectFormComponentProps) => {
 | 
			
		||||
    const { editMode } = props;
 | 
			
		||||
    const { refetch } = useUser();
 | 
			
		||||
    const { hasAccess } = useContext(AccessContext);
 | 
			
		||||
    const [project, setProject] = useState(props.project || {});
 | 
			
		||||
    const [errors, setErrors] = useState<any>({});
 | 
			
		||||
    const { isOss, loading } = useUiConfig();
 | 
			
		||||
    const ref = useLoading(loading);
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        if (!project.id && props.project.id) {
 | 
			
		||||
            setProject(props.project);
 | 
			
		||||
        }
 | 
			
		||||
        // eslint-disable-next-line react-hooks/exhaustive-deps
 | 
			
		||||
    }, [props.project]);
 | 
			
		||||
 | 
			
		||||
    const setValue = (field: string, value: string) => {
 | 
			
		||||
        const p = { ...project };
 | 
			
		||||
        p[field] = value;
 | 
			
		||||
        setProject(p);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateId = async (id: string) => {
 | 
			
		||||
        if (editMode) return true;
 | 
			
		||||
 | 
			
		||||
        const e = { ...errors };
 | 
			
		||||
        try {
 | 
			
		||||
            await props.validateId(id);
 | 
			
		||||
            e.id = undefined;
 | 
			
		||||
        } catch (err: any) {
 | 
			
		||||
            e.id = err.message;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        setErrors(e);
 | 
			
		||||
        if (e.id) return false;
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateName = () => {
 | 
			
		||||
        if (project.name.length === 0) {
 | 
			
		||||
            setErrors({ ...errors, name: 'Name can not be empty.' });
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validate = async (id: string) => {
 | 
			
		||||
        const validId = await validateId(id);
 | 
			
		||||
        const validName = validateName();
 | 
			
		||||
 | 
			
		||||
        return validId && validName;
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onCancel = (evt: Event) => {
 | 
			
		||||
        evt.preventDefault();
 | 
			
		||||
 | 
			
		||||
        if (editMode) {
 | 
			
		||||
            props.history.push(`/projects/${project.id}`);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        props.history.push(`/projects/`);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const onSubmit = async (evt: FormEvent<HTMLFormElement>) => {
 | 
			
		||||
        evt.preventDefault();
 | 
			
		||||
 | 
			
		||||
        const valid = await validate(project.id);
 | 
			
		||||
 | 
			
		||||
        if (valid) {
 | 
			
		||||
            const query = editMode ? 'edited=true' : 'created=true';
 | 
			
		||||
            await props.submit(project);
 | 
			
		||||
            refetch();
 | 
			
		||||
            props.history.push(`/projects/${project.id}?${query}`);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const submitText = editMode ? 'Update' : 'Create';
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div ref={ref}>
 | 
			
		||||
            <PageContent
 | 
			
		||||
                headerContent={
 | 
			
		||||
                    <HeaderTitle
 | 
			
		||||
                        title={`${submitText} ${props.project?.name} project`}
 | 
			
		||||
                    />
 | 
			
		||||
                }
 | 
			
		||||
            >
 | 
			
		||||
                <ConditionallyRender
 | 
			
		||||
                    condition={isOss()}
 | 
			
		||||
                    show={
 | 
			
		||||
                        <Alert data-loading severity="error">
 | 
			
		||||
                            {submitText} project requires a paid version of
 | 
			
		||||
                            Unleash. Check out{' '}
 | 
			
		||||
                            <a
 | 
			
		||||
                                href="https://www.getunleash.io"
 | 
			
		||||
                                target="_blank"
 | 
			
		||||
                                rel="noreferrer"
 | 
			
		||||
                            >
 | 
			
		||||
                                getunleash.io
 | 
			
		||||
                            </a>{' '}
 | 
			
		||||
                            to learn more.
 | 
			
		||||
                        </Alert>
 | 
			
		||||
                    }
 | 
			
		||||
                    elseShow={
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Typography
 | 
			
		||||
                                variant="subtitle1"
 | 
			
		||||
                                style={{ marginBottom: '0.5rem' }}
 | 
			
		||||
                            >
 | 
			
		||||
                                Projects allows you to group feature toggles
 | 
			
		||||
                                together in the management UI.
 | 
			
		||||
                            </Typography>
 | 
			
		||||
                            <form
 | 
			
		||||
                                data-loading
 | 
			
		||||
                                onSubmit={onSubmit}
 | 
			
		||||
                                className={classnames(
 | 
			
		||||
                                    commonStyles.contentSpacing,
 | 
			
		||||
                                    styles.formContainer
 | 
			
		||||
                                )}
 | 
			
		||||
                            >
 | 
			
		||||
                                <TextField
 | 
			
		||||
                                    label="Project Id"
 | 
			
		||||
                                    name="id"
 | 
			
		||||
                                    placeholder="A-unique-key"
 | 
			
		||||
                                    value={project.id}
 | 
			
		||||
                                    error={!!errors.id}
 | 
			
		||||
                                    helperText={errors.id}
 | 
			
		||||
                                    disabled={editMode}
 | 
			
		||||
                                    variant="outlined"
 | 
			
		||||
                                    size="small"
 | 
			
		||||
                                    onBlur={v => validateId(v.target.value)}
 | 
			
		||||
                                    onChange={v =>
 | 
			
		||||
                                        setValue('id', trim(v.target.value))
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                                <br />
 | 
			
		||||
                                <TextField
 | 
			
		||||
                                    label="Name"
 | 
			
		||||
                                    name="name"
 | 
			
		||||
                                    placeholder="Project name"
 | 
			
		||||
                                    value={project.name}
 | 
			
		||||
                                    error={!!errors.name}
 | 
			
		||||
                                    variant="outlined"
 | 
			
		||||
                                    size="small"
 | 
			
		||||
                                    helperText={errors.name}
 | 
			
		||||
                                    onChange={v =>
 | 
			
		||||
                                        setValue('name', v.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                                <TextField
 | 
			
		||||
                                    className={commonStyles.fullwidth}
 | 
			
		||||
                                    placeholder="A short description"
 | 
			
		||||
                                    maxRows={2}
 | 
			
		||||
                                    label="Description"
 | 
			
		||||
                                    error={!!errors.description}
 | 
			
		||||
                                    helperText={errors.description}
 | 
			
		||||
                                    variant="outlined"
 | 
			
		||||
                                    size="small"
 | 
			
		||||
                                    multiline
 | 
			
		||||
                                    value={project.description}
 | 
			
		||||
                                    onChange={v =>
 | 
			
		||||
                                        setValue('description', v.target.value)
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
 | 
			
		||||
                                <ConditionallyRender
 | 
			
		||||
                                    condition={
 | 
			
		||||
                                        hasAccess(CREATE_PROJECT) && !editMode
 | 
			
		||||
                                    }
 | 
			
		||||
                                    show={
 | 
			
		||||
                                        <div className={styles.formButtons}>
 | 
			
		||||
                                            <FormButtons
 | 
			
		||||
                                                submitText={submitText}
 | 
			
		||||
                                                onCancel={onCancel}
 | 
			
		||||
                                            />
 | 
			
		||||
                                        </div>
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
 | 
			
		||||
                                <ConditionallyRender
 | 
			
		||||
                                    condition={editMode}
 | 
			
		||||
                                    show={
 | 
			
		||||
                                        <PermissionButton
 | 
			
		||||
                                            permission={UPDATE_PROJECT}
 | 
			
		||||
                                            projectId={props.project.id}
 | 
			
		||||
                                            type="submit"
 | 
			
		||||
                                            style={{ marginTop: '1rem' }}
 | 
			
		||||
                                        >
 | 
			
		||||
                                            Update project
 | 
			
		||||
                                        </PermissionButton>
 | 
			
		||||
                                    }
 | 
			
		||||
                                />
 | 
			
		||||
                            </form>
 | 
			
		||||
                        </>
 | 
			
		||||
                    }
 | 
			
		||||
                />
 | 
			
		||||
            </PageContent>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
ProjectFormComponent.propTypes = {
 | 
			
		||||
    project: PropTypes.object.isRequired,
 | 
			
		||||
    validateId: PropTypes.func.isRequired,
 | 
			
		||||
    submit: PropTypes.func.isRequired,
 | 
			
		||||
    history: PropTypes.object.isRequired,
 | 
			
		||||
    editMode: PropTypes.bool.isRequired,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default ProjectFormComponent;
 | 
			
		||||
@ -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
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,64 @@
 | 
			
		||||
import useAPI from '../useApi/useApi';
 | 
			
		||||
 | 
			
		||||
interface ICreatePayload {
 | 
			
		||||
    id: string;
 | 
			
		||||
    name: string;
 | 
			
		||||
    description: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useProjectApi = () => {
 | 
			
		||||
    const { makeRequest, createRequest, errors } = useAPI({
 | 
			
		||||
    const { makeRequest, createRequest, errors, loading } = useAPI({
 | 
			
		||||
        propagateErrors: true,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    const createProject = async (payload: ICreatePayload) => {
 | 
			
		||||
        const path = `api/admin/projects`;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(payload),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await makeRequest(req.caller, req.id);
 | 
			
		||||
 | 
			
		||||
            return res;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const validateId = async (payload: ICreatePayload) => {
 | 
			
		||||
        const path = `api/admin/projects/validate`;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
            body: JSON.stringify(payload),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await makeRequest(req.caller, req.id);
 | 
			
		||||
 | 
			
		||||
            return res;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const editProject = async (id: string, payload: ICreatePayload) => {
 | 
			
		||||
        const path = `api/admin/projects/${id}`;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'PUT',
 | 
			
		||||
            body: JSON.stringify(payload),
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await makeRequest(req.caller, req.id);
 | 
			
		||||
 | 
			
		||||
            return res;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const deleteProject = async (projectId: string) => {
 | 
			
		||||
        const path = `api/admin/projects/${projectId}`;
 | 
			
		||||
        const req = createRequest(path, { method: 'DELETE' });
 | 
			
		||||
@ -18,7 +72,10 @@ const useProjectApi = () => {
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const addEnvironmentToProject = async (projectId: string, environment: string) => {
 | 
			
		||||
    const addEnvironmentToProject = async (
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        environment: string
 | 
			
		||||
    ) => {
 | 
			
		||||
        const path = `api/admin/projects/${projectId}/environments`;
 | 
			
		||||
        const req = createRequest(path, {
 | 
			
		||||
            method: 'POST',
 | 
			
		||||
@ -32,9 +89,12 @@ const useProjectApi = () => {
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const removeEnvironmentFromProject = async (projectId: string, environment: string) => {
 | 
			
		||||
    const removeEnvironmentFromProject = async (
 | 
			
		||||
        projectId: string,
 | 
			
		||||
        environment: string
 | 
			
		||||
    ) => {
 | 
			
		||||
        const path = `api/admin/projects/${projectId}/environments/${environment}`;
 | 
			
		||||
        const req = createRequest(path, { method: 'DELETE' });
 | 
			
		||||
 | 
			
		||||
@ -45,9 +105,18 @@ const useProjectApi = () => {
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
            throw e;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return { deleteProject, addEnvironmentToProject, removeEnvironmentFromProject, errors };
 | 
			
		||||
    return {
 | 
			
		||||
        createProject,
 | 
			
		||||
        validateId,
 | 
			
		||||
        editProject,
 | 
			
		||||
        deleteProject,
 | 
			
		||||
        addEnvironmentToProject,
 | 
			
		||||
        removeEnvironmentFromProject,
 | 
			
		||||
        errors,
 | 
			
		||||
        loading,
 | 
			
		||||
    };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default useProjectApi;
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
    persist?: boolean;
 | 
			
		||||
    confetti?: boolean;
 | 
			
		||||
    autoHideDuration?: number;
 | 
			
		||||
    show?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const useToast = () => {
 | 
			
		||||
 | 
			
		||||
@ -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"
 | 
			
		||||
 | 
			
		||||
@ -2037,6 +2037,11 @@
 | 
			
		||||
  resolved "https://registry.npmjs.org/@types/history/-/history-4.7.8.tgz"
 | 
			
		||||
  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":
 | 
			
		||||
  version "3.3.1"
 | 
			
		||||
  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"
 | 
			
		||||
  integrity sha512-XYmBiy+ohOR4Lh5jE379fV2IU+6Jn4g5qASinhitfyO71b/sCo6MKsMLF5tc7Zf2CE8hViVQyYSobJNke8OvUw==
 | 
			
		||||
 | 
			
		||||
"@types/node@14.18.7":
 | 
			
		||||
  version "14.18.7"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.7.tgz#bf973dbd8e156dbf860504a8811033cbd26967d1"
 | 
			
		||||
  integrity sha512-UpLEO1iBG7esNPusSAjoZhWFK5Mfd8QfwWhHRrg5io13POn/stsBgTCba9suQaFflNA4tc0+6AFM3R6BZNng6A==
 | 
			
		||||
"@types/node@14.18.9":
 | 
			
		||||
  version "14.18.9"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.9.tgz#0e5944eefe2b287391279a19b407aa98bd14436d"
 | 
			
		||||
  integrity sha512-j11XSuRuAlft6vLDEX4RvhqC0KxNxx6QIyMXNb0vHHSNPXTPeiy3algESWmOOIzEtiEL0qiowPU3ewW9hHVa7Q==
 | 
			
		||||
 | 
			
		||||
"@types/node@^14.14.31":
 | 
			
		||||
  version "14.17.19"
 | 
			
		||||
@ -2164,12 +2169,12 @@
 | 
			
		||||
    hoist-non-react-statics "^3.3.0"
 | 
			
		||||
    redux "^4.0.0"
 | 
			
		||||
 | 
			
		||||
"@types/react-router-dom@5.3.2":
 | 
			
		||||
  version "5.3.2"
 | 
			
		||||
  resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.2.tgz"
 | 
			
		||||
  integrity sha512-ELEYRUie2czuJzaZ5+ziIp9Hhw+juEw8b7C11YNA4QdLCVbQ3qLi2l4aq8XnlqM7V31LZX8dxUuFUCrzHm6sqQ==
 | 
			
		||||
"@types/react-router-dom@5.3.3":
 | 
			
		||||
  version "5.3.3"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz#e9d6b4a66fcdbd651a5f106c2656a30088cc1e83"
 | 
			
		||||
  integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    "@types/history" "*"
 | 
			
		||||
    "@types/history" "^4.7.11"
 | 
			
		||||
    "@types/react" "*"
 | 
			
		||||
    "@types/react-router" "*"
 | 
			
		||||
 | 
			
		||||
@ -11265,10 +11270,10 @@ sass-loader@^10.0.5:
 | 
			
		||||
    schema-utils "^3.0.0"
 | 
			
		||||
    semver "^7.3.2"
 | 
			
		||||
 | 
			
		||||
sass@1.48.0:
 | 
			
		||||
  version "1.48.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.48.0.tgz#b53cfccc1b8ab4be375cc54f306fda9d4711162c"
 | 
			
		||||
  integrity sha512-hQi5g4DcfjcipotoHZ80l7GNJHGqQS5LwMBjVYB/TaT0vcSSpbgM8Ad7cgfsB2M0MinbkEQQPO9+sjjSiwxqmw==
 | 
			
		||||
sass@1.49.0:
 | 
			
		||||
  version "1.49.0"
 | 
			
		||||
  resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.0.tgz#65ec1b1d9a6bc1bae8d2c9d4b392c13f5d32c078"
 | 
			
		||||
  integrity sha512-TVwVdNDj6p6b4QymJtNtRS2YtLJ/CqZriGg0eIAbAKMlN8Xy6kbv33FsEZSF7FufFFM705SQviHjjThfaQ4VNw==
 | 
			
		||||
  dependencies:
 | 
			
		||||
    chokidar ">=3.0.0 <4.0.0"
 | 
			
		||||
    immutable "^4.0.0"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user