mirror of
				https://github.com/inventree/InvenTree.git
				synced 2025-10-31 13:15:43 +00:00 
			
		
		
		
	feat(frontend): add grouping in the admin center (#9596)
* feat(frontend): Add grouping in the admin center * fix name * make sections more distinct * re-arrange sections * ensure plugin panels work * fix missing key issue * fix test name
This commit is contained in:
		| @@ -13,3 +13,10 @@ export type PanelType = { | |||||||
|   disabled?: boolean; |   disabled?: boolean; | ||||||
|   showHeadline?: boolean; |   showHeadline?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export type PanelGroupType = { | ||||||
|  |   id: string; | ||||||
|  |   label: string; | ||||||
|  |   panelIDs?: string[]; | ||||||
|  |   panels?: PanelType[]; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,11 +1,13 @@ | |||||||
| import { | import { | ||||||
|   ActionIcon, |   ActionIcon, | ||||||
|  |   Box, | ||||||
|   Divider, |   Divider, | ||||||
|   Group, |   Group, | ||||||
|   Loader, |   Loader, | ||||||
|   Paper, |   Paper, | ||||||
|   Stack, |   Stack, | ||||||
|   Tabs, |   Tabs, | ||||||
|  |   Text, | ||||||
|   Tooltip |   Tooltip | ||||||
| } from '@mantine/core'; | } from '@mantine/core'; | ||||||
| import { | import { | ||||||
| @@ -31,13 +33,15 @@ import { | |||||||
| import type { ModelType } from '@lib/enums/ModelType'; | import type { ModelType } from '@lib/enums/ModelType'; | ||||||
| import { cancelEvent } from '@lib/functions/Events'; | import { cancelEvent } from '@lib/functions/Events'; | ||||||
| import { navigateToLink } from '@lib/functions/Navigation'; | import { navigateToLink } from '@lib/functions/Navigation'; | ||||||
|  | import { t } from '@lingui/core/macro'; | ||||||
| import { useShallow } from 'zustand/react/shallow'; | import { useShallow } from 'zustand/react/shallow'; | ||||||
| import { identifierString } from '../../functions/conversion'; | import { identifierString } from '../../functions/conversion'; | ||||||
| import { usePluginPanels } from '../../hooks/UsePluginPanels'; | import { usePluginPanels } from '../../hooks/UsePluginPanels'; | ||||||
| import { useLocalState } from '../../states/LocalState'; | import { useLocalState } from '../../states/LocalState'; | ||||||
|  | import { vars } from '../../theme'; | ||||||
| import { Boundary } from '../Boundary'; | import { Boundary } from '../Boundary'; | ||||||
| import { StylishText } from '../items/StylishText'; | import { StylishText } from '../items/StylishText'; | ||||||
| import type { PanelType } from '../panels/Panel'; | import type { PanelGroupType, PanelType } from '../panels/Panel'; | ||||||
| import * as classes from './PanelGroup.css'; | import * as classes from './PanelGroup.css'; | ||||||
|  |  | ||||||
| /** | /** | ||||||
| @@ -56,6 +60,7 @@ import * as classes from './PanelGroup.css'; | |||||||
| export type PanelProps = { | export type PanelProps = { | ||||||
|   pageKey: string; |   pageKey: string; | ||||||
|   panels: PanelType[]; |   panels: PanelType[]; | ||||||
|  |   groups?: PanelGroupType[]; | ||||||
|   instance?: any; |   instance?: any; | ||||||
|   reloadInstance?: () => void; |   reloadInstance?: () => void; | ||||||
|   model?: ModelType | string; |   model?: ModelType | string; | ||||||
| @@ -63,18 +68,21 @@ export type PanelProps = { | |||||||
|   selectedPanel?: string; |   selectedPanel?: string; | ||||||
|   onPanelChange?: (panel: string) => void; |   onPanelChange?: (panel: string) => void; | ||||||
|   collapsible?: boolean; |   collapsible?: boolean; | ||||||
|  |   markCustomPanels?: boolean; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function BasePanelGroup({ | function BasePanelGroup({ | ||||||
|   pageKey, |   pageKey, | ||||||
|   panels, |   panels, | ||||||
|  |   groups, | ||||||
|   onPanelChange, |   onPanelChange, | ||||||
|   selectedPanel, |   selectedPanel, | ||||||
|   reloadInstance, |   reloadInstance, | ||||||
|   instance, |   instance, | ||||||
|   model, |   model, | ||||||
|   id, |   id, | ||||||
|   collapsible = true |   collapsible = true, | ||||||
|  |   markCustomPanels = false | ||||||
| }: Readonly<PanelProps>): ReactNode { | }: Readonly<PanelProps>): ReactNode { | ||||||
|   const localState = useLocalState(); |   const localState = useLocalState(); | ||||||
|   const location = useLocation(); |   const location = useLocation(); | ||||||
| @@ -93,29 +101,66 @@ function BasePanelGroup({ | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   // Rebuild the list of panels |   // Rebuild the list of panels | ||||||
|   const allPanels = useMemo(() => { |   const [allPanels, groupedPanels] = useMemo(() => { | ||||||
|  |     const _grouped_panels: PanelGroupType[] = []; | ||||||
|     const _panels = [...panels]; |     const _panels = [...panels]; | ||||||
|  |     const _allpanels: PanelType[] = [...panels]; | ||||||
|  |  | ||||||
|  |     groups?.forEach((group) => { | ||||||
|  |       const newVal: any = { ...group, panels: [] }; | ||||||
|  |       // Add panel to group and remove from main list | ||||||
|  |       group.panelIDs?.forEach((panelID) => { | ||||||
|  |         const index = _panels.findIndex((p) => p.name === panelID); | ||||||
|  |         if (index !== -1) { | ||||||
|  |           newVal.panels.push(_panels[index]); | ||||||
|  |           _panels.splice(index, 1); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       _grouped_panels.push(newVal); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Add remaining panels to group | ||||||
|  |     if (_panels.length > 0) { | ||||||
|  |       _grouped_panels.push({ | ||||||
|  |         id: 'ungrouped', | ||||||
|  |         label: '', | ||||||
|  |         panels: _panels | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     // Add plugin panels |     // Add plugin panels | ||||||
|  |     const pluginPanels: any = []; | ||||||
|     pluginPanelSet.panels?.forEach((panel) => { |     pluginPanelSet.panels?.forEach((panel) => { | ||||||
|       let panelKey = panel.name; |       let panelKey = panel.name; | ||||||
|  |  | ||||||
|       // Check if panel with this name already exists |       // Check if panel with this name already exists | ||||||
|       const existingPanel = _panels.find((p) => p.name === panelKey); |       const existingPanel = panels.find((p) => p.name === panelKey); | ||||||
|  |  | ||||||
|       if (existingPanel) { |       if (existingPanel) { | ||||||
|         // Create a unique key for the panel which includes the plugin slug |         // Create a unique key for the panel which includes the plugin slug | ||||||
|         panelKey = identifierString(`${panel.pluginName}-${panel.name}`); |         panelKey = identifierString(`${panel.pluginName}-${panel.name}`); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       _panels.push({ |       pluginPanels.push({ | ||||||
|  |         ...panel, | ||||||
|  |         name: panelKey | ||||||
|  |       }); | ||||||
|  |       _allpanels.push({ | ||||||
|         ...panel, |         ...panel, | ||||||
|         name: panelKey |         name: panelKey | ||||||
|       }); |       }); | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return _panels; |     if (pluginPanels.length > 0) { | ||||||
|   }, [panels, pluginPanelSet]); |       _grouped_panels.push({ | ||||||
|  |         id: 'plugins', | ||||||
|  |         label: markCustomPanels ? t`Plugin Provided` : '', | ||||||
|  |         panels: pluginPanels | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return [_allpanels, _grouped_panels]; | ||||||
|  |   }, [groups, panels, pluginPanelSet]); | ||||||
|  |  | ||||||
|   const activePanels = useMemo( |   const activePanels = useMemo( | ||||||
|     () => allPanels.filter((panel) => !panel.hidden && !panel.disabled), |     () => allPanels.filter((panel) => !panel.hidden && !panel.disabled), | ||||||
| @@ -170,32 +215,47 @@ function BasePanelGroup({ | |||||||
|           classNames={{ tab: classes.selectedPanelTab }} |           classNames={{ tab: classes.selectedPanelTab }} | ||||||
|         > |         > | ||||||
|           <Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}> |           <Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}> | ||||||
|             {allPanels.map( |             {groupedPanels.map((group) => ( | ||||||
|               (panel) => |               <Box key={`group-${group.id}`} w={'100%'}> | ||||||
|                 !panel.hidden && ( |                 <Text | ||||||
|                   <Tooltip |                   fs={'italic'} | ||||||
|                     label={panel.label ?? panel.name} |                   ml={'1rem'} | ||||||
|                     key={panel.name} |                   c={vars.colors.primaryColors[7]} | ||||||
|                     disabled={expanded} |                   key={`group-label-${group.id}`} | ||||||
|                     position='right' |                 > | ||||||
|                   > |                   {group.label} | ||||||
|                     <Tabs.Tab |                 </Text> | ||||||
|                       p='xs' |                 {group.panels?.map( | ||||||
|                       key={`panel-label-${panel.name}`} |                   (panel) => | ||||||
|                       value={panel.name} |                     !panel.hidden && ( | ||||||
|                       leftSection={panel.icon} |                       <Tooltip | ||||||
|                       hidden={panel.hidden} |                         label={panel.label ?? panel.name} | ||||||
|                       disabled={panel.disabled} |                         key={panel.name} | ||||||
|                       style={{ cursor: panel.disabled ? 'unset' : 'pointer' }} |                         disabled={expanded} | ||||||
|                       onClick={(event: any) => |                         position='right' | ||||||
|                         handlePanelChange(panel.name, event) |                       > | ||||||
|                       } |                         <Tabs.Tab | ||||||
|                     > |                           p='xs' | ||||||
|                       {expanded && panel.label} |                           key={`panel-label-${panel.name}`} | ||||||
|                     </Tabs.Tab> |                           w={'100%'} | ||||||
|                   </Tooltip> |                           value={panel.name} | ||||||
|                 ) |                           leftSection={panel.icon} | ||||||
|             )} |                           hidden={panel.hidden} | ||||||
|  |                           disabled={panel.disabled} | ||||||
|  |                           style={{ | ||||||
|  |                             cursor: panel.disabled ? 'unset' : 'pointer' | ||||||
|  |                           }} | ||||||
|  |                           onClick={(event: any) => | ||||||
|  |                             handlePanelChange(panel.name, event) | ||||||
|  |                           } | ||||||
|  |                         > | ||||||
|  |                           {expanded && panel.label} | ||||||
|  |                         </Tabs.Tab> | ||||||
|  |                       </Tooltip> | ||||||
|  |                     ) | ||||||
|  |                 )} | ||||||
|  |               </Box> | ||||||
|  |             ))} | ||||||
|             {collapsible && ( |             {collapsible && ( | ||||||
|               <Group wrap='nowrap' gap='xs'> |               <Group wrap='nowrap' gap='xs'> | ||||||
|                 <ActionIcon |                 <ActionIcon | ||||||
|   | |||||||
| @@ -25,7 +25,10 @@ import { UserRoles } from '@lib/enums/Roles'; | |||||||
| import PermissionDenied from '../../../../components/errors/PermissionDenied'; | import PermissionDenied from '../../../../components/errors/PermissionDenied'; | ||||||
| import PageTitle from '../../../../components/nav/PageTitle'; | import PageTitle from '../../../../components/nav/PageTitle'; | ||||||
| import { SettingsHeader } from '../../../../components/nav/SettingsHeader'; | import { SettingsHeader } from '../../../../components/nav/SettingsHeader'; | ||||||
| import type { PanelType } from '../../../../components/panels/Panel'; | import type { | ||||||
|  |   PanelGroupType, | ||||||
|  |   PanelType | ||||||
|  | } from '../../../../components/panels/Panel'; | ||||||
| import { PanelGroup } from '../../../../components/panels/PanelGroup'; | import { PanelGroup } from '../../../../components/panels/PanelGroup'; | ||||||
| import { GlobalSettingList } from '../../../../components/settings/SettingList'; | import { GlobalSettingList } from '../../../../components/settings/SettingList'; | ||||||
| import { Loadable } from '../../../../functions/loading'; | import { Loadable } from '../../../../functions/loading'; | ||||||
| @@ -104,7 +107,7 @@ export default function AdminCenter() { | |||||||
|     return [ |     return [ | ||||||
|       { |       { | ||||||
|         name: 'user', |         name: 'user', | ||||||
|         label: t`User Management`, |         label: t`Users / Access`, | ||||||
|         icon: <IconUsersGroup />, |         icon: <IconUsersGroup />, | ||||||
|         content: <UserManagementPanel />, |         content: <UserManagementPanel />, | ||||||
|         hidden: !user.hasViewRole(UserRoles.admin) |         hidden: !user.hasViewRole(UserRoles.admin) | ||||||
| @@ -224,6 +227,52 @@ export default function AdminCenter() { | |||||||
|       } |       } | ||||||
|     ]; |     ]; | ||||||
|   }, [user]); |   }, [user]); | ||||||
|  |   const grouping: PanelGroupType[] = useMemo(() => { | ||||||
|  |     return [ | ||||||
|  |       { | ||||||
|  |         id: 'ops', | ||||||
|  |         label: t`Operations`, | ||||||
|  |         panelIDs: [ | ||||||
|  |           'user', | ||||||
|  |           'barcode-history', | ||||||
|  |           'background', | ||||||
|  |           'errors', | ||||||
|  |           'currencies' | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'data', | ||||||
|  |         label: t`Data Management`, | ||||||
|  |         panelIDs: [ | ||||||
|  |           'import', | ||||||
|  |           'export', | ||||||
|  |           'project-codes', | ||||||
|  |           'custom-states', | ||||||
|  |           'custom-units' | ||||||
|  |         ] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'reporting', | ||||||
|  |         label: t`Reporting`, | ||||||
|  |         panelIDs: ['labels', 'reports'] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'extend', | ||||||
|  |         label: t`Extend / Integrate`, | ||||||
|  |         panelIDs: ['plugin', 'machine'] | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         id: 'plm', | ||||||
|  |         label: t`PLM`, | ||||||
|  |         panelIDs: [ | ||||||
|  |           'part-parameters', | ||||||
|  |           'category-parameters', | ||||||
|  |           'location-types', | ||||||
|  |           'stocktake' | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |     ]; | ||||||
|  |   }, []); | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
| @@ -238,7 +287,9 @@ export default function AdminCenter() { | |||||||
|           <PanelGroup |           <PanelGroup | ||||||
|             pageKey='admin-center' |             pageKey='admin-center' | ||||||
|             panels={adminCenterPanels} |             panels={adminCenterPanels} | ||||||
|  |             groups={grouping} | ||||||
|             collapsible={true} |             collapsible={true} | ||||||
|  |             markCustomPanels={true} | ||||||
|             model='admincenter' |             model='admincenter' | ||||||
|             id={null} |             id={null} | ||||||
|           /> |           /> | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ test('Permissions - Admin', async ({ browser, request }) => { | |||||||
|   // Check for expected tabs |   // Check for expected tabs | ||||||
|   await loadTab(page, 'Machines'); |   await loadTab(page, 'Machines'); | ||||||
|   await loadTab(page, 'Plugins'); |   await loadTab(page, 'Plugins'); | ||||||
|   await loadTab(page, 'User Management'); |   await loadTab(page, 'Users / Access'); | ||||||
|  |  | ||||||
|   // Let's create a new user |   // Let's create a new user | ||||||
|   await page.getByLabel('action-button-add-user').click(); |   await page.getByLabel('action-button-add-user').click(); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user