2
0
mirror of https://github.com/inventree/InvenTree.git synced 2025-06-13 18:45:40 +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;
showHeadline?: boolean;
};
export type PanelGroupType = {
id: string;
label: string;
panelIDs?: string[];
panels?: PanelType[];
};

View File

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

View File

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

View File

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