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 information."""
|
||||||
|
|
||||||
# InvenTree API version
|
# 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."""
|
"""Increment this API version number whenever there is a significant change to the API that any clients need to know about."""
|
||||||
|
|
||||||
INVENTREE_API_TEXT = """
|
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
|
- 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
|
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 django_q.tasks import async_task
|
||||||
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
from djmoney.contrib.exchange.models import ExchangeBackend, Rate
|
||||||
|
from error_report.models import Error
|
||||||
from rest_framework import permissions, serializers
|
from rest_framework import permissions, serializers
|
||||||
from rest_framework.exceptions import NotAcceptable, NotFound
|
from rest_framework.exceptions import NotAcceptable, NotFound
|
||||||
from rest_framework.permissions import IsAdminUser
|
from rest_framework.permissions import IsAdminUser
|
||||||
@ -484,6 +485,30 @@ class CustomUnitDetail(RetrieveUpdateDestroyAPI):
|
|||||||
permission_classes = [permissions.IsAuthenticated, IsStaffOrReadOnly]
|
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):
|
class FlagList(ListAPI):
|
||||||
"""List view for feature flags."""
|
"""List view for feature flags."""
|
||||||
|
|
||||||
@ -659,6 +684,14 @@ common_api_urls = [
|
|||||||
re_path(r'^.*$', NewsFeedEntryList.as_view(), name='api-news-list'),
|
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
|
# Flags
|
||||||
path(
|
path(
|
||||||
'flags/',
|
'flags/',
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from error_report.models import Error
|
||||||
from flags.state import flag_state
|
from flags.state import flag_state
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
@ -302,3 +303,16 @@ class CustomUnitSerializer(InvenTreeModelSerializer):
|
|||||||
|
|
||||||
model = common_models.CustomUnit
|
model = common_models.CustomUnit
|
||||||
fields = ['pk', 'name', 'symbol', 'definition']
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
"""Initialize this serializer with extra detail fields as required."""
|
"""Initialize this serializer with extra detail fields as required."""
|
||||||
# Check if 'available' quantity was supplied
|
# Check if 'available' quantity was supplied
|
||||||
|
|
||||||
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
self.has_available_quantity = 'available' in kwargs.get('data', {})
|
||||||
|
|
||||||
brief = kwargs.pop('brief', False)
|
brief = kwargs.pop('brief', False)
|
||||||
|
@ -1,7 +1,16 @@
|
|||||||
import { t } from '@lingui/macro';
|
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 { 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 { IconBarcode, IconPrinter } from '@tabler/icons-react';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
import { DataTable, DataTableSortStatus } from 'mantine-datatable';
|
||||||
@ -9,6 +18,7 @@ import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
|
|||||||
|
|
||||||
import { api } from '../../App';
|
import { api } from '../../App';
|
||||||
import { TableState } from '../../hooks/UseTable';
|
import { TableState } from '../../hooks/UseTable';
|
||||||
|
import { ActionButton } from '../buttons/ActionButton';
|
||||||
import { ButtonMenu } from '../buttons/ButtonMenu';
|
import { ButtonMenu } from '../buttons/ButtonMenu';
|
||||||
import { TableColumn } from './Column';
|
import { TableColumn } from './Column';
|
||||||
import { TableColumnSelect } from './ColumnSelect';
|
import { TableColumnSelect } from './ColumnSelect';
|
||||||
@ -27,6 +37,7 @@ const defaultPageSize: number = 25;
|
|||||||
* @param tableState : TableState - State manager for the table
|
* @param tableState : TableState - State manager for the table
|
||||||
* @param defaultSortColumn : string - Default column to sort by
|
* @param defaultSortColumn : string - Default column to sort by
|
||||||
* @param noRecordsText : string - Text to display when no records are found
|
* @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 enableDownload : boolean - Enable download actions
|
||||||
* @param enableFilters : boolean - Enable filter actions
|
* @param enableFilters : boolean - Enable filter actions
|
||||||
* @param enableSelection : boolean - Enable row selection
|
* @param enableSelection : boolean - Enable row selection
|
||||||
@ -46,6 +57,7 @@ export type InvenTreeTableProps<T = any> = {
|
|||||||
params?: any;
|
params?: any;
|
||||||
defaultSortColumn?: string;
|
defaultSortColumn?: string;
|
||||||
noRecordsText?: string;
|
noRecordsText?: string;
|
||||||
|
enableBulkDelete?: boolean;
|
||||||
enableDownload?: boolean;
|
enableDownload?: boolean;
|
||||||
enableFilters?: boolean;
|
enableFilters?: boolean;
|
||||||
enableSelection?: boolean;
|
enableSelection?: boolean;
|
||||||
@ -350,6 +362,58 @@ export function InvenTreeTable<T = any>({
|
|||||||
|
|
||||||
const [recordCount, setRecordCount] = useState<number>(0);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{tableProps.enableFilters &&
|
{tableProps.enableFilters &&
|
||||||
@ -385,6 +449,15 @@ export function InvenTreeTable<T = any>({
|
|||||||
actions={tableProps.printingActions ?? []}
|
actions={tableProps.printingActions ?? []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{(tableProps.enableBulkDelete ?? false) && (
|
||||||
|
<ActionButton
|
||||||
|
disabled={tableState.selectedRecords.length == 0}
|
||||||
|
icon={<IconTrash />}
|
||||||
|
color="red"
|
||||||
|
tooltip={t`Delete selected records`}
|
||||||
|
onClick={deleteSelectedRecords}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Space />
|
<Space />
|
||||||
<Group position="right" spacing={5}>
|
<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_reload = 'api-plugin-reload',
|
||||||
plugin_registry_status = 'api-plugin-registry-status',
|
plugin_registry_status = 'api-plugin-registry-status',
|
||||||
|
|
||||||
|
error_report_list = 'api-error-report-list',
|
||||||
project_code_list = 'api-project-code-list',
|
project_code_list = 'api-project-code-list',
|
||||||
custom_unit_list = 'api-custom-unit-list'
|
custom_unit_list = 'api-custom-unit-list'
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Trans, t } from '@lingui/macro';
|
import { Trans, t } from '@lingui/macro';
|
||||||
import { Divider, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
import { Divider, Paper, SimpleGrid, Stack, Text, Title } from '@mantine/core';
|
||||||
import {
|
import {
|
||||||
|
IconExclamationCircle,
|
||||||
IconList,
|
IconList,
|
||||||
IconListDetails,
|
IconListDetails,
|
||||||
IconPlugConnected,
|
IconPlugConnected,
|
||||||
@ -23,6 +24,10 @@ const PluginManagementPanel = Loadable(
|
|||||||
lazy(() => import('./PluginManagementPanel'))
|
lazy(() => import('./PluginManagementPanel'))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const ErrorReportTable = Loadable(
|
||||||
|
lazy(() => import('../../../../components/tables/settings/ErrorTable'))
|
||||||
|
);
|
||||||
|
|
||||||
const ProjectCodeTable = Loadable(
|
const ProjectCodeTable = Loadable(
|
||||||
lazy(() => import('../../../../components/tables/settings/ProjectCodeTable'))
|
lazy(() => import('../../../../components/tables/settings/ProjectCodeTable'))
|
||||||
);
|
);
|
||||||
@ -47,6 +52,12 @@ export default function AdminCenter() {
|
|||||||
icon: <IconUsersGroup />,
|
icon: <IconUsersGroup />,
|
||||||
content: <UserManagementPanel />
|
content: <UserManagementPanel />
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'errors',
|
||||||
|
label: t`Error Reports`,
|
||||||
|
icon: <IconExclamationCircle />,
|
||||||
|
content: <ErrorReportTable />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'projectcodes',
|
name: 'projectcodes',
|
||||||
label: t`Project Codes`,
|
label: t`Project Codes`,
|
||||||
|
@ -189,6 +189,8 @@ export function apiEndpoint(path: ApiPaths): string {
|
|||||||
return 'plugins/install/';
|
return 'plugins/install/';
|
||||||
case ApiPaths.plugin_reload:
|
case ApiPaths.plugin_reload:
|
||||||
return 'plugins/reload/';
|
return 'plugins/reload/';
|
||||||
|
case ApiPaths.error_report_list:
|
||||||
|
return 'error-report/';
|
||||||
case ApiPaths.project_code_list:
|
case ApiPaths.project_code_list:
|
||||||
return 'project-code/';
|
return 'project-code/';
|
||||||
case ApiPaths.custom_unit_list:
|
case ApiPaths.custom_unit_list:
|
||||||
|
Loading…
Reference in New Issue
Block a user