diff --git a/src/frontend/src/components/nav/Alerts.tsx b/src/frontend/src/components/nav/Alerts.tsx index a23d8120a0..7e689ca00b 100644 --- a/src/frontend/src/components/nav/Alerts.tsx +++ b/src/frontend/src/components/nav/Alerts.tsx @@ -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([]); - 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() { {alerts.map((alert) => ( - - {alert.code && `${alert.code}: `} - {alert.title} - - } - onClose={() => setDismissed([...dismissed, alert.key])} - > - - {alert.message} - {alert.code && errorCodeLink(alert.code)} - - + ))} @@ -131,6 +75,84 @@ export function Alerts() { ); return null; } + +export function ServerAlert({ + alert, + closeAlert +}: { alert: AlertInfo; closeAlert?: (key: string) => void }) { + return ( + + {alert.code && `${alert.code}: `} + {alert.title} + + } + onClose={closeAlert ? () => closeAlert(alert.key) : undefined} + > + + {alert.message} + {alert.code && errorCodeLink(alert.code)} + + + ); +} + +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 ( void; +} + +function ActionGrid({ items }: { items: ActionItem[] }) { + const slides = items.map((image) => ( + + + + + {image.title} +
+ {image.description} +
+
+ +
+
+ )); + + return ( + + {slides} + + ); +} + +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: , + 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: , + 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: , + 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: , + 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: , + buttonText: t`Add State`, + action: () => newCustomState.open() + } + ]; + + return ( + + + {newUser.modal} + {newGroup.modal} + {newProjectCode.modal} + {newCustomState.modal} + + ); +}; diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx new file mode 100644 index 0000000000..0458bb205d --- /dev/null +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/HomePanel.tsx @@ -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(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 ( + + {dismissed ? null : ( + setDismissed(true)} + > + + + + 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. + + + + + The admin center provides a centralized location for all + administration functionality and is meant to replace all + interaction with the (django) backend admin interface. + + + + + 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. + + + + + )} + + + + + Quick Actions + + + + + + + {accElements.map( + (item) => + item.elements.length > 0 && ( + + + {item.text} + + + + {item.elements.map((alert) => ( + + ))} + + + + ) + )} + + + ); +} diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 2de2f52575..7ee01d0e5c 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -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: , + content: , + 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`, diff --git a/src/frontend/src/tables/settings/GroupTable.tsx b/src/frontend/src/tables/settings/GroupTable.tsx index 6ca43a3a7d..98cf7c20f8 100644 --- a/src/frontend/src/tables/settings/GroupTable.tsx +++ b/src/frontend/src/tables/settings/GroupTable.tsx @@ -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 + }; +} diff --git a/src/frontend/src/tables/settings/UserTable.tsx b/src/frontend/src/tables/settings/UserTable.tsx index aa24738902..66bd277547 100644 --- a/src/frontend/src/tables/settings/UserTable.tsx +++ b/src/frontend/src/tables/settings/UserTable.tsx @@ -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), {