From b15eb3527346f08cb27b72cd502e0ad584e4b86f Mon Sep 17 00:00:00 2001 From: Oliver Date: Thu, 4 Jul 2024 00:34:52 +1000 Subject: [PATCH] [PUI] Error pages (#7554) * Add page * Check permissions for admin center * Wrap page in an error handler - Display client or server errors * Add error handlers to other detail pages * Refactor error pages * Add playwright tests * Refactor component locations * Get test to work --- .../src/components/errors/ClientError.tsx | 28 +++++++ .../components/errors/GenericErrorPage.tsx | 74 +++++++++++++++++ .../components/errors/NotAuthenticated.tsx | 12 +++ .../src/components/errors/NotFound.tsx | 12 +++ .../components/errors/PermissionDenied.tsx | 12 +++ .../src/components/errors/ServerError.tsx | 13 +++ src/frontend/src/components/forms/ApiForm.tsx | 2 +- .../src/components/nav/InstanceDetail.tsx | 28 +++++++ src/frontend/src/hooks/UseInstance.tsx | 10 ++- src/frontend/src/pages/ErrorPage.tsx | 22 ++--- .../Index/Settings/AdminCenter/Index.tsx | 38 +++++---- .../pages/Index/Settings/SystemSettings.tsx | 27 ++++-- src/frontend/src/pages/NotFound.tsx | 35 -------- src/frontend/src/pages/build/BuildDetail.tsx | 37 +++++---- .../src/pages/company/CompanyDetail.tsx | 31 +++---- .../pages/company/ManufacturerPartDetail.tsx | 29 ++++--- .../src/pages/company/SupplierPartDetail.tsx | 31 +++---- .../src/pages/part/CategoryDetail.tsx | 55 ++++++------ src/frontend/src/pages/part/PartDetail.tsx | 71 ++++++++-------- .../pages/purchasing/PurchaseOrderDetail.tsx | 32 +++---- .../src/pages/sales/ReturnOrderDetail.tsx | 32 +++---- .../src/pages/sales/SalesOrderDetail.tsx | 32 +++---- .../src/pages/stock/LocationDetail.tsx | 56 +++++++------ src/frontend/src/pages/stock/StockDetail.tsx | 67 ++++++++------- src/frontend/src/router.tsx | 4 +- src/frontend/tests/baseFixtures.ts | 3 +- src/frontend/tests/login.ts | 6 +- src/frontend/tests/pages/pui_part.spec.ts | 10 +++ src/frontend/tests/pui_basic.spec.ts | 1 + src/frontend/tests/pui_general.spec.ts | 53 +----------- src/frontend/tests/pui_settings.spec.ts | 83 +++++++++++++++++++ 31 files changed, 591 insertions(+), 355 deletions(-) create mode 100644 src/frontend/src/components/errors/ClientError.tsx create mode 100644 src/frontend/src/components/errors/GenericErrorPage.tsx create mode 100644 src/frontend/src/components/errors/NotAuthenticated.tsx create mode 100644 src/frontend/src/components/errors/NotFound.tsx create mode 100644 src/frontend/src/components/errors/PermissionDenied.tsx create mode 100644 src/frontend/src/components/errors/ServerError.tsx create mode 100644 src/frontend/src/components/nav/InstanceDetail.tsx delete mode 100644 src/frontend/src/pages/NotFound.tsx create mode 100644 src/frontend/tests/pui_settings.spec.ts diff --git a/src/frontend/src/components/errors/ClientError.tsx b/src/frontend/src/components/errors/ClientError.tsx new file mode 100644 index 0000000000..85811ac736 --- /dev/null +++ b/src/frontend/src/components/errors/ClientError.tsx @@ -0,0 +1,28 @@ +import { t } from '@lingui/macro'; + +import GenericErrorPage from './GenericErrorPage'; +import NotAuthenticated from './NotAuthenticated'; +import NotFound from './NotFound'; +import PermissionDenied from './PermissionDenied'; + +export default function ClientError({ status }: { status?: number }) { + switch (status) { + case 401: + return ; + case 403: + return ; + case 404: + return ; + default: + break; + } + + // Generic client error + return ( + + ); +} diff --git a/src/frontend/src/components/errors/GenericErrorPage.tsx b/src/frontend/src/components/errors/GenericErrorPage.tsx new file mode 100644 index 0000000000..94405ba281 --- /dev/null +++ b/src/frontend/src/components/errors/GenericErrorPage.tsx @@ -0,0 +1,74 @@ +import { Trans } from '@lingui/macro'; +import { + ActionIcon, + Alert, + Button, + Card, + Center, + Container, + Divider, + Group, + Stack, + Text +} from '@mantine/core'; +import { IconArrowBack, IconExclamationCircle } from '@tabler/icons-react'; +import { useNavigate } from 'react-router-dom'; + +import { LanguageContext } from '../../contexts/LanguageContext'; + +export default function ErrorPage({ + title, + message, + status +}: { + title: string; + message: string; + status?: number; + redirectMessage?: string; + redirectTarget?: string; +}) { + const navigate = useNavigate(); + + return ( + +
+ + + + + + + + {title} + + + + + + {message} + {status && ( + + Status Code: {status} + + )} + + + + +
+ +
+
+
+
+
+
+ ); +} diff --git a/src/frontend/src/components/errors/NotAuthenticated.tsx b/src/frontend/src/components/errors/NotAuthenticated.tsx new file mode 100644 index 0000000000..2d8920fd44 --- /dev/null +++ b/src/frontend/src/components/errors/NotAuthenticated.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import GenericErrorPage from './GenericErrorPage'; + +export default function NotAuthenticated() { + return ( + + ); +} diff --git a/src/frontend/src/components/errors/NotFound.tsx b/src/frontend/src/components/errors/NotFound.tsx new file mode 100644 index 0000000000..4d3f427f32 --- /dev/null +++ b/src/frontend/src/components/errors/NotFound.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import GenericErrorPage from './GenericErrorPage'; + +export default function NotFound() { + return ( + + ); +} diff --git a/src/frontend/src/components/errors/PermissionDenied.tsx b/src/frontend/src/components/errors/PermissionDenied.tsx new file mode 100644 index 0000000000..dd6e1e38fb --- /dev/null +++ b/src/frontend/src/components/errors/PermissionDenied.tsx @@ -0,0 +1,12 @@ +import { t } from '@lingui/macro'; + +import GenericErrorPage from './GenericErrorPage'; + +export default function PermissionDenied() { + return ( + + ); +} diff --git a/src/frontend/src/components/errors/ServerError.tsx b/src/frontend/src/components/errors/ServerError.tsx new file mode 100644 index 0000000000..4539645563 --- /dev/null +++ b/src/frontend/src/components/errors/ServerError.tsx @@ -0,0 +1,13 @@ +import { t } from '@lingui/macro'; + +import GenericErrorPage from './GenericErrorPage'; + +export default function ServerError({ status }: { status?: number }) { + return ( + + ); +} diff --git a/src/frontend/src/components/forms/ApiForm.tsx b/src/frontend/src/components/forms/ApiForm.tsx index c9b660174e..a6385b7250 100644 --- a/src/frontend/src/components/forms/ApiForm.tsx +++ b/src/frontend/src/components/forms/ApiForm.tsx @@ -308,7 +308,7 @@ export function ApiForm({ return response; } catch (error) { - console.error('Error fetching initial data:', error); + console.error('ERR: Error fetching initial data:', error); // Re-throw error to allow react-query to handle error throw error; } diff --git a/src/frontend/src/components/nav/InstanceDetail.tsx b/src/frontend/src/components/nav/InstanceDetail.tsx new file mode 100644 index 0000000000..3ee9a1cf51 --- /dev/null +++ b/src/frontend/src/components/nav/InstanceDetail.tsx @@ -0,0 +1,28 @@ +import { LoadingOverlay } from '@mantine/core'; + +import ClientError from '../errors/ClientError'; +import ServerError from '../errors/ServerError'; + +export default function InstanceDetail({ + status, + loading, + children +}: { + status: number; + loading: boolean; + children: React.ReactNode; +}) { + if (loading) { + return ; + } + + if (status >= 500) { + return ; + } + + if (status >= 400) { + return ; + } + + return <>{children}; +} diff --git a/src/frontend/src/hooks/UseInstance.tsx b/src/frontend/src/hooks/UseInstance.tsx index 88298cc1d0..e7c0c573b1 100644 --- a/src/frontend/src/hooks/UseInstance.tsx +++ b/src/frontend/src/hooks/UseInstance.tsx @@ -1,5 +1,5 @@ import { useQuery } from '@tanstack/react-query'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { api } from '../App'; import { ApiEndpoints } from '../enums/ApiEndpoints'; @@ -39,6 +39,8 @@ export function useInstance({ }) { const [instance, setInstance] = useState(defaultValue); + const [requestStatus, setRequestStatus] = useState(0); + const instanceQuery = useQuery({ queryKey: ['instance', endpoint, pk, params, pathParams], queryFn: async () => { @@ -62,6 +64,7 @@ export function useInstance({ params: params }) .then((response) => { + setRequestStatus(response.status); switch (response.status) { case 200: setInstance(response.data); @@ -72,8 +75,9 @@ export function useInstance({ } }) .catch((error) => { + setRequestStatus(error.response?.status || 0); setInstance(defaultValue); - console.error(`Error fetching instance ${url}:`, error); + console.error(`ERR: Error fetching instance ${url}:`, error); if (throwError) throw error; @@ -89,5 +93,5 @@ export function useInstance({ instanceQuery.refetch(); }, []); - return { instance, refreshInstance, instanceQuery }; + return { instance, refreshInstance, instanceQuery, requestStatus }; } diff --git a/src/frontend/src/pages/ErrorPage.tsx b/src/frontend/src/pages/ErrorPage.tsx index 624fb3bad6..db52eb7685 100644 --- a/src/frontend/src/pages/ErrorPage.tsx +++ b/src/frontend/src/pages/ErrorPage.tsx @@ -1,10 +1,9 @@ -import { Trans, t } from '@lingui/macro'; -import { Container, Text, Title } from '@mantine/core'; +import { t } from '@lingui/macro'; import { useDocumentTitle } from '@mantine/hooks'; import { useEffect, useState } from 'react'; import { useRouteError } from 'react-router-dom'; -import { LanguageContext } from '../contexts/LanguageContext'; +import GenericErrorPage from '../components/errors/GenericErrorPage'; import { ErrorResponse } from '../states/states'; export default function ErrorPage() { @@ -19,18 +18,9 @@ export default function ErrorPage() { }, [error]); return ( - - - - <Trans>Error</Trans> - - - Sorry, an unexpected error has occurred. - - - {error.statusText || error.message} - - - + ); } diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index a5d16d530a..2b801566f1 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -17,11 +17,13 @@ import { } from '@tabler/icons-react'; import { lazy, useMemo } from 'react'; +import PermissionDenied from '../../../../components/errors/PermissionDenied'; import { PlaceholderPill } from '../../../../components/items/Placeholder'; import { PanelGroup, PanelType } from '../../../../components/nav/PanelGroup'; import { SettingsHeader } from '../../../../components/nav/SettingsHeader'; import { GlobalSettingList } from '../../../../components/settings/SettingList'; import { Loadable } from '../../../../functions/loading'; +import { useUserState } from '../../../../states/UserState'; const ReportTemplatePanel = Loadable( lazy(() => import('./ReportTemplatePanel')) @@ -74,6 +76,8 @@ const CurrencyTable = Loadable( ); export default function AdminCenter() { + const user = useUserState(); + const adminCenterPanels: PanelType[] = useMemo(() => { return [ { @@ -187,19 +191,25 @@ export default function AdminCenter() { ); return ( - - - - - + <> + {user.isStaff() ? ( + + + + + + ) : ( + + )} + ); } diff --git a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx index bd0ac884b8..9a25f715d6 100644 --- a/src/frontend/src/pages/Index/Settings/SystemSettings.tsx +++ b/src/frontend/src/pages/Index/Settings/SystemSettings.tsx @@ -19,11 +19,13 @@ import { } from '@tabler/icons-react'; import { useMemo } from 'react'; +import PermissionDenied from '../../../components/errors/PermissionDenied'; import { PlaceholderPanel } from '../../../components/items/Placeholder'; import { PanelGroup, PanelType } from '../../../components/nav/PanelGroup'; import { SettingsHeader } from '../../../components/nav/SettingsHeader'; import { GlobalSettingList } from '../../../components/settings/SettingList'; import { useServerApiState } from '../../../states/ApiState'; +import { useUserState } from '../../../states/UserState'; /** * System settings page @@ -295,19 +297,26 @@ export default function SystemSettings() { } ]; }, []); + + const user = useUserState(); + const [server] = useServerApiState((state) => [state.server]); return ( <> - - Switch to User Setting} - /> - - + {user.isStaff() ? ( + + Switch to User Setting} + /> + + + ) : ( + + )} ); } diff --git a/src/frontend/src/pages/NotFound.tsx b/src/frontend/src/pages/NotFound.tsx deleted file mode 100644 index 53c1595a15..0000000000 --- a/src/frontend/src/pages/NotFound.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Trans } from '@lingui/macro'; -import { Button, Center, Container, Stack, Text, Title } from '@mantine/core'; -import { IconArrowBack } from '@tabler/icons-react'; -import { useNavigate } from 'react-router-dom'; - -import { LanguageContext } from '../contexts/LanguageContext'; - -export default function NotFound() { - const navigate = useNavigate(); - - return ( - -
- - - - <Trans>Not Found</Trans> - - - Sorry, this page is not known or was moved. - - - - -
-
- ); -} diff --git a/src/frontend/src/pages/build/BuildDetail.tsx b/src/frontend/src/pages/build/BuildDetail.tsx index ec49228af8..28e9929b9c 100644 --- a/src/frontend/src/pages/build/BuildDetail.tsx +++ b/src/frontend/src/pages/build/BuildDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconClipboardCheck, IconClipboardList, @@ -30,6 +30,7 @@ import { UnlinkBarcodeAction, ViewBarcodeAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; @@ -61,7 +62,8 @@ export default function BuildDetail() { const { instance: build, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.build_order_list, pk: id, @@ -410,21 +412,22 @@ export default function BuildDetail() { {editBuild.modal} {duplicateBuild.modal} {cancelBuild.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/company/CompanyDetail.tsx b/src/frontend/src/pages/company/CompanyDetail.tsx index d8102cecfb..3d542142bd 100644 --- a/src/frontend/src/pages/company/CompanyDetail.tsx +++ b/src/frontend/src/pages/company/CompanyDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconBuildingFactory2, IconBuildingWarehouse, @@ -30,6 +30,7 @@ import { EditItemAction } from '../../components/items/ActionDropdown'; import { Breadcrumb } from '../../components/nav/BreadcrumbList'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; @@ -66,7 +67,8 @@ export default function CompanyDetail(props: Readonly) { const { instance: company, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.company_list, pk: id, @@ -320,18 +322,19 @@ export default function CompanyDetail(props: Readonly) { return ( <> {editCompany.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx index 7a145ad9b7..49a8681064 100644 --- a/src/frontend/src/pages/company/ManufacturerPartDetail.tsx +++ b/src/frontend/src/pages/company/ManufacturerPartDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconBuildingWarehouse, IconDots, @@ -20,6 +20,7 @@ import { DuplicateItemAction, EditItemAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; @@ -44,7 +45,8 @@ export default function ManufacturerPartDetail() { const { instance: manufacturerPart, instanceQuery, - refreshInstance + refreshInstance, + requestStatus } = useInstance({ endpoint: ApiEndpoints.manufacturer_part_list, pk: id, @@ -244,17 +246,18 @@ export default function ManufacturerPartDetail() { return ( <> {editManufacturerPart.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/company/SupplierPartDetail.tsx b/src/frontend/src/pages/company/SupplierPartDetail.tsx index bef8168c48..d6e917e21e 100644 --- a/src/frontend/src/pages/company/SupplierPartDetail.tsx +++ b/src/frontend/src/pages/company/SupplierPartDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconCurrencyDollar, IconDots, @@ -21,6 +21,7 @@ import { DuplicateItemAction, EditItemAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { ApiEndpoints } from '../../enums/ApiEndpoints'; @@ -46,7 +47,8 @@ export default function SupplierPartDetail() { const { instance: supplierPart, instanceQuery, - refreshInstance + refreshInstance, + requestStatus } = useInstance({ endpoint: ApiEndpoints.supplier_part_list, pk: id, @@ -312,18 +314,19 @@ export default function SupplierPartDetail() { return ( <> {editSuppliertPart.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/part/CategoryDetail.tsx b/src/frontend/src/pages/part/CategoryDetail.tsx index a94999e396..2a5dca6661 100644 --- a/src/frontend/src/pages/part/CategoryDetail.tsx +++ b/src/frontend/src/pages/part/CategoryDetail.tsx @@ -18,6 +18,7 @@ import { DeleteItemAction, EditItemAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -56,7 +57,8 @@ export default function CategoryDetail({}: {}) { const { instance: category, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.category_list, hasPrimaryKey: true, @@ -275,29 +277,34 @@ export default function CategoryDetail({}: {}) { <> {editCategory.modal} {deleteCategory.modal} - - - { - setTreeOpen(false); - }} - selectedId={category?.pk} - /> - { - setTreeOpen(true); - }} - actions={categoryActions} - /> - - + + + + { + setTreeOpen(false); + }} + selectedId={category?.pk} + /> + { + setTreeOpen(true); + }} + actions={categoryActions} + /> + + + ); } diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index 3eafa7b23f..838d304b11 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -1,13 +1,5 @@ import { t } from '@lingui/macro'; -import { - Alert, - Divider, - Grid, - LoadingOverlay, - Skeleton, - Stack, - Table -} from '@mantine/core'; +import { Alert, Grid, Skeleton, Stack, Table } from '@mantine/core'; import { IconBookmarks, IconBuilding, @@ -31,7 +23,7 @@ import { IconVersions } from '@tabler/icons-react'; import { useSuspenseQuery } from '@tanstack/react-query'; -import { ReactNode, useMemo, useState } from 'react'; +import { ReactNode, useEffect, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { api } from '../../App'; @@ -54,6 +46,7 @@ import { ViewBarcodeAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -105,7 +98,8 @@ export default function PartDetail() { const { instance: part, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.part_list, pk: id, @@ -821,33 +815,34 @@ export default function PartDetail() { {duplicatePart.modal} {editPart.modal} {deletePart.modal} - - - { - setTreeOpen(false); - }} - selectedId={part?.category} - /> - { - setTreeOpen(true); - }} - actions={partActions} - /> - - {transferStockItems.modal} - {countStockItems.modal} - + + + { + setTreeOpen(false); + }} + selectedId={part?.category} + /> + { + setTreeOpen(true); + }} + actions={partActions} + /> + + {transferStockItems.modal} + {countStockItems.modal} + + ); } diff --git a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx index fe79915276..9fab1e1ccf 100644 --- a/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx +++ b/src/frontend/src/pages/purchasing/PurchaseOrderDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconDots, IconInfoCircle, @@ -27,6 +27,7 @@ import { UnlinkBarcodeAction, ViewBarcodeAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; @@ -40,7 +41,6 @@ import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; -import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; import { PurchaseOrderLineItemTable } from '../../tables/purchasing/PurchaseOrderLineItemTable'; @@ -57,7 +57,8 @@ export default function PurchaseOrderDetail() { const { instance: order, instanceQuery, - refreshInstance + refreshInstance, + requestStatus } = useInstance({ endpoint: ApiEndpoints.purchase_order_list, pk: id, @@ -355,18 +356,19 @@ export default function PurchaseOrderDetail() { return ( <> {editPurchaseOrder.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx index 4d62470f1a..c65f778faa 100644 --- a/src/frontend/src/pages/sales/ReturnOrderDetail.tsx +++ b/src/frontend/src/pages/sales/ReturnOrderDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconDots, IconInfoCircle, @@ -23,6 +23,7 @@ import { EditItemAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; @@ -36,7 +37,6 @@ import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; -import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; @@ -51,7 +51,8 @@ export default function ReturnOrderDetail() { const { instance: order, instanceQuery, - refreshInstance + refreshInstance, + requestStatus } = useInstance({ endpoint: ApiEndpoints.return_order_list, pk: id, @@ -320,18 +321,19 @@ export default function ReturnOrderDetail() { <> {editReturnOrder.modal} {duplicateReturnOrder.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/sales/SalesOrderDetail.tsx b/src/frontend/src/pages/sales/SalesOrderDetail.tsx index 12b31bb9e6..2222f7cbd2 100644 --- a/src/frontend/src/pages/sales/SalesOrderDetail.tsx +++ b/src/frontend/src/pages/sales/SalesOrderDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconDots, IconInfoCircle, @@ -26,6 +26,7 @@ import { EditItemAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { StatusRenderer } from '../../components/render/StatusRenderer'; @@ -39,7 +40,6 @@ import { useEditApiFormModal } from '../../hooks/UseForm'; import { useInstance } from '../../hooks/UseInstance'; -import { apiUrl } from '../../states/ApiState'; import { useUserState } from '../../states/UserState'; import { BuildOrderTable } from '../../tables/build/BuildOrderTable'; import { AttachmentTable } from '../../tables/general/AttachmentTable'; @@ -55,7 +55,8 @@ export default function SalesOrderDetail() { const { instance: order, instanceQuery, - refreshInstance + refreshInstance, + requestStatus } = useInstance({ endpoint: ApiEndpoints.sales_order_list, pk: id, @@ -344,18 +345,19 @@ export default function SalesOrderDetail() { return ( <> {editSalesOrder.modal} - - - - - + + + + + + ); } diff --git a/src/frontend/src/pages/stock/LocationDetail.tsx b/src/frontend/src/pages/stock/LocationDetail.tsx index 744e782e5b..4ad6330286 100644 --- a/src/frontend/src/pages/stock/LocationDetail.tsx +++ b/src/frontend/src/pages/stock/LocationDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { LoadingOverlay, Skeleton, Stack, Text } from '@mantine/core'; +import { Skeleton, Stack, Text } from '@mantine/core'; import { IconDots, IconInfoCircle, @@ -23,6 +23,7 @@ import { UnlinkBarcodeAction, ViewBarcodeAction } from '../../components/items/ActionDropdown'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -63,7 +64,8 @@ export default function Stock() { const { instance: location, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.stock_location_list, hasPrimaryKey: true, @@ -355,29 +357,33 @@ export default function Stock() { <> {editLocation.modal} {deleteLocation.modal} - - - setTreeOpen(false)} - selectedId={location?.pk} - /> - { - setTreeOpen(true); - }} - /> - - {transferStockItems.modal} - {countStockItems.modal} - + + + setTreeOpen(false)} + selectedId={location?.pk} + /> + { + setTreeOpen(true); + }} + /> + + {transferStockItems.modal} + {countStockItems.modal} + + ); } diff --git a/src/frontend/src/pages/stock/StockDetail.tsx b/src/frontend/src/pages/stock/StockDetail.tsx index 14face5201..c07814af48 100644 --- a/src/frontend/src/pages/stock/StockDetail.tsx +++ b/src/frontend/src/pages/stock/StockDetail.tsx @@ -1,5 +1,5 @@ import { t } from '@lingui/macro'; -import { Grid, LoadingOverlay, Skeleton, Stack } from '@mantine/core'; +import { Grid, Skeleton, Stack } from '@mantine/core'; import { IconBookmark, IconBoxPadding, @@ -33,6 +33,7 @@ import { ViewBarcodeAction } from '../../components/items/ActionDropdown'; import { PlaceholderPanel } from '../../components/items/Placeholder'; +import InstanceDetail from '../../components/nav/InstanceDetail'; import NavigationTree from '../../components/nav/NavigationTree'; import { PageDetail } from '../../components/nav/PageDetail'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; @@ -76,7 +77,8 @@ export default function StockDetail() { const { instance: stockitem, refreshInstance, - instanceQuery + instanceQuery, + requestStatus } = useInstance({ endpoint: ApiEndpoints.stock_item_list, pk: id, @@ -548,35 +550,36 @@ export default function StockDetail() { }, [stockitem, instanceQuery]); return ( - - - setTreeOpen(false)} - selectedId={stockitem?.location} - /> - { - setTreeOpen(true); - }} - actions={stockActions} - /> - - {editStockItem.modal} - {duplicateStockItem.modal} - {deleteStockItem.modal} - {countStockItem.modal} - {addStockItem.modal} - {removeStockItem.modal} - {transferStockItem.modal} - + + + setTreeOpen(false)} + selectedId={stockitem?.location} + /> + { + setTreeOpen(true); + }} + actions={stockActions} + /> + + {editStockItem.modal} + {duplicateStockItem.modal} + {deleteStockItem.modal} + {countStockItem.modal} + {addStockItem.modal} + {removeStockItem.modal} + {transferStockItem.modal} + + ); } diff --git a/src/frontend/src/router.tsx b/src/frontend/src/router.tsx index a80a0fdce1..b7a040458b 100644 --- a/src/frontend/src/router.tsx +++ b/src/frontend/src/router.tsx @@ -101,7 +101,9 @@ export const AdminCenter = Loadable( lazy(() => import('./pages/Index/Settings/AdminCenter/Index')) ); -export const NotFound = Loadable(lazy(() => import('./pages/NotFound'))); +export const NotFound = Loadable( + lazy(() => import('./components/errors/NotFound')) +); export const Login = Loadable(lazy(() => import('./pages/Auth/Login'))); export const Logout = Loadable(lazy(() => import('./pages/Auth/Logout'))); export const Logged_In = Loadable(lazy(() => import('./pages/Auth/Logged-In'))); diff --git a/src/frontend/tests/baseFixtures.ts b/src/frontend/tests/baseFixtures.ts index 841a91acaf..5789f7e975 100644 --- a/src/frontend/tests/baseFixtures.ts +++ b/src/frontend/tests/baseFixtures.ts @@ -64,7 +64,8 @@ export const test = baseTest.extend({ url != 'http://localhost:8000/api/barcode/' && url != 'http://localhost:8000/api/news/?search=&offset=0&limit=25' && url != 'https://docs.inventree.org/en/versions.json' && - !url.startsWith('chrome://') + !url.startsWith('chrome://') && + url.indexOf('99999') < 0 ) messages.push(msg); }); diff --git a/src/frontend/tests/login.ts b/src/frontend/tests/login.ts index 0be2945163..d4cef1d681 100644 --- a/src/frontend/tests/login.ts +++ b/src/frontend/tests/login.ts @@ -31,10 +31,14 @@ export const doQuickLogin = async ( password = password ?? user.password; url = url ?? baseUrl; - // await page.goto(logoutUrl); await page.goto(`${url}/login/?login=${username}&password=${password}`); await page.waitForURL('**/platform/home'); await page .getByRole('heading', { name: 'Welcome to your Dashboard,' }) .waitFor(); }; + +export const doLogout = async (page) => { + await page.goto(`${baseUrl}/logout/`); + await page.waitForURL('**/platform/login'); +}; diff --git a/src/frontend/tests/pages/pui_part.spec.ts b/src/frontend/tests/pages/pui_part.spec.ts index d411738184..bcf8d86ff1 100644 --- a/src/frontend/tests/pages/pui_part.spec.ts +++ b/src/frontend/tests/pages/pui_part.spec.ts @@ -224,3 +224,13 @@ test('PUI - Pages - Part - Notes', async ({ page }) => { // Check that the original notes are still present await page.getByText('This is some data').waitFor(); }); + +test('PUI - Pages - Part - 404', async ({ page }) => { + await doQuickLogin(page); + + await page.goto(`${baseUrl}/part/99999/`); + await page.getByText('Page Not Found', { exact: true }).waitFor(); + + // Clear out any console error messages + await page.evaluate(() => console.clear()); +}); diff --git a/src/frontend/tests/pui_basic.spec.ts b/src/frontend/tests/pui_basic.spec.ts index e7d8c3d74c..fa23b82279 100644 --- a/src/frontend/tests/pui_basic.spec.ts +++ b/src/frontend/tests/pui_basic.spec.ts @@ -54,4 +54,5 @@ test('PUI - Quick Login Test', async ({ page }) => { // Logout (via URL) await page.goto(`${baseUrl}/logout/`); await page.waitForURL('**/platform/login'); + await page.getByLabel('username'); }); diff --git a/src/frontend/tests/pui_general.spec.ts b/src/frontend/tests/pui_general.spec.ts index c436308c5e..a62547aca3 100644 --- a/src/frontend/tests/pui_general.spec.ts +++ b/src/frontend/tests/pui_general.spec.ts @@ -1,6 +1,6 @@ import { test } from './baseFixtures.js'; import { baseUrl } from './defaults.js'; -import { doQuickLogin } from './login.js'; +import { doLogout, doQuickLogin } from './login.js'; test('PUI - Parts', async ({ page }) => { await doQuickLogin(page); @@ -141,57 +141,6 @@ test('PUI - Scanning', async ({ page }) => { await page.getByRole('option', { name: 'Manual input' }).click(); }); -test('PUI - Admin', async ({ page }) => { - // Note here we login with admin access - await doQuickLogin(page, 'admin', 'inventree'); - - // User settings - await page.getByRole('button', { name: 'admin' }).click(); - await page.getByRole('menuitem', { name: 'Account settings' }).click(); - await page.getByRole('tab', { name: 'Security' }).click(); - //await page.getByRole('tab', { name: 'Dashboard' }).click(); - await page.getByRole('tab', { name: 'Display Options' }).click(); - await page.getByText('Date Format').waitFor(); - await page.getByRole('tab', { name: 'Search' }).click(); - await page.getByText('Regex Search').waitFor(); - await page.getByRole('tab', { name: 'Notifications' }).click(); - await page.getByRole('tab', { name: 'Reporting' }).click(); - await page.getByText('Inline report display').waitFor(); - - // System Settings - await page.getByRole('link', { name: 'Switch to System Setting' }).click(); - await page.getByText('Base URL', { exact: true }).waitFor(); - await page.getByRole('tab', { name: 'Login' }).click(); - await page.getByRole('tab', { name: 'Barcodes' }).click(); - await page.getByRole('tab', { name: 'Notifications' }).click(); - await page.getByRole('tab', { name: 'Pricing' }).click(); - await page.getByRole('tab', { name: 'Labels' }).click(); - await page.getByRole('tab', { name: 'Reporting' }).click(); - await page.getByRole('tab', { name: 'Part Categories' }).click(); - //wait page.locator('#mantine-9hqbwrml8-tab-parts').click(); - //await page.locator('#mantine-9hqbwrml8-tab-stock').click(); - await page.getByRole('tab', { name: 'Stocktake' }).click(); - await page.getByRole('tab', { name: 'Build Orders' }).click(); - await page.getByRole('tab', { name: 'Purchase Orders' }).click(); - await page.getByRole('tab', { name: 'Sales Orders' }).click(); - await page.getByRole('tab', { name: 'Return Orders' }).click(); - - // Admin Center - await page.getByRole('button', { name: 'admin' }).click(); - await page.getByRole('menuitem', { name: 'Admin Center' }).click(); - await page.getByRole('tab', { name: 'Background Tasks' }).click(); - await page.getByRole('tab', { name: 'Error Reports' }).click(); - await page.getByRole('tab', { name: 'Currencies' }).click(); - await page.getByRole('tab', { name: 'Project Codes' }).click(); - await page.getByRole('tab', { name: 'Custom Units' }).click(); - await page.getByRole('tab', { name: 'Part Parameters' }).click(); - await page.getByRole('tab', { name: 'Category Parameters' }).click(); - await page.getByRole('tab', { name: 'Label Templates' }).click(); - await page.getByRole('tab', { name: 'Report Templates' }).click(); - await page.getByRole('tab', { name: 'Plugins' }).click(); - await page.getByRole('tab', { name: 'Machines' }).click(); -}); - test('PUI - Language / Color', async ({ page }) => { await doQuickLogin(page); diff --git a/src/frontend/tests/pui_settings.spec.ts b/src/frontend/tests/pui_settings.spec.ts new file mode 100644 index 0000000000..b0962dcd36 --- /dev/null +++ b/src/frontend/tests/pui_settings.spec.ts @@ -0,0 +1,83 @@ +import { test } from './baseFixtures.js'; +import { baseUrl } from './defaults.js'; +import { doLogout, doQuickLogin } from './login.js'; + +test('PUI - Admin', async ({ page }) => { + // Note here we login with admin access + await doQuickLogin(page, 'admin', 'inventree'); + + // User settings + await page.getByRole('button', { name: 'admin' }).click(); + await page.getByRole('menuitem', { name: 'Account settings' }).click(); + await page.getByRole('tab', { name: 'Security' }).click(); + + await page.getByRole('tab', { name: 'Display Options' }).click(); + await page.getByText('Date Format').waitFor(); + await page.getByRole('tab', { name: 'Search' }).click(); + await page.getByText('Regex Search').waitFor(); + await page.getByRole('tab', { name: 'Notifications' }).click(); + await page.getByRole('tab', { name: 'Reporting' }).click(); + await page.getByText('Inline report display').waitFor(); + + // System Settings + await page.getByRole('link', { name: 'Switch to System Setting' }).click(); + await page.getByText('Base URL', { exact: true }).waitFor(); + await page.getByRole('tab', { name: 'Login' }).click(); + await page.getByRole('tab', { name: 'Barcodes' }).click(); + await page.getByRole('tab', { name: 'Notifications' }).click(); + await page.getByRole('tab', { name: 'Pricing' }).click(); + await page.getByRole('tab', { name: 'Labels' }).click(); + await page.getByRole('tab', { name: 'Reporting' }).click(); + await page.getByRole('tab', { name: 'Part Categories' }).click(); + + await page.getByRole('tab', { name: 'Stocktake' }).click(); + await page.getByRole('tab', { name: 'Build Orders' }).click(); + await page.getByRole('tab', { name: 'Purchase Orders' }).click(); + await page.getByRole('tab', { name: 'Sales Orders' }).click(); + await page.getByRole('tab', { name: 'Return Orders' }).click(); + + // Admin Center + await page.getByRole('button', { name: 'admin' }).click(); + await page.getByRole('menuitem', { name: 'Admin Center' }).click(); + await page.getByRole('tab', { name: 'Background Tasks' }).click(); + await page.getByRole('tab', { name: 'Error Reports' }).click(); + await page.getByRole('tab', { name: 'Currencies' }).click(); + await page.getByRole('tab', { name: 'Project Codes' }).click(); + await page.getByRole('tab', { name: 'Custom Units' }).click(); + await page.getByRole('tab', { name: 'Part Parameters' }).click(); + await page.getByRole('tab', { name: 'Category Parameters' }).click(); + await page.getByRole('tab', { name: 'Label Templates' }).click(); + await page.getByRole('tab', { name: 'Report Templates' }).click(); + await page.getByRole('tab', { name: 'Plugins' }).click(); + await page.getByRole('tab', { name: 'Machines' }).click(); +}); + +test('PUI - Admin - Unauthorized', async ({ page }) => { + // Try to access "admin" page with a non-staff user + await doQuickLogin(page, 'allaccess', 'nolimits'); + + await page.goto(`${baseUrl}/settings/admin/`); + await page.waitForURL('**/settings/admin/**'); + + // Should get a permission denied message + await page.getByText('Permission Denied').waitFor(); + await page + .getByRole('button', { name: 'Return to the index page' }) + .waitFor(); + + // Try to access user settings page (should be accessible) + await page.goto(`${baseUrl}/settings/user/`); + await page.waitForURL('**/settings/user/**'); + + await page.getByRole('tab', { name: 'Display Options' }).click(); + await page.getByRole('tab', { name: 'Account' }).click(); + + // Try to access global settings page + await page.goto(`${baseUrl}/settings/system/`); + await page.waitForURL('**/settings/system/**'); + + await page.getByText('Permission Denied').waitFor(); + await page + .getByRole('button', { name: 'Return to the index page' }) + .waitFor(); +});