mirror of
https://github.com/inventree/InvenTree.git
synced 2025-06-17 04:25:42 +00:00
Barcode logging (#8150)
* Add model for recording barcode scan results * Add "admin" interface for new model * Add API endpoints for barcode scan history * Add global setting to control barcode result save * Add frontend API endpoint * Add PUI table in "admin center" * Add API filter class * Enable table filtering * Update model definition * Allow more characters for barcode log * Log results to server * Add setting to control how long results are stored * Table updates * Add background task to delete old barcode scans * Add detail drawer for barcode scan * Log messages for BarcodePOReceive * Add warning message if barcode logging is not enabled * Add "context" data to BarcodeScanResult * Display context data (if available) * Add context data when scanning * Simplify / refactor BarcodeSOAllocate * Refactor BarcodePOAllocate * Limit the number of saved scans * Improve error message display in PUI * Simplify barcode logging * Improve table * Updates * Settings page fix * Fix panel tooltips * Adjust table * Add "result" field * Refactor calls to "log_scan" * Display result in PUI table * Updates * Fix typo * Update unit test * Improve exception handling * Unit test updates * Enhanced unit test * Ensure all database key config values are upper case * Refactor some playwright helpers * Adds playwright test for barcode scan history table * Requires some timeout * Add docs
This commit is contained in:
@ -3,6 +3,7 @@ import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
CopyButton as MantineCopyButton,
|
||||
MantineSize,
|
||||
Text,
|
||||
Tooltip
|
||||
} from '@mantine/core';
|
||||
@ -11,10 +12,14 @@ import { InvenTreeIcon } from '../../functions/icons';
|
||||
|
||||
export function CopyButton({
|
||||
value,
|
||||
label
|
||||
label,
|
||||
content,
|
||||
size
|
||||
}: Readonly<{
|
||||
value: any;
|
||||
label?: JSX.Element;
|
||||
label?: string;
|
||||
content?: JSX.Element;
|
||||
size?: MantineSize;
|
||||
}>) {
|
||||
const ButtonComponent = label ? Button : ActionIcon;
|
||||
|
||||
@ -26,15 +31,19 @@ export function CopyButton({
|
||||
color={copied ? 'teal' : 'gray'}
|
||||
onClick={copy}
|
||||
variant="transparent"
|
||||
size="sm"
|
||||
size={size ?? 'sm'}
|
||||
>
|
||||
{copied ? (
|
||||
<InvenTreeIcon icon="check" />
|
||||
) : (
|
||||
<InvenTreeIcon icon="copy" />
|
||||
)}
|
||||
|
||||
{label && <Text ml={10}>{label}</Text>}
|
||||
{content}
|
||||
{label && (
|
||||
<Text p={size ?? 'sm'} size={size ?? 'sm'}>
|
||||
{label}
|
||||
</Text>
|
||||
)}
|
||||
</ButtonComponent>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
@ -75,7 +75,7 @@ export const InvenTreeQRCode = ({
|
||||
const { data } = useQuery({
|
||||
queryKey: ['qr-code', mdl_prop.model, mdl_prop.pk],
|
||||
queryFn: async () => {
|
||||
const res = await api.post(apiUrl(ApiEndpoints.generate_barcode), {
|
||||
const res = await api.post(apiUrl(ApiEndpoints.barcode_generate), {
|
||||
model: mdl_prop.model,
|
||||
pk: mdl_prop.pk
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import {
|
||||
Anchor,
|
||||
Badge,
|
||||
@ -178,10 +178,7 @@ export function AboutInvenTreeModal({
|
||||
</Table>
|
||||
<Divider />
|
||||
<Group justify="space-between">
|
||||
<CopyButton
|
||||
value={copyval}
|
||||
label={<Trans>Copy version information</Trans>}
|
||||
/>
|
||||
<CopyButton value={copyval} label={t`Copy version information`} />
|
||||
<Space />
|
||||
<Button
|
||||
color="red"
|
||||
|
@ -134,7 +134,7 @@ function BasePanelGroup({
|
||||
(panel) =>
|
||||
!panel.hidden && (
|
||||
<Tooltip
|
||||
label={`tooltip-${panel.name}`}
|
||||
label={panel.label ?? panel.name}
|
||||
key={panel.name}
|
||||
disabled={expanded}
|
||||
position="right"
|
||||
|
@ -39,10 +39,6 @@ export enum ApiEndpoints {
|
||||
api_search = 'search/',
|
||||
settings_global_list = 'settings/global/',
|
||||
settings_user_list = 'settings/user/',
|
||||
barcode = 'barcode/',
|
||||
barcode_link = 'barcode/link/',
|
||||
barcode_unlink = 'barcode/unlink/',
|
||||
generate_barcode = 'barcode/generate/',
|
||||
news = 'news/',
|
||||
global_status = 'generic/status/',
|
||||
custom_state_list = 'generic/status/custom/',
|
||||
@ -54,6 +50,13 @@ export enum ApiEndpoints {
|
||||
content_type_list = 'contenttype/',
|
||||
icons = 'icons/',
|
||||
|
||||
// Barcode API endpoints
|
||||
barcode = 'barcode/',
|
||||
barcode_history = 'barcode/history/',
|
||||
barcode_link = 'barcode/link/',
|
||||
barcode_unlink = 'barcode/unlink/',
|
||||
barcode_generate = 'barcode/generate/',
|
||||
|
||||
// Data import endpoints
|
||||
import_session_list = 'importer/session/',
|
||||
import_session_accept_fields = 'importer/session/:id/accept_fields/',
|
||||
|
@ -22,5 +22,5 @@ export function shortenString({
|
||||
// Otherwise, shorten it
|
||||
let N = Math.floor(len / 2 - 1);
|
||||
|
||||
return str.slice(0, N) + '...' + str.slice(-N);
|
||||
return str.slice(0, N) + ' ... ' + str.slice(-N);
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
IconListDetails,
|
||||
IconPackages,
|
||||
IconPlugConnected,
|
||||
IconQrcode,
|
||||
IconReport,
|
||||
IconScale,
|
||||
IconSitemap,
|
||||
@ -68,6 +69,10 @@ const ErrorReportTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/ErrorTable'))
|
||||
);
|
||||
|
||||
const BarcodeScanHistoryTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/BarcodeScanHistoryTable'))
|
||||
);
|
||||
|
||||
const ImportSesssionTable = Loadable(
|
||||
lazy(() => import('../../../../tables/settings/ImportSessionTable'))
|
||||
);
|
||||
@ -111,6 +116,12 @@ export default function AdminCenter() {
|
||||
icon: <IconFileUpload />,
|
||||
content: <ImportSesssionTable />
|
||||
},
|
||||
{
|
||||
name: 'barcode-history',
|
||||
label: t`Barcode Scans`,
|
||||
icon: <IconQrcode />,
|
||||
content: <BarcodeScanHistoryTable />
|
||||
},
|
||||
{
|
||||
name: 'background',
|
||||
label: t`Background Tasks`,
|
||||
|
@ -98,7 +98,9 @@ export default function SystemSettings() {
|
||||
'BARCODE_INPUT_DELAY',
|
||||
'BARCODE_WEBCAM_SUPPORT',
|
||||
'BARCODE_SHOW_TEXT',
|
||||
'BARCODE_GENERATION_PLUGIN'
|
||||
'BARCODE_GENERATION_PLUGIN',
|
||||
'BARCODE_STORE_RESULTS',
|
||||
'BARCODE_RESULTS_MAX_NUM'
|
||||
]}
|
||||
/>
|
||||
)
|
||||
|
@ -243,6 +243,10 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
};
|
||||
}, [props]);
|
||||
|
||||
const enableSelection: boolean = useMemo(() => {
|
||||
return tableProps.enableSelection || tableProps.enableBulkDelete || false;
|
||||
}, [tableProps]);
|
||||
|
||||
// Check if any columns are switchable (can be hidden)
|
||||
const hasSwitchableColumns: boolean = useMemo(() => {
|
||||
if (props.enableColumnSwitching == false) {
|
||||
@ -309,7 +313,6 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
columns,
|
||||
fieldNames,
|
||||
tableProps.rowActions,
|
||||
tableProps.enableSelection,
|
||||
tableState.hiddenColumns,
|
||||
tableState.selectedRecords
|
||||
]);
|
||||
@ -641,7 +644,7 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
actions={tableProps.barcodeActions ?? []}
|
||||
/>
|
||||
)}
|
||||
{(tableProps.enableBulkDelete ?? false) && (
|
||||
{tableProps.enableBulkDelete && (
|
||||
<ActionButton
|
||||
disabled={!tableState.hasSelectedRecords}
|
||||
icon={<IconTrash />}
|
||||
@ -726,12 +729,10 @@ export function InvenTreeTable<T extends Record<string, any>>({
|
||||
sortStatus={sortStatus}
|
||||
onSortStatusChange={handleSortStatusChange}
|
||||
selectedRecords={
|
||||
tableProps.enableSelection
|
||||
? tableState.selectedRecords
|
||||
: undefined
|
||||
enableSelection ? tableState.selectedRecords : undefined
|
||||
}
|
||||
onSelectedRecordsChange={
|
||||
tableProps.enableSelection ? onSelectedRecordsChange : undefined
|
||||
enableSelection ? onSelectedRecordsChange : undefined
|
||||
}
|
||||
rowExpansion={tableProps.rowExpansion}
|
||||
rowStyle={tableProps.rowStyle}
|
||||
|
290
src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx
Normal file
290
src/frontend/src/tables/settings/BarcodeScanHistoryTable.tsx
Normal file
@ -0,0 +1,290 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Divider,
|
||||
Drawer,
|
||||
Group,
|
||||
MantineStyleProp,
|
||||
Stack,
|
||||
Table,
|
||||
Text
|
||||
} from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { IconExclamationCircle } from '@tabler/icons-react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { CopyButton } from '../../components/buttons/CopyButton';
|
||||
import { PassFailButton } from '../../components/buttons/YesNoButton';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { RenderUser } from '../../components/render/User';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { UserRoles } from '../../enums/Roles';
|
||||
import { shortenString } from '../../functions/tables';
|
||||
import { useUserFilters } from '../../hooks/UseFilter';
|
||||
import { useDeleteApiFormModal } from '../../hooks/UseForm';
|
||||
import { useTable } from '../../hooks/UseTable';
|
||||
import { apiUrl } from '../../states/ApiState';
|
||||
import { useGlobalSettingsState } from '../../states/SettingsState';
|
||||
import { useUserState } from '../../states/UserState';
|
||||
import { TableColumn } from '../Column';
|
||||
import { TableFilter } from '../Filter';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowDeleteAction } from '../RowActions';
|
||||
|
||||
/*
|
||||
* Render detail information for a particular barcode scan result.
|
||||
*/
|
||||
function BarcodeScanDetail({ scan }: { scan: any }) {
|
||||
const dataStyle: MantineStyleProp = {
|
||||
textWrap: 'wrap',
|
||||
lineBreak: 'auto',
|
||||
wordBreak: 'break-word'
|
||||
};
|
||||
|
||||
const hasResponseData = useMemo(() => {
|
||||
return scan.response && Object.keys(scan.response).length > 0;
|
||||
}, [scan.response]);
|
||||
|
||||
const hasContextData = useMemo(() => {
|
||||
return scan.context && Object.keys(scan.context).length > 0;
|
||||
}, [scan.context]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack gap="xs">
|
||||
<Divider />
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<StylishText size="sm">{t`Barcode Information`}</StylishText>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Barcode`}</Table.Th>
|
||||
<Table.Td>
|
||||
<Text size="sm" style={dataStyle}>
|
||||
{scan.data}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<CopyButton value={scan.data} size="xs" />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Timestamp`}</Table.Th>
|
||||
<Table.Td>{scan.timestamp}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`User`}</Table.Th>
|
||||
<Table.Td>
|
||||
<RenderUser instance={scan.user_detail} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Endpoint`}</Table.Th>
|
||||
<Table.Td>{scan.endpoint}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Result`}</Table.Th>
|
||||
<Table.Td>
|
||||
<PassFailButton value={scan.result} />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
{hasContextData && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<StylishText size="sm">{t`Context`}</StylishText>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
{hasContextData &&
|
||||
Object.keys(scan.context).map((key) => (
|
||||
<Table.Tr key={key}>
|
||||
<Table.Th>{key}</Table.Th>
|
||||
<Table.Td>
|
||||
<Text size="sm" style={dataStyle}>
|
||||
{scan.context[key]}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<CopyButton value={scan.context[key]} size="xs" />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
{hasResponseData && (
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<StylishText size="sm">{t`Response`}</StylishText>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)}
|
||||
{hasResponseData &&
|
||||
Object.keys(scan.response).map((key) => (
|
||||
<Table.Tr key={key}>
|
||||
<Table.Th>{key}</Table.Th>
|
||||
<Table.Td>
|
||||
<Text size="sm" style={dataStyle}>
|
||||
{scan.response[key]}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<CopyButton value={scan.response[key]} size="xs" />
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Display the barcode scan history table
|
||||
*/
|
||||
export default function BarcodeScanHistoryTable() {
|
||||
const user = useUserState();
|
||||
const table = useTable('barcode-history');
|
||||
|
||||
const globalSettings = useGlobalSettingsState();
|
||||
|
||||
const userFilters = useUserFilters();
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
const tableColumns: TableColumn[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
accessor: 'timestamp',
|
||||
sortable: true,
|
||||
switchable: false,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Group justify="space-between" wrap="nowrap">
|
||||
<Text>{record.timestamp}</Text>
|
||||
{record.user_detail && (
|
||||
<Badge size="xs">{record.user_detail.username}</Badge>
|
||||
)}
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'data',
|
||||
sortable: false,
|
||||
switchable: true,
|
||||
render: (record: any) => {
|
||||
return (
|
||||
<Text
|
||||
size="xs"
|
||||
style={{
|
||||
textWrap: 'wrap',
|
||||
lineBreak: 'auto',
|
||||
wordBreak: 'break-word'
|
||||
}}
|
||||
>
|
||||
{shortenString({ str: record.data, len: 100 })}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
accessor: 'endpoint',
|
||||
sortable: true
|
||||
},
|
||||
{
|
||||
accessor: 'result',
|
||||
sortable: true,
|
||||
render: (record: any) => {
|
||||
return <PassFailButton value={record.result} />;
|
||||
}
|
||||
}
|
||||
];
|
||||
}, []);
|
||||
|
||||
const filters: TableFilter[] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
name: 'user',
|
||||
label: t`User`,
|
||||
choices: userFilters.choices,
|
||||
description: t`Filter by user`
|
||||
},
|
||||
{
|
||||
name: 'result',
|
||||
label: t`Result`,
|
||||
description: t`Filter by result`
|
||||
}
|
||||
];
|
||||
}, [userFilters]);
|
||||
|
||||
const canDelete: boolean = useMemo(() => {
|
||||
return user.isStaff() && user.hasDeleteRole(UserRoles.admin);
|
||||
}, [user]);
|
||||
|
||||
const [selectedResult, setSelectedResult] = useState<any>({});
|
||||
|
||||
const deleteResult = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.barcode_history,
|
||||
pk: selectedResult.pk,
|
||||
title: t`Delete Barcode Scan Record`,
|
||||
table: table
|
||||
});
|
||||
|
||||
const rowActions = useCallback(
|
||||
(record: any) => {
|
||||
return [
|
||||
RowDeleteAction({
|
||||
hidden: !canDelete,
|
||||
onClick: () => {
|
||||
setSelectedResult(record);
|
||||
deleteResult.open();
|
||||
}
|
||||
})
|
||||
];
|
||||
},
|
||||
[canDelete, user]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{deleteResult.modal}
|
||||
<Drawer
|
||||
opened={opened}
|
||||
size="xl"
|
||||
position="right"
|
||||
title={<StylishText>{t`Barcode Scan Details`}</StylishText>}
|
||||
onClose={close}
|
||||
>
|
||||
<BarcodeScanDetail scan={selectedResult} />
|
||||
</Drawer>
|
||||
<Stack gap="xs">
|
||||
{!globalSettings.isSet('BARCODE_STORE_RESULTS') && (
|
||||
<Alert
|
||||
color="red"
|
||||
icon={<IconExclamationCircle />}
|
||||
title={t`Logging Disabled`}
|
||||
>
|
||||
<Text>{t`Barcode logging is not enabled`}</Text>
|
||||
</Alert>
|
||||
)}
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.barcode_history)}
|
||||
tableState={table}
|
||||
columns={tableColumns}
|
||||
props={{
|
||||
tableFilters: filters,
|
||||
enableBulkDelete: canDelete,
|
||||
rowActions: rowActions,
|
||||
onRowClick: (row) => {
|
||||
setSelectedResult(row);
|
||||
open();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,8 +1,9 @@
|
||||
import { t } from '@lingui/macro';
|
||||
import { Drawer, Text } from '@mantine/core';
|
||||
import { Drawer, Group, Stack, Table, Text } from '@mantine/core';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
|
||||
import { CopyButton } from '../../components/buttons/CopyButton';
|
||||
import { StylishText } from '../../components/items/StylishText';
|
||||
import { ApiEndpoints } from '../../enums/ApiEndpoints';
|
||||
import { useDeleteApiFormModal } from '../../hooks/UseForm';
|
||||
@ -13,6 +14,48 @@ import { TableColumn } from '../Column';
|
||||
import { InvenTreeTable } from '../InvenTreeTable';
|
||||
import { RowAction, RowDeleteAction } from '../RowActions';
|
||||
|
||||
function ErrorDetail({ error }: { error: any }) {
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Table>
|
||||
<Table.Tbody>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Message`}</Table.Th>
|
||||
<Table.Td>{error.info}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Timestamp`}</Table.Th>
|
||||
<Table.Td>{error.when}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Path`}</Table.Th>
|
||||
<Table.Td>{error.path}</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Th>{t`Traceback`}</Table.Th>
|
||||
<Table.Td>
|
||||
<Group justify="right">
|
||||
<CopyButton value={error.data} size="sm" />
|
||||
</Group>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
<Table.Tr>
|
||||
<Table.Td colSpan={2}>
|
||||
<Stack gap={3}>
|
||||
{error.data.split('\n').map((line: string, index: number) => (
|
||||
<Text size="xs" key={`error-line-${index}`}>
|
||||
{line}
|
||||
</Text>
|
||||
))}
|
||||
</Stack>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Table for display server error information
|
||||
*/
|
||||
@ -20,8 +63,6 @@ export default function ErrorReportTable() {
|
||||
const table = useTable('error-report');
|
||||
const user = useUserState();
|
||||
|
||||
const [error, setError] = useState<string>('');
|
||||
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
|
||||
const columns: TableColumn[] = useMemo(() => {
|
||||
@ -43,13 +84,11 @@ export default function ErrorReportTable() {
|
||||
];
|
||||
}, []);
|
||||
|
||||
const [selectedError, setSelectedError] = useState<number | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [selectedError, setSelectedError] = useState<any>({});
|
||||
|
||||
const deleteErrorModal = useDeleteApiFormModal({
|
||||
url: ApiEndpoints.error_report_list,
|
||||
pk: selectedError,
|
||||
pk: selectedError.pk,
|
||||
title: t`Delete Error Report`,
|
||||
preFormContent: (
|
||||
<Text c="red">{t`Are you sure you want to delete this error report?`}</Text>
|
||||
@ -62,7 +101,7 @@ export default function ErrorReportTable() {
|
||||
return [
|
||||
RowDeleteAction({
|
||||
onClick: () => {
|
||||
setSelectedError(record.pk);
|
||||
setSelectedError(record);
|
||||
deleteErrorModal.open();
|
||||
}
|
||||
})
|
||||
@ -79,13 +118,7 @@ export default function ErrorReportTable() {
|
||||
title={<StylishText>{t`Error Details`}</StylishText>}
|
||||
onClose={close}
|
||||
>
|
||||
{error.split('\n').map((line: string) => {
|
||||
return (
|
||||
<Text key={line} size="sm">
|
||||
{line}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
<ErrorDetail error={selectedError} />
|
||||
</Drawer>
|
||||
<InvenTreeTable
|
||||
url={apiUrl(ApiEndpoints.error_report_list)}
|
||||
@ -96,7 +129,7 @@ export default function ErrorReportTable() {
|
||||
enableSelection: true,
|
||||
rowActions: rowActions,
|
||||
onRowClick: (row) => {
|
||||
setError(row.data);
|
||||
setSelectedError(row);
|
||||
open();
|
||||
}
|
||||
}}
|
||||
|
@ -1,5 +1,6 @@
|
||||
export const classicUrl = 'http://127.0.0.1:8000';
|
||||
|
||||
export const apiUrl = `${classicUrl}/api`;
|
||||
export const baseUrl = './platform';
|
||||
export const loginUrl = `${baseUrl}/login`;
|
||||
export const logoutUrl = `${baseUrl}/logout`;
|
||||
|
@ -1,58 +1,8 @@
|
||||
import test, { expect } from 'playwright/test';
|
||||
import test from 'playwright/test';
|
||||
|
||||
import { baseUrl } from './defaults.js';
|
||||
import { doQuickLogin } from './login.js';
|
||||
|
||||
/*
|
||||
* Set the value of a global setting in the database
|
||||
*/
|
||||
const setSettingState = async ({
|
||||
request,
|
||||
setting,
|
||||
value
|
||||
}: {
|
||||
request: any;
|
||||
setting: string;
|
||||
value: any;
|
||||
}) => {
|
||||
const url = `http://localhost:8000/api/settings/global/${setting}/`;
|
||||
|
||||
const response = await request.patch(url, {
|
||||
data: {
|
||||
value: value
|
||||
},
|
||||
headers: {
|
||||
// Basic username: password authorization
|
||||
Authorization: `Basic ${btoa('admin:inventree')}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(await response.status()).toBe(200);
|
||||
};
|
||||
|
||||
const setPluginState = async ({
|
||||
request,
|
||||
plugin,
|
||||
state
|
||||
}: {
|
||||
request: any;
|
||||
plugin: string;
|
||||
state: boolean;
|
||||
}) => {
|
||||
const url = `http://localhost:8000/api/plugins/${plugin}/activate/`;
|
||||
|
||||
const response = await request.patch(url, {
|
||||
data: {
|
||||
active: state
|
||||
},
|
||||
headers: {
|
||||
// Basic username: password authorization
|
||||
Authorization: `Basic ${btoa('admin:inventree')}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(await response.status()).toBe(200);
|
||||
};
|
||||
import { setPluginState, setSettingState } from './settings.js';
|
||||
|
||||
test('Plugins - Panels', async ({ page, request }) => {
|
||||
await doQuickLogin(page, 'admin', 'inventree');
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { expect, test } from './baseFixtures.js';
|
||||
import { baseUrl } from './defaults.js';
|
||||
import { apiUrl, baseUrl } from './defaults.js';
|
||||
import { doQuickLogin } from './login.js';
|
||||
import { setSettingState } from './settings.js';
|
||||
|
||||
test('PUI - Admin', async ({ page }) => {
|
||||
// Note here we login with admin access
|
||||
@ -85,6 +86,43 @@ test('PUI - Admin', async ({ page }) => {
|
||||
await page.getByRole('button', { name: 'Submit' }).click();
|
||||
});
|
||||
|
||||
test('PUI - Admin - Barcode History', async ({ page, request }) => {
|
||||
// Login with admin credentials
|
||||
await doQuickLogin(page, 'admin', 'inventree');
|
||||
|
||||
// Ensure that the "save scans" setting is enabled
|
||||
await setSettingState({
|
||||
request: request,
|
||||
setting: 'BARCODE_STORE_RESULTS',
|
||||
value: true
|
||||
});
|
||||
|
||||
// Scan some barcodes (via API calls)
|
||||
const barcodes = ['ABC1234', 'XYZ5678', 'QRS9012'];
|
||||
|
||||
barcodes.forEach(async (barcode) => {
|
||||
await request.post(`${apiUrl}/barcode/`, {
|
||||
data: {
|
||||
barcode: barcode
|
||||
},
|
||||
headers: {
|
||||
Authorization: `Basic ${btoa('admin:inventree')}`
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await page.getByRole('button', { name: 'admin' }).click();
|
||||
await page.getByRole('menuitem', { name: 'Admin Center' }).click();
|
||||
await page.getByRole('tab', { name: 'Barcode Scans' }).click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
// Barcode history is displayed in table
|
||||
barcodes.forEach(async (barcode) => {
|
||||
await page.getByText(barcode).first().waitFor();
|
||||
});
|
||||
});
|
||||
|
||||
test('PUI - Admin - Unauthorized', async ({ page }) => {
|
||||
// Try to access "admin" page with a non-staff user
|
||||
await doQuickLogin(page, 'allaccess', 'nolimits');
|
||||
|
54
src/frontend/tests/settings.ts
Normal file
54
src/frontend/tests/settings.ts
Normal file
@ -0,0 +1,54 @@
|
||||
import { expect } from 'playwright/test';
|
||||
|
||||
import { apiUrl } from './defaults';
|
||||
|
||||
/*
|
||||
* Set the value of a global setting in the database
|
||||
*/
|
||||
export const setSettingState = async ({
|
||||
request,
|
||||
setting,
|
||||
value
|
||||
}: {
|
||||
request: any;
|
||||
setting: string;
|
||||
value: any;
|
||||
}) => {
|
||||
const url = `${apiUrl}/settings/global/${setting}/`;
|
||||
|
||||
const response = await request.patch(url, {
|
||||
data: {
|
||||
value: value
|
||||
},
|
||||
headers: {
|
||||
// Basic username: password authorization
|
||||
Authorization: `Basic ${btoa('admin:inventree')}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(await response.status()).toBe(200);
|
||||
};
|
||||
|
||||
export const setPluginState = async ({
|
||||
request,
|
||||
plugin,
|
||||
state
|
||||
}: {
|
||||
request: any;
|
||||
plugin: string;
|
||||
state: boolean;
|
||||
}) => {
|
||||
const url = `${apiUrl}/plugins/${plugin}/activate/`;
|
||||
|
||||
const response = await request.patch(url, {
|
||||
data: {
|
||||
active: state
|
||||
},
|
||||
headers: {
|
||||
// Basic username: password authorization
|
||||
Authorization: `Basic ${btoa('admin:inventree')}`
|
||||
}
|
||||
});
|
||||
|
||||
expect(await response.status()).toBe(200);
|
||||
};
|
Reference in New Issue
Block a user