mirror of
https://github.com/inventree/InvenTree.git
synced 2025-11-02 14:15:45 +00:00
feat(frontend): Add start page with quick actions to Admin Center (#7995)
* add option to set leftMargin * Add home tab and action button * make home button actually go to home * Add general info text * Add dependeant quick action section * Add Quickaction to home page * use Carousel * style check * small fixes * add permanent alerts to Admin Center Home * also show inactive alerts * fix order of alerts * simplify attrs * remove security section for now * bring quick actions alive * adjust text * Use StylishText * Make alert columns reactive * Adjust text formatting * Refactor <QuickActions /> - Use responsive grid instead of carousel - Add icons - Translate text --------- Co-authored-by: Oliver Walters <oliver.henry.walters@gmail.com>
This commit is contained in:
@@ -2,12 +2,14 @@ import { ActionIcon, Alert, Group, Menu, Stack, Tooltip } from '@mantine/core';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import type { SettingsStateProps } from '@lib/types/Settings';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { docLinks } from '../../defaults/links';
|
||||
import { useServerApiState } from '../../states/ServerApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsStates';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import type { ServerAPIProps } from '../../states/states';
|
||||
|
||||
interface AlertInfo {
|
||||
key: string;
|
||||
@@ -32,64 +34,21 @@ export function Alerts() {
|
||||
|
||||
const [dismissed, setDismissed] = useState<string[]>([]);
|
||||
|
||||
const alerts: AlertInfo[] = useMemo(() => {
|
||||
const _alerts: AlertInfo[] = [];
|
||||
|
||||
if (server?.debug_mode) {
|
||||
_alerts.push({
|
||||
key: 'debug',
|
||||
title: t`Debug Mode`,
|
||||
code: 'INVE-W4',
|
||||
message: t`The server is running in debug mode.`
|
||||
});
|
||||
}
|
||||
|
||||
if (!server?.worker_running) {
|
||||
_alerts.push({
|
||||
key: 'worker',
|
||||
title: t`Background Worker`,
|
||||
code: 'INVE-W5',
|
||||
message: t`The background worker process is not running.`
|
||||
});
|
||||
}
|
||||
|
||||
if (!server?.email_configured) {
|
||||
_alerts.push({
|
||||
key: 'email',
|
||||
title: t`Email settings`,
|
||||
code: 'INVE-W7',
|
||||
message: t`Email settings not configured.`
|
||||
});
|
||||
}
|
||||
|
||||
if (globalSettings.isSet('SERVER_RESTART_REQUIRED')) {
|
||||
_alerts.push({
|
||||
key: 'restart',
|
||||
title: t`Server Restart`,
|
||||
code: 'INVE-W6',
|
||||
message: t`The server requires a restart to apply changes.`
|
||||
});
|
||||
}
|
||||
|
||||
const n_migrations =
|
||||
Number.parseInt(globalSettings.getSetting('_PENDING_MIGRATIONS')) ?? 0;
|
||||
|
||||
if (n_migrations > 0) {
|
||||
_alerts.push({
|
||||
key: 'migrations',
|
||||
title: t`Database Migrations`,
|
||||
code: 'INVE-W8',
|
||||
message: t`There are pending database migrations.`
|
||||
});
|
||||
}
|
||||
|
||||
return _alerts.filter((alert) => !dismissed.includes(alert.key));
|
||||
}, [server, dismissed, globalSettings]);
|
||||
const alerts: AlertInfo[] = useMemo(
|
||||
() =>
|
||||
getAlerts(server, globalSettings).filter(
|
||||
(alert) => !dismissed.includes(alert.key)
|
||||
),
|
||||
[server, dismissed, globalSettings]
|
||||
);
|
||||
|
||||
const anyErrors: boolean = useMemo(
|
||||
() => alerts.some((alert) => alert.error),
|
||||
[alerts]
|
||||
);
|
||||
function closeAlert(key: string) {
|
||||
setDismissed([...dismissed, key]);
|
||||
}
|
||||
|
||||
if (user.isStaff() && alerts.length > 0)
|
||||
return (
|
||||
@@ -108,22 +67,7 @@ export function Alerts() {
|
||||
<Menu.Dropdown>
|
||||
{alerts.map((alert) => (
|
||||
<Menu.Item key={`alert-item-${alert.key}`}>
|
||||
<Alert
|
||||
withCloseButton
|
||||
color={alert.error ? 'red' : 'orange'}
|
||||
title={
|
||||
<Group gap='xs'>
|
||||
{alert.code && `${alert.code}: `}
|
||||
{alert.title}
|
||||
</Group>
|
||||
}
|
||||
onClose={() => setDismissed([...dismissed, alert.key])}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
{alert.message}
|
||||
{alert.code && errorCodeLink(alert.code)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
<ServerAlert alert={alert} closeAlert={closeAlert} />
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
@@ -131,6 +75,84 @@ export function Alerts() {
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function ServerAlert({
|
||||
alert,
|
||||
closeAlert
|
||||
}: { alert: AlertInfo; closeAlert?: (key: string) => void }) {
|
||||
return (
|
||||
<Alert
|
||||
withCloseButton={!!closeAlert}
|
||||
color={alert.error ? 'red' : 'orange'}
|
||||
title={
|
||||
<Group gap='xs'>
|
||||
{alert.code && `${alert.code}: `}
|
||||
{alert.title}
|
||||
</Group>
|
||||
}
|
||||
onClose={closeAlert ? () => closeAlert(alert.key) : undefined}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
{alert.message}
|
||||
{alert.code && errorCodeLink(alert.code)}
|
||||
</Stack>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
type ExtendedAlertInfo = AlertInfo & {
|
||||
condition: boolean;
|
||||
};
|
||||
|
||||
export function getAlerts(
|
||||
server: ServerAPIProps,
|
||||
globalSettings: SettingsStateProps,
|
||||
inactive = false
|
||||
): ExtendedAlertInfo[] {
|
||||
const n_migrations =
|
||||
Number.parseInt(globalSettings.getSetting('_PENDING_MIGRATIONS')) ?? 0;
|
||||
|
||||
const allalerts: ExtendedAlertInfo[] = [
|
||||
{
|
||||
key: 'debug',
|
||||
title: t`Debug Mode`,
|
||||
code: 'INVE-W4',
|
||||
message: t`The server is running in debug mode.`,
|
||||
condition: server?.debug_mode || false
|
||||
},
|
||||
{
|
||||
key: 'worker',
|
||||
title: t`Background Worker`,
|
||||
code: 'INVE-W5',
|
||||
message: t`The background worker process is not running.`,
|
||||
condition: !server?.worker_running
|
||||
},
|
||||
{
|
||||
key: 'restart',
|
||||
title: t`Server Restart`,
|
||||
code: 'INVE-W6',
|
||||
message: t`The server requires a restart to apply changes.`,
|
||||
condition: globalSettings.isSet('SERVER_RESTART_REQUIRED')
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
title: t`Email settings`,
|
||||
code: 'INVE-W7',
|
||||
message: t`Email settings not configured.`,
|
||||
condition: !server?.email_configured
|
||||
},
|
||||
{
|
||||
key: 'migrations',
|
||||
title: t`Database Migrations`,
|
||||
code: 'INVE-W8',
|
||||
message: t`There are pending database migrations.`,
|
||||
condition: n_migrations > 0
|
||||
}
|
||||
];
|
||||
|
||||
return allalerts.filter((alert) => inactive || alert.condition);
|
||||
}
|
||||
|
||||
export function errorCodeLink(code: string) {
|
||||
return (
|
||||
<a
|
||||
|
||||
138
src/frontend/src/components/settings/QuickAction.tsx
Normal file
138
src/frontend/src/components/settings/QuickAction.tsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Button, Group, Paper, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import {
|
||||
IconBrandGithub,
|
||||
IconListCheck,
|
||||
IconUserPlus,
|
||||
IconUsersGroup,
|
||||
type ReactNode
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
import { ApiEndpoints } from '@lib/index';
|
||||
import {
|
||||
projectCodeFields,
|
||||
useCustomStateFields
|
||||
} from '../../forms/CommonForms';
|
||||
import { useCreateApiFormModal } from '../../hooks/UseForm';
|
||||
import { groupFields } from '../../tables/settings/GroupTable';
|
||||
import { userFields } from '../../tables/settings/UserTable';
|
||||
|
||||
interface ActionItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon?: ReactNode;
|
||||
buttonText?: string;
|
||||
action: () => void;
|
||||
}
|
||||
|
||||
function ActionGrid({ items }: { items: ActionItem[] }) {
|
||||
const slides = items.map((image) => (
|
||||
<Paper shadow='xs' p='sm' withBorder>
|
||||
<Group justify='space-between' wrap='nowrap'>
|
||||
<Stack>
|
||||
<Text>
|
||||
<strong>{image.title}</strong>
|
||||
<br />
|
||||
{image.description}
|
||||
</Text>
|
||||
</Stack>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='light'
|
||||
onClick={image.action}
|
||||
leftSection={image.icon}
|
||||
>
|
||||
{image.buttonText ?? <Trans>Act</Trans>}
|
||||
</Button>
|
||||
</Group>
|
||||
</Paper>
|
||||
));
|
||||
|
||||
return (
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
'600px': 2,
|
||||
'1200px': 3
|
||||
}}
|
||||
type='container'
|
||||
spacing='sm'
|
||||
>
|
||||
{slides}
|
||||
</SimpleGrid>
|
||||
);
|
||||
}
|
||||
|
||||
export const QuickAction = () => {
|
||||
const newUser = useCreateApiFormModal(userFields());
|
||||
const newGroup = useCreateApiFormModal(groupFields());
|
||||
const newProjectCode = useCreateApiFormModal({
|
||||
url: ApiEndpoints.project_code_list,
|
||||
title: t`Add Project Code`,
|
||||
fields: projectCodeFields()
|
||||
});
|
||||
const newCustomState = useCreateApiFormModal({
|
||||
url: ApiEndpoints.custom_state_list,
|
||||
title: t`Add State`,
|
||||
fields: useCustomStateFields()
|
||||
});
|
||||
|
||||
const items = [
|
||||
{
|
||||
id: '0',
|
||||
title: t`Open an Issue`,
|
||||
description: t`Report a bug or request a feature on GitHub`,
|
||||
icon: <IconBrandGithub />,
|
||||
buttonText: t`Open Issue`,
|
||||
action: () =>
|
||||
window.open(
|
||||
'https://github.com/inventree/inventree/issues/new',
|
||||
'_blank'
|
||||
)
|
||||
},
|
||||
{
|
||||
id: '1',
|
||||
title: t`Add New Group`,
|
||||
description: t`Create a new group to manage your users`,
|
||||
icon: <IconUsersGroup />,
|
||||
buttonText: t`New Group`,
|
||||
action: () => newGroup.open()
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: t`Add New User`,
|
||||
description: t`Create a new user to manage your groups`,
|
||||
icon: <IconUserPlus />,
|
||||
buttonText: t`New User`,
|
||||
action: () => newUser.open()
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: t`Add Project Code`,
|
||||
description: t`Create a new project code to organize your items`,
|
||||
icon: <IconListCheck />,
|
||||
buttonText: t`Add Code`,
|
||||
action: () => newProjectCode.open()
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: t`Add Custom State`,
|
||||
description: t`Create a new custom state for your workflow`,
|
||||
icon: <IconListCheck />,
|
||||
buttonText: t`Add State`,
|
||||
action: () => newCustomState.open()
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Stack gap={'xs'} ml={'sm'}>
|
||||
<ActionGrid items={items} />
|
||||
{newUser.modal}
|
||||
{newGroup.modal}
|
||||
{newProjectCode.modal}
|
||||
{newCustomState.modal}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
111
src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx
Normal file
111
src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { Accordion, Alert, SimpleGrid, Stack, Text } from '@mantine/core';
|
||||
import { type JSX, useMemo, useState } from 'react';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { StylishText } from '../../../../components/items/StylishText';
|
||||
import { ServerAlert, getAlerts } from '../../../../components/nav/Alerts';
|
||||
import { QuickAction } from '../../../../components/settings/QuickAction';
|
||||
import { useServerApiState } from '../../../../states/ServerApiState';
|
||||
import { useGlobalSettingsState } from '../../../../states/SettingsStates';
|
||||
|
||||
export default function HomePanel(): JSX.Element {
|
||||
const [dismissed, setDismissed] = useState<boolean>(false);
|
||||
const [server] = useServerApiState(useShallow((state) => [state.server]));
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const accElements = useMemo(() => {
|
||||
const _alerts = getAlerts(server, globalSettings, true);
|
||||
return [
|
||||
{
|
||||
key: 'active',
|
||||
text: t`Active Alerts`,
|
||||
elements: _alerts.filter((alert) => alert.condition)
|
||||
},
|
||||
{
|
||||
key: 'inactive',
|
||||
text: t`Inactive Alerts`,
|
||||
elements: _alerts.filter((alert) => !alert.condition)
|
||||
}
|
||||
];
|
||||
}, [server, globalSettings]);
|
||||
|
||||
return (
|
||||
<Stack gap='xs'>
|
||||
{dismissed ? null : (
|
||||
<Alert
|
||||
color='blue'
|
||||
title={t`Admin Center Information`}
|
||||
withCloseButton
|
||||
onClose={() => setDismissed(true)}
|
||||
>
|
||||
<Stack gap='xs'>
|
||||
<Text>
|
||||
<Trans>
|
||||
The home panel (and the whole Admin Center) is a new feature
|
||||
starting with the new UI and was previously (before 1.0) not
|
||||
available.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
The admin center provides a centralized location for all
|
||||
administration functionality and is meant to replace all
|
||||
interaction with the (django) backend admin interface.
|
||||
</Trans>
|
||||
</Text>
|
||||
<Text>
|
||||
<Trans>
|
||||
Please open feature requests (after checking the tracker) for
|
||||
any existing backend admin functionality you are missing in this
|
||||
UI. The backend admin interface should be used carefully and
|
||||
seldom.
|
||||
</Trans>
|
||||
</Text>
|
||||
</Stack>
|
||||
</Alert>
|
||||
)}
|
||||
<Accordion
|
||||
multiple
|
||||
defaultValue={['quick-actions', 'active']}
|
||||
variant='contained'
|
||||
>
|
||||
<Accordion.Item value='quick-actions'>
|
||||
<Accordion.Control>
|
||||
<StylishText size='md'>
|
||||
<Trans>Quick Actions</Trans>
|
||||
</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<QuickAction />
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
{accElements.map(
|
||||
(item) =>
|
||||
item.elements.length > 0 && (
|
||||
<Accordion.Item key={item.key} value={item.key}>
|
||||
<Accordion.Control>
|
||||
<StylishText size='md'>{item.text}</StylishText>
|
||||
</Accordion.Control>
|
||||
<Accordion.Panel>
|
||||
<SimpleGrid
|
||||
cols={{
|
||||
base: 1,
|
||||
'600px': 2,
|
||||
'1200px': 3
|
||||
}}
|
||||
type='container'
|
||||
spacing='sm'
|
||||
>
|
||||
{item.elements.map((alert) => (
|
||||
<ServerAlert alert={alert} />
|
||||
))}
|
||||
</SimpleGrid>
|
||||
</Accordion.Panel>
|
||||
</Accordion.Item>
|
||||
)
|
||||
)}
|
||||
</Accordion>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
IconExclamationCircle,
|
||||
IconFileDownload,
|
||||
IconFileUpload,
|
||||
IconHome,
|
||||
IconList,
|
||||
IconListDetails,
|
||||
IconMail,
|
||||
@@ -40,6 +41,8 @@ const ReportTemplatePanel = Loadable(
|
||||
|
||||
const LabelTemplatePanel = Loadable(lazy(() => import('./LabelTemplatePanel')));
|
||||
|
||||
const HomePanel = Loadable(lazy(() => import('./HomePanel')));
|
||||
|
||||
const UserManagementPanel = Loadable(
|
||||
lazy(() => import('./UserManagementPanel'))
|
||||
);
|
||||
@@ -107,6 +110,13 @@ export default function AdminCenter() {
|
||||
|
||||
const adminCenterPanels: PanelType[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'home',
|
||||
label: t`Home`,
|
||||
icon: <IconHome />,
|
||||
content: <HomePanel />,
|
||||
showHeadline: false
|
||||
},
|
||||
{
|
||||
name: 'user',
|
||||
label: t`Users / Access`,
|
||||
@@ -231,6 +241,7 @@ export default function AdminCenter() {
|
||||
}, [user]);
|
||||
const grouping: PanelGroupType[] = useMemo(() => {
|
||||
return [
|
||||
{ id: 'home', label: '', panelIDs: ['home'] },
|
||||
{
|
||||
id: 'ops',
|
||||
label: t`Operations`,
|
||||
|
||||
@@ -13,8 +13,8 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl } from '@lib/index';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import { type ApiFormModalProps, getDetailUrl } from '@lib/index';
|
||||
import type { TableColumn, TableState } from '@lib/types/Tables';
|
||||
import { IconUsersGroup } from '@tabler/icons-react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { EditApiForm } from '../../components/forms/ApiForm';
|
||||
@@ -188,17 +188,7 @@ export function GroupTable({
|
||||
preFormWarning: t`Are you sure you want to delete this group?`
|
||||
});
|
||||
|
||||
const newGroup = useCreateApiFormModal({
|
||||
url: ApiEndpoints.group_list,
|
||||
title: t`Add Group`,
|
||||
fields: {
|
||||
name: {
|
||||
label: t`Name`,
|
||||
description: t`Name of the user group`
|
||||
}
|
||||
},
|
||||
table: table
|
||||
});
|
||||
const newGroup = useCreateApiFormModal(groupFields(table));
|
||||
|
||||
const tableActions = useMemo(() => {
|
||||
const actions = [];
|
||||
@@ -256,3 +246,17 @@ export function GroupTable({
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function groupFields(table?: TableState): ApiFormModalProps {
|
||||
return {
|
||||
url: ApiEndpoints.group_list,
|
||||
title: t`Add Group`,
|
||||
fields: {
|
||||
name: {
|
||||
label: t`Name`,
|
||||
description: t`Name of the user group`
|
||||
}
|
||||
},
|
||||
table: table ?? undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -20,9 +20,9 @@ import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { ModelType } from '@lib/enums/ModelType';
|
||||
import { UserRoles } from '@lib/enums/Roles';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { getDetailUrl } from '@lib/index';
|
||||
import { type ApiFormModalProps, getDetailUrl } from '@lib/index';
|
||||
import type { TableFilter } from '@lib/types/Filters';
|
||||
import type { TableColumn } from '@lib/types/Tables';
|
||||
import type { TableColumn, TableState } from '@lib/types/Tables';
|
||||
import { showNotification } from '@mantine/notifications';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
@@ -363,18 +363,7 @@ export function UserTable({
|
||||
});
|
||||
|
||||
// Table Actions - Add New User
|
||||
const newUser = useCreateApiFormModal({
|
||||
url: ApiEndpoints.user_list,
|
||||
title: t`Add User`,
|
||||
fields: {
|
||||
username: {},
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
table: table,
|
||||
successMessage: t`Added user`
|
||||
});
|
||||
const newUser = useCreateApiFormModal(userFields(table));
|
||||
|
||||
const setPassword = useApiFormModal({
|
||||
url: ApiEndpoints.user_set_password,
|
||||
@@ -468,6 +457,21 @@ export function UserTable({
|
||||
);
|
||||
}
|
||||
|
||||
export function userFields(table?: TableState): ApiFormModalProps {
|
||||
return {
|
||||
url: ApiEndpoints.user_list,
|
||||
title: t`Add User`,
|
||||
fields: {
|
||||
username: {},
|
||||
email: {},
|
||||
first_name: {},
|
||||
last_name: {}
|
||||
},
|
||||
table: table ?? undefined,
|
||||
successMessage: t`Added user`
|
||||
};
|
||||
}
|
||||
|
||||
async function setUserActiveState(userId: number, active: boolean) {
|
||||
try {
|
||||
await api.patch(apiUrl(ApiEndpoints.user_list, userId), {
|
||||
|
||||
Reference in New Issue
Block a user