Skip to content

Commit 3fbc14c

Browse files
craig[bot]kev-cao
andcommitted
Merge #142842
142842: ui: refactor jobs detail page to functional components r=xinhaoz a=kev-cao 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 Co-authored-by: Kevin Cao <[email protected]>
2 parents a14544a + 2bad9c2 commit 3fbc14c

File tree

6 files changed

+542
-194
lines changed

6 files changed

+542
-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)