diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobDataService.ts b/packages/diracx-web-components/src/components/JobMonitor/JobDataService.ts index b6b4424..dd41087 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobDataService.ts +++ b/packages/diracx-web-components/src/components/JobMonitor/JobDataService.ts @@ -2,6 +2,9 @@ import useSWR, { mutate } from "swr"; import dayjs from "dayjs"; +import utc from "dayjs/plugin/utc"; + +dayjs.extend(utc); import { fetcher } from "../../hooks/utils"; import { Filter, SearchBody, Job, JobHistory } from "../../types"; @@ -68,17 +71,35 @@ export const refreshJobs = ( * * @param selectedIds - An array of job IDs to delete. * @param accessToken - The authentication token. - * @returns A Promise that resolves to an object containing the response headers and data. */ export function deleteJobs( selectedIds: readonly number[], accessToken: string, -): Promise<{ headers: Headers; data: number[] }> { +) { const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); const deleteUrl = `/api/jobs/?${queryString}`; - return fetcher([deleteUrl, accessToken, "DELETE"]); + fetcher([deleteUrl, accessToken, "DELETE"]); } +type JobBulkResponse = { + failed: { + [jobId: number]: { detail: string }; + }; + success: { + [jobId: number]: { SucessContent: unknown }; + }; +}; + +type StatusBody = { + [jobId: number]: { + [timestamp: string]: { + Status: string; + MinorStatus: string; + Source: string; + }; + }; +}; + /** * Kills the specified jobs. * @@ -89,10 +110,25 @@ export function deleteJobs( export function killJobs( selectedIds: readonly number[], accessToken: string, -): Promise<{ headers: Headers; data: number[] }> { - const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); - const killUrl = `/api/jobs/kill?${queryString}`; - return fetcher([killUrl, accessToken, "POST"]); +): Promise<{ headers: Headers; data: JobBulkResponse }> { + const killUrl = `/api/jobs/status`; + + const currentDate = dayjs() + .utc() + .format("YYYY-MM-DDTHH:mm:ss.SSSSSS[Z]") + .toString(); + + const body = selectedIds.reduce((acc: StatusBody, jobId) => { + acc[jobId] = { + [currentDate]: { + Status: "Killed", + MinorStatus: "Marked for termination", + Source: "JobManager", + }, + }; + return acc; + }, {}); + return fetcher([killUrl, accessToken, "PATCH", body]); } /** @@ -105,7 +141,7 @@ export function killJobs( export function rescheduleJobs( selectedIds: readonly number[], accessToken: string, -): Promise<{ headers: Headers; data: number[] }> { +): Promise<{ headers: Headers; data: JobBulkResponse }> { const queryString = selectedIds.map((id) => `job_ids=${id}`).join("&"); const rescheduleUrl = `/api/jobs/reschedule?${queryString}`; return fetcher([rescheduleUrl, accessToken, "POST"]); @@ -117,10 +153,26 @@ export function rescheduleJobs( * @param token - The authentication token. * @returns A Promise that resolves to an object containing the headers and data of the job history. */ -export function getJobHistory( +export async function getJobHistory( jobId: number, accessToken: string, -): Promise<{ headers: Headers; data: { [key: number]: JobHistory[] } }> { - const historyUrl = `/api/jobs/${jobId}/status/history`; - return fetcher([historyUrl, accessToken]); +): Promise<{ data: JobHistory[] }> { + const historyUrl = `/api/jobs/search`; + + const body = { + parameters: ["LoggingInfo"], + search: [ + { + parameter: "JobID", + operator: "eq", + value: jobId, + }, + ], + }; + // Expect the response to be an array of objects with JobID and LoggingInfo + const { data } = await fetcher< + Array<{ JobID: number; LoggingInfo: JobHistory[] }> + >([historyUrl, accessToken, "POST", body]); + + return { data: data[0].LoggingInfo }; } diff --git a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx index 75b28d1..ed7ac57 100644 --- a/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx +++ b/packages/diracx-web-components/src/components/JobMonitor/JobDataTable.tsx @@ -297,7 +297,15 @@ export function JobDataTable() { setBackdropOpen(true); try { const selectedIds = Object.keys(rowSelection).map(Number); - await killJobs(selectedIds, accessToken); + const { data } = await killJobs(selectedIds, accessToken); + + const failedJobs = Object.entries(data.failed).map( + ([jobId, error]) => `Job ${jobId}: ${error.detail}`, + ); + const successfulJobs = Object.keys(data.success).map( + (jobId) => `Job ${jobId}`, + ); + setBackdropOpen(false); refreshJobs( accessToken, @@ -306,11 +314,26 @@ export function JobDataTable() { pagination.pageSize, ); clearSelected(); - setSnackbarInfo({ - open: true, - message: "Killed successfully", - severity: "success", - }); + // Handle Snackbar Messaging + if (successfulJobs.length > 0 && failedJobs.length > 0) { + setSnackbarInfo({ + open: true, + message: `Kill operation summary. Success: ${successfulJobs.join(", ")}. Failed: ${failedJobs.join("; ")}`, + severity: "warning", + }); + } else if (successfulJobs.length > 0) { + setSnackbarInfo({ + open: true, + message: `Kill operation summary. Success: ${successfulJobs.join(", ")}`, + severity: "success", + }); + } else { + setSnackbarInfo({ + open: true, + message: `Kill operation summary. Failed: ${failedJobs.join("; ")}`, + severity: "error", + }); + } } catch (error: unknown) { let errorMessage = "An unknown error occurred"; @@ -319,7 +342,7 @@ export function JobDataTable() { } setSnackbarInfo({ open: true, - message: "Kill failed: " + errorMessage, + message: "Kill operation failed: " + errorMessage, severity: "error", }); } finally { @@ -340,7 +363,15 @@ export function JobDataTable() { setBackdropOpen(true); try { const selectedIds = Object.keys(rowSelection).map(Number); - await rescheduleJobs(selectedIds, accessToken); + const { data } = await rescheduleJobs(selectedIds, accessToken); + + const failedJobs = Object.entries(data.failed).map( + ([jobId, error]) => `Job ${jobId}: ${error.detail}`, + ); + const successfulJobs = Object.keys(data.success).map( + (jobId) => `Job ${jobId}`, + ); + setBackdropOpen(false); refreshJobs( accessToken, @@ -349,11 +380,26 @@ export function JobDataTable() { pagination.pageSize, ); clearSelected(); - setSnackbarInfo({ - open: true, - message: "Rescheduled successfully", - severity: "success", - }); + // Handle Snackbar Messaging + if (successfulJobs.length > 0 && failedJobs.length > 0) { + setSnackbarInfo({ + open: true, + message: `Reschedule operation summary. Success: ${successfulJobs.join(", ")}. Failed: ${failedJobs.join("; ")}`, + severity: "warning", + }); + } else if (successfulJobs.length > 0) { + setSnackbarInfo({ + open: true, + message: `Reschedule operation summary. Success: ${successfulJobs.join(", ")}`, + severity: "success", + }); + } else { + setSnackbarInfo({ + open: true, + message: `Reschedule operation summary. Failed: ${failedJobs.join("; ")}`, + severity: "error", + }); + } } catch (error: unknown) { let errorMessage = "An unknown error occurred"; @@ -362,7 +408,7 @@ export function JobDataTable() { } setSnackbarInfo({ open: true, - message: "Reschedule failed: " + errorMessage, + message: "Reschedule operation failed: " + errorMessage, severity: "error", }); } finally { @@ -388,7 +434,7 @@ export function JobDataTable() { const { data } = await getJobHistory(selectedId, accessToken); setBackdropOpen(false); // Show the history - setJobHistoryData(data[selectedId]); + setJobHistoryData(data); setIsHistoryDialogOpen(true); } catch (error: unknown) { let errorMessage = "An unknown error occurred";