Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import utc from "dayjs/plugin/utc";

dayjs.extend(utc);
import { fetcher } from "../../hooks/utils";
import { Filter, SearchBody, Job, JobHistory } from "../../types";
import {
Filter,
SearchBody,
Job,
JobHistory,
JobSandboxPFNResponse,
SandboxUrlResponse,
} from "../../types";

function processSearchBody(searchBody: SearchBody) {
searchBody.search = searchBody.search?.map((filter: Filter) => {
Expand Down Expand Up @@ -203,3 +210,35 @@ export async function getJobHistory(

return { data: data[0].LoggingInfo };
}

/**
* Retrieves the sandbox information for a given job ID and sandbox type.
* @param jobId - The ID of the job.
* @param sbType - The type of the sandbox (input or output).
* @param accessToken - The authentication token.
* @returns A Promise that resolves to an object containing the headers and data of the sandboxes.
*/
export function getJobSandbox(
diracxUrl: string | null,
jobId: number,
sbType: "input" | "output",
accessToken: string,
): Promise<{ headers: Headers; data: JobSandboxPFNResponse }> {
const url = `${diracxUrl}/api/jobs/${jobId}/sandbox/${sbType}`;
return fetcher([url, accessToken]);
}

/**
* Retrieves the sandbox URL for a given PFN.
* @param pfn - The PFN of the job.
* @param accessToken - The authentication token.
* @returns A Promise that resolves to an object containing the headers and data of the sandbox URL.
*/
export function getJobSandboxUrl(
diracxUrl: string | null,
pfn: string,
accessToken: string,
): Promise<{ headers: Headers; data: SandboxUrlResponse }> {
const url = `${diracxUrl}/api/jobs/sandbox?pfn=${pfn}`;
return fetcher([url, accessToken]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { JobHistoryDialog } from "./JobHistoryDialog";
import {
deleteJobs,
getJobHistory,
getJobSandbox,
getJobSandboxUrl,
killJobs,
refreshJobs,
rescheduleJobs,
Expand Down Expand Up @@ -362,6 +364,53 @@ export function JobDataTable({
setIsHistoryDialogOpen(false);
};

const handleSandboxDownload = async (
jobId: number | null,
sbType: "input" | "output",
) => {
if (!jobId) return;
setBackdropOpen(true);
try {
const { data } = await getJobSandbox(
diracxUrl,
jobId,
sbType,
accessToken,
);
if (!data) throw new Error(`No sandbox found`);
const pfn = data[0];
setBackdropOpen(false);
if (pfn) {
const { data } = await getJobSandboxUrl(diracxUrl, pfn, accessToken);
if (data?.url) {
const link = document.createElement("a");
link.href = data.url;
link.download = `${sbType}-sandbox-${jobId}.tar.gz`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
setSnackbarInfo({
open: true,
message: `Downloading ${sbType} sandbox of ${jobId}...`,
severity: "info",
});
} else throw new Error(`Could not fetch the sandbox from ${data.url}`);
} else throw new Error(`No ${sbType} sandbox found`);
} catch (error: unknown) {
let errorMessage = "An unknown error occurred";
if (error instanceof Error) {
errorMessage = error.message;
}
setSnackbarInfo({
open: true,
message: `Fetching sandbox of ${jobId} failed: ` + errorMessage,
severity: "error",
});
} finally {
setBackdropOpen(false);
}
};

/**
* The toolbar components for the data grid
*/
Expand Down Expand Up @@ -396,6 +445,17 @@ export function JobDataTable({
{
label: "Get history",
onClick: (id: number | null) => handleHistory(id),
dataTestId: "get-history-button",
},
{
label: "Download input sandbox",
onClick: (id: number | null) => handleSandboxDownload(id, "input"),
dataTestId: "download-input-sandbox-button",
},
{
label: "Download output sandbox",
onClick: (id: number | null) => handleSandboxDownload(id, "output"),
dataTestId: "download-output-sandbox-button",
},
],
[handleHistory],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import { SearchBody } from "../../types";
export interface MenuItem {
label: string;
onClick: (id: number | null) => void;
dataTestId?: string;
}

/**
Expand Down Expand Up @@ -579,6 +580,7 @@ export function DataTable<T extends Record<string, unknown>>({
{menuItems.map((menuItem, index: number) => (
<MenuItem
key={index}
data-testid={menuItem.dataTestId}
onClick={() => {
handleCloseContextMenu();
menuItem.onClick(contextMenu.id);
Expand Down
10 changes: 10 additions & 0 deletions packages/diracx-web-components/src/types/Sandbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Types for sandbox-related API responses

// Response for /api/jobs/<jobId>/sandbox/<sbType>
export type JobSandboxPFNResponse = string[];

// Response for /api/jobs/sandbox?pfn=...
export interface SandboxUrlResponse {
url: string;
expires_in: number;
}
1 change: 1 addition & 0 deletions packages/diracx-web-components/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./DashboardItem";
export * from "./SearchBody";
export * from "./Job";
export * from "./JobHistory";
export * from "./Sandbox";
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,28 @@ export function rescheduleJobs(
},
});
}

// Mock implementation of getJobSandbox
export function getJobSandbox(
diracxUrl: string | null,
jobId: number,
sbType: "input" | "output",
accessToken: string,
): Promise<{ headers: Headers; data: any }> {
return Promise.resolve({
headers: new Headers(),
data: [],
});
}

// Mock implementation of getJobSandboxUrl
export function getJobSandboxUrl(
diracxUrl: string | null,
pfn: string,
accessToken: string,
): Promise<{ headers: Headers; data: any }> {
return Promise.resolve({
headers: new Headers(),
data: {},
});
}
56 changes: 54 additions & 2 deletions packages/diracx-web-components/test/JobMonitor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,11 +78,63 @@ describe("JobDataTable", () => {
expect(getByText("Job accepted")).toBeInTheDocument();
});
});

it("displays the snackbar: no input sandbox", async () => {
const { getByText, getByTestId } = render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<Default />
</VirtuosoMockContext.Provider>,
);

await act(async () => {
fireEvent.contextMenu(getByText("Job 1"));
});

// Now wait for the context menu to appear and click Get history
await act(async () => {
fireEvent.click(getByTestId("download-input-sandbox-button"));
// Allow time for state updates to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});

// Now check for the dialog
await waitFor(() => {
expect(screen.getByText(/No input sandbox found/)).toBeInTheDocument();
});
});

it("displays the snackbar: no output sandbox", async () => {
const { getByText, getByTestId } = render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
<Default />
</VirtuosoMockContext.Provider>,
);

await act(async () => {
fireEvent.contextMenu(getByText("Job 1"));
});

// Now wait for the context menu to appear and click Get history
await act(async () => {
fireEvent.click(getByTestId("download-output-sandbox-button"));
// Allow time for state updates to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});

// Now check for the dialog
await waitFor(() => {
expect(screen.getByText(/No output sandbox found/)).toBeInTheDocument();
});
});
});

describe("JobHistoryDialog", () => {
it("renders the dialog with correct data", async () => {
const { getByText } = render(
const { getByText, getByTestId } = render(
<VirtuosoMockContext.Provider
value={{ viewportHeight: 300, itemHeight: 100 }}
>
Expand All @@ -96,7 +148,7 @@ describe("JobHistoryDialog", () => {

// Now wait for the context menu to appear and click Get history
await act(async () => {
fireEvent.click(getByText("Get history"));
fireEvent.click(getByTestId("get-history-button"));
// Allow time for state updates to complete
await new Promise((resolve) => setTimeout(resolve, 0));
});
Expand Down
Loading