mirror of
https://github.com/inventree/InvenTree.git
synced 2026-04-15 15:58:48 +00:00
[API] Monitor task (#11527)
* Enhance docstring * Return the ID of an offloaded task * Add API endpoint for background task detail * Add UI hook for monitoring background task progress * Handle queued tasks (not yet started) * Improve UX * Update frontend lib version * Bump API version * Fix notification * Simplify UI interface * Implement internal hook * Fix API path sequence * Add unit tests for task detail endpoint * Refactor code into reusable model * Explicit operation_id for API endpoints * Further refactoring * Use 200 response code - axios does not like 202, simplify it * Return task response for validation of part BOM * Fix schema * Cleanup * Run background worker during playwright tests - For full e2e integration testing * Improve hooks and unit testing * Rename custom hooks to meet react naming requirements
This commit is contained in:
119
src/frontend/lib/hooks/MonitorBackgroundTask.tsx
Normal file
119
src/frontend/lib/hooks/MonitorBackgroundTask.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
import { ApiEndpoints } from '@lib/enums/ApiEndpoints';
|
||||
import { apiUrl } from '@lib/functions/Api';
|
||||
import { useDocumentVisibility } from '@mantine/hooks';
|
||||
import { notifications, showNotification } from '@mantine/notifications';
|
||||
import {
|
||||
IconCircleCheck,
|
||||
IconCircleX,
|
||||
IconExclamationCircle
|
||||
} from '@tabler/icons-react';
|
||||
import { type QueryClient, useQuery } from '@tanstack/react-query';
|
||||
import type { AxiosInstance } from 'axios';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { queryClient } from '../../src/App';
|
||||
|
||||
export type MonitorBackgroundTaskProps = {
|
||||
api: AxiosInstance;
|
||||
queryClient?: QueryClient;
|
||||
title?: string;
|
||||
message: string;
|
||||
errorMessage?: string;
|
||||
successMessage?: string;
|
||||
failureMessage?: string;
|
||||
taskId?: string;
|
||||
onSuccess?: () => void;
|
||||
onFailure?: () => void;
|
||||
onComplete?: () => void;
|
||||
onError?: (error: Error) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for monitoring a background task running on the server
|
||||
*/
|
||||
export default function useMonitorBackgroundTask(
|
||||
props: MonitorBackgroundTaskProps
|
||||
) {
|
||||
const visibility = useDocumentVisibility();
|
||||
|
||||
const [tracking, setTracking] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!props.taskId) {
|
||||
setTracking(true);
|
||||
showNotification({
|
||||
id: `background-task-${props.taskId}`,
|
||||
title: props.title,
|
||||
message: props.message,
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
withCloseButton: false
|
||||
});
|
||||
} else {
|
||||
setTracking(false);
|
||||
}
|
||||
}, [props.taskId]);
|
||||
|
||||
useQuery(
|
||||
{
|
||||
enabled: !!props.taskId && tracking && visibility === 'visible',
|
||||
refetchInterval: 500,
|
||||
queryKey: ['background-task', props.taskId],
|
||||
queryFn: () =>
|
||||
props.api
|
||||
.get(apiUrl(ApiEndpoints.task_overview, props.taskId))
|
||||
.then((response) => {
|
||||
const data = response?.data ?? {};
|
||||
|
||||
if (data.complete) {
|
||||
setTracking(false);
|
||||
props.onComplete?.();
|
||||
|
||||
notifications.update({
|
||||
id: `background-task-${props.taskId}`,
|
||||
title: props.title,
|
||||
loading: false,
|
||||
color: data.success ? 'green' : 'red',
|
||||
message: response.data?.success
|
||||
? (props.successMessage ?? props.message)
|
||||
: (props.failureMessage ?? props.message),
|
||||
icon: response.data?.success ? (
|
||||
<IconCircleCheck />
|
||||
) : (
|
||||
<IconCircleX />
|
||||
),
|
||||
autoClose: 1000,
|
||||
withCloseButton: true
|
||||
});
|
||||
|
||||
if (data.success) {
|
||||
props.onSuccess?.();
|
||||
} else {
|
||||
props.onFailure?.();
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
`Error fetching background task status for task ${props.taskId}:`,
|
||||
error
|
||||
);
|
||||
setTracking(false);
|
||||
props.onError?.(error);
|
||||
|
||||
notifications.update({
|
||||
id: `background-task-${props.taskId}`,
|
||||
title: props.title,
|
||||
loading: false,
|
||||
color: 'red',
|
||||
message: props.errorMessage ?? props.message,
|
||||
icon: <IconExclamationCircle color='red' />,
|
||||
autoClose: 5000,
|
||||
withCloseButton: true
|
||||
});
|
||||
})
|
||||
},
|
||||
queryClient
|
||||
);
|
||||
}
|
||||
@@ -9,48 +9,44 @@ import { ProgressBar } from '../components/ProgressBar';
|
||||
import { ApiEndpoints } from '../enums/ApiEndpoints';
|
||||
import { apiUrl } from '../functions/Api';
|
||||
|
||||
/**
|
||||
* Hook for monitoring a data output process running on the server
|
||||
*/
|
||||
export default function monitorDataOutput({
|
||||
api,
|
||||
queryClient,
|
||||
title,
|
||||
hostname,
|
||||
id
|
||||
}: {
|
||||
export type MonitorDataOutputProps = {
|
||||
api: AxiosInstance;
|
||||
queryClient?: QueryClient;
|
||||
title: string;
|
||||
hostname?: string;
|
||||
id?: number;
|
||||
}) {
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook for monitoring a data output process running on the server
|
||||
*/
|
||||
export default function useMonitorDataOutput(props: MonitorDataOutputProps) {
|
||||
const visibility = useDocumentVisibility();
|
||||
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!id) {
|
||||
if (!!props.id) {
|
||||
setLoading(true);
|
||||
showNotification({
|
||||
id: `data-output-${id}`,
|
||||
title: title,
|
||||
id: `data-output-${props.id}`,
|
||||
title: props.title,
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
withCloseButton: false,
|
||||
message: <ProgressBar size='lg' value={0} progressLabel />
|
||||
});
|
||||
} else setLoading(false);
|
||||
}, [id, title]);
|
||||
}, [props.id, props.title]);
|
||||
|
||||
useQuery(
|
||||
{
|
||||
enabled: !!id && loading && visibility === 'visible',
|
||||
enabled: !!props.id && loading && visibility === 'visible',
|
||||
refetchInterval: 500,
|
||||
queryKey: ['data-output', id, title],
|
||||
queryKey: ['data-output', props.id, props.title],
|
||||
queryFn: () =>
|
||||
api
|
||||
.get(apiUrl(ApiEndpoints.data_output, id))
|
||||
props.api
|
||||
.get(apiUrl(ApiEndpoints.data_output, props.id))
|
||||
.then((response) => {
|
||||
const data = response?.data ?? {};
|
||||
|
||||
@@ -61,21 +57,21 @@ export default function monitorDataOutput({
|
||||
data?.error ?? data?.errors?.error ?? t`Process failed`;
|
||||
|
||||
notifications.update({
|
||||
id: `data-output-${id}`,
|
||||
id: `data-output-${props.id}`,
|
||||
loading: false,
|
||||
icon: <IconExclamationCircle />,
|
||||
autoClose: 2500,
|
||||
title: title,
|
||||
title: props.title,
|
||||
message: error,
|
||||
color: 'red'
|
||||
});
|
||||
} else if (data.complete) {
|
||||
setLoading(false);
|
||||
notifications.update({
|
||||
id: `data-output-${id}`,
|
||||
id: `data-output-${props.id}`,
|
||||
loading: false,
|
||||
autoClose: 2500,
|
||||
title: title,
|
||||
title: props.title,
|
||||
message: t`Process completed successfully`,
|
||||
color: 'green',
|
||||
icon: <IconCircleCheck />
|
||||
@@ -83,7 +79,7 @@ export default function monitorDataOutput({
|
||||
|
||||
if (data.output) {
|
||||
const url = data.output;
|
||||
const base = hostname ?? window.location.origin;
|
||||
const base = props.hostname ?? window.location.origin;
|
||||
|
||||
const downloadUrl = new URL(url, base);
|
||||
|
||||
@@ -91,7 +87,7 @@ export default function monitorDataOutput({
|
||||
}
|
||||
} else {
|
||||
notifications.update({
|
||||
id: `data-output-${id}`,
|
||||
id: `data-output-${props.id}`,
|
||||
loading: true,
|
||||
autoClose: false,
|
||||
withCloseButton: false,
|
||||
@@ -110,19 +106,19 @@ export default function monitorDataOutput({
|
||||
return data;
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
console.error('Error in monitorDataOutput:', error);
|
||||
console.error('Error in useMonitorDataOutput:', error);
|
||||
setLoading(false);
|
||||
notifications.update({
|
||||
id: `data-output-${id}`,
|
||||
id: `data-output-${props.id}`,
|
||||
loading: false,
|
||||
autoClose: 2500,
|
||||
title: title,
|
||||
title: props.title,
|
||||
message: error.message || t`Process failed`,
|
||||
color: 'red'
|
||||
});
|
||||
return {};
|
||||
})
|
||||
},
|
||||
queryClient
|
||||
props.queryClient
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,4 +73,11 @@ export {
|
||||
} from './components/RowActions';
|
||||
|
||||
// Shared hooks
|
||||
export { default as monitorDataOutput } from './hooks/MonitorDataOutput';
|
||||
export {
|
||||
default as useMonitorDataOutput,
|
||||
type MonitorDataOutputProps
|
||||
} from './hooks/MonitorDataOutput';
|
||||
export {
|
||||
default as useMonitorBackgroundTask,
|
||||
type MonitorBackgroundTaskProps
|
||||
} from './hooks/MonitorBackgroundTask';
|
||||
|
||||
Reference in New Issue
Block a user