8000 Add virtualization to `RunLogs` component (#17688) · CodersSampling/prefect@dfbafc1 · GitHub
[go: up one dir, main page]

Skip to content

Commit dfbafc1

Browse files
authored
Add virtualization to RunLogs component (PrefectHQ#17688)
1 parent c44d2de commit dfbafc1

File tree

10 files changed

+238
-78
lines changed

10 files changed

+238
-78
lines changed

.pre-commit-config.yaml

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,21 +71,6 @@ repos:
7171
src/prefect/settings/models/.*|
7272
scripts/generate_settings_ref.py
7373
)$
74-
- id: lint-ui-v2
75-
name: Lint UI v2
76-
language: system
77-
entry: sh
78-
args:
79-
[
80-
"-c",
81-
". $NVM_DIR/nvm.sh || true && cd ui-v2 && nvm use || true && npm i --no-upgrade --silent --no-progress && npm run lint-staged",
82-
]
83-
files: |
84-
(?x)^(
85-
.pre-commit-config.yaml|
86-
ui-v2/.*
87-
)$
88-
pass_filenames: false
8974
- id: format-ui-v2
9075
name: Format UI v2
9176
language: system

ui-v2/package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ui-v2/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
"@tanstack/react-query-devtools": "^5.71.1",
5050
"@tanstack/react-router": "^1.114.29",
5151
"@tanstack/react-table": "^8.21.2",
52+
"@tanstack/react-virtual": "^3.13.6",
5253
"@tanstack/zod-adapter": "^1.114.29",
5354
"@uiw/react-codemirror": "^4.23.10",
5455
"class-variance-authority": "^0.7.1",

ui-v2/src/api/logs/index.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { components } from "@/api/prefect";
2-
import { queryOptions } from "@tanstack/react-query";
2+
import { infiniteQueryOptions, queryOptions } from "@tanstack/react-query";
33
import { getQueryService } from "../service";
44

55
type LogsFilter = components["schemas"]["Body_read_logs_logs_filter_post"];
@@ -21,6 +21,9 @@ export const queryKeyFactory = {
2121
all: () => ["logs"] as const,
2222
lists: () => [...queryKeyFactory.all(), "list"] as const,
2323
list: (filter: LogsFilter) => [...queryKeyFactory.lists(), filter] as const,
24+
infiniteLists: () => [...queryKeyFactory.lists(), "infinite"] as const,
25+
infiniteList: (filter: Omit<LogsFilter, "offset">) =>
26+
[...queryKeyFactory.infiniteLists(), filter] as const,
2427
};
2528

2629
/**
@@ -56,3 +59,23 @@ export const buildFilterLogsQuery = (filter: LogsFilter) =>
5659
return res.data ?? [];
5760
},
5861
});
62+
63+
export const buildInfiniteFilterLogsQuery = (
64+
filter: Omit<LogsFilter, "offset">,
65+
) =>
66+
infiniteQueryOptions({
67+
queryKey: queryKeyFactory.infiniteList(filter),
68+
queryFn: async ({ pageParam = { offset: 0 } }) => {
69+
const res = await getQueryService().POST("/logs/filter", {
70+
body: { ...filter, offset: pageParam.offset },
71+
});
72+
return res.data ?? [];
73+
},
74+
initialPageParam: { offset: 0 },
75+
getNextPageParam: (lastPage, pages) => {
76+
if (lastPage.length === 0) {
77+
return;
78+
}
79+
return { offset: pages.reduce((sum, page) => sum + page.length, 0) };
80+
},
81+
});

ui-v2/src/components/task-runs/task-run-logs/index.tsx

Lines changed: 34 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { buildFilterLogsQuery } from "@/api/logs";
1+
import { buildInfiniteFilterLogsQuery } from "@/api/logs";
22
import type { components } from "@/api/prefect";
33
import { RunLogs } from "@/components/ui/run-logs";
44
import {
@@ -8,8 +8,8 @@ import {
88
SelectTrigger,
99
SelectValue,
1010
} from "@/components/ui/select";
11-
import { useSuspenseQuery } from "@tanstack/react-query";
12-
import { useState } from "react";
11+
import { useSuspenseInfiniteQuery } from "@tanstack/react-query";
12+
import { useMemo, useState } from "react";
1313

1414
type TaskRunLogsProps = {
1515
taskRun: components["schemas"]["TaskRun"];
@@ -21,25 +21,29 @@ export const TaskRunLogs = ({ taskRun }: TaskRunLogsProps) => {
2121
"TIMESTAMP_ASC" | "TIMESTAMP_DESC"
2222
>("TIMESTAMP_ASC");
2323

24-
const { data: logs } = useSuspenseQuery(
25-
buildFilterLogsQuery({
26-
limit: 200,
27-
offset: 0,
28-
sort: sortOrder,
29-
logs: {
30-
operator: "and_",
31-
level: {
32-
ge_: levelFilter,
24+
const queryOptions = useMemo(
25+
() =>
26+
buildInfiniteFilterLogsQuery({
27+
limit: 50,
28+
sort: sortOrder,
29+
logs: {
30+
operator: "and_",
31+
level: { ge_: levelFilter },
32+
task_run_id: { any_: [taskRun.id] },
3333
},
34-
task_run_id: {
35-
any_: [taskRun.id],
36-
},
37-
},
38-
}),
34+
}),
35+
[levelFilter, sortOrder, taskRun.id],
3936
);
4037

38+
const {
39+
data: logs,
40+
hasNextPage,
41+
fetchNextPage,
42+
isFetchingNextPage,
43+
} = useSuspenseInfiniteQuery(queryOptions);
44+
const noLogs = logs.pages.length === 1 && logs.pages[0].length === 0;
4145
let message = "This run did not produce any logs.";
42-
if (logs.length === 0) {
46+
if (noLogs) {
4347
if (levelFilter > 0) {
4448
message = "No logs match your filter criteria";
4549
} else if (
@@ -49,8 +53,6 @@ export const TaskRunLogs = ({ taskRun }: TaskRunLogsProps) => {
4953
message = "Run has not yet started. Check back soon for logs.";
5054
} else if (taskRun.state_type === "RUNNING") {
5155
message = "Waiting for logs...";
52-
} else {
53-
message = "This run did not produce any logs.";
5456
}
5557
}
5658

@@ -63,12 +65,22 @@ export const TaskRunLogs = ({ taskRun }: TaskRunLogsProps) => {
6365
/>
6466
<LogSortOrder sortOrder={sortOrder} setSortOrder={setSortOrder} />
6567
</div>
66-
{logs.length === 0 ? (
68+
{noLogs ? (
6769
<div className="flex flex-col gap-2 text-center bg-gray-100 p-2 rounded-md">
6870
<span className="text-gray-500">{message}</span>
6971
</div>
7072
) : (
71-
<RunLogs taskRun={taskRun} logs={logs} />
73+
<RunLogs
74+
taskRun={taskRun}
75+
logs={logs.pages.flat()}
76+
onBottomReached={() => {
77+
if (hasNextPage && !isFetchingNextPage) {
78+
fetchNextPage().catch((error) => {
79+
console.error(error);
80+
});
81+
}
82+
}}
83+
/>
7284
)}
7385
</div>
7486
);

ui-v2/src/components/task-runs/task-run-logs/task-run-logs.stories.tsx

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TaskRunLogs } from ".";
88

99
const MOCK_TASK_RUN_WITH_LOGS = createFakeTaskRun();
1010
const MOCK_TASK_RUN_WITHOUT_LOGS = createFakeTaskRun();
11+
const MOCK_TASK_RUN_WITH_INFINITE_LOGS = createFakeTaskRun();
1112
// Create a range of logs with different levels
1213
const ALL_MOCK_LOGS = [
1314
createFakeLog({
@@ -106,3 +107,23 @@ export const NoLogs: Story = {
106107
taskRun: MOCK_TASK_RUN_WITHOUT_LOGS,
107108
},
108109
};
110+
111+
export const Infinite: Story = {
112+
args: {
113+
taskRun: MOCK_TASK_RUN_WITH_INFINITE_LOGS,
114+
},
115+
parameters: {
116+
msw: {
117+
handlers: [
118+
http.post(buildApiUrl("/logs/filter"), async ({ request }) => {
119+
const body = (await request.json()) as LogsFilterBody;
120+
return HttpResponse.json(
121+
Array.from({ length: body.limit as number }, createFakeLog).sort(
122+
(a, b) => a.timestamp.localeCompare(b.timestamp),
123+
),
124+
);
125+
}),
126+
],
127+
},
128+
},
129+
};

ui-v2/src/components/task-runs/task-run-logs/task-run-logs.test.tsx

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import userEvent from "@testing-library/user-event";
55
import { buildApiUrl, createWrapper, server } from "@tests/utils";
66
import { mockPointerEvents } from "@tests/utils/browser";
77
import { http, HttpResponse } from "msw";
8-
import { beforeAll, describe, expect, it } from "vitest";
8+
import { beforeEach, describe, expect, it } from "vitest";
99
import { TaskRunLogs } from ".";
1010

1111
const MOCK_LOGS = [
@@ -21,15 +21,34 @@ const MOCK_LOGS = [
2121
type LogsFilterBody = components["schemas"]["Body_read_logs_logs_filter_post"];
2222

2323
describe("TaskRunLogs", () => {
24-
beforeAll(mockPointerEvents);
25-
it("displays logs with default filter (all levels)", async () => {
24+
beforeEach(() => {
25+
mockPointerEvents();
2626
// Setup mock API response
2727
server.use(
28-
http.post(buildApiUrl("/logs/filter"), () => {
29-
return HttpResponse.json(MOCK_LOGS);
28+
http.post(buildApiUrl("/logs/filter"), async ({ request }) => {
29+
const body = (await request.json()) as LogsFilterBody;
30+
31+
let filteredLogs = [...MOCK_LOGS];
32+
33+
// Filter logs by level if specified
34+
const minLevel = body.logs?.level?.ge_;
35+
if (typeof minLevel === "number") {
36+
filteredLogs = filteredLogs.filter((log) => log.level >= minLevel);
37+
}
38+
39+
// Sort logs based on the sort parameter
40+
if (body.sort === "TIMESTAMP_DESC") {
41+
filteredLogs = filteredLogs.reverse();
42+
}
43+
44+
if (body.offset) {
45+
filteredLogs = filteredLogs.slice(body.offset);
46+
}
47+
return HttpResponse.json(filteredLogs);
3048
}),
3149
);
32-
50+
});
51+
it("displays logs with default filter (all levels)", async () => {
3352
// Render component
3453
const taskRun = createFakeTaskRun();
3554
render(<TaskRunLogs taskRun={taskRun} />, {
@@ -51,25 +70,16 @@ describe("TaskRunLogs", () => {
5170
it("filters logs by level", async () => {
5271
const user = userEvent.setup();
5372

54-
// Setup mock API response for filtered logs
55-
server.use(
56-
http.post(buildApiUrl("/logs/filter"), async ({ request }) => {
57-
const body = (await request.json()) as LogsFilterBody;
58-
const minLevel = body.logs?.level?.ge_ ?? 0;
59-
60-
const filteredLogs = MOCK_LOGS.filter((log) => log.level >= minLevel);
61-
return HttpResponse.json(filteredLogs);
62-
}),
63-
);
64-
6573
// Render component
6674
const taskRun = createFakeTaskRun();
6775
render(<TaskRunLogs taskRun={taskRun} />, {
6876
wrapper: createWrapper(),
6977
});
7078

7179
await waitFor(() => {
72-
expect(screen.getByText("Critical error in task")).toBeInTheDocument();
80+
expect(
81+
screen.getByRole("combobox", { name: /log level filter/i }),
82+
).toBeInTheDocument();
7383
});
7484

7585
// Change filter to "Error and above"
@@ -218,7 +228,9 @@ describe("TaskRunLogs", () => {
218228
});
219229

220230
await waitFor(() => {
221-
expect(screen.getByText("Critical error in task")).toBeInTheDocument();
231+
expect(
232+
screen.getByRole("combobox", { name: /log sort order/i }),
233+
).toBeInTheDocument();
222234
});
223235

224236
// Change sort order to newest first

0 commit comments

Comments
 (0)
0