Skip to content

Commit 6beae57

Browse files
github-actions[bot]jason810496guan404ming
authored andcommitted
[v3-0-test] Update TaskLogContent to support virtualized rendering (#50746) (#51202)
* Fix OpenAPI schema for `get_log` API (#50547) * Fix openapi schema for get_log API * Fix test_log (cherry picked from commit 08cc57d) * [v3-0-test] Update `TaskLogContent` to support virtualized rendering (#50746) * Update TaskLogContent to support virtualized rendering * Update TaskLogPreview and Logs to handle undefined parsedLogs (cherry picked from commit 813f3e3) Co-authored-by: Guan Ming(Wesley) Chiu <[email protected]> --------- Co-authored-by: LIU ZHE YOU <[email protected]> Co-authored-by: Guan Ming(Wesley) Chiu <[email protected]>
1 parent 37fc12c commit 6beae57

File tree

7 files changed

+77
-17
lines changed

7 files changed

+77
-17
lines changed

airflow-core/src/airflow/ui/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"@emotion/react": "^11.14.0",
2323
"@tanstack/react-query": "^5.75.1",
2424
"@tanstack/react-table": "^8.21.3",
25+
"@tanstack/react-virtual": "^3.13.8",
2526
"@types/debounce-promise": "^3.1.9",
2627
"@uiw/codemirror-themes-all": "^4.23.12",
2728
"@uiw/react-codemirror": "^4.23.12",

airflow-core/src/airflow/ui/pnpm-lock.yaml

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

airflow-core/src/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export const TaskLogPreview = ({
6767
error={error}
6868
isLoading={isLoading}
6969
logError={error}
70-
parsedLogs={data.parsedLogs}
70+
parsedLogs={data.parsedLogs ?? []}
7171
wrap={wrap}
7272
/>
7373
</Box>

airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.test.tsx

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,25 @@
1717
* under the License.
1818
*/
1919
import "@testing-library/jest-dom";
20-
import { render, screen, waitFor } from "@testing-library/react";
20+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
2121
import { setupServer, type SetupServerApi } from "msw/node";
2222
import { afterEach, describe, it, expect, beforeAll, afterAll } from "vitest";
2323

2424
import { handlers } from "src/mocks/handlers";
2525
import { AppWrapper } from "src/utils/AppWrapper";
2626

2727
let server: SetupServerApi;
28+
const ITEM_HEIGHT = 20;
2829

2930
beforeAll(() => {
3031
server = setupServer(...handlers);
3132
server.listen({ onUnhandledRequest: "bypass" });
33+
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
34+
value: ITEM_HEIGHT,
35+
});
36+
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
37+
value: 800,
38+
});
3239
});
3340

3441
afterEach(() => server.resetHandlers());
@@ -39,14 +46,18 @@ describe("Task log grouping", () => {
3946
render(
4047
<AppWrapper initialEntries={["/dags/log_grouping/runs/manual__2025-02-18T12:19/tasks/generate"]} />,
4148
);
49+
await waitFor(() => expect(screen.queryByTestId("virtualized-list")).toBeInTheDocument());
50+
await waitFor(() => expect(screen.queryByTestId("virtualized-item-0")).toBeInTheDocument());
51+
await waitFor(() => expect(screen.queryByTestId("virtualized-item-10")).toBeInTheDocument());
4252

43-
await waitFor(() => expect(screen.queryByTestId("summary-Pre task execution logs")).toBeInTheDocument(), {
44-
timeout: 10_000,
45-
});
53+
fireEvent.scroll(screen.getByTestId("virtualized-list"), { target: { scrollTop: ITEM_HEIGHT * 6 } });
54+
await waitFor(() => expect(screen.queryByTestId("virtualized-item-16")).toBeInTheDocument());
55+
56+
await waitFor(() => expect(screen.queryByTestId("summary-Pre task execution logs")).toBeInTheDocument());
4657
await waitFor(() => expect(screen.getByTestId("summary-Pre task execution logs")).toBeVisible());
4758
await waitFor(() => expect(screen.queryByText(/Task instance is in running state/iu)).not.toBeVisible());
4859

4960
await waitFor(() => screen.getByTestId("summary-Pre task execution logs").click());
5061
await waitFor(() => expect(screen.queryByText(/Task instance is in running state/iu)).toBeVisible());
51-
});
62+
}, 10_000);
5263
});

airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ export const Logs = () => {
8888
});
8989

9090
return (
91-
<Box p={2}>
91+
<Box display="flex" flexDirection="column" h="100%" p={2}>
9292
<TaskLogHeader
9393
onSelectTryNumber={onSelectTryNumber}
9494
sourceOptions={data.sources}
@@ -102,7 +102,7 @@ export const Logs = () => {
102102
error={error}
103103
isLoading={isLoading || isLoadingLogs}
104104
logError={logError}
105-
parsedLogs={data.parsedLogs}
105+
parsedLogs={data.parsedLogs ?? []}
106106
wrap={wrap}
107107
/>
108108
<Dialog.Root onOpenChange={onOpenChange} open={fullscreen} scrollBehavior="inside" size="full">
@@ -124,12 +124,12 @@ export const Logs = () => {
124124

125125
<Dialog.CloseTrigger />
126126

127-
<Dialog.Body>
127+
<Dialog.Body display="flex" flexDirection="column">
128128
<TaskLogContent
129129
error={error}
130130
isLoading={isLoading || isLoadingLogs}
131131
logError={logError}
132-
parsedLogs={data.parsedLogs}
132+
parsedLogs={data.parsedLogs ?? []}
133133
wrap={wrap}
134134
/>
135135
</Dialog.Body>

airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
* under the License.
1818
*/
1919
import { Box, Code, VStack, useToken } from "@chakra-ui/react";
20-
import type { ReactNode } from "react";
21-
import { useLayoutEffect } from "react";
20+
import { useVirtualizer } from "@tanstack/react-virtual";
21+
import { useLayoutEffect, useRef } from "react";
2222

2323
import { ErrorAlert } from "src/components/ErrorAlert";
2424
import { ProgressBar } from "src/components/ui";
@@ -27,12 +27,19 @@ type Props = {
2727
readonly error: unknown;
2828
readonly isLoading: boolean;
2929
readonly logError: unknown;
30-
readonly parsedLogs: ReactNode;
30+
readonly parsedLogs: Array<JSX.Element | string | undefined>;
3131
readonly wrap: boolean;
3232
};
3333

3434
export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: Props) => {
3535
const [bgLine] = useToken("colors", ["blue.emphasized"]);
36+
const parentRef = useRef(null);
37+
const rowVirtualizer = useVirtualizer({
38+
count: parsedLogs.length,
39+
estimateSize: () => 20,
40+
getScrollElement: () => parentRef.current,
41+
overscan: 10,
42+
});
3643

3744
useLayoutEffect(() => {
3845
if (location.hash) {
@@ -53,7 +60,7 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
5360
}, [isLoading, bgLine]);
5461

5562
return (
56-
<Box>
63+
<Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0}>
5764
<ErrorAlert error={error ?? logError} />
5865
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
5966
<Code
@@ -62,13 +69,32 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
6269
bg: "blue.subtle",
6370
},
6471
}}
72+
data-testid="virtualized-list"
73+
flexGrow={1}
74+
h="auto"
6575
overflow="auto"
76+
position="relative"
6677
py={3}
78+
ref={parentRef}
6779
textWrap={wrap ? "pre" : "nowrap"}
6880
width="100%"
6981
>
70-
<VStack alignItems="flex-start" gap={0}>
71-
{parsedLogs}
82+
<VStack alignItems="flex-start" gap={0} h={`${rowVirtualizer.getTotalSize()}px`}>
83+
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
84+
<Box
85+
data-index={virtualRow.index}
86+
data-testid={`virtualized-item-${virtualRow.index}`}
87+
key={virtualRow.key}
88+
left={0}
89+
position="absolute"
90+
ref={rowVirtualizer.measureElement}
91+
top={0}
92+
transform={`translateY(${virtualRow.start}px)`}
93+
width={wrap ? "100%" : "max-content"}
94+
>
95+
{parsedLogs[virtualRow.index] ?? undefined}
96+
</Box>
97+
))}
7298
</VStack>
7399
</Code>
74100
</Box>

airflow-core/src/airflow/ui/src/queries/useLogs.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { isStatePending, useAutoRefresh } from "src/utils";
2828
import { getTaskInstanceLink } from "src/utils/links";
2929

3030
type Props = {
31+
accept?: "*/*" | "application/json" | "application/x-ndjson";
3132
dagId: string;
3233
logLevelFilters?: Array<string>;
3334
sourceFilters?: Array<string>;
@@ -120,13 +121,14 @@ const parseLogs = ({ data, logLevelFilters, sourceFilters, taskInstance, tryNumb
120121
};
121122

122123
export const useLogs = (
123-
{ dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }: Props,
124+
{ accept = "application/json", dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }: Props,
124125
options?: Omit<UseQueryOptions<TaskInstancesLogResponse>, "queryFn" | "queryKey">,
125126
) => {
126127
const refetchInterval = useAutoRefresh({ dagId });
127128

128129
const { data, ...rest } = useTaskInstanceServiceGetLog(
129130
{
131+
accept,
130132
dagId,
131133
dagRunId: taskInstance?.dag_run_id ?? "",
132134
mapIndex: taskInstance?.map_index ?? -1,

0 commit comments

Comments
 (0)