Skip to content

Commit 2bad9c2

Browse files
committed
ui: refactor jobs detail page to functional components
This commit refactors the job details page to use React functional components and hooks. It also begins some work on moving the job details API fetch out of the global state and into local `useSWR` hooks. Epic: none Release note: None
1 parent 5c17046 commit 2bad9c2

File tree

7 files changed

+566
-194
lines changed

7 files changed

+566
-194
lines changed

pkg/ui/workspaces/cluster-ui/src/api/jobsApi.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
// included in the /LICENSE file.
55

66
import { cockroach } from "@cockroachlabs/crdb-protobuf-client";
7+
import long from "long";
8+
import { SWRConfiguration } from "swr";
79

8-
import { propsToQueryString } from "../util";
10+
import { propsToQueryString, useSwrWithClusterId } from "../util";
911

1012
import { fetchData } from "./fetchData";
1113

@@ -47,9 +49,19 @@ export const getJobs = (
4749
);
4850
};
4951

50-
export const getJob = (
51-
req: JobRequest,
52-
): Promise<cockroach.server.serverpb.JobResponse> => {
52+
export function useJobDetails(jobId: long, opts: SWRConfiguration = {}) {
53+
return useSwrWithClusterId<JobResponse>(
54+
{ name: "jobDetailsById", jobId },
55+
() => getJob({ job_id: jobId }),
56+
{
57+
revalidateOnFocus: false,
58+
revalidateOnReconnect: false,
59+
...opts,
60+
},
61+
);
62+
}
63+
64+
export const getJob = (req: JobRequest): Promise<JobResponse> => {
5365
return fetchData(
5466
cockroach.server.serverpb.JobResponse,
5567
`${JOBS_PATH}/${req.job_id}`,
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2025 The Cockroach Authors.
2+
//
3+
// Use of this software is governed by the CockroachDB Software License
4+
// included in the /LICENSE file.
5+
6+
import "@testing-library/jest-dom";
7+
import { configureStore } from "@reduxjs/toolkit";
8+
import { render, waitFor } from "@testing-library/react";
9+
import userEvent from "@testing-library/user-event";
10+
import * as H from "history";
11+
import Long from "long";
12+
import React from "react";
13+
import { Provider } from "react-redux";
14+
import { MemoryRouter } from "react-router";
15+
16+
import * as jobApi from "src/api/jobsApi";
17+
import { CockroachCloudContext } from "src/contexts";
18+
19+
import { JOB_STATUS_RUNNING, JOB_STATUS_SUCCEEDED } from "../util";
20+
21+
import { JobDetailsPropsV2, JobDetailsV2 } from "./jobDetails";
22+
23+
const mockGetJob = (jobStatus: string | null) => {
24+
jest.spyOn(jobApi, "getJob").mockResolvedValue({
25+
id: Long.fromNumber(1),
26+
status: jobStatus,
27+
running_status: "test running status",
28+
type: "test job type",
29+
statement: "test job statement",
30+
description: "test job description",
31+
username: "test user",
32+
descriptor_ids: [],
33+
fraction_completed: jobStatus === JOB_STATUS_SUCCEEDED ? 1 : 0.5,
34+
error: "",
35+
highwater_decimal: "1",
36+
execution_failures: [],
37+
coordinator_id: Long.fromNumber(1),
38+
messages: [],
39+
num_runs: Long.fromNumber(1),
40+
});
41+
};
42+
43+
const mockFetchExecutionDetailFiles = jest.fn().mockResolvedValue({
44+
files: ["file1", "file2"],
45+
});
46+
47+
const mockCollectExecutionDetails = jest.fn().mockImplementation(() => {
48+
mockFetchExecutionDetailFiles.mockResolvedValue({
49+
files: ["file1", "file2", "file3"],
50+
});
51+
return Promise.resolve({ req_resp: true });
52+
});
53+
54+
const createJobDetailsPageProps = (): JobDetailsPropsV2 => {
55+
const history = H.createHashHistory();
56+
return {
57+
adminRoleSelector: jest.fn().mockReturnValue(true),
58+
refreshUserSQLRoles: jest.fn(),
59+
onFetchExecutionDetailFiles: mockFetchExecutionDetailFiles,
60+
onCollectExecutionDetails: mockCollectExecutionDetails,
61+
onDownloadExecutionFile: jest.fn(),
62+
history: history,
63+
location: history.location,
64+
match: {
65+
url: "",
66+
path: history.location.pathname,
67+
isExact: false,
68+
params: { id: "1" },
69+
},
70+
};
71+
};
72+
73+
describe("JobDetailsV2", () => {
74+
afterEach(() => {
75+
jest.clearAllMocks();
76+
jest.useRealTimers();
77+
});
78+
79+
const renderPage = () => {
80+
const store = configureStore({
81+
reducer: {
82+
noop: (state = {}) => state,
83+
},
84+
});
85+
return render(
86+
<Provider store={store}>
87+
<CockroachCloudContext.Provider value={false}>
88+
<MemoryRouter>
89+
<JobDetailsV2 {...createJobDetailsPageProps()} />
90+
</MemoryRouter>
91+
</CockroachCloudContext.Provider>
92+
</Provider>,
93+
);
94+
};
95+
96+
it("refreshes job details periodically", async () => {
97+
jest.useFakeTimers();
98+
mockGetJob(JOB_STATUS_RUNNING);
99+
const { getByText } = renderPage();
100+
await waitFor(() => getByText("50.0%"));
101+
mockGetJob(JOB_STATUS_SUCCEEDED);
102+
jest.advanceTimersByTime(10 * 1000);
103+
await waitFor(() => getByText(JOB_STATUS_SUCCEEDED));
104+
});
105+
106+
it("refetches execution files after collection", async () => {
107+
mockGetJob(JOB_STATUS_SUCCEEDED);
108+
const { getByText, queryByText } = renderPage();
109+
await waitFor(() => getByText(JOB_STATUS_SUCCEEDED));
110+
userEvent.click(getByText("Advanced Debugging"));
111+
await waitFor(() => getByText("file1"));
112+
getByText("file2");
113+
expect(queryByText("file3")).toBeNull();
114+
userEvent.click(getByText("Request Execution Details"));
115+
await waitFor(() => getByText("file3"));
116+
});
117+
});

0 commit comments

Comments
 (0)