mirror of
https://github.com/inventree/InvenTree
synced 2024-08-30 18:33:04 +00:00
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
This commit is contained in:
parent
5180d86388
commit
ef679b1663
@ -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
|
||||
|
@ -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/',
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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}>
|
||||
|
91
src/frontend/src/components/tables/settings/ErrorTable.tsx
Normal file
91
src/frontend/src/components/tables/settings/ErrorTable.tsx
Normal 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();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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'
|
||||
}
|
||||
|
@ -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`,
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user