mirror of
				https://github.com/Unleash/unleash.git
				synced 2025-10-27 11:02:16 +01:00 
			
		
		
		
	feat: add create and edit screen for tag-types (NEW) (#603)
* feat: add create and edit screen for tag-types * feat: update Edit and create component with permissions * refactor: add TagForm type to react FC * fix: routes * fix: add edit button * fix: update snapshot * fix: update permission * fix: permission Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
This commit is contained in:
		
							parent
							
								
									80e80805f7
								
							
						
					
					
						commit
						7baf8400ca
					
				| @ -263,15 +263,6 @@ Array [ | |||||||
|     "title": "Tag types", |     "title": "Tag types", | ||||||
|     "type": "protected", |     "type": "protected", | ||||||
|   }, |   }, | ||||||
|   Object { |  | ||||||
|     "component": [Function], |  | ||||||
|     "layout": "main", |  | ||||||
|     "menu": Object {}, |  | ||||||
|     "parent": "/tags", |  | ||||||
|     "path": "/tags/create", |  | ||||||
|     "title": "Create", |  | ||||||
|     "type": "protected", |  | ||||||
|   }, |  | ||||||
|   Object { |   Object { | ||||||
|     "component": [Function], |     "component": [Function], | ||||||
|     "layout": "main", |     "layout": "main", | ||||||
|  | |||||||
| @ -15,10 +15,6 @@ import CreateContextField from '../../page/context/create'; | |||||||
| import EditContextField from '../../page/context/edit'; | import EditContextField from '../../page/context/edit'; | ||||||
| import CreateProject from '../../page/project/create'; | import CreateProject from '../../page/project/create'; | ||||||
| import ListTagTypes from '../../page/tag-types'; | import ListTagTypes from '../../page/tag-types'; | ||||||
| import CreateTagType from '../../page/tag-types/create'; |  | ||||||
| import EditTagType from '../../page/tag-types/edit'; |  | ||||||
| import ListTags from '../../page/tags'; |  | ||||||
| import CreateTag from '../../page/tags/create'; |  | ||||||
| import Addons from '../../page/addons'; | import Addons from '../../page/addons'; | ||||||
| import AddonsCreate from '../../page/addons/create'; | import AddonsCreate from '../../page/addons/create'; | ||||||
| import AddonsEdit from '../../page/addons/edit'; | import AddonsEdit from '../../page/addons/edit'; | ||||||
| @ -48,6 +44,8 @@ import CreateApiToken from '../admin/api-token/CreateApiToken/CreateApiToken'; | |||||||
| import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment'; | import CreateEnvironment from '../environments/CreateEnvironment/CreateEnvironment'; | ||||||
| import EditEnvironment from '../environments/EditEnvironment/EditEnvironment'; | import EditEnvironment from '../environments/EditEnvironment/EditEnvironment'; | ||||||
| 
 | 
 | ||||||
|  | import EditTagType from '../tagTypes/EditTagType/EditTagType'; | ||||||
|  | import CreateTagType from '../tagTypes/CreateTagType/CreateTagType'; | ||||||
| 
 | 
 | ||||||
| export const routes = [ | export const routes = [ | ||||||
|     // Project
 |     // Project
 | ||||||
| @ -304,24 +302,6 @@ export const routes = [ | |||||||
|         layout: 'main', |         layout: 'main', | ||||||
|         menu: { mobile: true, advanced: true }, |         menu: { mobile: true, advanced: true }, | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|         path: '/tags/create', |  | ||||||
|         parent: '/tags', |  | ||||||
|         title: 'Create', |  | ||||||
|         component: CreateTag, |  | ||||||
|         type: 'protected', |  | ||||||
|         layout: 'main', |  | ||||||
|         menu: {}, |  | ||||||
|     }, |  | ||||||
|     { |  | ||||||
|         path: '/tags', |  | ||||||
|         title: 'Tags', |  | ||||||
|         component: ListTags, |  | ||||||
|         hidden: true, |  | ||||||
|         type: 'protected', |  | ||||||
|         layout: 'main', |  | ||||||
|         menu: {}, |  | ||||||
|     }, |  | ||||||
| 
 | 
 | ||||||
|     // Addons
 |     // Addons
 | ||||||
|     { |     { | ||||||
|  | |||||||
| @ -13,7 +13,6 @@ export const DELETE_CONTEXT_FIELD = 'DELETE_CONTEXT_FIELD'; | |||||||
| export const CREATE_PROJECT = 'CREATE_PROJECT'; | export const CREATE_PROJECT = 'CREATE_PROJECT'; | ||||||
| export const UPDATE_PROJECT = 'UPDATE_PROJECT'; | export const UPDATE_PROJECT = 'UPDATE_PROJECT'; | ||||||
| export const DELETE_PROJECT = 'DELETE_PROJECT'; | export const DELETE_PROJECT = 'DELETE_PROJECT'; | ||||||
| export const CREATE_TAG_TYPE = 'CREATE_TAG_TYPE'; |  | ||||||
| export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; | export const DELETE_TAG_TYPE = 'DELETE_TAG_TYPE'; | ||||||
| export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; | export const UPDATE_TAG_TYPE = 'UPDATE_TAG_TYPE'; | ||||||
| export const CREATE_TAG = 'CREATE_TAG'; | export const CREATE_TAG = 'CREATE_TAG'; | ||||||
|  | |||||||
| @ -2,6 +2,10 @@ | |||||||
|     min-width: 100px; |     min-width: 100px; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .icon { | ||||||
|  |     fill: #757575; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .textfield { | .textfield { | ||||||
|     margin-left: 15px; |     margin-left: 15px; | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,7 +1,6 @@ | |||||||
| import { useContext, useEffect, useState } from 'react'; | import { useContext, useState } from 'react'; | ||||||
| import PropTypes from 'prop-types'; | import PropTypes from 'prop-types'; | ||||||
| import { Link, useHistory } from 'react-router-dom'; | import { Link, useHistory } from 'react-router-dom'; | ||||||
| 
 |  | ||||||
| import { | import { | ||||||
|     List, |     List, | ||||||
|     ListItem, |     ListItem, | ||||||
| @ -11,38 +10,53 @@ import { | |||||||
|     Button, |     Button, | ||||||
|     Tooltip, |     Tooltip, | ||||||
| } from '@material-ui/core'; | } from '@material-ui/core'; | ||||||
| import { Add, Delete, Label } from '@material-ui/icons'; | import { Add, Delete, Edit, Label } from '@material-ui/icons'; | ||||||
| 
 |  | ||||||
| import HeaderTitle from '../../common/HeaderTitle'; | import HeaderTitle from '../../common/HeaderTitle'; | ||||||
| import PageContent from '../../common/PageContent/PageContent'; | import PageContent from '../../common/PageContent/PageContent'; | ||||||
| import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; | import ConditionallyRender from '../../common/ConditionallyRender/ConditionallyRender'; | ||||||
| import { | import { | ||||||
|     CREATE_TAG_TYPE, |  | ||||||
|     DELETE_TAG_TYPE, |     DELETE_TAG_TYPE, | ||||||
|  |     UPDATE_TAG_TYPE, | ||||||
| } from '../../providers/AccessProvider/permissions'; | } from '../../providers/AccessProvider/permissions'; | ||||||
| import Dialogue from '../../common/Dialogue/Dialogue'; | import Dialogue from '../../common/Dialogue/Dialogue'; | ||||||
| import useMediaQuery from '@material-ui/core/useMediaQuery'; | import useMediaQuery from '@material-ui/core/useMediaQuery'; | ||||||
| 
 |  | ||||||
| import styles from '../TagType.module.scss'; | import styles from '../TagType.module.scss'; | ||||||
| import AccessContext from '../../../contexts/AccessContext'; | import AccessContext from '../../../contexts/AccessContext'; | ||||||
|  | import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||||
|  | import useTagTypes from '../../../hooks/api/getters/useTagTypes/useTagTypes'; | ||||||
|  | import useToast from '../../../hooks/useToast'; | ||||||
|  | import PermissionIconButton from '../../common/PermissionIconButton/PermissionIconButton'; | ||||||
| 
 | 
 | ||||||
| const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | const TagTypeList = () => { | ||||||
|     const { hasAccess } = useContext(AccessContext); |     const { hasAccess } = useContext(AccessContext); | ||||||
|     const [deletion, setDeletion] = useState({ open: false }); |     const [deletion, setDeletion] = useState({ open: false }); | ||||||
|     const history = useHistory(); |     const history = useHistory(); | ||||||
|     const smallScreen = useMediaQuery('(max-width:700px)'); |     const smallScreen = useMediaQuery('(max-width:700px)'); | ||||||
|  |     const { deleteTagType } = useTagTypesApi(); | ||||||
|  |     const { tagTypes, refetch } = useTagTypes(); | ||||||
|  |     const { setToastData, setToastApiError } = useToast(); | ||||||
| 
 | 
 | ||||||
|     useEffect(() => { |     const deleteTag = async () => { | ||||||
|         fetchTagTypes(); |         try { | ||||||
|         // eslint-disable-next-line react-hooks/exhaustive-deps |             await deleteTagType(deletion.name); | ||||||
|     }, []); |             refetch(); | ||||||
|  |             setDeletion({ open: false }); | ||||||
|  |             setToastData({ | ||||||
|  |                 type: 'success', | ||||||
|  |                 show: true, | ||||||
|  |                 text: 'Successfully deleted tag type.', | ||||||
|  |             }); | ||||||
|  |         } catch (e) { | ||||||
|  |             setToastApiError(e.toString()); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
| 
 | 
 | ||||||
|     let header = ( |     let header = ( | ||||||
|         <HeaderTitle |         <HeaderTitle | ||||||
|             title="Tag Types" |             title="Tag Types" | ||||||
|             actions={ |             actions={ | ||||||
|                 <ConditionallyRender |                 <ConditionallyRender | ||||||
|                     condition={hasAccess(CREATE_TAG_TYPE)} |                     condition={hasAccess(UPDATE_TAG_TYPE)} | ||||||
|                     show={ |                     show={ | ||||||
|                         <ConditionallyRender |                         <ConditionallyRender | ||||||
|                             condition={smallScreen} |                             condition={smallScreen} | ||||||
| @ -96,6 +110,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | |||||||
|                 </IconButton> |                 </IconButton> | ||||||
|             </Tooltip> |             </Tooltip> | ||||||
|         ); |         ); | ||||||
|  | 
 | ||||||
|         return ( |         return ( | ||||||
|             <ListItem |             <ListItem | ||||||
|                 key={`${tagType.name}`} |                 key={`${tagType.name}`} | ||||||
| @ -105,6 +120,13 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | |||||||
|                     <Label /> |                     <Label /> | ||||||
|                 </ListItemIcon> |                 </ListItemIcon> | ||||||
|                 <ListItemText primary={link} secondary={tagType.description} /> |                 <ListItemText primary={link} secondary={tagType.description} /> | ||||||
|  |                 <PermissionIconButton | ||||||
|  |                     permission={UPDATE_TAG_TYPE} | ||||||
|  |                     component={Link} | ||||||
|  |                     to={`/tag-types/edit/${tagType.name}`} | ||||||
|  |                 > | ||||||
|  |                     <Edit className={styles.icon} /> | ||||||
|  |                 </PermissionIconButton> | ||||||
|                 <ConditionallyRender |                 <ConditionallyRender | ||||||
|                     condition={hasAccess(DELETE_TAG_TYPE)} |                     condition={hasAccess(DELETE_TAG_TYPE)} | ||||||
|                     show={deleteButton} |                     show={deleteButton} | ||||||
| @ -124,10 +146,7 @@ const TagTypeList = ({ tagTypes, fetchTagTypes, removeTagType }) => { | |||||||
|             <Dialogue |             <Dialogue | ||||||
|                 title="Really delete Tag type?" |                 title="Really delete Tag type?" | ||||||
|                 open={deletion.open} |                 open={deletion.open} | ||||||
|                 onClick={() => { |                 onClick={deleteTag} | ||||||
|                     removeTagType(deletion.name); |  | ||||||
|                     setDeletion({ open: false }); |  | ||||||
|                 }} |  | ||||||
|                 onClose={() => { |                 onClose={() => { | ||||||
|                     setDeletion({ open: false }); |                     setDeletion({ open: false }); | ||||||
|                 }} |                 }} | ||||||
|  | |||||||
| @ -38,44 +38,10 @@ exports[`renders a list with elements correctly 1`] = ` | |||||||
|       className="MuiList-root MuiList-padding" |       className="MuiList-root MuiList-padding" | ||||||
|     > |     > | ||||||
|       <li |       <li | ||||||
|         className="MuiListItem-root tagListItem MuiListItem-gutters" |         className="MuiListItem-root MuiListItem-gutters" | ||||||
|         disabled={false} |         disabled={false} | ||||||
|       > |       > | ||||||
|         <div |         No entries | ||||||
|           className="MuiListItemIcon-root" |  | ||||||
|         > |  | ||||||
|           <svg |  | ||||||
|             aria-hidden={true} |  | ||||||
|             className="MuiSvgIcon-root" |  | ||||||
|             focusable="false" |  | ||||||
|             viewBox="0 0 24 24" |  | ||||||
|           > |  | ||||||
|             <path |  | ||||||
|               d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z" |  | ||||||
|             /> |  | ||||||
|           </svg> |  | ||||||
|         </div> |  | ||||||
|         <div |  | ||||||
|           className="MuiListItemText-root MuiListItemText-multiline" |  | ||||||
|         > |  | ||||||
|           <span |  | ||||||
|             className="MuiTypography-root MuiListItemText-primary MuiTypography-body1 MuiTypography-displayBlock" |  | ||||||
|           > |  | ||||||
|             <a |  | ||||||
|               href="/tag-types/edit/simple" |  | ||||||
|               onClick={[Function]} |  | ||||||
|             > |  | ||||||
|               <strong> |  | ||||||
|                 simple |  | ||||||
|               </strong> |  | ||||||
|             </a> |  | ||||||
|           </span> |  | ||||||
|           <p |  | ||||||
|             className="MuiTypography-root MuiListItemText-secondary MuiTypography-body2 MuiTypography-colorTextSecondary MuiTypography-displayBlock" |  | ||||||
|           > |  | ||||||
|             Some simple description |  | ||||||
|           </p> |  | ||||||
|         </div> |  | ||||||
|       </li> |       </li> | ||||||
|     </ul> |     </ul> | ||||||
|   </div> |   </div> | ||||||
|  | |||||||
| @ -14,11 +14,13 @@ import { | |||||||
|     UPDATE_TAG_TYPE, |     UPDATE_TAG_TYPE, | ||||||
|     DELETE_TAG_TYPE, |     DELETE_TAG_TYPE, | ||||||
| } from '../../providers/AccessProvider/permissions'; | } from '../../providers/AccessProvider/permissions'; | ||||||
|  | import UIProvider from '../../providers/UIProvider/UIProvider'; | ||||||
| 
 | 
 | ||||||
| test('renders an empty list correctly', () => { | test('renders an empty list correctly', () => { | ||||||
|     const tree = renderer.create( |     const tree = renderer.create( | ||||||
|         <MemoryRouter> |         <MemoryRouter> | ||||||
|             <ThemeProvider theme={theme}> |             <ThemeProvider theme={theme}> | ||||||
|  |                 <UIProvider> | ||||||
|                     <AccessProvider |                     <AccessProvider | ||||||
|                         store={createFakeStore([{ permission: ADMIN }])} |                         store={createFakeStore([{ permission: ADMIN }])} | ||||||
|                     > |                     > | ||||||
| @ -29,6 +31,7 @@ test('renders an empty list correctly', () => { | |||||||
|                             history={{}} |                             history={{}} | ||||||
|                         /> |                         /> | ||||||
|                     </AccessProvider> |                     </AccessProvider> | ||||||
|  |                 </UIProvider> | ||||||
|             </ThemeProvider> |             </ThemeProvider> | ||||||
|         </MemoryRouter> |         </MemoryRouter> | ||||||
|     ); |     ); | ||||||
| @ -39,6 +42,7 @@ test('renders a list with elements correctly', () => { | |||||||
|     const tree = renderer.create( |     const tree = renderer.create( | ||||||
|         <ThemeProvider theme={theme}> |         <ThemeProvider theme={theme}> | ||||||
|             <MemoryRouter> |             <MemoryRouter> | ||||||
|  |                 <UIProvider> | ||||||
|                     <AccessProvider |                     <AccessProvider | ||||||
|                         store={createFakeStore([ |                         store={createFakeStore([ | ||||||
|                             { permission: CREATE_TAG_TYPE }, |                             { permission: CREATE_TAG_TYPE }, | ||||||
| @ -59,6 +63,7 @@ test('renders a list with elements correctly', () => { | |||||||
|                             history={{}} |                             history={{}} | ||||||
|                         /> |                         /> | ||||||
|                     </AccessProvider> |                     </AccessProvider> | ||||||
|  |                 </UIProvider> | ||||||
|             </MemoryRouter> |             </MemoryRouter> | ||||||
|         </ThemeProvider> |         </ThemeProvider> | ||||||
|     ); |     ); | ||||||
|  | |||||||
| @ -0,0 +1,91 @@ | |||||||
|  | import { useHistory } from 'react-router-dom'; | ||||||
|  | import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||||
|  | import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
|  | import useToast from '../../../hooks/useToast'; | ||||||
|  | import FormTemplate from '../../common/FormTemplate/FormTemplate'; | ||||||
|  | import PermissionButton from '../../common/PermissionButton/PermissionButton'; | ||||||
|  | import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; | ||||||
|  | import useTagForm from '../hooks/useTagForm'; | ||||||
|  | import TagTypeForm from '../TagTypeForm/TagTypeForm'; | ||||||
|  | 
 | ||||||
|  | const CreateTagType = () => { | ||||||
|  |     const { setToastData, setToastApiError } = useToast(); | ||||||
|  |     const { uiConfig } = useUiConfig(); | ||||||
|  |     const history = useHistory(); | ||||||
|  |     const { | ||||||
|  |         tagName, | ||||||
|  |         tagDesc, | ||||||
|  |         setTagName, | ||||||
|  |         setTagDesc, | ||||||
|  |         getTagPayload, | ||||||
|  |         validateNameUniqueness, | ||||||
|  |         errors, | ||||||
|  |         clearErrors, | ||||||
|  |     } = useTagForm(); | ||||||
|  |     const { createTag, loading } = useTagTypesApi(); | ||||||
|  | 
 | ||||||
|  |     const handleSubmit = async (e: Event) => { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         clearErrors(); | ||||||
|  |         const validName = await validateNameUniqueness(); | ||||||
|  |         if (validName) { | ||||||
|  |             const payload = getTagPayload(); | ||||||
|  |             try { | ||||||
|  |                 await createTag(payload); | ||||||
|  |                 history.push('/tag-types'); | ||||||
|  |                 setToastData({ | ||||||
|  |                     title: 'Tag type created', | ||||||
|  |                     confetti: true, | ||||||
|  |                     type: 'success', | ||||||
|  |                 }); | ||||||
|  |             } catch (e: any) { | ||||||
|  |                 setToastApiError(e.toString()); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const formatApiCode = () => { | ||||||
|  |         return `curl --location --request POST '${ | ||||||
|  |             uiConfig.unleashUrl | ||||||
|  |         }/api/admin/tag-types' \\ | ||||||
|  | --header 'Authorization: INSERT_API_KEY' \\ | ||||||
|  | --header 'Content-Type: application/json' \\ | ||||||
|  | --data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleCancel = () => { | ||||||
|  |         history.goBack(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <FormTemplate | ||||||
|  |             loading={loading} | ||||||
|  |             title="Create tag type" | ||||||
|  |             description="Tag types allow you to group tags together in the management UI" | ||||||
|  |             documentationLink="https://docs.getunleash.io/advanced/tags" | ||||||
|  |             formatApiCode={formatApiCode} | ||||||
|  |         > | ||||||
|  |             <TagTypeForm | ||||||
|  |                 errors={errors} | ||||||
|  |                 handleSubmit={handleSubmit} | ||||||
|  |                 handleCancel={handleCancel} | ||||||
|  |                 tagName={tagName} | ||||||
|  |                 setTagName={setTagName} | ||||||
|  |                 tagDesc={tagDesc} | ||||||
|  |                 setTagDesc={setTagDesc} | ||||||
|  |                 mode="Create" | ||||||
|  |                 clearErrors={clearErrors} | ||||||
|  |             > | ||||||
|  |                 <PermissionButton | ||||||
|  |                     onClick={handleSubmit} | ||||||
|  |                     permission={UPDATE_TAG_TYPE} | ||||||
|  |                     type="submit" | ||||||
|  |                 > | ||||||
|  |                     Create type | ||||||
|  |                 </PermissionButton> | ||||||
|  |             </TagTypeForm> | ||||||
|  |         </FormTemplate> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default CreateTagType; | ||||||
							
								
								
									
										89
									
								
								frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								frontend/src/component/tagTypes/EditTagType/EditTagType.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,89 @@ | |||||||
|  | import { useHistory, useParams } from 'react-router-dom'; | ||||||
|  | import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||||
|  | import useTagType from '../../../hooks/api/getters/useTagType/useTagType'; | ||||||
|  | import useUiConfig from '../../../hooks/api/getters/useUiConfig/useUiConfig'; | ||||||
|  | import useToast from '../../../hooks/useToast'; | ||||||
|  | import FormTemplate from '../../common/FormTemplate/FormTemplate'; | ||||||
|  | import PermissionButton from '../../common/PermissionButton/PermissionButton'; | ||||||
|  | import { UPDATE_TAG_TYPE } from '../../providers/AccessProvider/permissions'; | ||||||
|  | import useTagForm from '../hooks/useTagForm'; | ||||||
|  | import TagForm from '../TagTypeForm/TagTypeForm'; | ||||||
|  | 
 | ||||||
|  | const EditTagType = () => { | ||||||
|  |     const { setToastData, setToastApiError } = useToast(); | ||||||
|  |     const { uiConfig } = useUiConfig(); | ||||||
|  |     const history = useHistory(); | ||||||
|  |     const { name } = useParams<{ name: string }>(); | ||||||
|  |     const { tagType } = useTagType(name); | ||||||
|  |     const { | ||||||
|  |         tagName, | ||||||
|  |         tagDesc, | ||||||
|  |         setTagName, | ||||||
|  |         setTagDesc, | ||||||
|  |         getTagPayload, | ||||||
|  |         errors, | ||||||
|  |         clearErrors, | ||||||
|  |     } = useTagForm(tagType?.name, tagType?.description); | ||||||
|  |     const { updateTagType, loading } = useTagTypesApi(); | ||||||
|  | 
 | ||||||
|  |     const handleSubmit = async (e: Event) => { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         clearErrors(); | ||||||
|  |         const payload = getTagPayload(); | ||||||
|  |         try { | ||||||
|  |             await updateTagType(tagName, payload); | ||||||
|  |             history.push('/tag-types'); | ||||||
|  |             setToastData({ | ||||||
|  |                 title: 'Tag type updated', | ||||||
|  |                 type: 'success', | ||||||
|  |             }); | ||||||
|  |         } catch (e: any) { | ||||||
|  |             setToastApiError(e.toString()); | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const formatApiCode = () => { | ||||||
|  |         return `curl --location --request PUT '${ | ||||||
|  |             uiConfig.unleashUrl | ||||||
|  |         }/api/admin/tag-types/${name}' \\ | ||||||
|  | --header 'Authorization: INSERT_API_KEY' \\ | ||||||
|  | --header 'Content-Type: application/json' \\ | ||||||
|  | --data-raw '${JSON.stringify(getTagPayload(), undefined, 2)}'`;
 | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const handleCancel = () => { | ||||||
|  |         history.goBack(); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <FormTemplate | ||||||
|  |             loading={loading} | ||||||
|  |             title="Edit tag type" | ||||||
|  |             description="Tag types allow you to group tags together in the management UI" | ||||||
|  |             documentationLink="https://docs.getunleash.io/" | ||||||
|  |             formatApiCode={formatApiCode} | ||||||
|  |         > | ||||||
|  |             <TagForm | ||||||
|  |                 errors={errors} | ||||||
|  |                 handleSubmit={handleSubmit} | ||||||
|  |                 handleCancel={handleCancel} | ||||||
|  |                 tagName={tagName} | ||||||
|  |                 setTagName={setTagName} | ||||||
|  |                 tagDesc={tagDesc} | ||||||
|  |                 setTagDesc={setTagDesc} | ||||||
|  |                 mode="Edit" | ||||||
|  |                 clearErrors={clearErrors} | ||||||
|  |             > | ||||||
|  |                 <PermissionButton | ||||||
|  |                     onClick={handleSubmit} | ||||||
|  |                     permission={UPDATE_TAG_TYPE} | ||||||
|  |                     type="submit" | ||||||
|  |                 > | ||||||
|  |                     Edit type | ||||||
|  |                 </PermissionButton> | ||||||
|  |             </TagForm> | ||||||
|  |         </FormTemplate> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default EditTagType; | ||||||
| @ -0,0 +1,47 @@ | |||||||
|  | import { makeStyles } from '@material-ui/core/styles'; | ||||||
|  | 
 | ||||||
|  | export const useStyles = makeStyles(theme => ({ | ||||||
|  |     container: { | ||||||
|  |         maxWidth: '400px', | ||||||
|  |     }, | ||||||
|  |     form: { | ||||||
|  |         display: 'flex', | ||||||
|  |         flexDirection: 'column', | ||||||
|  |         height: '100%', | ||||||
|  |     }, | ||||||
|  |     input: { width: '100%', marginBottom: '1rem' }, | ||||||
|  |     label: { | ||||||
|  |         minWidth: '300px', | ||||||
|  |         [theme.breakpoints.down(600)]: { | ||||||
|  |             minWidth: 'auto', | ||||||
|  |         }, | ||||||
|  |     }, | ||||||
|  |     buttonContainer: { | ||||||
|  |         marginTop: 'auto', | ||||||
|  |         display: 'flex', | ||||||
|  |         justifyContent: 'flex-end', | ||||||
|  |     }, | ||||||
|  |     cancelButton: { | ||||||
|  |         marginRight: '1.5rem', | ||||||
|  |     }, | ||||||
|  |     inputDescription: { | ||||||
|  |         marginBottom: '0.5rem', | ||||||
|  |     }, | ||||||
|  |     formHeader: { | ||||||
|  |         fontWeight: 'normal', | ||||||
|  |         marginTop: '0', | ||||||
|  |     }, | ||||||
|  |     header: { | ||||||
|  |         fontWeight: 'normal', | ||||||
|  |     }, | ||||||
|  |     permissionErrorContainer: { | ||||||
|  |         position: 'relative', | ||||||
|  |     }, | ||||||
|  |     errorMessage: { | ||||||
|  |         //@ts-ignore
 | ||||||
|  |         fontSize: theme.fontSizes.smallBody, | ||||||
|  |         color: theme.palette.error.main, | ||||||
|  |         position: 'absolute', | ||||||
|  |         top: '-8px', | ||||||
|  |     }, | ||||||
|  | })); | ||||||
							
								
								
									
										77
									
								
								frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								frontend/src/component/tagTypes/TagTypeForm/TagTypeForm.tsx
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,77 @@ | |||||||
|  | import Input from '../../common/Input/Input'; | ||||||
|  | import { TextField, Button } from '@material-ui/core'; | ||||||
|  | 
 | ||||||
|  | import { useStyles } from './TagTypeForm.styles'; | ||||||
|  | import React from 'react'; | ||||||
|  | import { trim } from '../../common/util'; | ||||||
|  | import { EDIT } from '../../../constants/misc'; | ||||||
|  | 
 | ||||||
|  | interface ITagTypeForm { | ||||||
|  |     tagName: string; | ||||||
|  |     tagDesc: string; | ||||||
|  |     setTagName: React.Dispatch<React.SetStateAction<string>>; | ||||||
|  |     setTagDesc: React.Dispatch<React.SetStateAction<string>>; | ||||||
|  |     handleSubmit: (e: any) => void; | ||||||
|  |     handleCancel: () => void; | ||||||
|  |     errors: { [key: string]: string }; | ||||||
|  |     mode: string; | ||||||
|  |     clearErrors: () => void; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | const TagTypeForm: React.FC<ITagTypeForm> = ({ | ||||||
|  |     children, | ||||||
|  |     handleSubmit, | ||||||
|  |     handleCancel, | ||||||
|  |     tagName, | ||||||
|  |     tagDesc, | ||||||
|  |     setTagName, | ||||||
|  |     setTagDesc, | ||||||
|  |     errors, | ||||||
|  |     mode, | ||||||
|  |     clearErrors, | ||||||
|  | }) => { | ||||||
|  |     const styles = useStyles(); | ||||||
|  | 
 | ||||||
|  |     return ( | ||||||
|  |         <form onSubmit={handleSubmit} className={styles.form}> | ||||||
|  |             <h3 className={styles.formHeader}>Tag information</h3> | ||||||
|  | 
 | ||||||
|  |             <div className={styles.container}> | ||||||
|  |                 <p className={styles.inputDescription}> | ||||||
|  |                     What is your tag name? | ||||||
|  |                 </p> | ||||||
|  |                 <Input | ||||||
|  |                     className={styles.input} | ||||||
|  |                     label="Tag name" | ||||||
|  |                     value={tagName} | ||||||
|  |                     onChange={e => setTagName(trim(e.target.value))} | ||||||
|  |                     error={Boolean(errors.name)} | ||||||
|  |                     errorText={errors.name} | ||||||
|  |                     onFocus={() => clearErrors()} | ||||||
|  |                     disabled={mode === EDIT} | ||||||
|  |                 /> | ||||||
|  | 
 | ||||||
|  |                 <p className={styles.inputDescription}> | ||||||
|  |                     What is this role for? | ||||||
|  |                 </p> | ||||||
|  |                 <TextField | ||||||
|  |                     className={styles.input} | ||||||
|  |                     label="Tag description" | ||||||
|  |                     variant="outlined" | ||||||
|  |                     multiline | ||||||
|  |                     maxRows={4} | ||||||
|  |                     value={tagDesc} | ||||||
|  |                     onChange={e => setTagDesc(e.target.value)} | ||||||
|  |                 /> | ||||||
|  |             </div> | ||||||
|  |             <div className={styles.buttonContainer}> | ||||||
|  |                 <Button onClick={handleCancel} className={styles.cancelButton}> | ||||||
|  |                     Cancel | ||||||
|  |                 </Button> | ||||||
|  |                 {children} | ||||||
|  |             </div> | ||||||
|  |         </form> | ||||||
|  |     ); | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default TagTypeForm; | ||||||
							
								
								
									
										69
									
								
								frontend/src/component/tagTypes/hooks/useTagForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								frontend/src/component/tagTypes/hooks/useTagForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | import { useEffect, useState } from 'react'; | ||||||
|  | import useTagTypesApi from '../../../hooks/api/actions/useTagTypesApi/useTagTypesApi'; | ||||||
|  | 
 | ||||||
|  | const useTagForm = (initialTagName = '', initialTagDesc = '') => { | ||||||
|  |     const [tagName, setTagName] = useState(initialTagName); | ||||||
|  |     const [tagDesc, setTagDesc] = useState(initialTagDesc); | ||||||
|  |     const [errors, setErrors] = useState({}); | ||||||
|  |     const { validateTagName } = useTagTypesApi(); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         setTagName(initialTagName); | ||||||
|  |     }, [initialTagName]); | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         setTagDesc(initialTagDesc); | ||||||
|  |     }, [initialTagDesc]); | ||||||
|  | 
 | ||||||
|  |     const getTagPayload = () => { | ||||||
|  |         return { | ||||||
|  |             name: tagName, | ||||||
|  |             description: tagDesc, | ||||||
|  |         }; | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const NAME_EXISTS_ERROR = | ||||||
|  |         'There already exists a tag-type with the name simple'; | ||||||
|  |     const validateNameUniqueness = async () => { | ||||||
|  |         if (tagName.length === 0) { | ||||||
|  |             setErrors(prev => ({ ...prev, name: 'Name can not be empty.' })); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         if (tagName.length < 2) { | ||||||
|  |             setErrors(prev => ({ | ||||||
|  |                 ...prev, | ||||||
|  |                 name: 'Tag name length must be at least 2 characters long', | ||||||
|  |             })); | ||||||
|  |             return false; | ||||||
|  |         } | ||||||
|  |         try { | ||||||
|  |             await validateTagName(tagName); | ||||||
|  |             return true; | ||||||
|  |         } catch (e: any) { | ||||||
|  |             if (e.toString().includes(NAME_EXISTS_ERROR)) { | ||||||
|  |                 setErrors(prev => ({ | ||||||
|  |                     ...prev, | ||||||
|  |                     name: NAME_EXISTS_ERROR, | ||||||
|  |                 })); | ||||||
|  |                 return false; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const clearErrors = () => { | ||||||
|  |         setErrors({}); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         tagName, | ||||||
|  |         tagDesc, | ||||||
|  |         setTagName, | ||||||
|  |         setTagDesc, | ||||||
|  |         getTagPayload, | ||||||
|  |         clearErrors, | ||||||
|  |         validateNameUniqueness, | ||||||
|  |         errors, | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default useTagForm; | ||||||
| @ -50,6 +50,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | |||||||
|                 <Label /> |                 <Label /> | ||||||
|             </ListItemIcon> |             </ListItemIcon> | ||||||
|             <ListItemText primary={tag.value} secondary={tag.type} /> |             <ListItemText primary={tag.value} secondary={tag.type} /> | ||||||
|  | 
 | ||||||
|             <ConditionallyRender |             <ConditionallyRender | ||||||
|                 condition={hasAccess(DELETE_TAG)} |                 condition={hasAccess(DELETE_TAG)} | ||||||
|                 show={<DeleteButton tagType={tag.type} tagValue={tag.value} />} |                 show={<DeleteButton tagType={tag.type} tagValue={tag.value} />} | ||||||
| @ -81,7 +82,7 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | |||||||
|                     show={ |                     show={ | ||||||
|                         <IconButton |                         <IconButton | ||||||
|                             aria-label="add tag" |                             aria-label="add tag" | ||||||
|                             onClick={() => history.push('/tags/create')} |                             onClick={() => history.push('/tag-types/create')} | ||||||
|                         > |                         > | ||||||
|                             <Add /> |                             <Add /> | ||||||
|                         </IconButton> |                         </IconButton> | ||||||
| @ -91,7 +92,9 @@ const TagList = ({ tags, fetchTags, removeTag }) => { | |||||||
|                             <Button |                             <Button | ||||||
|                                 color="primary" |                                 color="primary" | ||||||
|                                 startIcon={<Add />} |                                 startIcon={<Add />} | ||||||
|                                 onClick={() => history.push('/tags/create')} |                                 onClick={() => | ||||||
|  |                                     history.push('/tag-types/create') | ||||||
|  |                                 } | ||||||
|                                 variant="contained" |                                 variant="contained" | ||||||
|                             > |                             > | ||||||
|                                 Add new tag |                                 Add new tag | ||||||
|  | |||||||
| @ -0,0 +1,75 @@ | |||||||
|  | import { ITagPayload } from '../../../../interfaces/tags'; | ||||||
|  | import useAPI from '../useApi/useApi'; | ||||||
|  | 
 | ||||||
|  | const useTagTypesApi = () => { | ||||||
|  |     const { makeRequest, createRequest, errors, loading } = useAPI({ | ||||||
|  |         propagateErrors: true, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const createTag = async (payload: ITagPayload) => { | ||||||
|  |         const path = `api/admin/tag-types`; | ||||||
|  |         const req = createRequest(path, { | ||||||
|  |             method: 'POST', | ||||||
|  |             body: JSON.stringify(payload), | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const res = await makeRequest(req.caller, req.id); | ||||||
|  | 
 | ||||||
|  |             return res; | ||||||
|  |         } catch (e) { | ||||||
|  |             throw e; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const validateTagName = async (name: string) => { | ||||||
|  |         const path = `api/admin/tag-types/validate`; | ||||||
|  |         const req = createRequest(path, { | ||||||
|  |             method: 'POST', | ||||||
|  |             body: JSON.stringify({ name }), | ||||||
|  |         }); | ||||||
|  |         try { | ||||||
|  |             const res = await makeRequest(req.caller, req.id); | ||||||
|  |             return res; | ||||||
|  |         } catch (e) { | ||||||
|  |             throw e; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  |     const updateTagType = async (tagName: string, payload: ITagPayload) => { | ||||||
|  |         const path = `api/admin/tag-types/${tagName}`; | ||||||
|  |         const req = createRequest(path, { | ||||||
|  |             method: 'PUT', | ||||||
|  |             body: JSON.stringify(payload), | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const res = await makeRequest(req.caller, req.id); | ||||||
|  |             return res; | ||||||
|  |         } catch (e) { | ||||||
|  |             throw e; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const deleteTagType = async (tagName: string) => { | ||||||
|  |         const path = `api/admin/tag-types/${tagName}`; | ||||||
|  |         const req = createRequest(path, { method: 'DELETE' }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const res = await makeRequest(req.caller, req.id); | ||||||
|  |             return res; | ||||||
|  |         } catch (e) { | ||||||
|  |             throw e; | ||||||
|  |         } | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         createTag, | ||||||
|  |         validateTagName, | ||||||
|  |         updateTagType, | ||||||
|  |         deleteTagType, | ||||||
|  |         errors, | ||||||
|  |         loading | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default useTagTypesApi; | ||||||
							
								
								
									
										41
									
								
								frontend/src/hooks/api/getters/useTagType/useTagType.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								frontend/src/hooks/api/getters/useTagType/useTagType.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,41 @@ | |||||||
|  | import useSWR, { mutate, SWRConfiguration } from 'swr'; | ||||||
|  | import { useState, useEffect } from 'react'; | ||||||
|  | import { formatApiPath } from '../../../../utils/format-path'; | ||||||
|  | import handleErrorResponses from '../httpErrorResponseHandler'; | ||||||
|  | 
 | ||||||
|  | const useTagType = (name: string, options: SWRConfiguration = {}) => { | ||||||
|  |     const fetcher = async () => { | ||||||
|  |         const path = formatApiPath(`api/admin/tag-types/${name}`); | ||||||
|  |         return fetch(path, { | ||||||
|  |             method: 'GET', | ||||||
|  |         }) | ||||||
|  |             .then(handleErrorResponses('Tag data')) | ||||||
|  |             .then(res => res.json()); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     const FEATURE_CACHE_KEY = `api/admin/tag-types/${name}`; | ||||||
|  | 
 | ||||||
|  |     const { data, error } = useSWR(FEATURE_CACHE_KEY, fetcher, { | ||||||
|  |         ...options, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     const [loading, setLoading] = useState(!error && !data); | ||||||
|  | 
 | ||||||
|  |     const refetch = () => { | ||||||
|  |         mutate(FEATURE_CACHE_KEY); | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     useEffect(() => { | ||||||
|  |         setLoading(!error && !data); | ||||||
|  |     }, [data, error]); | ||||||
|  | 
 | ||||||
|  |     return { | ||||||
|  |         tagType: data?.tagType || { name: '', description: '' }, | ||||||
|  |         error, | ||||||
|  |         loading, | ||||||
|  |         refetch, | ||||||
|  |         FEATURE_CACHE_KEY, | ||||||
|  |     }; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | export default useTagType; | ||||||
| @ -8,3 +8,8 @@ export interface ITagType { | |||||||
|     description: string; |     description: string; | ||||||
|     icon: string; |     icon: string; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export interface ITagPayload { | ||||||
|  |     name: string; | ||||||
|  |     description: string; | ||||||
|  | } | ||||||
|  | |||||||
| @ -10,7 +10,6 @@ | |||||||
|     "skipLibCheck": true, |     "skipLibCheck": true, | ||||||
|     "esModuleInterop": true, |     "esModuleInterop": true, | ||||||
|     "allowSyntheticDefaultImports": true, |     "allowSyntheticDefaultImports": true, | ||||||
|     "strict": true, |  | ||||||
|     "forceConsistentCasingInFileNames": true, |     "forceConsistentCasingInFileNames": true, | ||||||
|     "noFallthroughCasesInSwitch": true, |     "noFallthroughCasesInSwitch": true, | ||||||
|     "module": "esnext", |     "module": "esnext", | ||||||
| @ -18,7 +17,8 @@ | |||||||
|     "resolveJsonModule": true, |     "resolveJsonModule": true, | ||||||
|     "isolatedModules": true, |     "isolatedModules": true, | ||||||
|     "noEmit": true, |     "noEmit": true, | ||||||
|     "jsx": "react-jsx" |     "jsx": "react-jsx", | ||||||
|  |     "strict": true | ||||||
|   }, |   }, | ||||||
|   "include": [ |   "include": [ | ||||||
|     "src" |     "src" | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user