From 40c21c56e49f025fe01264080e7eb109e4fa79c2 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 05:44:41 +0000 Subject: [PATCH 01/73] Refactor plugin components into --- .../src/components/plugins/PluginContext.tsx | 2 + .../src/components/plugins/PluginPanel.tsx | 59 ++++----------- .../plugins/PluginSettingsPanel.tsx | 57 ++------------ .../components/plugins/RemoteComponent.tsx | 75 +++++++++++++++++++ .../src/tables/plugin/PluginListTable.tsx | 2 +- 5 files changed, 98 insertions(+), 97 deletions(-) create mode 100644 src/frontend/src/components/plugins/RemoteComponent.tsx diff --git a/src/frontend/src/components/plugins/PluginContext.tsx b/src/frontend/src/components/plugins/PluginContext.tsx index 992d32a1a18..cfff92f7387 100644 --- a/src/frontend/src/components/plugins/PluginContext.tsx +++ b/src/frontend/src/components/plugins/PluginContext.tsx @@ -28,6 +28,7 @@ import { UserStateProps, useUserState } from '../../states/UserState'; * @param navigate - The navigation function (see react-router-dom) * @param theme - The current Mantine theme * @param colorScheme - The current Mantine color scheme (e.g. 'light' / 'dark') + * @param context - Any additional context data which may be passed to the plugin */ export type InvenTreeContext = { api: AxiosInstance; @@ -38,6 +39,7 @@ export type InvenTreeContext = { navigate: NavigateFunction; theme: MantineTheme; colorScheme: MantineColorScheme; + context?: any; }; export const useInvenTreeContext = () => { diff --git a/src/frontend/src/components/plugins/PluginPanel.tsx b/src/frontend/src/components/plugins/PluginPanel.tsx index 7e17a364f9d..9adfe3b20bb 100644 --- a/src/frontend/src/components/plugins/PluginPanel.tsx +++ b/src/frontend/src/components/plugins/PluginPanel.tsx @@ -5,6 +5,7 @@ import { ReactNode, useEffect, useRef, useState } from 'react'; import { InvenTreeContext } from './PluginContext'; import { findExternalPluginFunction } from './PluginSource'; +import RemoteComponent from './RemoteComponent'; // Definition of the plugin panel properties, provided by the server API export type PluginPanelProps = { @@ -69,57 +70,25 @@ export default function PluginPanelContent({ pluginProps: PluginPanelProps; pluginContext: InvenTreeContext; }>): ReactNode { - const ref = useRef(); - - const [error, setError] = useState(undefined); - - const reloadPluginContent = async () => { - // If a "source" URL is provided, load the content from that URL - if (pluginProps.source) { - findExternalPluginFunction(pluginProps.source, 'renderPanel').then( - (func) => { - if (func) { - try { - func(ref.current, pluginContext); - setError(''); - } catch (error) { - setError( - t`Error occurred while rendering plugin content` + `: ${error}` - ); - } - } else { - setError(t`Plugin did not provide panel rendering function`); - } - } - ); - } else if (pluginProps.content) { - // If content is provided directly, render it into the panel - if (ref.current) { - ref.current?.setHTMLUnsafe(pluginProps.content.toString()); - setError(''); - } - } else { - // If no content is provided, display a placeholder - setError(t`No content provided for this plugin`); - } - }; + // Div for rendering raw content for the panel (if provided) + const pluginContentRef = useRef(); useEffect(() => { - reloadPluginContent(); - }, [pluginProps, pluginContext]); + if (pluginProps.content && pluginContentRef.current) { + pluginContentRef.current?.setHTMLUnsafe(pluginProps.content.toString()); + } + }, [pluginProps.content, pluginContentRef]); return ( - {error && ( - } - > - {error} - + {pluginProps.content &&
} + {pluginProps.source && ( + )} -
); } diff --git a/src/frontend/src/components/plugins/PluginSettingsPanel.tsx b/src/frontend/src/components/plugins/PluginSettingsPanel.tsx index 309de7b6759..adba39021e3 100644 --- a/src/frontend/src/components/plugins/PluginSettingsPanel.tsx +++ b/src/frontend/src/components/plugins/PluginSettingsPanel.tsx @@ -5,6 +5,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useInvenTreeContext } from './PluginContext'; import { findExternalPluginFunction } from './PluginSource'; +import RemoteComponent from './RemoteComponent'; /** * Interface for the plugin admin data @@ -28,59 +29,13 @@ export default function PluginSettingsPanel({ pluginInstance: any; pluginAdmin: PluginAdminInterface; }) { - const ref = useRef(); - const [error, setError] = useState(undefined); - const pluginContext = useInvenTreeContext(); - const pluginSourceFile = useMemo(() => pluginAdmin?.source, [pluginInstance]); - - const loadPluginSettingsContent = async () => { - if (pluginSourceFile) { - findExternalPluginFunction(pluginSourceFile, 'renderPluginSettings').then( - (func) => { - if (func) { - try { - func(ref.current, { - ...pluginContext, - context: pluginAdmin.context - }); - setError(''); - } catch (error) { - setError( - t`Error occurred while rendering plugin settings` + `: ${error}` - ); - } - } else { - setError(t`Plugin did not provide settings rendering function`); - } - } - ); - } - }; - - useEffect(() => { - loadPluginSettingsContent(); - }, [pluginSourceFile]); - - if (!pluginSourceFile) { - return null; - } - return ( - <> - - {error && ( - } - > - {error} - - )} -
-
- + ); } diff --git a/src/frontend/src/components/plugins/RemoteComponent.tsx b/src/frontend/src/components/plugins/RemoteComponent.tsx new file mode 100644 index 00000000000..88cad0c1f54 --- /dev/null +++ b/src/frontend/src/components/plugins/RemoteComponent.tsx @@ -0,0 +1,75 @@ +import { t } from '@lingui/macro'; +import { Alert, Stack, Text } from '@mantine/core'; +import { IconExclamationCircle } from '@tabler/icons-react'; +import { useEffect, useRef, useState } from 'react'; + +import { Boundary } from '../Boundary'; +import { InvenTreeContext } from './PluginContext'; +import { findExternalPluginFunction } from './PluginSource'; + +/** + * A remote component which can be used to display plugin content. + * Content is loaded dynamically (from an external source). + */ +export default function RemoteComponent({ + source, + funcName, + context +}: { + source: string; + funcName: string; + context: InvenTreeContext; +}) { + const componentRef = useRef(); + const [renderingError, setRenderingError] = useState( + undefined + ); + + const reloadPluginContent = async () => { + if (!componentRef.current) { + return; + } + + if (source && funcName) { + findExternalPluginFunction(source, funcName).then((func) => { + if (func) { + try { + func(componentRef.current, context); + setRenderingError(''); + } catch (error) { + setRenderingError(`${error}`); + } + } else { + setRenderingError(`${source}.${funcName}`); + } + }); + } + }; + + // Reload the plugin content dynamically + useEffect(() => { + reloadPluginContent(); + }, [source, funcName, context]); + + return ( + <> + + + {renderingError && ( + } + > + + {t`Error occurred while loading plugin content`}:{' '} + {renderingError} + + + )} +
+
+
+ + ); +} diff --git a/src/frontend/src/tables/plugin/PluginListTable.tsx b/src/frontend/src/tables/plugin/PluginListTable.tsx index 3644bf2ec6a..2413af8c30e 100644 --- a/src/frontend/src/tables/plugin/PluginListTable.tsx +++ b/src/frontend/src/tables/plugin/PluginListTable.tsx @@ -374,7 +374,7 @@ export default function PluginListTable() { {deletePluginModal.modal} {activatePluginModal.modal} { if (!pluginKey) return; From 1f29019e2a4d00022f22b4315b8ffa907851329e Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 05:48:55 +0000 Subject: [PATCH 02/73] Clean up footer --- src/frontend/src/components/nav/Footer.tsx | 23 +++------------------- src/frontend/src/defaults/links.tsx | 17 ---------------- src/frontend/src/main.css.ts | 18 ----------------- 3 files changed, 3 insertions(+), 55 deletions(-) diff --git a/src/frontend/src/components/nav/Footer.tsx b/src/frontend/src/components/nav/Footer.tsx index f915cfb2d85..6a04855418a 100644 --- a/src/frontend/src/components/nav/Footer.tsx +++ b/src/frontend/src/components/nav/Footer.tsx @@ -1,28 +1,11 @@ -import { Anchor, Container, Group } from '@mantine/core'; - -import { footerLinks } from '../../defaults/links'; import * as classes from '../../main.css'; -import { InvenTreeLogoHomeButton } from '../items/InvenTreeLogo'; export function Footer() { - const items = footerLinks.map((link) => ( - - c="dimmed" - key={link.key} - href={link.link} - onClick={(event) => event.preventDefault()} - size="sm" - > - {link.label} - - )); - return (
- - - {items} - + { + // Placeholder for footer links + }
); } diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index 1646bab73e1..eccc29b280b 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -6,23 +6,6 @@ import { StylishText } from '../components/items/StylishText'; import { UserRoles } from '../enums/Roles'; import { IS_DEV_OR_DEMO } from '../main'; -export const footerLinks = [ - { - link: 'https://inventree.org/', - label: Website, - key: 'website' - }, - { - link: 'https://github.com/invenhost/InvenTree', - label: GitHub, - key: 'github' - }, - { - link: 'https://demo.inventree.org/', - label: Demo, - key: 'demo' - } -]; export const navTabs = [ { text: Home, name: 'home' }, { text: Dashboard, name: 'dashboard' }, diff --git a/src/frontend/src/main.css.ts b/src/frontend/src/main.css.ts index c1a14f434ee..8055aa5efce 100644 --- a/src/frontend/src/main.css.ts +++ b/src/frontend/src/main.css.ts @@ -106,24 +106,6 @@ export const layoutContent = style({ width: '100%' }); -export const layoutFooterLinks = style({ - [vars.smallerThan('xs')]: { - marginTop: vars.spacing.md - } -}); - -export const layoutFooterInner = style({ - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', - paddingTop: vars.spacing.xl, - paddingBottom: vars.spacing.xl, - - [vars.smallerThan('xs')]: { - flexDirection: 'column' - } -}); - export const tabs = style({ [vars.smallerThan('sm')]: { display: 'none' From 7cbb699fc2f0b5e2cdcc336371352528a0f18c95 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 07:59:58 +0000 Subject: [PATCH 03/73] Allow BuildOrder list to be sorted by 'outstanding' --- src/backend/InvenTree/build/api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/backend/InvenTree/build/api.py b/src/backend/InvenTree/build/api.py index 6e2ba21a72d..0dd70d76e52 100644 --- a/src/backend/InvenTree/build/api.py +++ b/src/backend/InvenTree/build/api.py @@ -42,6 +42,9 @@ class Meta: active = rest_filters.BooleanFilter(label='Build is active', method='filter_active') + # 'outstanding' is an alias for 'active' here + outstanding = rest_filters.BooleanFilter(label='Build is outstanding', method='filter_active') + def filter_active(self, queryset, name, value): """Filter the queryset to either include or exclude orders which are active.""" if str2bool(value): From f12778b026b3c50e8b602d0e5517d3025adea613 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 08:00:09 +0000 Subject: [PATCH 04/73] Fix model name --- src/frontend/src/components/render/ModelType.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 41cc6408d11..8ad0b85b424 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -103,8 +103,8 @@ export const ModelInformationDict: ModelDict = { api_endpoint: ApiEndpoints.stock_tracking_list }, build: { - label: () => t`Build`, - label_multiple: () => t`Builds`, + label: () => t`Build Order`, + label_multiple: () => t`Build Orders`, url_overview: '/build', url_detail: '/build/:pk/', cui_detail: '/build/:pk/', From c457697a25c32864de33194198853332df779f62 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 08:00:22 +0000 Subject: [PATCH 05/73] Update BuildOrderTable filter --- src/frontend/src/tables/build/BuildOrderTable.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/tables/build/BuildOrderTable.tsx b/src/frontend/src/tables/build/BuildOrderTable.tsx index 553ad44f3b6..7fc53e8ebbc 100644 --- a/src/frontend/src/tables/build/BuildOrderTable.tsx +++ b/src/frontend/src/tables/build/BuildOrderTable.tsx @@ -104,10 +104,10 @@ export function BuildOrderTable({ const tableFilters: TableFilter[] = useMemo(() => { return [ { - name: 'active', + name: 'outstanding', type: 'boolean', - label: t`Active`, - description: t`Show active orders` + label: t`Outstanding`, + description: t`Show outstanding orders` }, { name: 'status', From 4adac192e9ee8747a2da61429252996af9869e63 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 08:00:39 +0000 Subject: [PATCH 06/73] Add StockItemTable column --- src/frontend/src/tables/stock/StockItemTable.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/tables/stock/StockItemTable.tsx b/src/frontend/src/tables/stock/StockItemTable.tsx index 8bfbe3f0781..2d6412e775d 100644 --- a/src/frontend/src/tables/stock/StockItemTable.tsx +++ b/src/frontend/src/tables/stock/StockItemTable.tsx @@ -232,7 +232,12 @@ function stockItemTableColumns(): TableColumn[] { accessor: 'updated' }), // TODO: purchase order - // TODO: Supplier part + { + accessor: 'supplier_part_detail.SKU', + title: t`Supplier SKU`, + ordering: 'SKU', + sortable: true + }, { accessor: 'purchase_price', sortable: true, From 55f11bef58349272290baefeb8c2358eee6eef5c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 13:18:07 +0000 Subject: [PATCH 07/73] Working towards new dashboard --- .../components/dashboard/DashboardLayout.tsx | 187 ++++++++++++++++++ .../components/dashboard/DashboardMenu.tsx | 110 +++++++++++ .../components/dashboard/DashboardWidget.tsx | 41 ++++ .../widgets/OrdersOverviewWidget.tsx | 105 ++++++++++ .../dashboard/widgets/QueryCountWidget.tsx | 71 +++++++ src/frontend/src/pages/Index/Home.tsx | 11 +- 6 files changed, 520 insertions(+), 5 deletions(-) create mode 100644 src/frontend/src/components/dashboard/DashboardLayout.tsx create mode 100644 src/frontend/src/components/dashboard/DashboardMenu.tsx create mode 100644 src/frontend/src/components/dashboard/DashboardWidget.tsx create mode 100644 src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx create mode 100644 src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx new file mode 100644 index 00000000000..2f01420bb62 --- /dev/null +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -0,0 +1,187 @@ +import { + Card, + Center, + Container, + Divider, + Group, + Loader, + LoadingOverlay, + Paper, + Stack, + Text +} from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { json } from 'stream/consumers'; + +import { Boundary } from '../Boundary'; +import DashboardMenu from './DashboardMenu'; +import { DashboardWidgetProps } from './DashboardWidget'; + +const ReactGridLayout = WidthProvider(Responsive); + +/** + * Dashboard item properties. Describes a current item in the dashboard. + */ + +export interface DashboardItemProps { + label: string; + widget: ReactNode; // DashboardWidgetProps; + width?: number; + height?: number; +} + +// Default items for the dashboard +function getDefaultItems(): DashboardItemProps[] { + return [ + { + label: 'widget-1', + width: 2, + height: 1, + widget: Widget 1 + }, + { + label: 'widget-2', + width: 5, + height: 2, + widget: Widget 2 + }, + { + label: 'widget-3', + width: 4, + height: 3, + widget: Widget 3 + }, + { + label: 'widget-4', + width: 4, + widget: Widget 4 + } + ]; +} + +/** + * Save the dashboard layout to local storage + */ +function saveDashboardLayout(layouts: any): void { + localStorage?.setItem('dashboard-layout', JSON.stringify(layouts)); +} + +/** + * Load the dashboard layout from local storage + */ +function loadDashboardLayout(): Record { + const layout = localStorage?.getItem('dashboard-layout'); + + if (layout) { + return JSON.parse(layout); + } else { + return {}; + } +} + +export default function DashboardLayout({}: {}) { + const [layouts, setLayouts] = useState({}); + const [editable, setEditable] = useDisclosure(false); + const [loaded, setLoaded] = useState(false); + + const widgets = useMemo(() => getDefaultItems(), []); + + // When the layout is rendered, ensure that the widget attributes are observed + const updateLayoutForWidget = useCallback( + (layout: any[]) => { + return layout.map((item: Layout): Layout => { + // Find the matching widget + let widget = widgets.find((widget) => widget.label === item.i); + + const minH = widget?.height ?? 2; + const minW = widget?.width ?? 1; + + const w = Math.max(item.w ?? 1, minW); + const h = Math.max(item.h ?? 1, minH); + + return { + ...item, + w: w, + h: h, + minH: minH, + minW: minW + }; + }); + }, + [widgets] + ); + + // Rebuild layout when the widget list changes + useEffect(() => { + onLayoutChange({}, layouts); + }, [widgets]); + + const onLayoutChange = useCallback( + (layout: any, newLayouts: any) => { + // Reconstruct layouts based on the widget requirements + Object.keys(newLayouts).forEach((key) => { + newLayouts[key] = updateLayoutForWidget(newLayouts[key]); + }); + + if (layouts && loaded) { + saveDashboardLayout(newLayouts); + setLayouts(newLayouts); + } + }, + [loaded] + ); + + // Load the dashboard layout from local storage + useEffect(() => { + const initialLayouts = loadDashboardLayout(); + + // onLayoutChange({}, initialLayouts); + setLayouts(initialLayouts); + + setLoaded(true); + }, []); + + return ( + <> + + + + {layouts && loaded ? ( + + {widgets.map((item) => { + return DashboardLayoutItem(item); + })} + + ) : ( +
+ +
+ )} + + ); +} + +/** + * Wrapper for a dashboard item + */ +function DashboardLayoutItem(item: DashboardItemProps) { + return ( + + {item.widget} + + ); +} diff --git a/src/frontend/src/components/dashboard/DashboardMenu.tsx b/src/frontend/src/components/dashboard/DashboardMenu.tsx new file mode 100644 index 00000000000..a2def83d756 --- /dev/null +++ b/src/frontend/src/components/dashboard/DashboardMenu.tsx @@ -0,0 +1,110 @@ +import { Trans } from '@lingui/macro'; +import { ActionIcon, Group, Indicator, Menu, Paper } from '@mantine/core'; +import { + IconArrowBackUpDouble, + IconCirclePlus, + IconDotsVertical, + IconLayout2 +} from '@tabler/icons-react'; +import { useMemo } from 'react'; + +import { useGlobalSettingsState } from '../../states/SettingsState'; +import { useUserState } from '../../states/UserState'; +import { StylishText } from '../items/StylishText'; + +/** + * A menu for editing the dashboard layout + */ +export default function DashboardMenu({ + editing, + onToggleEdit +}: { + editing: boolean; + onToggleEdit: () => void; +}) { + const globalSettings = useGlobalSettingsState(); + const user = useUserState(); + + const title = useMemo(() => { + const instance = globalSettings.getSetting( + 'INVENTREE_INSTANCE', + 'InvenTree' + ); + const username = user.username(); + + return {`${instance} - ${username}`}; + }, [user, globalSettings]); + + return ( + + + {title} + + + + + + + + + + + + + + Dashboard + + + {editing && ( + } + onClick={() => { + // TODO: Add item + }} + > + Add Widget + + )} + + {editing && ( + + )} + + + + + + + ); +} diff --git a/src/frontend/src/components/dashboard/DashboardWidget.tsx b/src/frontend/src/components/dashboard/DashboardWidget.tsx new file mode 100644 index 00000000000..8aaf7b00209 --- /dev/null +++ b/src/frontend/src/components/dashboard/DashboardWidget.tsx @@ -0,0 +1,41 @@ +import { Card } from '@mantine/core'; + +import { Boundary } from '../Boundary'; +import { StylishText } from '../items/StylishText'; + +/** + * Dashboard widget properties. + * + * @param title The title of the widget + * @param visible A function that returns whether the widget should be visible + * @param render A function that renders the widget + */ +export interface DashboardWidgetProps { + title: string; + label: string; + minWidth: number; + minHeight: number; + visible: () => boolean; + render: () => JSX.Element; + onClick?: (event: any) => void; +} + +/** + * Wrapper for a + */ +export default function DashboardWidget(props: DashboardWidgetProps) { + if (!props.visible()) { + return null; + } + + return ( + + + + {props.title} + + {props.render()} + + + ); +} diff --git a/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx new file mode 100644 index 00000000000..7aa4e99321d --- /dev/null +++ b/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx @@ -0,0 +1,105 @@ +import { t } from '@lingui/macro'; +import { + Card, + Divider, + Group, + Paper, + Skeleton, + Stack, + Text +} from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; + +import { api } from '../../../App'; +import { ModelType } from '../../../enums/ModelType'; +import { apiUrl } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; +import { Boundary } from '../../Boundary'; +import { StylishText } from '../../items/StylishText'; +import { ModelInformationDict } from '../../render/ModelType'; +import DashboardWidget, { DashboardWidgetProps } from '../DashboardWidget'; +import QueryCountWidget from './QueryCountWidget'; + +function OrderSlide({ modelType }: { modelType: ModelType }) { + const modelProperties = ModelInformationDict[modelType]; + + const openOrders = useQuery({ + queryKey: ['dashboard-open-orders', modelProperties.api_endpoint], + queryFn: () => { + return api + .get(apiUrl(modelProperties.api_endpoint), { + params: { + outstanding: true, + limit: 1 + } + }) + .then((res) => res.data); + } + }); + + return ( + + + {modelProperties.label_multiple()} + {openOrders.isFetching ? ( + + ) : ( + {openOrders.data?.count ?? '-'} + )} + + + ); +} + +/** + * A dashboard widget for displaying a quick summary of all open orders + */ +export default function OrdersOverviewWidget() { + const user = useUserState(); + + const widgets: DashboardWidgetProps[] = [ + QueryCountWidget({ + modelType: ModelType.build, + title: t`Outstanding Build Orders`, + params: { outstanding: true } + }), + QueryCountWidget({ + modelType: ModelType.purchaseorder, + title: t`Outstanding Purchase Orders`, + params: { outstanding: true } + }), + QueryCountWidget({ + modelType: ModelType.build, + title: t`Overdue Build Orders`, + params: { overdue: true } + }) + ]; + + return ( + + + + {t`Open Orders`} + + + {user.hasViewPermission(ModelType.build) && ( + + )} + {user.hasViewPermission(ModelType.purchaseorder) && ( + + )} + {user.hasViewPermission(ModelType.salesorder) && ( + + )} + {user.hasViewPermission(ModelType.returnorder) && ( + + )} + + {widgets.map((widget, index) => ( + + ))} + + + + ); +} diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx new file mode 100644 index 00000000000..c264570cdec --- /dev/null +++ b/src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx @@ -0,0 +1,71 @@ +import { Card, Group, Skeleton, Text } from '@mantine/core'; +import { useQuery } from '@tanstack/react-query'; +import { useCallback } from 'react'; + +import { api } from '../../../App'; +import { ModelType } from '../../../enums/ModelType'; +import { identifierString } from '../../../functions/conversion'; +import { apiUrl } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; +import { StylishText } from '../../items/StylishText'; +import { ModelInformationDict } from '../../render/ModelType'; +import { DashboardWidgetProps } from '../DashboardWidget'; + +/** + * A simple dashboard widget for displaying the number of results for a particular query + */ +export default function QueryCountWidget({ + modelType, + title, + params +}: { + modelType: ModelType; + title: string; + params: any; +}): DashboardWidgetProps { + const user = useUserState(); + + const modelProperties = ModelInformationDict[modelType]; + + const query = useQuery({ + queryKey: ['dashboard-query-count', modelType, params], + enabled: user.hasViewPermission(modelType), + refetchOnMount: true, + queryFn: () => { + return api + .get(apiUrl(modelProperties.api_endpoint), { + params: { + ...params, + limit: 1 + } + }) + .then((res) => res.data); + } + }); + + const renderFunc = useCallback(() => { + return ( + + + + {modelProperties.label_multiple()} + + {query.isFetching ? ( + + ) : ( + {query.data?.count ?? '-'} + )} + + + ); + }, [query.isFetching, query.data, title]); + + return { + title: title, + label: `count-${identifierString(title)}`, + width: 3, + height: 1, + visible: () => user.hasViewPermission(modelType), + render: renderFunc + }; +} diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index 67fecb7f2c8..bd189c3013e 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -1,7 +1,8 @@ import { Trans } from '@lingui/macro'; -import { Title } from '@mantine/core'; +import { Divider, Title } from '@mantine/core'; import { lazy } from 'react'; +import DashboardLayout from '../../components/dashboard/DashboardLayout'; import { LayoutItemType, WidgetLayout @@ -51,13 +52,13 @@ const vals: LayoutItemType[] = [ ]; export default function Home() { - const [username] = useUserState((state) => [state.username()]); return ( <> - - <Trans>Welcome to your Dashboard{username && `, ${username}`}</Trans> - + + + + ); } From 126bebe6e24052faeb78040dd7792e93a6224af5 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 13:19:37 +0000 Subject: [PATCH 08/73] Cleanup unused imports --- .../src/components/dashboard/DashboardLayout.tsx | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index 2f01420bb62..6db0c49d95c 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -1,23 +1,10 @@ -import { - Card, - Center, - Container, - Divider, - Group, - Loader, - LoadingOverlay, - Paper, - Stack, - Text -} from '@mantine/core'; +import { Center, Divider, Loader, Paper, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; -import { json } from 'stream/consumers'; import { Boundary } from '../Boundary'; import DashboardMenu from './DashboardMenu'; -import { DashboardWidgetProps } from './DashboardWidget'; const ReactGridLayout = WidthProvider(Responsive); From 103fdf8e38d3241bedab09dfa246f5bf89477649 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 13:56:38 +0000 Subject: [PATCH 09/73] Updates: Now rendering some custom widgets --- .../components/dashboard/DashboardLayout.tsx | 116 +++++++++--------- .../components/dashboard/DashboardWidget.tsx | 31 ++--- .../widgets/OrdersOverviewWidget.tsx | 2 +- ...dget.tsx => QueryCountDashboardWidget.tsx} | 72 +++++++---- .../components/widgets/GetStartedWidget.tsx | 9 +- src/frontend/src/pages/Index/Home.tsx | 4 - 6 files changed, 124 insertions(+), 110 deletions(-) rename src/frontend/src/components/dashboard/widgets/{QueryCountWidget.tsx => QueryCountDashboardWidget.tsx} (52%) diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index 6db0c49d95c..dd1346f5b94 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -1,53 +1,20 @@ +import { t } from '@lingui/macro'; import { Center, Divider, Loader, Paper, Text } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; -import { ReactNode, useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { ModelType } from '../../enums/ModelType'; import { Boundary } from '../Boundary'; +import DisplayWidget from '../widgets/DisplayWidget'; +import GetStartedWidget from '../widgets/GetStartedWidget'; import DashboardMenu from './DashboardMenu'; +import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; +import QueryCountWidget from './widgets/QueryCountDashboardWidget'; +import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; const ReactGridLayout = WidthProvider(Responsive); -/** - * Dashboard item properties. Describes a current item in the dashboard. - */ - -export interface DashboardItemProps { - label: string; - widget: ReactNode; // DashboardWidgetProps; - width?: number; - height?: number; -} - -// Default items for the dashboard -function getDefaultItems(): DashboardItemProps[] { - return [ - { - label: 'widget-1', - width: 2, - height: 1, - widget: Widget 1 - }, - { - label: 'widget-2', - width: 5, - height: 2, - widget: Widget 2 - }, - { - label: 'widget-3', - width: 4, - height: 3, - widget: Widget 3 - }, - { - label: 'widget-4', - width: 4, - widget: Widget 4 - } - ]; -} - /** * Save the dashboard layout to local storage */ @@ -73,7 +40,53 @@ export default function DashboardLayout({}: {}) { const [editable, setEditable] = useDisclosure(false); const [loaded, setLoaded] = useState(false); - const widgets = useMemo(() => getDefaultItems(), []); + const widgets = useMemo(() => { + return [ + { + label: 'widget-1', + minWidth: 2, + minHeight: 1, + render: () => Widget 1 + }, + { + label: 'widget-2', + minWidth: 2, + minHeight: 1, + render: () => Widget 2 + }, + { + label: 'widget-3', + minWidth: 3, + minHeight: 2, + render: () => Widget 3 + }, + QueryCountDashboardWidget({ + title: t`Outstanding Purchase Orders`, + modelType: ModelType.purchaseorder, + params: { + outstanding: true + } + }), + QueryCountDashboardWidget({ + title: t`Outstanding Sales Orders`, + modelType: ModelType.salesorder, + params: { + outstanding: true + } + }), + QueryCountDashboardWidget({ + title: t`Stock Items`, + modelType: ModelType.stockitem, + params: {} + }), + { + label: 'get-started', + render: () => , + minWidth: 5, + minHeight: 4 + } + ]; + }, []); // When the layout is rendered, ensure that the widget attributes are observed const updateLayoutForWidget = useCallback( @@ -82,8 +95,8 @@ export default function DashboardLayout({}: {}) { // Find the matching widget let widget = widgets.find((widget) => widget.label === item.i); - const minH = widget?.height ?? 2; - const minW = widget?.width ?? 1; + const minH = widget?.minHeight ?? 2; + const minW = widget?.minWidth ?? 1; const w = Math.max(item.w ?? 1, minW); const h = Math.max(item.h ?? 1, minH); @@ -149,8 +162,8 @@ export default function DashboardLayout({}: {}) { margin={[10, 10]} containerPadding={[0, 0]} > - {widgets.map((item) => { - return DashboardLayoutItem(item); + {widgets.map((item: DashboardWidgetProps) => { + return DashboardWidget(item); })} ) : ( @@ -161,14 +174,3 @@ export default function DashboardLayout({}: {}) { ); } - -/** - * Wrapper for a dashboard item - */ -function DashboardLayoutItem(item: DashboardItemProps) { - return ( - - {item.widget} - - ); -} diff --git a/src/frontend/src/components/dashboard/DashboardWidget.tsx b/src/frontend/src/components/dashboard/DashboardWidget.tsx index 8aaf7b00209..117e7f9cda4 100644 --- a/src/frontend/src/components/dashboard/DashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidget.tsx @@ -1,7 +1,6 @@ -import { Card } from '@mantine/core'; +import { Paper } from '@mantine/core'; import { Boundary } from '../Boundary'; -import { StylishText } from '../items/StylishText'; /** * Dashboard widget properties. @@ -11,31 +10,27 @@ import { StylishText } from '../items/StylishText'; * @param render A function that renders the widget */ export interface DashboardWidgetProps { - title: string; label: string; - minWidth: number; - minHeight: number; - visible: () => boolean; + minWidth?: number; + minHeight?: number; render: () => JSX.Element; - onClick?: (event: any) => void; + visible?: () => boolean; } /** - * Wrapper for a + * Wrapper for a dashboard widget. */ export default function DashboardWidget(props: DashboardWidgetProps) { - if (!props.visible()) { - return null; - } + // TODO: Implement visibility check + // if (!props?.visible?.() == false) { + // return null; + // } return ( - - - - {props.title} - + + {props.render()} - - + + ); } diff --git a/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx index 7aa4e99321d..2942b31acf8 100644 --- a/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx @@ -18,7 +18,7 @@ import { Boundary } from '../../Boundary'; import { StylishText } from '../../items/StylishText'; import { ModelInformationDict } from '../../render/ModelType'; import DashboardWidget, { DashboardWidgetProps } from '../DashboardWidget'; -import QueryCountWidget from './QueryCountWidget'; +import QueryCountWidget from './QueryCountDashboardWidget'; function OrderSlide({ modelType }: { modelType: ModelType }) { const modelProperties = ModelInformationDict[modelType]; diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx similarity index 52% rename from src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx rename to src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index c264570cdec..a5a51042e83 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -1,6 +1,15 @@ -import { Card, Group, Skeleton, Text } from '@mantine/core'; +import { + ActionIcon, + Card, + Group, + Loader, + Skeleton, + Stack, + Text +} from '@mantine/core'; +import { IconExternalLink } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; -import { useCallback } from 'react'; +import { ReactNode, useCallback } from 'react'; import { api } from '../../../App'; import { ModelType } from '../../../enums/ModelType'; @@ -14,7 +23,7 @@ import { DashboardWidgetProps } from '../DashboardWidget'; /** * A simple dashboard widget for displaying the number of results for a particular query */ -export default function QueryCountWidget({ +function QueryCountWidget({ modelType, title, params @@ -22,7 +31,7 @@ export default function QueryCountWidget({ modelType: ModelType; title: string; params: any; -}): DashboardWidgetProps { +}): ReactNode { const user = useUserState(); const modelProperties = ModelInformationDict[modelType]; @@ -43,29 +52,42 @@ export default function QueryCountWidget({ } }); - const renderFunc = useCallback(() => { - return ( - - - - {modelProperties.label_multiple()} - - {query.isFetching ? ( - - ) : ( - {query.data?.count ?? '-'} - )} - - - ); - }, [query.isFetching, query.data, title]); + return ( + + + {title} + {query.isFetching ? ( + + ) : ( + {query.data?.count ?? '-'} + )} + + + + + + ); +} +/** + * Construct a dashboard widget descriptor, which displays the number of results for a particular query + */ +export default function QueryCountDashboardWidget({ + title, + modelType, + params +}: { + title: string; + modelType: ModelType; + params: any; +}): DashboardWidgetProps { return { title: title, - label: `count-${identifierString(title)}`, - width: 3, - height: 1, - visible: () => user.hasViewPermission(modelType), - render: renderFunc + label: identifierString(title), + minWidth: 2, + minHeight: 1, + render: () => ( + + ) }; } diff --git a/src/frontend/src/components/widgets/GetStartedWidget.tsx b/src/frontend/src/components/widgets/GetStartedWidget.tsx index 125d5540d11..bc814d7125f 100644 --- a/src/frontend/src/components/widgets/GetStartedWidget.tsx +++ b/src/frontend/src/components/widgets/GetStartedWidget.tsx @@ -1,15 +1,14 @@ -import { Trans } from '@lingui/macro'; -import { Title } from '@mantine/core'; +import { t } from '@lingui/macro'; +import { Divider } from '@mantine/core'; import { navDocLinks } from '../../defaults/links'; import { GettingStartedCarousel } from '../items/GettingStartedCarousel'; +import { StylishText } from '../items/StylishText'; export default function GetStartedWidget() { return ( - - <Trans>Getting Started</Trans> - + {t`Getting Started`} ); diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index bd189c3013e..0bf192b3b1e 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -55,10 +55,6 @@ export default function Home() { return ( <> - - - - ); } From f910dab7faf069bb0dc0605a4b21e384231be426 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:16:53 +0000 Subject: [PATCH 10/73] Define icons for model types --- .../src/components/render/ModelType.tsx | 95 +++++++++++++------ src/frontend/src/functions/icons.tsx | 11 ++- 2 files changed, 74 insertions(+), 32 deletions(-) diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 8ad0b85b424..4bf40dcdf7a 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -2,6 +2,7 @@ import { t } from '@lingui/macro'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; +import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; export interface ModelInformationInterface { label: string; @@ -11,6 +12,7 @@ export interface ModelInformationInterface { api_endpoint: ApiEndpoints; cui_detail?: string; admin_url?: string; + icon: InvenTreeIconType; } export interface TranslatableModelInformationInterface @@ -31,21 +33,24 @@ export const ModelInformationDict: ModelDict = { url_detail: '/part/:pk/', cui_detail: '/part/:pk/', api_endpoint: ApiEndpoints.part_list, - admin_url: '/part/part/' + admin_url: '/part/part/', + icon: 'part' }, partparametertemplate: { label: () => t`Part Parameter Template`, label_multiple: () => t`Part Parameter Templates`, url_overview: '/partparametertemplate', url_detail: '/partparametertemplate/:pk/', - api_endpoint: ApiEndpoints.part_parameter_template_list + api_endpoint: ApiEndpoints.part_parameter_template_list, + icon: 'test_templates' }, parttesttemplate: { label: () => t`Part Test Template`, label_multiple: () => t`Part Test Templates`, url_overview: '/parttesttemplate', url_detail: '/parttesttemplate/:pk/', - api_endpoint: ApiEndpoints.part_test_template_list + api_endpoint: ApiEndpoints.part_test_template_list, + icon: 'test' }, supplierpart: { label: () => t`Supplier Part`, @@ -54,7 +59,8 @@ export const ModelInformationDict: ModelDict = { url_detail: '/purchasing/supplier-part/:pk/', cui_detail: '/supplier-part/:pk/', api_endpoint: ApiEndpoints.supplier_part_list, - admin_url: '/company/supplierpart/' + admin_url: '/company/supplierpart/', + icon: 'supplier_part' }, manufacturerpart: { label: () => t`Manufacturer Part`, @@ -63,7 +69,8 @@ export const ModelInformationDict: ModelDict = { url_detail: '/purchasing/manufacturer-part/:pk/', cui_detail: '/manufacturer-part/:pk/', api_endpoint: ApiEndpoints.manufacturer_part_list, - admin_url: '/company/manufacturerpart/' + admin_url: '/company/manufacturerpart/', + icon: 'manufacturers' }, partcategory: { label: () => t`Part Category`, @@ -72,7 +79,8 @@ export const ModelInformationDict: ModelDict = { url_detail: '/part/category/:pk/', cui_detail: '/part/category/:pk/', api_endpoint: ApiEndpoints.category_list, - admin_url: '/part/partcategory/' + admin_url: '/part/partcategory/', + icon: 'category' }, stockitem: { label: () => t`Stock Item`, @@ -81,7 +89,8 @@ export const ModelInformationDict: ModelDict = { url_detail: '/stock/item/:pk/', cui_detail: '/stock/item/:pk/', api_endpoint: ApiEndpoints.stock_item_list, - admin_url: '/stock/stockitem/' + admin_url: '/stock/stockitem/', + icon: 'stock' }, stocklocation: { label: () => t`Stock Location`, @@ -90,17 +99,20 @@ export const ModelInformationDict: ModelDict = { url_detail: '/stock/location/:pk/', cui_detail: '/stock/location/:pk/', api_endpoint: ApiEndpoints.stock_location_list, - admin_url: '/stock/stocklocation/' + admin_url: '/stock/stocklocation/', + icon: 'location' }, stocklocationtype: { label: () => t`Stock Location Type`, label_multiple: () => t`Stock Location Types`, - api_endpoint: ApiEndpoints.stock_location_type_list + api_endpoint: ApiEndpoints.stock_location_type_list, + icon: 'location' }, stockhistory: { label: () => t`Stock History`, label_multiple: () => t`Stock Histories`, - api_endpoint: ApiEndpoints.stock_tracking_list + api_endpoint: ApiEndpoints.stock_tracking_list, + icon: 'history' }, build: { label: () => t`Build Order`, @@ -109,7 +121,8 @@ export const ModelInformationDict: ModelDict = { url_detail: '/build/:pk/', cui_detail: '/build/:pk/', api_endpoint: ApiEndpoints.build_order_list, - admin_url: '/build/build/' + admin_url: '/build/build/', + icon: 'build_order' }, buildline: { label: () => t`Build Line`, @@ -117,12 +130,14 @@ export const ModelInformationDict: ModelDict = { url_overview: '/build/line', url_detail: '/build/line/:pk/', cui_detail: '/build/line/:pk/', - api_endpoint: ApiEndpoints.build_line_list + api_endpoint: ApiEndpoints.build_line_list, + icon: 'build_order' }, builditem: { label: () => t`Build Item`, label_multiple: () => t`Build Items`, - api_endpoint: ApiEndpoints.build_item_list + api_endpoint: ApiEndpoints.build_item_list, + icon: 'build_order' }, company: { label: () => t`Company`, @@ -131,14 +146,16 @@ export const ModelInformationDict: ModelDict = { url_detail: '/company/:pk/', cui_detail: '/company/:pk/', api_endpoint: ApiEndpoints.company_list, - admin_url: '/company/company/' + admin_url: '/company/company/', + icon: 'building' }, projectcode: { label: () => t`Project Code`, label_multiple: () => t`Project Codes`, url_overview: '/project-code', url_detail: '/project-code/:pk/', - api_endpoint: ApiEndpoints.project_code_list + api_endpoint: ApiEndpoints.project_code_list, + icon: 'list_details' }, purchaseorder: { label: () => t`Purchase Order`, @@ -147,12 +164,14 @@ export const ModelInformationDict: ModelDict = { url_detail: '/purchasing/purchase-order/:pk/', cui_detail: '/order/purchase-order/:pk/', api_endpoint: ApiEndpoints.purchase_order_list, - admin_url: '/order/purchaseorder/' + admin_url: '/order/purchaseorder/', + icon: 'purchase_orders' }, purchaseorderlineitem: { label: () => t`Purchase Order Line`, label_multiple: () => t`Purchase Order Lines`, - api_endpoint: ApiEndpoints.purchase_order_line_list + api_endpoint: ApiEndpoints.purchase_order_line_list, + icon: 'purchase_orders' }, salesorder: { label: () => t`Sales Order`, @@ -161,14 +180,16 @@ export const ModelInformationDict: ModelDict = { url_detail: '/sales/sales-order/:pk/', cui_detail: '/order/sales-order/:pk/', api_endpoint: ApiEndpoints.sales_order_list, - admin_url: '/order/salesorder/' + admin_url: '/order/salesorder/', + icon: 'sales_orders' }, salesordershipment: { label: () => t`Sales Order Shipment`, label_multiple: () => t`Sales Order Shipments`, url_overview: '/sales/shipment/', url_detail: '/sales/shipment/:pk/', - api_endpoint: ApiEndpoints.sales_order_shipment_list + api_endpoint: ApiEndpoints.sales_order_shipment_list, + icon: 'sales_orders' }, returnorder: { label: () => t`Return Order`, @@ -177,40 +198,46 @@ export const ModelInformationDict: ModelDict = { url_detail: '/sales/return-order/:pk/', cui_detail: '/order/return-order/:pk/', api_endpoint: ApiEndpoints.return_order_list, - admin_url: '/order/returnorder/' + admin_url: '/order/returnorder/', + icon: 'return_orders' }, returnorderlineitem: { label: () => t`Return Order Line Item`, label_multiple: () => t`Return Order Line Items`, - api_endpoint: ApiEndpoints.return_order_line_list + api_endpoint: ApiEndpoints.return_order_line_list, + icon: 'return_orders' }, address: { label: () => t`Address`, label_multiple: () => t`Addresses`, url_overview: '/address', url_detail: '/address/:pk/', - api_endpoint: ApiEndpoints.address_list + api_endpoint: ApiEndpoints.address_list, + icon: 'address' }, contact: { label: () => t`Contact`, label_multiple: () => t`Contacts`, url_overview: '/contact', url_detail: '/contact/:pk/', - api_endpoint: ApiEndpoints.contact_list + api_endpoint: ApiEndpoints.contact_list, + icon: 'group' }, owner: { label: () => t`Owner`, label_multiple: () => t`Owners`, url_overview: '/owner', url_detail: '/owner/:pk/', - api_endpoint: ApiEndpoints.owner_list + api_endpoint: ApiEndpoints.owner_list, + icon: 'group' }, user: { label: () => t`User`, label_multiple: () => t`Users`, url_overview: '/user', url_detail: '/user/:pk/', - api_endpoint: ApiEndpoints.user_list + api_endpoint: ApiEndpoints.user_list, + icon: 'user' }, group: { label: () => t`Group`, @@ -218,40 +245,46 @@ export const ModelInformationDict: ModelDict = { url_overview: '/user/group', url_detail: '/user/group-:pk', api_endpoint: ApiEndpoints.group_list, - admin_url: '/auth/group/' + admin_url: '/auth/group/', + icon: 'group' }, importsession: { label: () => t`Import Session`, label_multiple: () => t`Import Sessions`, url_overview: '/import', url_detail: '/import/:pk/', - api_endpoint: ApiEndpoints.import_session_list + api_endpoint: ApiEndpoints.import_session_list, + icon: 'import' }, labeltemplate: { label: () => t`Label Template`, label_multiple: () => t`Label Templates`, url_overview: '/labeltemplate', url_detail: '/labeltemplate/:pk/', - api_endpoint: ApiEndpoints.label_list + api_endpoint: ApiEndpoints.label_list, + icon: 'labels' }, reporttemplate: { label: () => t`Report Template`, label_multiple: () => t`Report Templates`, url_overview: '/reporttemplate', url_detail: '/reporttemplate/:pk/', - api_endpoint: ApiEndpoints.report_list + api_endpoint: ApiEndpoints.report_list, + icon: 'reports' }, pluginconfig: { label: () => t`Plugin Configuration`, label_multiple: () => t`Plugin Configurations`, url_overview: '/pluginconfig', url_detail: '/pluginconfig/:pk/', - api_endpoint: ApiEndpoints.plugin_list + api_endpoint: ApiEndpoints.plugin_list, + icon: 'plugin' }, contenttype: { label: () => t`Content Type`, label_multiple: () => t`Content Types`, - api_endpoint: ApiEndpoints.content_type_list + api_endpoint: ApiEndpoints.content_type_list, + icon: 'list_details' } }; diff --git a/src/frontend/src/functions/icons.tsx b/src/frontend/src/functions/icons.tsx index 192bdbaf628..8691bdfd812 100644 --- a/src/frontend/src/functions/icons.tsx +++ b/src/frontend/src/functions/icons.tsx @@ -31,6 +31,7 @@ import { IconEdit, IconExclamationCircle, IconExternalLink, + IconFileArrowLeft, IconFileDownload, IconFileUpload, IconFlag, @@ -40,13 +41,16 @@ import { IconHandStop, IconHash, IconHierarchy, + IconHistory, IconInfoCircle, IconLayersLinked, IconLink, IconList, + IconListDetails, IconListTree, IconLock, IconMail, + IconMap2, IconMapPin, IconMapPinHeart, IconMinusVertical, @@ -120,6 +124,7 @@ const icons = { details: IconInfoCircle, parameters: IconList, list: IconList, + list_details: IconListDetails, stock: IconPackages, variants: IconVersions, allocations: IconBookmarks, @@ -159,6 +164,8 @@ const icons = { issue: IconBrandTelegram, complete: IconCircleCheck, deliver: IconTruckDelivery, + address: IconMap2, + import: IconFileArrowLeft, // Part Icons active: IconCheck, @@ -197,6 +204,7 @@ const icons = { arrow_down: IconArrowBigDownLineFilled, transfer: IconTransfer, actions: IconDots, + labels: IconTag, reports: IconPrinter, buy: IconShoppingCartPlus, add: IconCirclePlus, @@ -223,7 +231,8 @@ const icons = { repeat_destination: IconFlagShare, unlink: IconUnlink, success: IconCircleCheck, - plugin: IconPlug + plugin: IconPlug, + history: IconHistory }; export type InvenTreeIconType = keyof typeof icons; From 7e11789116f817b0cece55b87a515f623f67290c Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:19:17 +0000 Subject: [PATCH 11/73] Add icon --- .../widgets/QueryCountDashboardWidget.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index a5a51042e83..168d5891559 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -4,6 +4,7 @@ import { Group, Loader, Skeleton, + Space, Stack, Text } from '@mantine/core'; @@ -14,6 +15,7 @@ import { ReactNode, useCallback } from 'react'; import { api } from '../../../App'; import { ModelType } from '../../../enums/ModelType'; import { identifierString } from '../../../functions/conversion'; +import { InvenTreeIcon } from '../../../functions/icons'; import { apiUrl } from '../../../states/ApiState'; import { useUserState } from '../../../states/UserState'; import { StylishText } from '../../items/StylishText'; @@ -55,15 +57,19 @@ function QueryCountWidget({ return ( + {title} - {query.isFetching ? ( - - ) : ( - {query.data?.count ?? '-'} - )} - - - + + {query.isFetching ? ( + + ) : ( + {query.data?.count ?? '-'} + )} + + + + + ); @@ -82,7 +88,6 @@ export default function QueryCountDashboardWidget({ params: any; }): DashboardWidgetProps { return { - title: title, label: identifierString(title), minWidth: 2, minHeight: 1, From e77064c6333c01b4a83f599051692b0d97db0746 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:20:26 +0000 Subject: [PATCH 12/73] Cleanup / refactor / delete - Complete transfer of files into new structure --- .../src/components/DashboardItemProxy.tsx | 52 ---- .../components/dashboard/DashboardLayout.tsx | 47 +--- .../dashboard/DashboardWidgetLibrary.tsx | 93 +++++++ .../components/widgets/GetStartedWidget.tsx | 1 - .../components/widgets/WidgetLayout.css.ts | 16 -- .../src/components/widgets/WidgetLayout.tsx | 232 ------------------ src/frontend/src/defaults/dashboardItems.tsx | 132 ---------- src/frontend/src/pages/Index/Dashboard.tsx | 39 --- src/frontend/src/pages/Index/Home.tsx | 51 ---- src/frontend/src/router.tsx | 4 - 10 files changed, 95 insertions(+), 572 deletions(-) delete mode 100644 src/frontend/src/components/DashboardItemProxy.tsx create mode 100644 src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx delete mode 100644 src/frontend/src/components/widgets/WidgetLayout.css.ts delete mode 100644 src/frontend/src/components/widgets/WidgetLayout.tsx delete mode 100644 src/frontend/src/defaults/dashboardItems.tsx delete mode 100644 src/frontend/src/pages/Index/Dashboard.tsx diff --git a/src/frontend/src/components/DashboardItemProxy.tsx b/src/frontend/src/components/DashboardItemProxy.tsx deleted file mode 100644 index b2a99089ee6..00000000000 --- a/src/frontend/src/components/DashboardItemProxy.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { t } from '@lingui/macro'; -import { useQuery } from '@tanstack/react-query'; -import { useEffect, useState } from 'react'; - -import { api } from '../App'; -import { ApiEndpoints } from '../enums/ApiEndpoints'; -import { apiUrl } from '../states/ApiState'; -import { StatisticItem } from './items/DashboardItem'; -import { ErrorItem } from './items/ErrorItem'; - -export function DashboardItemProxy({ - id, - text, - url, - params, - autoupdate = true -}: Readonly<{ - id: string; - text: string; - url: ApiEndpoints; - params: any; - autoupdate: boolean; -}>) { - function fetchData() { - return api - .get(`${apiUrl(url)}?search=&offset=0&limit=25`, { params: params }) - .then((res) => res.data); - } - const { isLoading, error, data, isFetching } = useQuery({ - queryKey: [`dash_${id}`], - queryFn: fetchData, - refetchOnWindowFocus: autoupdate - }); - const [dashData, setDashData] = useState({ title: t`Title`, value: '000' }); - - useEffect(() => { - if (data) { - setDashData({ title: text, value: data.count }); - } - }, [data]); - - if (error != null) return ; - return ( -
- -
- ); -} diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index dd1346f5b94..dbc13aa97be 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -10,6 +10,7 @@ import DisplayWidget from '../widgets/DisplayWidget'; import GetStartedWidget from '../widgets/GetStartedWidget'; import DashboardMenu from './DashboardMenu'; import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; +import BuiltinDashboardWidgets from './DashboardWidgetLibrary'; import QueryCountWidget from './widgets/QueryCountDashboardWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; @@ -41,51 +42,7 @@ export default function DashboardLayout({}: {}) { const [loaded, setLoaded] = useState(false); const widgets = useMemo(() => { - return [ - { - label: 'widget-1', - minWidth: 2, - minHeight: 1, - render: () => Widget 1 - }, - { - label: 'widget-2', - minWidth: 2, - minHeight: 1, - render: () => Widget 2 - }, - { - label: 'widget-3', - minWidth: 3, - minHeight: 2, - render: () => Widget 3 - }, - QueryCountDashboardWidget({ - title: t`Outstanding Purchase Orders`, - modelType: ModelType.purchaseorder, - params: { - outstanding: true - } - }), - QueryCountDashboardWidget({ - title: t`Outstanding Sales Orders`, - modelType: ModelType.salesorder, - params: { - outstanding: true - } - }), - QueryCountDashboardWidget({ - title: t`Stock Items`, - modelType: ModelType.stockitem, - params: {} - }), - { - label: 'get-started', - render: () => , - minWidth: 5, - minHeight: 4 - } - ]; + return BuiltinDashboardWidgets(); }, []); // When the layout is rendered, ensure that the widget attributes are observed diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx new file mode 100644 index 00000000000..c1d4cad5e89 --- /dev/null +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -0,0 +1,93 @@ +import { t } from '@lingui/macro'; + +import { ModelType } from '../../enums/ModelType'; +import { DashboardWidgetProps } from './DashboardWidget'; +import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; + +/** + * + * @returns A list of built-in dashboard widgets which display the number of results for a particular query + */ +export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { + return [ + QueryCountDashboardWidget({ + title: t`Subscribed Parts`, + modelType: ModelType.part, + params: { starred: true } + }), + QueryCountDashboardWidget({ + title: t`Subscribed Categories`, + modelType: ModelType.partcategory, + params: { starred: true } + }), + // TODO: 'latest parts' + // TODO: 'BOM waiting validation' + // TODO: 'recently updated stock' + QueryCountDashboardWidget({ + title: t`Low Stock`, + modelType: ModelType.part, + params: { low_stock: true } + }), + // TODO: Required for build orders + QueryCountDashboardWidget({ + title: t`Expired Stock Items`, + modelType: ModelType.stockitem, + params: { expired: true } + // TODO: Hide if expiry is disabled + }), + QueryCountDashboardWidget({ + title: t`Stale Stock Items`, + modelType: ModelType.stockitem, + params: { stale: true } + // TODO: Hide if expiry is disabled + }), + QueryCountDashboardWidget({ + title: t`Active Build Orders`, + modelType: ModelType.build, + params: { outstanding: true } + }), + QueryCountDashboardWidget({ + title: t`Overdue Build Orders`, + modelType: ModelType.build, + params: { overdue: true } + }), + QueryCountDashboardWidget({ + title: t`Active Sales Orders`, + modelType: ModelType.salesorder, + params: { outstanding: true } + }), + QueryCountDashboardWidget({ + title: t`Overdue Sales Orders`, + modelType: ModelType.salesorder, + params: { overdue: true } + }), + QueryCountDashboardWidget({ + title: t`Active Purchase Orders`, + modelType: ModelType.purchaseorder, + params: { outstanding: true } + }), + QueryCountDashboardWidget({ + title: t`Overdue Purchase Orders`, + modelType: ModelType.purchaseorder, + params: { overdue: true } + }), + QueryCountDashboardWidget({ + title: t`Active Return Orders`, + modelType: ModelType.returnorder, + params: { outstanding: true } + }), + QueryCountDashboardWidget({ + title: t`Overdue Return Orders`, + modelType: ModelType.returnorder, + params: { overdue: true } + }) + ]; +} + +/** + * + * @returns A list of built-in dashboard widgets + */ +export default function BuiltinDashboardWidgets(): DashboardWidgetProps[] { + return [...BuiltinQueryCountWidgets()]; +} diff --git a/src/frontend/src/components/widgets/GetStartedWidget.tsx b/src/frontend/src/components/widgets/GetStartedWidget.tsx index bc814d7125f..b875f49f905 100644 --- a/src/frontend/src/components/widgets/GetStartedWidget.tsx +++ b/src/frontend/src/components/widgets/GetStartedWidget.tsx @@ -1,5 +1,4 @@ import { t } from '@lingui/macro'; -import { Divider } from '@mantine/core'; import { navDocLinks } from '../../defaults/links'; import { GettingStartedCarousel } from '../items/GettingStartedCarousel'; diff --git a/src/frontend/src/components/widgets/WidgetLayout.css.ts b/src/frontend/src/components/widgets/WidgetLayout.css.ts deleted file mode 100644 index 467dd24b608..00000000000 --- a/src/frontend/src/components/widgets/WidgetLayout.css.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { style } from '@vanilla-extract/css'; - -import { vars } from '../../theme'; - -export const backgroundItem = style({ - maxWidth: '100%', - padding: '8px', - boxShadow: vars.shadows.md, - [vars.lightSelector]: { backgroundColor: vars.colors.white }, - [vars.darkSelector]: { backgroundColor: vars.colors.dark[5] } -}); - -export const baseItem = style({ - maxWidth: '100%', - padding: '8px' -}); diff --git a/src/frontend/src/components/widgets/WidgetLayout.tsx b/src/frontend/src/components/widgets/WidgetLayout.tsx deleted file mode 100644 index c7532bee1f2..00000000000 --- a/src/frontend/src/components/widgets/WidgetLayout.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { - ActionIcon, - Container, - Group, - Indicator, - Menu, - Text -} from '@mantine/core'; -import { useDisclosure, useHotkeys } from '@mantine/hooks'; -import { - IconArrowBackUpDouble, - IconDotsVertical, - IconLayout2, - IconSquare, - IconSquareCheck -} from '@tabler/icons-react'; -import { useEffect, useState } from 'react'; -import { Responsive, WidthProvider } from 'react-grid-layout'; - -import * as classes from './WidgetLayout.css'; - -const ReactGridLayout = WidthProvider(Responsive); - -interface LayoutStorage { - [key: string]: {}; -} - -const compactType = 'vertical'; - -export interface LayoutItemType { - i: number; - val: string | JSX.Element | JSX.Element[] | (() => JSX.Element); - w?: number; - h?: number; - x?: number; - y?: number; - minH?: number; -} - -export function WidgetLayout({ - items = [], - className = 'layout', - localstorageName = 'argl', - rowHeight = 30 -}: Readonly<{ - items: LayoutItemType[]; - className?: string; - localstorageName?: string; - rowHeight?: number; -}>) { - const [layouts, setLayouts] = useState({}); - const [editable, setEditable] = useDisclosure(false); - const [boxShown, setBoxShown] = useDisclosure(true); - - useEffect(() => { - let layout = getFromLS('layouts') || []; - const new_layout = JSON.parse(JSON.stringify(layout)); - setLayouts(new_layout); - }, []); - - function getFromLS(key: string) { - let ls: LayoutStorage = {}; - if (localStorage) { - try { - ls = JSON.parse(localStorage.getItem(localstorageName) || '') || {}; - } catch (e) { - /*Ignore*/ - } - } - return ls[key]; - } - - function saveToLS(key: string, value: any) { - if (localStorage) { - localStorage.setItem( - localstorageName, - JSON.stringify({ - [key]: value - }) - ); - } - } - - function resetLayout() { - setLayouts({}); - } - - function onLayoutChange(layout: any, layouts: any) { - saveToLS('layouts', layouts); - setLayouts(layouts); - } - - return ( -
- - {layouts ? ( - onLayoutChange(layout, layouts)} - compactType={compactType} - isDraggable={editable} - isResizable={editable} - > - {items.map((item) => { - return LayoutItem(item, boxShown, classes); - })} - - ) : ( -
- Loading -
- )} -
- ); -} - -function WidgetControlBar({ - editable, - editFnc, - resetLayout, - boxShown, - boxFnc -}: Readonly<{ - editable: boolean; - editFnc: () => void; - resetLayout: () => void; - boxShown: boolean; - boxFnc: () => void; -}>) { - useHotkeys([['mod+E', () => editFnc()]]); - - return ( - - - - - - - - - - - - - Layout - - } - onClick={resetLayout} - > - Reset Layout - - - } - onClick={editFnc} - rightSection={ - - ⌘E - - } - > - {editable ? Stop Edit : Edit Layout} - - - - - - Appearance - - - ) : ( - - ) - } - onClick={boxFnc} - > - Show Boxes - - - - - ); -} - -function LayoutItem( - item: any, - backgroundColor: boolean, - classes: { backgroundItem: string; baseItem: string } -) { - return ( - - {item.val} - - ); -} diff --git a/src/frontend/src/defaults/dashboardItems.tsx b/src/frontend/src/defaults/dashboardItems.tsx deleted file mode 100644 index 22ec6eb6f53..00000000000 --- a/src/frontend/src/defaults/dashboardItems.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { t } from '@lingui/macro'; - -import { ApiEndpoints } from '../enums/ApiEndpoints'; - -interface DashboardItems { - id: string; - text: string; - icon: string; - url: ApiEndpoints; - params: any; -} -export const dashboardItems: DashboardItems[] = [ - { - id: 'starred-parts', - text: t`Subscribed Parts`, - icon: 'fa-bell', - url: ApiEndpoints.part_list, - params: { starred: true } - }, - { - id: 'starred-categories', - text: t`Subscribed Categories`, - icon: 'fa-bell', - url: ApiEndpoints.category_list, - params: { starred: true } - }, - { - id: 'latest-parts', - text: t`Latest Parts`, - icon: 'fa-newspaper', - url: ApiEndpoints.part_list, - params: { ordering: '-creation_date', limit: 10 } - }, - { - id: 'bom-validation', - text: t`BOM Waiting Validation`, - icon: 'fa-times-circle', - url: ApiEndpoints.part_list, - params: { bom_valid: false } - }, - { - id: 'recently-updated-stock', - text: t`Recently Updated`, - icon: 'fa-clock', - url: ApiEndpoints.stock_item_list, - params: { part_detail: true, ordering: '-updated', limit: 10 } - }, - { - id: 'low-stock', - text: t`Low Stock`, - icon: 'fa-flag', - url: ApiEndpoints.part_list, - params: { low_stock: true } - }, - { - id: 'depleted-stock', - text: t`Depleted Stock`, - icon: 'fa-times', - url: ApiEndpoints.part_list, - params: { depleted_stock: true } - }, - { - id: 'stock-to-build', - text: t`Required for Build Orders`, - icon: 'fa-bullhorn', - url: ApiEndpoints.part_list, - params: { stock_to_build: true } - }, - { - id: 'expired-stock', - text: t`Expired Stock`, - icon: 'fa-calendar-times', - url: ApiEndpoints.stock_item_list, - params: { expired: true } - }, - { - id: 'stale-stock', - text: t`Stale Stock`, - icon: 'fa-stopwatch', - url: ApiEndpoints.stock_item_list, - params: { stale: true, expired: true } - }, - { - id: 'build-pending', - text: t`Build Orders In Progress`, - icon: 'fa-cogs', - url: ApiEndpoints.build_order_list, - params: { active: true } - }, - { - id: 'build-overdue', - text: t`Overdue Build Orders`, - icon: 'fa-calendar-times', - url: ApiEndpoints.build_order_list, - params: { overdue: true } - }, - { - id: 'po-outstanding', - text: t`Outstanding Purchase Orders`, - icon: 'fa-sign-in-alt', - url: ApiEndpoints.purchase_order_list, - params: { supplier_detail: true, outstanding: true } - }, - { - id: 'po-overdue', - text: t`Overdue Purchase Orders`, - icon: 'fa-calendar-times', - url: ApiEndpoints.purchase_order_list, - params: { supplier_detail: true, overdue: true } - }, - { - id: 'so-outstanding', - text: t`Outstanding Sales Orders`, - icon: 'fa-sign-out-alt', - url: ApiEndpoints.sales_order_list, - params: { customer_detail: true, outstanding: true } - }, - { - id: 'so-overdue', - text: t`Overdue Sales Orders`, - icon: 'fa-calendar-times', - url: ApiEndpoints.sales_order_list, - params: { customer_detail: true, overdue: true } - }, - { - id: 'news', - text: t`Current News`, - icon: 'fa-newspaper', - url: ApiEndpoints.news, - params: {} - } -]; diff --git a/src/frontend/src/pages/Index/Dashboard.tsx b/src/frontend/src/pages/Index/Dashboard.tsx deleted file mode 100644 index 7b4e1d59a70..00000000000 --- a/src/frontend/src/pages/Index/Dashboard.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { Chip, Group, SimpleGrid, Text } from '@mantine/core'; - -import { DashboardItemProxy } from '../../components/DashboardItemProxy'; -import { StylishText } from '../../components/items/StylishText'; -import { dashboardItems } from '../../defaults/dashboardItems'; -import { useLocalState } from '../../states/LocalState'; - -export default function Dashboard() { - const [autoupdate, toggleAutoupdate] = useLocalState((state) => [ - state.autoupdate, - state.toggleAutoupdate - ]); - - return ( - <> - - - Dashboard - - toggleAutoupdate()}> - Autoupdate - - - - - This page is a replacement for the old start page with the same - information. This page will be deprecated and replaced by the home - page. - - - - {dashboardItems.map((item) => ( - - ))} - - - ); -} diff --git a/src/frontend/src/pages/Index/Home.tsx b/src/frontend/src/pages/Index/Home.tsx index 0bf192b3b1e..2c1ff4ba2fb 100644 --- a/src/frontend/src/pages/Index/Home.tsx +++ b/src/frontend/src/pages/Index/Home.tsx @@ -1,55 +1,4 @@ -import { Trans } from '@lingui/macro'; -import { Divider, Title } from '@mantine/core'; -import { lazy } from 'react'; - import DashboardLayout from '../../components/dashboard/DashboardLayout'; -import { - LayoutItemType, - WidgetLayout -} from '../../components/widgets/WidgetLayout'; -import { LoadingItem } from '../../functions/loading'; -import { useUserState } from '../../states/UserState'; - -const vals: LayoutItemType[] = [ - { - i: 1, - val: ( - import('../../components/widgets/GetStartedWidget'))} - /> - ), - w: 12, - h: 6, - x: 0, - y: 0, - minH: 6 - }, - { - i: 2, - val: ( - import('../../components/widgets/DisplayWidget'))} - /> - ), - w: 3, - h: 3, - x: 0, - y: 7, - minH: 3 - }, - { - i: 4, - val: ( - import('../../components/widgets/FeedbackWidget'))} - /> - ), - w: 4, - h: 6, - x: 0, - y: 9 - } -]; export default function Home() { return ( diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index e3bfb19cf3a..6e9ca10326e 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -84,9 +84,6 @@ export const ReturnOrderDetail = Loadable( export const Scan = Loadable(lazy(() => import('./pages/Index/Scan'))); -export const Dashboard = Loadable( - lazy(() => import('./pages/Index/Dashboard')) -); export const ErrorPage = Loadable(lazy(() => import('./pages/ErrorPage'))); export const Notifications = Loadable( @@ -123,7 +120,6 @@ export const routes = ( } errorElement={}> } />, } />, - } />, } />, } />, } />, From 326eb072f2c7f40fb2a7d6acdcb3dc3d572f996d Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:26:25 +0000 Subject: [PATCH 13/73] Follow link for query count widgets --- .../widgets/QueryCountDashboardWidget.tsx | 19 ++++++++++++++++++- .../src/components/render/ModelType.tsx | 8 ++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 168d5891559..c7b0a94599b 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -10,12 +10,15 @@ import { } from '@mantine/core'; import { IconExternalLink } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; +import { on } from 'events'; import { ReactNode, useCallback } from 'react'; +import { useNavigate } from 'react-router-dom'; import { api } from '../../../App'; import { ModelType } from '../../../enums/ModelType'; import { identifierString } from '../../../functions/conversion'; import { InvenTreeIcon } from '../../../functions/icons'; +import { navigateToLink } from '../../../functions/navigation'; import { apiUrl } from '../../../states/ApiState'; import { useUserState } from '../../../states/UserState'; import { StylishText } from '../../items/StylishText'; @@ -35,6 +38,7 @@ function QueryCountWidget({ params: any; }): ReactNode { const user = useUserState(); + const navigate = useNavigate(); const modelProperties = ModelInformationDict[modelType]; @@ -54,6 +58,19 @@ function QueryCountWidget({ } }); + const onFollowLink = useCallback( + (event: any) => { + // TODO: Pass the query parameters to the target page, so that the user can see the results + + // TODO: Note that the url_overview properties are out of date and need to be updated!! + + if (modelProperties.url_overview) { + navigateToLink(modelProperties.url_overview, navigate, event); + } + }, + [modelProperties, params] + ); + return ( @@ -66,7 +83,7 @@ function QueryCountWidget({ {query.data?.count ?? '-'} )} - + diff --git a/src/frontend/src/components/render/ModelType.tsx b/src/frontend/src/components/render/ModelType.tsx index 4bf40dcdf7a..ccfcc5b3f5a 100644 --- a/src/frontend/src/components/render/ModelType.tsx +++ b/src/frontend/src/components/render/ModelType.tsx @@ -2,7 +2,7 @@ import { t } from '@lingui/macro'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; import { ModelType } from '../../enums/ModelType'; -import { InvenTreeIcon, InvenTreeIconType } from '../../functions/icons'; +import { InvenTreeIconType } from '../../functions/icons'; export interface ModelInformationInterface { label: string; @@ -29,7 +29,7 @@ export const ModelInformationDict: ModelDict = { part: { label: () => t`Part`, label_multiple: () => t`Parts`, - url_overview: '/part', + url_overview: '/part/category/index/parts', url_detail: '/part/:pk/', cui_detail: '/part/:pk/', api_endpoint: ApiEndpoints.part_list, @@ -75,7 +75,7 @@ export const ModelInformationDict: ModelDict = { partcategory: { label: () => t`Part Category`, label_multiple: () => t`Part Categories`, - url_overview: '/part/category', + url_overview: '/part/category/parts/subcategories', url_detail: '/part/category/:pk/', cui_detail: '/part/category/:pk/', api_endpoint: ApiEndpoints.category_list, @@ -85,7 +85,7 @@ export const ModelInformationDict: ModelDict = { stockitem: { label: () => t`Stock Item`, label_multiple: () => t`Stock Items`, - url_overview: '/stock/item', + url_overview: '/stock/location/index/stock-items', url_detail: '/stock/item/:pk/', cui_detail: '/stock/item/:pk/', api_endpoint: ApiEndpoints.stock_item_list, From c863eab4bc65285c821f6df0d0b4c1776d9f6049 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:33:20 +0000 Subject: [PATCH 14/73] Add some more widgets to the library --- .../dashboard/DashboardWidgetLibrary.tsx | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index c1d4cad5e89..de5d76ae077 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -1,6 +1,8 @@ import { t } from '@lingui/macro'; import { ModelType } from '../../enums/ModelType'; +import DisplayWidget from '../widgets/DisplayWidget'; +import GetStartedWidget from '../widgets/GetStartedWidget'; import { DashboardWidgetProps } from './DashboardWidget'; import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; @@ -84,10 +86,36 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { ]; } +export function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { + return [ + { + label: 'getting-started', + minWidth: 5, + minHeight: 4, + render: () => + } + ]; +} + +export function BuiltingSettinsWidgets(): DashboardWidgetProps[] { + return [ + { + label: 'color-settings', + minWidth: 3, + minHeight: 2, + render: () => + } + ]; +} + /** * * @returns A list of built-in dashboard widgets */ export default function BuiltinDashboardWidgets(): DashboardWidgetProps[] { - return [...BuiltinQueryCountWidgets()]; + return [ + ...BuiltinQueryCountWidgets(), + ...BuiltinGettingStartedWidgets(), + ...BuiltingSettinsWidgets() + ]; } From bc136e0107b090f5456fc53d3bee544e32dd46d8 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:38:34 +0000 Subject: [PATCH 15/73] Remove old dashboard link in header --- src/frontend/src/defaults/links.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index eccc29b280b..668855d6adc 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -8,7 +8,6 @@ import { IS_DEV_OR_DEMO } from '../main'; export const navTabs = [ { text: Home, name: 'home' }, - { text: Dashboard, name: 'dashboard' }, { text: Parts, name: 'part', role: UserRoles.part }, { text: Stock, name: 'stock', role: UserRoles.stock }, { text: Build, name: 'build', role: UserRoles.build }, From 07062693aaeb3bc851b0b8ad0858d03b4691bd00 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:39:23 +0000 Subject: [PATCH 16/73] Remove feedback widget --- .../src/components/widgets/FeedbackWidget.tsx | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/frontend/src/components/widgets/FeedbackWidget.tsx diff --git a/src/frontend/src/components/widgets/FeedbackWidget.tsx b/src/frontend/src/components/widgets/FeedbackWidget.tsx deleted file mode 100644 index e716d2298b2..00000000000 --- a/src/frontend/src/components/widgets/FeedbackWidget.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { Button, Stack, Title, useMantineColorScheme } from '@mantine/core'; -import { IconExternalLink } from '@tabler/icons-react'; - -import { vars } from '../../theme'; - -export default function FeedbackWidget() { - const { colorScheme } = useMantineColorScheme(); - return ( - - - <Trans>Something is new: Platform UI</Trans> - - - We are building a new UI with a modern stack. What you currently see is - not fixed and will be redesigned but demonstrates the UI/UX - possibilities we will have going forward. - - - - ); -} From 5694445d90e0bb38bf4506681d2c4f61148ad256 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:40:37 +0000 Subject: [PATCH 17/73] Bump API version --- src/backend/InvenTree/InvenTree/api_version.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backend/InvenTree/InvenTree/api_version.py b/src/backend/InvenTree/InvenTree/api_version.py index ab2ab9d3273..3d8d709e18a 100644 --- a/src/backend/InvenTree/InvenTree/api_version.py +++ b/src/backend/InvenTree/InvenTree/api_version.py @@ -1,13 +1,16 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 268 +INVENTREE_API_VERSION = 269 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ +269 - 2024-10-13 : https://github.com/inventree/InvenTree/pull/8278 + - Allow build order list to be filtered by "outstanding" (alias for "active") + 268 - 2024-10-11 : https://github.com/inventree/InvenTree/pull/8274 - Adds "in_stock" attribute to the StockItem serializer From 3c6e11d9c472f742ea29352c3d7df051c7ed3ec1 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:43:14 +0000 Subject: [PATCH 18/73] Remove test widget --- .../widgets/OrdersOverviewWidget.tsx | 105 ------------------ 1 file changed, 105 deletions(-) delete mode 100644 src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx diff --git a/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx b/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx deleted file mode 100644 index 2942b31acf8..00000000000 --- a/src/frontend/src/components/dashboard/widgets/OrdersOverviewWidget.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { t } from '@lingui/macro'; -import { - Card, - Divider, - Group, - Paper, - Skeleton, - Stack, - Text -} from '@mantine/core'; -import { useQuery } from '@tanstack/react-query'; - -import { api } from '../../../App'; -import { ModelType } from '../../../enums/ModelType'; -import { apiUrl } from '../../../states/ApiState'; -import { useUserState } from '../../../states/UserState'; -import { Boundary } from '../../Boundary'; -import { StylishText } from '../../items/StylishText'; -import { ModelInformationDict } from '../../render/ModelType'; -import DashboardWidget, { DashboardWidgetProps } from '../DashboardWidget'; -import QueryCountWidget from './QueryCountDashboardWidget'; - -function OrderSlide({ modelType }: { modelType: ModelType }) { - const modelProperties = ModelInformationDict[modelType]; - - const openOrders = useQuery({ - queryKey: ['dashboard-open-orders', modelProperties.api_endpoint], - queryFn: () => { - return api - .get(apiUrl(modelProperties.api_endpoint), { - params: { - outstanding: true, - limit: 1 - } - }) - .then((res) => res.data); - } - }); - - return ( - - - {modelProperties.label_multiple()} - {openOrders.isFetching ? ( - - ) : ( - {openOrders.data?.count ?? '-'} - )} - - - ); -} - -/** - * A dashboard widget for displaying a quick summary of all open orders - */ -export default function OrdersOverviewWidget() { - const user = useUserState(); - - const widgets: DashboardWidgetProps[] = [ - QueryCountWidget({ - modelType: ModelType.build, - title: t`Outstanding Build Orders`, - params: { outstanding: true } - }), - QueryCountWidget({ - modelType: ModelType.purchaseorder, - title: t`Outstanding Purchase Orders`, - params: { outstanding: true } - }), - QueryCountWidget({ - modelType: ModelType.build, - title: t`Overdue Build Orders`, - params: { overdue: true } - }) - ]; - - return ( - - - - {t`Open Orders`} - - - {user.hasViewPermission(ModelType.build) && ( - - )} - {user.hasViewPermission(ModelType.purchaseorder) && ( - - )} - {user.hasViewPermission(ModelType.salesorder) && ( - - )} - {user.hasViewPermission(ModelType.returnorder) && ( - - )} - - {widgets.map((widget, index) => ( - - ))} - - - - ); -} From 7e2d140a045d2c8b5e68284e5a386ecec8c63757 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 14:49:46 +0000 Subject: [PATCH 19/73] Rename "Home" -> "Dashboard" --- src/frontend/src/defaults/links.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/defaults/links.tsx b/src/frontend/src/defaults/links.tsx index 668855d6adc..e5f3ad4a194 100644 --- a/src/frontend/src/defaults/links.tsx +++ b/src/frontend/src/defaults/links.tsx @@ -7,7 +7,7 @@ import { UserRoles } from '../enums/Roles'; import { IS_DEV_OR_DEMO } from '../main'; export const navTabs = [ - { text: Home, name: 'home' }, + { text: Dashboard, name: 'home' }, { text: Parts, name: 'part', role: UserRoles.part }, { text: Stock, name: 'stock', role: UserRoles.stock }, { text: Build, name: 'build', role: UserRoles.build }, From d93c51a170543619ac4abdc55aeded802b688858 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 21:33:23 +0000 Subject: [PATCH 20/73] Add some more widgets --- .../dashboard/DashboardWidgetLibrary.tsx | 20 +++++++++++++++++++ .../widgets/QueryCountDashboardWidget.tsx | 12 +++++------ 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index de5d76ae077..d93b021adbd 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -53,6 +53,11 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { modelType: ModelType.build, params: { overdue: true } }), + QueryCountDashboardWidget({ + title: t`Build Orders Assigned to Me`, + modelType: ModelType.build, + params: { assigned_to_me: true } + }), QueryCountDashboardWidget({ title: t`Active Sales Orders`, modelType: ModelType.salesorder, @@ -63,6 +68,11 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { modelType: ModelType.salesorder, params: { overdue: true } }), + QueryCountDashboardWidget({ + title: t`Sales Orders Assigned to Me`, + modelType: ModelType.salesorder, + params: { assigned_to_me: true } + }), QueryCountDashboardWidget({ title: t`Active Purchase Orders`, modelType: ModelType.purchaseorder, @@ -73,6 +83,11 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { modelType: ModelType.purchaseorder, params: { overdue: true } }), + QueryCountDashboardWidget({ + title: t`Purchase Orders Assigned to Me`, + modelType: ModelType.purchaseorder, + params: { assigned_to_me: true } + }), QueryCountDashboardWidget({ title: t`Active Return Orders`, modelType: ModelType.returnorder, @@ -82,6 +97,11 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { title: t`Overdue Return Orders`, modelType: ModelType.returnorder, params: { overdue: true } + }), + QueryCountDashboardWidget({ + title: t`Return Orders Assigned to Me`, + modelType: ModelType.returnorder, + params: { assigned_to_me: true } }) ]; } diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index c7b0a94599b..e37b53ecb51 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -71,21 +71,21 @@ function QueryCountWidget({ [modelProperties, params] ); + // TODO: Improve visual styling + return ( - {title} + + {title} + {query.isFetching ? ( ) : ( {query.data?.count ?? '-'} )} - - - - @@ -106,7 +106,7 @@ export default function QueryCountDashboardWidget({ }): DashboardWidgetProps { return { label: identifierString(title), - minWidth: 2, + minWidth: 3, minHeight: 1, render: () => ( From 4a16b569146bef7c292e59f80b7d742e36043bc4 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 23:45:57 +0000 Subject: [PATCH 21/73] Pass 'editable' property through to widgets --- .../components/dashboard/DashboardLayout.tsx | 5 +++- .../components/dashboard/DashboardWidget.tsx | 28 +++++++++++++++---- .../widgets/QueryCountDashboardWidget.tsx | 6 ++-- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index dbc13aa97be..abfd5fca243 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -120,7 +120,10 @@ export default function DashboardLayout({}: {}) { containerPadding={[0, 0]} > {widgets.map((item: DashboardWidgetProps) => { - return DashboardWidget(item); + return DashboardWidget({ + item: item, + editing: editable + }); })} ) : ( diff --git a/src/frontend/src/components/dashboard/DashboardWidget.tsx b/src/frontend/src/components/dashboard/DashboardWidget.tsx index 117e7f9cda4..3ea37dd6b6c 100644 --- a/src/frontend/src/components/dashboard/DashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidget.tsx @@ -1,4 +1,5 @@ -import { Paper } from '@mantine/core'; +import { Box, Indicator, Overlay, Paper } from '@mantine/core'; +import { IconX } from '@tabler/icons-react'; import { Boundary } from '../Boundary'; @@ -20,16 +21,33 @@ export interface DashboardWidgetProps { /** * Wrapper for a dashboard widget. */ -export default function DashboardWidget(props: DashboardWidgetProps) { +export default function DashboardWidget({ + item, + editing +}: { + item: DashboardWidgetProps; + editing: boolean; +}) { // TODO: Implement visibility check // if (!props?.visible?.() == false) { // return null; // } + // TODO: Add button to remove widget (if "editing") + return ( - - - {props.render()} + + + + {item.render()} + ); diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index e37b53ecb51..9b6ebb4a415 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -17,7 +17,7 @@ import { useNavigate } from 'react-router-dom'; import { api } from '../../../App'; import { ModelType } from '../../../enums/ModelType'; import { identifierString } from '../../../functions/conversion'; -import { InvenTreeIcon } from '../../../functions/icons'; +import { InvenTreeIcon, InvenTreeIconType } from '../../../functions/icons'; import { navigateToLink } from '../../../functions/navigation'; import { apiUrl } from '../../../states/ApiState'; import { useUserState } from '../../../states/UserState'; @@ -31,10 +31,12 @@ import { DashboardWidgetProps } from '../DashboardWidget'; function QueryCountWidget({ modelType, title, + icon, params }: { modelType: ModelType; title: string; + icon?: InvenTreeIconType; params: any; }): ReactNode { const user = useUserState(); @@ -76,7 +78,7 @@ function QueryCountWidget({ return ( - + {title} From 3230169dd9c294b1d7c7149c287a63ffdd24facb Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sat, 12 Oct 2024 23:46:23 +0000 Subject: [PATCH 22/73] Cleanup --- .../src/components/dashboard/DashboardLayout.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index abfd5fca243..ce343c6073b 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -1,18 +1,11 @@ -import { t } from '@lingui/macro'; -import { Center, Divider, Loader, Paper, Text } from '@mantine/core'; +import { Center, Divider, Loader } from '@mantine/core'; import { useDisclosure } from '@mantine/hooks'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; -import { ModelType } from '../../enums/ModelType'; -import { Boundary } from '../Boundary'; -import DisplayWidget from '../widgets/DisplayWidget'; -import GetStartedWidget from '../widgets/GetStartedWidget'; import DashboardMenu from './DashboardMenu'; import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; import BuiltinDashboardWidgets from './DashboardWidgetLibrary'; -import QueryCountWidget from './widgets/QueryCountDashboardWidget'; -import QueryCountDashboardWidget from './widgets/QueryCountDashboardWidget'; const ReactGridLayout = WidthProvider(Responsive); From 533f69921172ab3fd40676848c50bafc5ae8bb14 Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 13 Oct 2024 00:21:13 +0000 Subject: [PATCH 23/73] Add drawer for selecting new widgets --- .../components/dashboard/DashboardLayout.tsx | 48 ++++++++- .../components/dashboard/DashboardMenu.tsx | 18 ++-- .../components/dashboard/DashboardWidget.tsx | 2 + .../dashboard/DashboardWidgetDrawer.tsx | 97 +++++++++++++++++++ .../dashboard/DashboardWidgetLibrary.tsx | 46 +++++++-- .../widgets/QueryCountDashboardWidget.tsx | 6 +- 6 files changed, 192 insertions(+), 25 deletions(-) create mode 100644 src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index ce343c6073b..d3482140df6 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -5,7 +5,8 @@ import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; import DashboardMenu from './DashboardMenu'; import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; -import BuiltinDashboardWidgets from './DashboardWidgetLibrary'; +import DashboardWidgetDrawer from './DashboardWidgetDrawer'; +import AvailableDashboardWidgets from './DashboardWidgetLibrary'; const ReactGridLayout = WidthProvider(Responsive); @@ -32,18 +33,44 @@ function loadDashboardLayout(): Record { export default function DashboardLayout({}: {}) { const [layouts, setLayouts] = useState({}); const [editable, setEditable] = useDisclosure(false); + const [ + widgetDrawerOpened, + { open: openWidgetDrawer, close: closeWidgetDrawer } + ] = useDisclosure(false); const [loaded, setLoaded] = useState(false); - const widgets = useMemo(() => { - return BuiltinDashboardWidgets(); + // Memoize all available widgets + const allWidgets: DashboardWidgetProps[] = useMemo( + () => AvailableDashboardWidgets(), + [] + ); + + // Initial widget selection + + // TODO: Save this to local storage! + const widgets: DashboardWidgetProps[] = useMemo(() => { + return allWidgets.filter((widget, index) => index < 5); }, []); + const widgetLabels = useMemo(() => { + return widgets.map((widget: DashboardWidgetProps) => widget.label); + }, [widgets]); + + const addWidget = useCallback( + (widget: string) => { + console.log('adding widget:', widget); + }, + [allWidgets, widgets] + ); + // When the layout is rendered, ensure that the widget attributes are observed const updateLayoutForWidget = useCallback( (layout: any[]) => { return layout.map((item: Layout): Layout => { // Find the matching widget - let widget = widgets.find((widget) => widget.label === item.i); + let widget = widgets.find( + (widget: DashboardWidgetProps) => widget.label === item.i + ); const minH = widget?.minHeight ?? 2; const minW = widget?.minWidth ?? 1; @@ -95,7 +122,17 @@ export default function DashboardLayout({}: {}) { return ( <> - + + {layouts && loaded ? ( @@ -111,6 +148,7 @@ export default function DashboardLayout({}: {}) { isResizable={editable} margin={[10, 10]} containerPadding={[0, 0]} + resizeHandles={['ne', 'se', 'sw', 'nw']} > {widgets.map((item: DashboardWidgetProps) => { return DashboardWidget({ diff --git a/src/frontend/src/components/dashboard/DashboardMenu.tsx b/src/frontend/src/components/dashboard/DashboardMenu.tsx index a2def83d756..26fcc5f0631 100644 --- a/src/frontend/src/components/dashboard/DashboardMenu.tsx +++ b/src/frontend/src/components/dashboard/DashboardMenu.tsx @@ -17,9 +17,11 @@ import { StylishText } from '../items/StylishText'; */ export default function DashboardMenu({ editing, + onAddWidget, onToggleEdit }: { editing: boolean; + onAddWidget: () => void; onToggleEdit: () => void; }) { const globalSettings = useGlobalSettingsState(); @@ -66,16 +68,12 @@ export default function DashboardMenu({ Dashboard - {editing && ( - } - onClick={() => { - // TODO: Add item - }} - > - Add Widget - - )} + } + onClick={onAddWidget} + > + Add Widget + {editing && ( JSX.Element; diff --git a/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx new file mode 100644 index 00000000000..2f205c0f1e9 --- /dev/null +++ b/src/frontend/src/components/dashboard/DashboardWidgetDrawer.tsx @@ -0,0 +1,97 @@ +import { t } from '@lingui/macro'; +import { + ActionIcon, + Alert, + Divider, + Drawer, + Group, + Stack, + Table, + Text, + Tooltip +} from '@mantine/core'; +import { IconLayoutGridAdd } from '@tabler/icons-react'; +import { useMemo } from 'react'; + +import { StylishText } from '../items/StylishText'; +import AvailableDashboardWidgets from './DashboardWidgetLibrary'; + +/** + * Drawer allowing the user to add new widgets to the dashboard. + */ +export default function DashboardWidgetDrawer({ + opened, + onClose, + onAddWidget, + currentWidgets +}: { + opened: boolean; + onClose: () => void; + onAddWidget: (widget: string) => void; + currentWidgets: string[]; +}) { + // Memoize all available widgets + const allWidgets = useMemo(() => AvailableDashboardWidgets(), []); + + // Memoize available (not currently used) widgets + const availableWidgets = useMemo(() => { + return ( + allWidgets.filter((widget) => !currentWidgets.includes(widget.label)) ?? + [] + ); + }, [allWidgets, currentWidgets]); + + return ( + + Add Dashboard Widgets + + } + > + + + + + {availableWidgets.map((widget) => ( + + + + { + onAddWidget(widget.label); + }} + > + + + + + + {widget.title} + + + {widget.description} + + + ))} + +
+ {availableWidgets.length === 0 && ( + + {t`There are no more widgets available for the dashboard`} + + )} +
+ + ); +} diff --git a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx index d93b021adbd..66b0e477b3f 100644 --- a/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx +++ b/src/frontend/src/components/dashboard/DashboardWidgetLibrary.tsx @@ -14,11 +14,13 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { return [ QueryCountDashboardWidget({ title: t`Subscribed Parts`, + description: t`Show the number of parts which you have subscribed to`, modelType: ModelType.part, params: { starred: true } }), QueryCountDashboardWidget({ title: t`Subscribed Categories`, + description: t`Show the number of part categories which you have subscribed to`, modelType: ModelType.partcategory, params: { starred: true } }), @@ -27,81 +29,96 @@ export function BuiltinQueryCountWidgets(): DashboardWidgetProps[] { // TODO: 'recently updated stock' QueryCountDashboardWidget({ title: t`Low Stock`, + description: t`Show the number of parts which are low on stock`, modelType: ModelType.part, params: { low_stock: true } }), // TODO: Required for build orders QueryCountDashboardWidget({ title: t`Expired Stock Items`, + description: t`Show the number of stock items which have expired`, modelType: ModelType.stockitem, params: { expired: true } // TODO: Hide if expiry is disabled }), QueryCountDashboardWidget({ title: t`Stale Stock Items`, + description: t`Show the number of stock items which are stale`, modelType: ModelType.stockitem, params: { stale: true } // TODO: Hide if expiry is disabled }), QueryCountDashboardWidget({ title: t`Active Build Orders`, + description: t`Show the number of build orders which are currently active`, modelType: ModelType.build, params: { outstanding: true } }), QueryCountDashboardWidget({ title: t`Overdue Build Orders`, + description: t`Show the number of build orders which are overdue`, modelType: ModelType.build, params: { overdue: true } }), QueryCountDashboardWidget({ - title: t`Build Orders Assigned to Me`, + title: t`Assigned Build Orders`, + description: t`Show the number of build orders which are assigned to you`, modelType: ModelType.build, - params: { assigned_to_me: true } + params: { assigned_to_me: true, outstanding: true } }), QueryCountDashboardWidget({ title: t`Active Sales Orders`, + description: t`Show the number of sales orders which are currently active`, modelType: ModelType.salesorder, params: { outstanding: true } }), QueryCountDashboardWidget({ title: t`Overdue Sales Orders`, + description: t`Show the number of sales orders which are overdue`, modelType: ModelType.salesorder, params: { overdue: true } }), QueryCountDashboardWidget({ - title: t`Sales Orders Assigned to Me`, + title: t`Assigned Sales Orders`, + description: t`Show the number of sales orders which are assigned to you`, modelType: ModelType.salesorder, - params: { assigned_to_me: true } + params: { assigned_to_me: true, outstanding: true } }), QueryCountDashboardWidget({ title: t`Active Purchase Orders`, + description: t`Show the number of purchase orders which are currently active`, modelType: ModelType.purchaseorder, params: { outstanding: true } }), QueryCountDashboardWidget({ title: t`Overdue Purchase Orders`, + description: t`Show the number of purchase orders which are overdue`, modelType: ModelType.purchaseorder, params: { overdue: true } }), QueryCountDashboardWidget({ - title: t`Purchase Orders Assigned to Me`, + title: t`Assigned Purchase Orders`, + description: t`Show the number of purchase orders which are assigned to you`, modelType: ModelType.purchaseorder, - params: { assigned_to_me: true } + params: { assigned_to_me: true, outstanding: true } }), QueryCountDashboardWidget({ title: t`Active Return Orders`, + description: t`Show the number of return orders which are currently active`, modelType: ModelType.returnorder, params: { outstanding: true } }), QueryCountDashboardWidget({ title: t`Overdue Return Orders`, + description: t`Show the number of return orders which are overdue`, modelType: ModelType.returnorder, params: { overdue: true } }), QueryCountDashboardWidget({ - title: t`Return Orders Assigned to Me`, + title: t`Assigned Return Orders`, + description: t`Show the number of return orders which are assigned to you`, modelType: ModelType.returnorder, - params: { assigned_to_me: true } + params: { assigned_to_me: true, outstanding: true } }) ]; } @@ -110,6 +127,8 @@ export function BuiltinGettingStartedWidgets(): DashboardWidgetProps[] { return [ { label: 'getting-started', + title: t`Getting Started`, + description: t`Getting started with InvenTree`, minWidth: 5, minHeight: 4, render: () => @@ -121,6 +140,8 @@ export function BuiltingSettinsWidgets(): DashboardWidgetProps[] { return [ { label: 'color-settings', + title: t`Color Settings`, + description: t`Toggle user interface color mode`, minWidth: 3, minHeight: 2, render: () => @@ -132,10 +153,17 @@ export function BuiltingSettinsWidgets(): DashboardWidgetProps[] { * * @returns A list of built-in dashboard widgets */ -export default function BuiltinDashboardWidgets(): DashboardWidgetProps[] { +export function BuiltinDashboardWidgets(): DashboardWidgetProps[] { return [ ...BuiltinQueryCountWidgets(), ...BuiltinGettingStartedWidgets(), ...BuiltingSettinsWidgets() ]; } + +export default function AvailableDashboardWidgets(): DashboardWidgetProps[] { + return [ + ...BuiltinDashboardWidgets() + // TODO: Add plugin widgets here + ]; +} diff --git a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx index 9b6ebb4a415..432824be95c 100644 --- a/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx +++ b/src/frontend/src/components/dashboard/widgets/QueryCountDashboardWidget.tsx @@ -99,16 +99,20 @@ function QueryCountWidget({ */ export default function QueryCountDashboardWidget({ title, + description, modelType, params }: { title: string; + description: string; modelType: ModelType; params: any; }): DashboardWidgetProps { return { label: identifierString(title), - minWidth: 3, + title: title, + description: description, + minWidth: 2, minHeight: 1, render: () => ( From b2872d24c510b3a42346dc9647e691a8e4ac46fc Mon Sep 17 00:00:00 2001 From: Oliver Walters Date: Sun, 13 Oct 2024 00:44:57 +0000 Subject: [PATCH 24/73] Allow different layouts per user on the same machine --- .../components/dashboard/DashboardLayout.tsx | 117 +++++++++++++++--- .../components/dashboard/DashboardMenu.tsx | 17 +-- .../components/dashboard/DashboardWidget.tsx | 3 +- src/frontend/src/states/UserState.tsx | 5 + 4 files changed, 111 insertions(+), 31 deletions(-) diff --git a/src/frontend/src/components/dashboard/DashboardLayout.tsx b/src/frontend/src/components/dashboard/DashboardLayout.tsx index d3482140df6..10e38e60cd7 100644 --- a/src/frontend/src/components/dashboard/DashboardLayout.tsx +++ b/src/frontend/src/components/dashboard/DashboardLayout.tsx @@ -3,6 +3,7 @@ import { useDisclosure } from '@mantine/hooks'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { Layout, Responsive, WidthProvider } from 'react-grid-layout'; +import { useUserState } from '../../states/UserState'; import DashboardMenu from './DashboardMenu'; import DashboardWidget, { DashboardWidgetProps } from './DashboardWidget'; import DashboardWidgetDrawer from './DashboardWidgetDrawer'; @@ -13,15 +14,28 @@ const ReactGridLayout = WidthProvider(Responsive); /** * Save the dashboard layout to local storage */ -function saveDashboardLayout(layouts: any): void { - localStorage?.setItem('dashboard-layout', JSON.stringify(layouts)); +function saveDashboardLayout(layouts: any, userId: number | undefined): void { + const data = JSON.stringify(layouts); + + if (userId) { + localStorage?.setItem(`dashboard-layout-${userId}`, data); + } + + localStorage?.setItem('dashboard-layout', data); } /** * Load the dashboard layout from local storage */ -function loadDashboardLayout(): Record { - const layout = localStorage?.getItem('dashboard-layout'); +function loadDashboardLayout( + userId: number | undefined +): Record { + let layout = userId && localStorage?.getItem(`dashboard-layout-${userId}`); + + if (!layout) { + // Fallback to global layout + layout = localStorage?.getItem('dashboard-layout'); + } if (layout) { return JSON.parse(layout); @@ -30,13 +44,56 @@ function loadDashboardLayout(): Record { } } +/** + * Save the list of selected widgets to local storage + */ +function saveDashboardWidgets( + widgets: string[], + userId: number | undefined +): void { + const data = JSON.stringify(widgets); + + if (userId) { + localStorage?.setItem(`dashboard-widgets-${userId}`, data); + } + + localStorage?.setItem('dashboard-widgets', data); +} + +/** + * Load the list of selected widgets from local storage + */ +function loadDashboardWidgets(userId: number | undefined): string[] { + let widgets = userId && localStorage?.getItem(`dashboard-widgets-${userId}`); + + if (!widgets) { + // Fallback to global widget list + widgets = localStorage?.getItem('dashboard-widgets'); + } + + if (widgets) { + return JSON.parse(widgets); + } else { + return []; + } +} + export default function DashboardLayout({}: {}) { + const user = useUserState(); + + // Dashboard layout definition const [layouts, setLayouts] = useState({}); + + // Dashboard widget selection + const [widgets, setWidgets] = useState([]); + const [editable, setEditable] = useDisclosure(false); + const [ widgetDrawerOpened, { open: openWidgetDrawer, close: closeWidgetDrawer } ] = useDisclosure(false); + const [loaded, setLoaded] = useState(false); // Memoize all available widgets @@ -46,26 +103,44 @@ export default function DashboardLayout({}: {}) { ); // Initial widget selection - - // TODO: Save this to local storage! - const widgets: DashboardWidgetProps[] = useMemo(() => { - return allWidgets.filter((widget, index) => index < 5); + useEffect(() => { + setWidgets(allWidgets.filter((widget, index) => index < 5)); }, []); const widgetLabels = useMemo(() => { return widgets.map((widget: DashboardWidgetProps) => widget.label); }, [widgets]); + // Save the selected widgets to local storage when the selection changes + useEffect(() => { + if (loaded) { + saveDashboardWidgets(widgetLabels, user.userId()); + } + }, [widgetLabels]); + const addWidget = useCallback( (widget: string) => { - console.log('adding widget:', widget); + let newWidget = allWidgets.find((wid) => wid.label === widget); + + if (newWidget) { + setWidgets([...widgets, newWidget]); + } + + // Update the layouts to include the new widget (and enforce initial size) + let _layouts: any = { ...layouts }; + + Object.keys(_layouts).forEach((key) => { + _layouts[key] = updateLayoutForWidget(_layouts[key], true); + }); + + setLayouts(_layouts); }, - [allWidgets, widgets] + [allWidgets, widgets, layouts] ); // When the layout is rendered, ensure that the widget attributes are observed const updateLayoutForWidget = useCallback( - (layout: any[]) => { + (layout: any[], overrideSize: boolean) => { return layout.map((item: Layout): Layout => { // Find the matching widget let widget = widgets.find( @@ -75,8 +150,13 @@ export default function DashboardLayout({}: {}) { const minH = widget?.minHeight ?? 2; const minW = widget?.minWidth ?? 1; - const w = Math.max(item.w ?? 1, minW); - const h = Math.max(item.h ?? 1, minH); + let w = Math.max(item.w ?? 1, minW); + let h = Math.max(item.h ?? 1, minH); + + if (overrideSize) { + w = minW; + h = minH; + } return { ...item, @@ -99,11 +179,11 @@ export default function DashboardLayout({}: {}) { (layout: any, newLayouts: any) => { // Reconstruct layouts based on the widget requirements Object.keys(newLayouts).forEach((key) => { - newLayouts[key] = updateLayoutForWidget(newLayouts[key]); + newLayouts[key] = updateLayoutForWidget(newLayouts[key], false); }); if (layouts && loaded) { - saveDashboardLayout(newLayouts); + saveDashboardLayout(newLayouts, user.userId()); setLayouts(newLayouts); } }, @@ -112,10 +192,13 @@ export default function DashboardLayout({}: {}) { // Load the dashboard layout from local storage useEffect(() => { - const initialLayouts = loadDashboardLayout(); + const initialLayouts = loadDashboardLayout(user.userId()); + const initialWidgetLabels = loadDashboardWidgets(user.userId()); - // onLayoutChange({}, initialLayouts); setLayouts(initialLayouts); + setWidgets( + allWidgets.filter((widget) => initialWidgetLabels.includes(widget.label)) + ); setLoaded(true); }, []); diff --git a/src/frontend/src/components/dashboard/DashboardMenu.tsx b/src/frontend/src/components/dashboard/DashboardMenu.tsx index 26fcc5f0631..8a9e67fef66 100644 --- a/src/frontend/src/components/dashboard/DashboardMenu.tsx +++ b/src/frontend/src/components/dashboard/DashboardMenu.tsx @@ -74,22 +74,13 @@ export default function DashboardMenu({ > Add Widget - - {editing && ( - - )}