* 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
This commit is contained in:
Oliver 2024-01-13 19:27:47 +11:00 committed by GitHub
parent 5180d86388
commit ef679b1663
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 233 additions and 4 deletions

View File

@ -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

View File

@ -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'<int:pk>/', ErrorMessageDetail.as_view(), name='api-error-detail'),
re_path(r'^.*$', ErrorMessageList.as_view(), name='api-error-list'),
]),
),
# Flags
path(
'flags/',

View File

@ -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']

View File

@ -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)

View File

@ -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<T = any> = {
params?: any;
defaultSortColumn?: string;
noRecordsText?: string;
enableBulkDelete?: boolean;
enableDownload?: boolean;
enableFilters?: boolean;
enableSelection?: boolean;
@ -350,6 +362,58 @@ export function InvenTreeTable<T = any>({
const [recordCount, setRecordCount] = useState<number>(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: (
<Alert
color="red"
title={t`Are you sure you want to delete the selected records?`}
>
{t`This action cannot be undone!`}
</Alert>
),
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<T = any>({
actions={tableProps.printingActions ?? []}
/>
)}
{(tableProps.enableBulkDelete ?? false) && (
<ActionButton
disabled={tableState.selectedRecords.length == 0}
icon={<IconTrash />}
color="red"
tooltip={t`Delete selected records`}
onClick={deleteSelectedRecords}
/>
)}
</Group>
<Space />
<Group position="right" spacing={5}>

View File

@ -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<string>('');
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 (
<>
<Drawer
opened={opened}
size="xl"
position="right"
title={<StylishText>{t`Error Details`}</StylishText>}
onClose={close}
>
{error.split('\n').map((line: string) => {
return <Text size="sm">{line}</Text>;
})}
</Drawer>
<InvenTreeTable
url={apiUrl(ApiPaths.error_report_list)}
tableState={table}
columns={columns}
props={{
enableBulkDelete: true,
enableSelection: true,
rowActions: rowActions,
onRowClick: (row) => {
console.log(row);
setError(row.data);
open();
}
}}
/>
</>
);
}

View File

@ -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'
}

View File

@ -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: <IconUsersGroup />,
content: <UserManagementPanel />
},
{
name: 'errors',
label: t`Error Reports`,
icon: <IconExclamationCircle />,
content: <ErrorReportTable />
},
{
name: 'projectcodes',
label: t`Project Codes`,

View File

@ -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: