From ef679b1663585b2d6d320e7d283223987cca3f79 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 13 Jan 2024 19:27:47 +1100 Subject: [PATCH] Error API (#6222) * Adds API endpoint for fetching error information * Bump API version * Implement table for displaying server errors * Add support for "bulk delete" in new table component * Update API version with PR * Fix unused variables * Enable table sorting * Display error details --- InvenTree/InvenTree/api_version.py | 7 +- InvenTree/common/api.py | 33 +++++++ InvenTree/common/serializers.py | 14 +++ InvenTree/company/serializers.py | 1 + .../src/components/tables/InvenTreeTable.tsx | 77 +++++++++++++++- .../components/tables/settings/ErrorTable.tsx | 91 +++++++++++++++++++ src/frontend/src/enums/ApiEndpoints.tsx | 1 + .../Index/Settings/AdminCenter/Index.tsx | 11 +++ src/frontend/src/states/ApiState.tsx | 2 + 9 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 src/frontend/src/components/tables/settings/ErrorTable.tsx diff --git a/InvenTree/InvenTree/api_version.py b/InvenTree/InvenTree/api_version.py index 696a125e67..d90e01972a 100644 --- a/InvenTree/InvenTree/api_version.py +++ b/InvenTree/InvenTree/api_version.py @@ -1,12 +1,15 @@ """InvenTree API version information.""" # InvenTree API version -INVENTREE_API_VERSION = 160 +INVENTREE_API_VERSION = 161 """Increment this API version number whenever there is a significant change to the API that any clients need to know about.""" INVENTREE_API_TEXT = """ -v160 -> 2023-012-11 : https://github.com/inventree/InvenTree/pull/6072 +v161 -> 2024-01-13 : https://github.com/inventree/InvenTree/pull/6222 + - Adds API endpoint for system error information + +v160 -> 2023-12-11 : https://github.com/inventree/InvenTree/pull/6072 - Adds API endpoint for allocating stock items against a sales order via barcode scan v159 -> 2023-12-08 : https://github.com/inventree/InvenTree/pull/6056 diff --git a/InvenTree/common/api.py b/InvenTree/common/api.py index 204d79db74..3e0e3d1701 100644 --- a/InvenTree/common/api.py +++ b/InvenTree/common/api.py @@ -10,6 +10,7 @@ from django.views.decorators.csrf import csrf_exempt from django_q.tasks import async_task from djmoney.contrib.exchange.models import ExchangeBackend, Rate +from error_report.models import Error from rest_framework import permissions, serializers from rest_framework.exceptions import NotAcceptable, NotFound from rest_framework.permissions import IsAdminUser @@ -484,6 +485,30 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI): permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly] +class ErrorMessageList(BulkDeleteMixin, ListAPI): + """List view for server error messages.""" + + queryset = Error.objects.all() + serializer_class = common.serializers.ErrorMessageSerializer + permission_classes = [permissions.IsAuthenticated, IsAdminUser] + + filter_backends = SEARCH_ORDER_FILTER + + ordering = '-when' + + ordering_fields = ['when', 'info'] + + search_fields = ['info', 'data'] + + +class ErrorMessageDetail(RetrieveUpdateDestroyAPI): + """Detail view for a single error message.""" + + queryset = Error.objects.all() + serializer_class = common.serializers.ErrorMessageSerializer + permission_classes = [permissions.IsAuthenticated, IsAdminUser] + + class FlagList(ListAPI): """List view for feature flags.""" @@ -659,6 +684,14 @@ common_api_urls = [ re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'), ]), ), + # Error information + re_path( + r'^error-report/', + include([ + path(r'/', ErrorMessageDetail.as_view(), name='api-error-detail'), + re_path(r'^.*$', ErrorMessageList.as_view(), name='api-error-list'), + ]), + ), # Flags path( 'flags/', diff --git a/InvenTree/common/serializers.py b/InvenTree/common/serializers.py index f59950313e..fc00067b20 100644 --- a/InvenTree/common/serializers.py +++ b/InvenTree/common/serializers.py @@ -2,6 +2,7 @@ from django.urls import reverse +from error_report.models import Error from flags.state import flag_state from rest_framework import serializers @@ -302,3 +303,16 @@ class CustomUnitSerializer(InvenTreeModelSerializer): model = common_models.CustomUnit fields = ['pk', 'name', 'symbol', 'definition'] + + +class ErrorMessageSerializer(InvenTreeModelSerializer): + """DRF serializer for server error messages.""" + + class Meta: + """Metaclass options for ErrorMessageSerializer.""" + + model = Error + + fields = ['when', 'info', 'data', 'path', 'pk'] + + read_only_fields = ['when', 'info', 'data', 'path', 'pk'] diff --git a/InvenTree/company/serializers.py b/InvenTree/company/serializers.py index b0acb76137..8370d22510 100644 --- a/InvenTree/company/serializers.py +++ b/InvenTree/company/serializers.py @@ -337,6 +337,7 @@ class SupplierPartSerializer(InvenTreeTagModelSerializer): def __init__(self, *args, **kwargs): """Initialize this serializer with extra detail fields as required.""" # Check if 'available' quantity was supplied + self.has_available_quantity = 'available' in kwargs.get('data', {}) brief = kwargs.pop('brief', False) diff --git a/src/frontend/src/components/tables/InvenTreeTable.tsx b/src/frontend/src/components/tables/InvenTreeTable.tsx index 41c506a3c5..281cc99626 100644 --- a/src/frontend/src/components/tables/InvenTreeTable.tsx +++ b/src/frontend/src/components/tables/InvenTreeTable.tsx @@ -1,7 +1,16 @@ import { t } from '@lingui/macro'; -import { ActionIcon, Indicator, Space, Stack, Tooltip } from '@mantine/core'; +import { + ActionIcon, + Alert, + Indicator, + Space, + Stack, + Tooltip +} from '@mantine/core'; import { Group } from '@mantine/core'; -import { IconFilter, IconRefresh } from '@tabler/icons-react'; +import { modals } from '@mantine/modals'; +import { showNotification } from '@mantine/notifications'; +import { IconFilter, IconRefresh, IconTrash } from '@tabler/icons-react'; import { IconBarcode, IconPrinter } from '@tabler/icons-react'; import { useQuery } from '@tanstack/react-query'; import { DataTable, DataTableSortStatus } from 'mantine-datatable'; @@ -9,6 +18,7 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'; import { api } from '../../App'; import { TableState } from '../../hooks/UseTable'; +import { ActionButton } from '../buttons/ActionButton'; import { ButtonMenu } from '../buttons/ButtonMenu'; import { TableColumn } from './Column'; import { TableColumnSelect } from './ColumnSelect'; @@ -27,6 +37,7 @@ const defaultPageSize: number = 25; * @param tableState : TableState - State manager for the table * @param defaultSortColumn : string - Default column to sort by * @param noRecordsText : string - Text to display when no records are found + * @param enableBulkDelete : boolean - Enable bulk deletion of records * @param enableDownload : boolean - Enable download actions * @param enableFilters : boolean - Enable filter actions * @param enableSelection : boolean - Enable row selection @@ -46,6 +57,7 @@ export type InvenTreeTableProps = { params?: any; defaultSortColumn?: string; noRecordsText?: string; + enableBulkDelete?: boolean; enableDownload?: boolean; enableFilters?: boolean; enableSelection?: boolean; @@ -350,6 +362,58 @@ export function InvenTreeTable({ const [recordCount, setRecordCount] = useState(0); + // Callback function to delete the selected records in the table + const deleteSelectedRecords = useCallback(() => { + if (tableState.selectedRecords.length == 0) { + // Ignore if no records are selected + return; + } + + modals.openConfirmModal({ + title: t`Delete selected records`, + children: ( + + {t`This action cannot be undone!`} + + ), + labels: { + confirm: t`Delete`, + cancel: t`Cancel` + }, + confirmProps: { + color: 'red' + }, + onConfirm: () => { + // Delete the selected records + let selection = tableState.selectedRecords.map((record) => record.pk); + + api + .delete(url, { + data: { + items: selection + } + }) + .then((_response) => { + // Refresh the table + refetch(); + + // Show notification + showNotification({ + title: t`Deleted records`, + message: t`Records were deleted successfully`, + color: 'green' + }); + }) + .catch((_error) => { + console.warn(`Bulk delete operation failed at ${url}`); + }); + } + }); + }, [tableState.selectedRecords]); + return ( <> {tableProps.enableFilters && @@ -385,6 +449,15 @@ export function InvenTreeTable({ actions={tableProps.printingActions ?? []} /> )} + {(tableProps.enableBulkDelete ?? false) && ( + } + color="red" + tooltip={t`Delete selected records`} + onClick={deleteSelectedRecords} + /> + )} diff --git a/src/frontend/src/components/tables/settings/ErrorTable.tsx b/src/frontend/src/components/tables/settings/ErrorTable.tsx new file mode 100644 index 0000000000..c02522d0ea --- /dev/null +++ b/src/frontend/src/components/tables/settings/ErrorTable.tsx @@ -0,0 +1,91 @@ +import { t } from '@lingui/macro'; +import { Drawer, Text } from '@mantine/core'; +import { useDisclosure } from '@mantine/hooks'; +import { useCallback, useMemo, useState } from 'react'; + +import { ApiPaths } from '../../../enums/ApiEndpoints'; +import { openDeleteApiForm } from '../../../functions/forms'; +import { useTable } from '../../../hooks/UseTable'; +import { apiUrl } from '../../../states/ApiState'; +import { StylishText } from '../../items/StylishText'; +import { TableColumn } from '../Column'; +import { InvenTreeTable } from '../InvenTreeTable'; +import { RowAction, RowDeleteAction } from '../RowActions'; + +/* + * Table for display server error information + */ +export default function ErrorReportTable() { + const table = useTable('error-report'); + + const [error, setError] = useState(''); + + const [opened, { open, close }] = useDisclosure(false); + + const columns: TableColumn[] = useMemo(() => { + return [ + { + accessor: 'when', + title: t`When`, + sortable: true + }, + { + accessor: 'path', + title: t`Path`, + sortable: true + }, + { + accessor: 'info', + title: t`Error Information` + } + ]; + }, []); + + const rowActions = useCallback((record: any): RowAction[] => { + return [ + RowDeleteAction({ + onClick: () => { + openDeleteApiForm({ + url: ApiPaths.error_report_list, + pk: record.pk, + title: t`Delete error report`, + onFormSuccess: table.refreshTable, + successMessage: t`Error report deleted`, + preFormWarning: t`Are you sure you want to delete this error report?` + }); + } + }) + ]; + }, []); + + return ( + <> + {t`Error Details`}} + onClose={close} + > + {error.split('\n').map((line: string) => { + return {line}; + })} + + { + console.log(row); + setError(row.data); + open(); + } + }} + /> + + ); +} diff --git a/src/frontend/src/enums/ApiEndpoints.tsx b/src/frontend/src/enums/ApiEndpoints.tsx index c2d2308de5..d12573573a 100644 --- a/src/frontend/src/enums/ApiEndpoints.tsx +++ b/src/frontend/src/enums/ApiEndpoints.tsx @@ -89,6 +89,7 @@ export enum ApiPaths { plugin_reload = 'api-plugin-reload', plugin_registry_status = 'api-plugin-registry-status', + error_report_list = 'api-error-report-list', project_code_list = 'api-project-code-list', custom_unit_list = 'api-custom-unit-list' } diff --git a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx index 551731b136..a672650c43 100644 --- a/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx +++ b/src/frontend/src/pages/Index/Settings/AdminCenter/Index.tsx @@ -1,6 +1,7 @@ import { Trans, t } from '@lingui/macro'; import { Divider, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core'; import { + IconExclamationCircle, IconList, IconListDetails, IconPlugConnected, @@ -23,6 +24,10 @@ const PluginManagementPanel = Loadable( lazy(() => import('./PluginManagementPanel')) ); +const ErrorReportTable = Loadable( + lazy(() => import('../../../../components/tables/settings/ErrorTable')) +); + const ProjectCodeTable = Loadable( lazy(() => import('../../../../components/tables/settings/ProjectCodeTable')) ); @@ -47,6 +52,12 @@ export default function AdminCenter() { icon: , content: }, + { + name: 'errors', + label: t`Error Reports`, + icon: , + content: + }, { name: 'projectcodes', label: t`Project Codes`, diff --git a/src/frontend/src/states/ApiState.tsx b/src/frontend/src/states/ApiState.tsx index c99fb598ef..9ef5bf9c58 100644 --- a/src/frontend/src/states/ApiState.tsx +++ b/src/frontend/src/states/ApiState.tsx @@ -189,6 +189,8 @@ export function apiEndpoint(path: ApiPaths): string { return 'plugins/install/'; case ApiPaths.plugin_reload: return 'plugins/reload/'; + case ApiPaths.error_report_list: + return 'error-report/'; case ApiPaths.project_code_list: return 'project-code/'; case ApiPaths.custom_unit_list: