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
1 change: 1 addition & 0 deletions airflow-core/src/airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"@emotion/react": "^11.14.0",
"@tanstack/react-query": "^5.75.1",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.8",
"@types/debounce-promise": "^3.1.9",
"@uiw/codemirror-themes-all": "^4.23.12",
"@uiw/react-codemirror": "^4.23.12",
Expand Down
20 changes: 20 additions & 0 deletions airflow-core/src/airflow/ui/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ export const TaskLogPreview = ({
error={error}
isLoading={isLoading}
logError={error}
parsedLogs={data.parsedLogs}
parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
</Box>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,18 +17,25 @@
* under the License.
*/
import "@testing-library/jest-dom";
import { render, screen, waitFor } from "@testing-library/react";
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { setupServer, type SetupServerApi } from "msw/node";
import { afterEach, describe, it, expect, beforeAll, afterAll } from "vitest";

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

let server: SetupServerApi;
const ITEM_HEIGHT = 20;

beforeAll(() => {
server = setupServer(...handlers);
server.listen({ onUnhandledRequest: "bypass" });
Object.defineProperty(HTMLElement.prototype, "offsetHeight", {
value: ITEM_HEIGHT,
});
Object.defineProperty(HTMLElement.prototype, "offsetWidth", {
value: 800,
});
});

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

await waitFor(() => expect(screen.queryByTestId("summary-Pre task execution logs")).toBeInTheDocument(), {
timeout: 10_000,
});
fireEvent.scroll(screen.getByTestId("virtualized-list"), { target: { scrollTop: ITEM_HEIGHT * 6 } });
await waitFor(() => expect(screen.queryByTestId("virtualized-item-16")).toBeInTheDocument());

await waitFor(() => expect(screen.queryByTestId("summary-Pre task execution logs")).toBeInTheDocument());
await waitFor(() => expect(screen.getByTestId("summary-Pre task execution logs")).toBeVisible());
await waitFor(() => expect(screen.queryByText(/Task instance is in running state/iu)).not.toBeVisible());

await waitFor(() => screen.getByTestId("summary-Pre task execution logs").click());
await waitFor(() => expect(screen.queryByText(/Task instance is in running state/iu)).toBeVisible());
});
}, 10_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const Logs = () => {
const showExternalLogRedirect = Boolean(useConfig("show_external_log_redirect"));

return (
<Box p={2}>
<Box display="flex" flexDirection="column" h="100%" p={2}>
<TaskLogHeader
onSelectTryNumber={onSelectTryNumber}
sourceOptions={data.sources}
Expand All @@ -117,7 +117,7 @@ export const Logs = () => {
error={error}
isLoading={isLoading || isLoadingLogs}
logError={logError}
parsedLogs={data.parsedLogs}
parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
<Dialog.Root onOpenChange={onOpenChange} open={fullscreen} scrollBehavior="inside" size="full">
Expand All @@ -139,12 +139,12 @@ export const Logs = () => {

<Dialog.CloseTrigger />

<Dialog.Body>
<Dialog.Body display="flex" flexDirection="column">
<TaskLogContent
error={error}
isLoading={isLoading || isLoadingLogs}
logError={logError}
parsedLogs={data.parsedLogs}
parsedLogs={data.parsedLogs ?? []}
wrap={wrap}
/>
</Dialog.Body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@
* under the License.
*/
import { Box, Code, VStack, useToken } from "@chakra-ui/react";
import type { ReactNode } from "react";
import { useLayoutEffect } from "react";
import { useVirtualizer } from "@tanstack/react-virtual";
import { useLayoutEffect, useRef } from "react";

import { ErrorAlert } from "src/components/ErrorAlert";
import { ProgressBar } from "src/components/ui";
Expand All @@ -27,12 +27,19 @@ type Props = {
readonly error: unknown;
readonly isLoading: boolean;
readonly logError: unknown;
readonly parsedLogs: ReactNode;
readonly parsedLogs: Array<JSX.Element | string | undefined>;
readonly wrap: boolean;
};

export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }: Props) => {
const [bgLine] = useToken("colors", ["blue.emphasized"]);
const parentRef = useRef(null);
const rowVirtualizer = useVirtualizer({
count: parsedLogs.length,
estimateSize: () => 20,
getScrollElement: () => parentRef.current,
overscan: 10,
});

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

return (
<Box>
<Box display="flex" flexDirection="column" flexGrow={1} h="100%" minHeight={0}>
<ErrorAlert error={error ?? logError} />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<Code
Expand All @@ -62,13 +69,32 @@ export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap }:
bg: "blue.subtle",
},
}}
data-testid="virtualized-list"
flexGrow={1}
h="auto"
overflow="auto"
position="relative"
py={3}
ref={parentRef}
textWrap={wrap ? "pre" : "nowrap"}
width="100%"
>
<VStack alignItems="flex-start" gap={0}>
{parsedLogs}
<VStack alignItems="flex-start" gap={0} h={`${rowVirtualizer.getTotalSize()}px`}>
{rowVirtualizer.getVirtualItems().map((virtualRow) => (
<Box
data-index={virtualRow.index}
data-testid={`virtualized-item-${virtualRow.index}`}
key={virtualRow.key}
left={0}
position="absolute"
ref={rowVirtualizer.measureElement}
top={0}
transform={`translateY(${virtualRow.start}px)`}
width={wrap ? "100%" : "max-content"}
>
{parsedLogs[virtualRow.index] ?? undefined}
</Box>
))}
</VStack>
</Code>
</Box>
Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/ui/src/queries/useLogs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { isStatePending, useAutoRefresh } from "src/utils";
import { getTaskInstanceLink } from "src/utils/links";

type Props = {
accept?: "*/*" | "application/json" | "application/x-ndjson";
dagId: string;
logLevelFilters?: Array<string>;
sourceFilters?: Array<string>;
Expand Down Expand Up @@ -120,13 +121,14 @@ const parseLogs = ({ data, logLevelFilters, sourceFilters, taskInstance, tryNumb
};

export const useLogs = (
{ dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }: Props,
{ accept = "application/json", dagId, logLevelFilters, sourceFilters, taskInstance, tryNumber = 1 }: Props,
options?: Omit<UseQueryOptions<TaskInstancesLogResponse>, "queryFn" | "queryKey">,
) => {
const refetchInterval = useAutoRefresh({ dagId });

const { data, ...rest } = useTaskInstanceServiceGetLog(
{
accept,
dagId,
dagRunId: taskInstance?.dag_run_id ?? "",
mapIndex: taskInstance?.map_index ?? -1,
Expand Down