diff --git a/src/frontend/src/components/panels/Panel.tsx b/src/frontend/src/components/panels/Panel.tsx index a1e407cd5f..e1940bbb93 100644 --- a/src/frontend/src/components/panels/Panel.tsx +++ b/src/frontend/src/components/panels/Panel.tsx @@ -13,3 +13,10 @@ export type PanelType = { disabled?: boolean; showHeadline?: boolean; }; + +export type PanelGroupType = { + id: string; + label: string; + panelIDs?: string[]; + panels?: PanelType[]; +}; diff --git a/src/frontend/src/components/panels/PanelGroup.tsx b/src/frontend/src/components/panels/PanelGroup.tsx index f3688c8c87..6b13ed3603 100644 --- a/src/frontend/src/components/panels/PanelGroup.tsx +++ b/src/frontend/src/components/panels/PanelGroup.tsx @@ -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): 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 }} > - {allPanels.map( - (panel) => - !panel.hidden && ( - - - - ) - )} + {groupedPanels.map((group) => ( + + + {group.label} + + {group.panels?.map( + (panel) => + !panel.hidden && ( + + + + ) + )} + + ))} {collapsible && ( , content: , 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() { diff --git a/src/frontend/tests/pui_permissions.spec.ts b/src/frontend/tests/pui_permissions.spec.ts index 0d19c4db13..6ca4aad69a 100644 --- a/src/frontend/tests/pui_permissions.spec.ts +++ b/src/frontend/tests/pui_permissions.spec.ts @@ -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();