2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-15 03:25:42 +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:
Matthias Mair
2025-06-02 14:10:14 +02:00
committed by GitHub
parent fa1b056b40
commit 782ca55887
4 changed files with 154 additions and 36 deletions

View File

@ -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[];
};

View File

@ -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

View File

@ -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}
/> />

View File

@ -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();