Skip to content
Merged
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 @@ -31,6 +31,8 @@ import { JobHistoryDialog } from "./JobHistoryDialog";
import {
deleteJobs,
getJobHistory,
getJobSandbox,
getJobSandboxUrl,
killJobs,
rescheduleJobs,
useJobs,
Expand Down Expand Up @@ -182,7 +184,6 @@ export function JobDataTable({
diracxUrl,
rowSelection,
clearSelected,
setSearchBody,
mutateJobs,
]);

Expand Down Expand Up @@ -251,7 +252,6 @@ export function JobDataTable({
diracxUrl,
rowSelection,
clearSelected,
setSearchBody,
mutateJobs,
]);

Expand Down Expand Up @@ -311,29 +311,19 @@ export function JobDataTable({
} finally {
setBackdropOpen(false);
}
}, [
accessToken,
diracxUrl,
rowSelection,
clearSelected,
setSearchBody,
mutateJobs,
]);
}, [accessToken, diracxUrl, rowSelection, clearSelected, mutateJobs]);

/**
* Handle the history of the selected job
*/
const handleHistory = useCallback(
async (selectedId: number | null) => {
async (selectedId: string | null) => {
if (!selectedId) return;
const jobId = Number(selectedId);
setBackdropOpen(true);
setSelectedJobId(selectedId);
setSelectedJobId(jobId);
try {
const { data } = await getJobHistory(
diracxUrl,
selectedId,
accessToken,
);
const { data } = await getJobHistory(diracxUrl, jobId, accessToken);
setBackdropOpen(false);
// Show the history
setJobHistoryData(data);
Expand All @@ -360,6 +350,61 @@ export function JobDataTable({
setIsHistoryDialogOpen(false);
};

const handleSandboxDownload = useCallback(
async (selectedId: string | null, sbType: "input" | "output") => {
if (!selectedId) return;
const jobId = Number(selectedId);
setBackdropOpen(true);
try {
const { data: sandboxData } = await getJobSandbox(
diracxUrl,
jobId,
sbType,
accessToken,
);
if (sandboxData.length === 0)
throw new Error(`No ${sbType} sandbox found`);
const pfn = sandboxData[0];
if (pfn) {
const { data: urlData } = await getJobSandboxUrl(
diracxUrl,
pfn,
accessToken,
);
if (urlData?.url) {
const link = document.createElement("a");
link.href = urlData.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 retrieve a download URL for the sandbox",
);
} 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);
}
},
[accessToken, diracxUrl],
);

/**
* The toolbar components for the data grid
*/
Expand Down Expand Up @@ -405,10 +450,21 @@ export function JobDataTable({
() => [
{
label: "Get history",
onClick: (id: string | null) => handleHistory(Number(id)),
onClick: (id: string | null) => handleHistory(id),
dataTestId: "get-history-button",
},
{
label: "Download input sandbox",
onClick: (id: string | null) => handleSandboxDownload(id, "input"),
dataTestId: "download-input-sandbox-button",
},
{
label: "Download output sandbox",
onClick: (id: string | null) => handleSandboxDownload(id, "output"),
dataTestId: "download-output-sandbox-button",
},
],
[handleHistory],
[handleHistory, handleSandboxDownload],
);

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import dayjs from "dayjs";
import utc from "dayjs/plugin/utc";

import { fetcher } from "../../hooks/utils";
import { Filter, SearchBody, Job, JobHistory } from "../../types";
import type { JobSummary } from "../../types";
import {
Filter,
SearchBody,
Job,
JobHistory,
JobSandboxPFNResponse,
SandboxUrlResponse,
} from "../../types";

type TimeUnit = "minute" | "hour" | "day" | "month" | "year";

Expand Down Expand Up @@ -194,6 +201,38 @@ 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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unrelated to this PR because this is the currently adopted pattern but I think we have to do something about OIDC fetching and auth in general because having and passing the access token into the fetchers and around like this is not ideal.

At least the access token should be fetched and substituted into a utility fetch function defined in one place and supplied from a hook, where this hook is used everywhere in the specific data fetchers. We can use useSWR to build data fetchers, and we can provide special utility functions for doing POSTs. In an ideal world we never touch access tokens in the browser anyway...

I will make an issue about it

): 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=${encodeURIComponent(pfn)}`;
return fetcher([url, accessToken]);
}

/**
* Retrieves the job summary for a given grouping.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import { SearchBody } from "../../types";
export interface MenuItem {
label: string;
onClick: (id: string | null) => void;
dataTestId?: string;
}

/**
Expand Down Expand Up @@ -655,6 +656,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 | null)[];

// 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 @@ -15,3 +15,4 @@ export * from "./EquationStatus";
export * from "./operators";
export * from "./SearchBarTokenNature";
export * from "./CategoryType";
export * from "./Sandbox";
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/* eslint-disable */
import { Job, JobHistory, SearchBody, JobSummary } from "../../src/types";
import {
Job,
JobHistory,
SearchBody,
JobSummary,
JobSandboxPFNResponse,
SandboxUrlResponse,
} from "../../src/types";

import { useJobMockContext } from "./contexts.mock";

Expand Down Expand Up @@ -67,7 +74,7 @@ export function useJobs(
};
}

// Mock implementation of `getJobHistory`
// Mock implementation of getJobHistory
export const getJobHistory = async (
_diracxUrl: string | null,
_jobId: number,
Expand All @@ -79,6 +86,31 @@ export const getJobHistory = async (
return { data: mockJobHistoryResponse.jobHistory || [] };
};

// Mock implementation of getJobSandbox
export function getJobSandbox(
diracxUrl: string | null,
jobId: number,
sbType: "input" | "output",
accessToken: string,
): Promise<{ headers: Headers; data: JobSandboxPFNResponse }> {
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: SandboxUrlResponse }> {
return Promise.resolve({
headers: new Headers(),
data: { url: "", expires_in: 0 },
});
}

// Mock implementation of refreshJobs
export const refreshJobs = (
_diracxUrl: string | null,
Expand Down
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 @@ -74,11 +74,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 Download input sandbox
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 Download output sandbox
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 @@ -92,7 +144,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
Loading