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; | ||||
|   showHeadline?: boolean; | ||||
| }; | ||||
|  | ||||
| export type PanelGroupType = { | ||||
|   id: string; | ||||
|   label: string; | ||||
|   panelIDs?: string[]; | ||||
|   panels?: PanelType[]; | ||||
| }; | ||||
|   | ||||
| @@ -1,11 +1,13 @@ | ||||
| import { | ||||
|   ActionIcon, | ||||
|   Box, | ||||
|   Divider, | ||||
|   Group, | ||||
|   Loader, | ||||
|   Paper, | ||||
|   Stack, | ||||
|   Tabs, | ||||
|   Text, | ||||
|   Tooltip | ||||
| } from '@mantine/core'; | ||||
| import { | ||||
| @@ -31,13 +33,15 @@ import { | ||||
| import type { ModelType } from '@lib/enums/ModelType'; | ||||
| import { cancelEvent } from '@lib/functions/Events'; | ||||
| import { navigateToLink } from '@lib/functions/Navigation'; | ||||
| import { t } from '@lingui/core/macro'; | ||||
| import { useShallow } from 'zustand/react/shallow'; | ||||
| import { identifierString } from '../../functions/conversion'; | ||||
| import { usePluginPanels } from '../../hooks/UsePluginPanels'; | ||||
| import { useLocalState } from '../../states/LocalState'; | ||||
| import { vars } from '../../theme'; | ||||
| import { Boundary } from '../Boundary'; | ||||
| import { StylishText } from '../items/StylishText'; | ||||
| import type { PanelType } from '../panels/Panel'; | ||||
| import type { PanelGroupType, PanelType } from '../panels/Panel'; | ||||
| import * as classes from './PanelGroup.css'; | ||||
|  | ||||
| /** | ||||
| @@ -56,6 +60,7 @@ import * as classes from './PanelGroup.css'; | ||||
| export type PanelProps = { | ||||
|   pageKey: string; | ||||
|   panels: PanelType[]; | ||||
|   groups?: PanelGroupType[]; | ||||
|   instance?: any; | ||||
|   reloadInstance?: () => void; | ||||
|   model?: ModelType | string; | ||||
| @@ -63,18 +68,21 @@ export type PanelProps = { | ||||
|   selectedPanel?: string; | ||||
|   onPanelChange?: (panel: string) => void; | ||||
|   collapsible?: boolean; | ||||
|   markCustomPanels?: boolean; | ||||
| }; | ||||
|  | ||||
| function BasePanelGroup({ | ||||
|   pageKey, | ||||
|   panels, | ||||
|   groups, | ||||
|   onPanelChange, | ||||
|   selectedPanel, | ||||
|   reloadInstance, | ||||
|   instance, | ||||
|   model, | ||||
|   id, | ||||
|   collapsible = true | ||||
|   collapsible = true, | ||||
|   markCustomPanels = false | ||||
| }: Readonly<PanelProps>): ReactNode { | ||||
|   const localState = useLocalState(); | ||||
|   const location = useLocation(); | ||||
| @@ -93,29 +101,66 @@ function BasePanelGroup({ | ||||
|   }); | ||||
|  | ||||
|   // Rebuild the list of panels | ||||
|   const allPanels = useMemo(() => { | ||||
|   const [allPanels, groupedPanels] = useMemo(() => { | ||||
|     const _grouped_panels: PanelGroupType[] = []; | ||||
|     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 | ||||
|     const pluginPanels: any = []; | ||||
|     pluginPanelSet.panels?.forEach((panel) => { | ||||
|       let panelKey = panel.name; | ||||
|  | ||||
|       // 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) { | ||||
|         // Create a unique key for the panel which includes the plugin slug | ||||
|         panelKey = identifierString(`${panel.pluginName}-${panel.name}`); | ||||
|       } | ||||
|  | ||||
|       _panels.push({ | ||||
|       pluginPanels.push({ | ||||
|         ...panel, | ||||
|         name: panelKey | ||||
|       }); | ||||
|       _allpanels.push({ | ||||
|         ...panel, | ||||
|         name: panelKey | ||||
|       }); | ||||
|     }); | ||||
|  | ||||
|     return _panels; | ||||
|   }, [panels, pluginPanelSet]); | ||||
|     if (pluginPanels.length > 0) { | ||||
|       _grouped_panels.push({ | ||||
|         id: 'plugins', | ||||
|         label: markCustomPanels ? t`Plugin Provided` : '', | ||||
|         panels: pluginPanels | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return [_allpanels, _grouped_panels]; | ||||
|   }, [groups, panels, pluginPanelSet]); | ||||
|  | ||||
|   const activePanels = useMemo( | ||||
|     () => allPanels.filter((panel) => !panel.hidden && !panel.disabled), | ||||
| @@ -170,32 +215,47 @@ function BasePanelGroup({ | ||||
|           classNames={{ tab: classes.selectedPanelTab }} | ||||
|         > | ||||
|           <Tabs.List justify='left' aria-label={`panel-tabs-${pageKey}`}> | ||||
|             {allPanels.map( | ||||
|               (panel) => | ||||
|                 !panel.hidden && ( | ||||
|                   <Tooltip | ||||
|                     label={panel.label ?? panel.name} | ||||
|                     key={panel.name} | ||||
|                     disabled={expanded} | ||||
|                     position='right' | ||||
|                   > | ||||
|                     <Tabs.Tab | ||||
|                       p='xs' | ||||
|                       key={`panel-label-${panel.name}`} | ||||
|                       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> | ||||
|                 ) | ||||
|             )} | ||||
|             {groupedPanels.map((group) => ( | ||||
|               <Box key={`group-${group.id}`} w={'100%'}> | ||||
|                 <Text | ||||
|                   fs={'italic'} | ||||
|                   ml={'1rem'} | ||||
|                   c={vars.colors.primaryColors[7]} | ||||
|                   key={`group-label-${group.id}`} | ||||
|                 > | ||||
|                   {group.label} | ||||
|                 </Text> | ||||
|                 {group.panels?.map( | ||||
|                   (panel) => | ||||
|                     !panel.hidden && ( | ||||
|                       <Tooltip | ||||
|                         label={panel.label ?? panel.name} | ||||
|                         key={panel.name} | ||||
|                         disabled={expanded} | ||||
|                         position='right' | ||||
|                       > | ||||
|                         <Tabs.Tab | ||||
|                           p='xs' | ||||
|                           key={`panel-label-${panel.name}`} | ||||
|                           w={'100%'} | ||||
|                           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 && ( | ||||
|               <Group wrap='nowrap' gap='xs'> | ||||
|                 <ActionIcon | ||||
|   | ||||
| @@ -25,7 +25,10 @@ import { UserRoles } from '@lib/enums/Roles'; | ||||
| import PermissionDenied from '../../../../components/errors/PermissionDenied'; | ||||
| import PageTitle from '../../../../components/nav/PageTitle'; | ||||
| 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 { GlobalSettingList } from '../../../../components/settings/SettingList'; | ||||
| import { Loadable } from '../../../../functions/loading'; | ||||
| @@ -104,7 +107,7 @@ export default function AdminCenter() { | ||||
|     return [ | ||||
|       { | ||||
|         name: 'user', | ||||
|         label: t`User Management`, | ||||
|         label: t`Users / Access`, | ||||
|         icon: <IconUsersGroup />, | ||||
|         content: <UserManagementPanel />, | ||||
|         hidden: !user.hasViewRole(UserRoles.admin) | ||||
| @@ -224,6 +227,52 @@ export default function AdminCenter() { | ||||
|       } | ||||
|     ]; | ||||
|   }, [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 ( | ||||
|     <> | ||||
| @@ -238,7 +287,9 @@ export default function AdminCenter() { | ||||
|           <PanelGroup | ||||
|             pageKey='admin-center' | ||||
|             panels={adminCenterPanels} | ||||
|             groups={grouping} | ||||
|             collapsible={true} | ||||
|             markCustomPanels={true} | ||||
|             model='admincenter' | ||||
|             id={null} | ||||
|           /> | ||||
|   | ||||
| @@ -21,7 +21,7 @@ test('Permissions - Admin', async ({ browser, request }) => { | ||||
|   // Check for expected tabs | ||||
|   await loadTab(page, 'Machines'); | ||||
|   await loadTab(page, 'Plugins'); | ||||
|   await loadTab(page, 'User Management'); | ||||
|   await loadTab(page, 'Users / Access'); | ||||
|  | ||||
|   // Let's create a new user | ||||
|   await page.getByLabel('action-button-add-user').click(); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user