This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 78928609d0f Add failed task log preview to Dag Overview page (#47224)
78928609d0f is described below
commit 78928609d0fc31efa158c521d92e51bf47f2e1c6
Author: Brent Bovenzi <[email protected]>
AuthorDate: Mon Mar 3 09:07:06 2025 -0500
Add failed task log preview to Dag Overview page (#47224)
* Add quick log view to Dag Overview page
* Cleanup log preview layout
---
airflow/ui/src/layouts/Details/DagBreadcrumb.tsx | 2 +-
airflow/ui/src/layouts/Details/DetailsLayout.tsx | 2 +-
airflow/ui/src/pages/Dag/Overview/FailedLogs.tsx | 63 +++++++++++++++++++
airflow/ui/src/pages/Dag/Overview/Overview.tsx | 3 +
.../ui/src/pages/Dag/Overview/TaskLogPreview.tsx | 70 ++++++++++++++++++++++
.../src/pages/TaskInstance/Logs/TaskLogContent.tsx | 3 +-
airflow/ui/src/queries/useLogs.tsx | 1 +
7 files changed, 140 insertions(+), 4 deletions(-)
diff --git a/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
b/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
index 4b656764a65..cb1973a5763 100644
--- a/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
+++ b/airflow/ui/src/layouts/Details/DagBreadcrumb.tsx
@@ -105,7 +105,7 @@ export const DagBreadcrumb = () => {
}
return (
- <Breadcrumb.Root mb={1} separator={<LiaSlashSolid />}>
+ <Breadcrumb.Root separator={<LiaSlashSolid />}>
{links.map((link, index) => (
// eslint-disable-next-line react/no-array-index-key
<Stat.Root gap={0} key={`${link.title}-${index}`}>
diff --git a/airflow/ui/src/layouts/Details/DetailsLayout.tsx
b/airflow/ui/src/layouts/Details/DetailsLayout.tsx
index e2fa8cf77b5..c93c8fe8b5f 100644
--- a/airflow/ui/src/layouts/Details/DetailsLayout.tsx
+++ b/airflow/ui/src/layouts/Details/DetailsLayout.tsx
@@ -59,7 +59,7 @@ export const DetailsLayout = ({ children, error, isLoading,
tabs }: Props) => {
return (
<OpenGroupsProvider dagId={dagId}>
- <HStack justifyContent="space-between" mb={2}>
+ <HStack justifyContent="space-between">
<DagBreadcrumb />
<Flex gap={1}>
<SearchDagsButton />
diff --git a/airflow/ui/src/pages/Dag/Overview/FailedLogs.tsx
b/airflow/ui/src/pages/Dag/Overview/FailedLogs.tsx
new file mode 100644
index 00000000000..53b0655e809
--- /dev/null
+++ b/airflow/ui/src/pages/Dag/Overview/FailedLogs.tsx
@@ -0,0 +1,63 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Flex, Heading, Button } from "@chakra-ui/react";
+import { useState } from "react";
+
+import type { TaskInstanceCollectionResponse } from
"openapi/requests/types.gen";
+import { useConfig } from "src/queries/useConfig";
+
+import { TaskLogPreview } from "./TaskLogPreview";
+
+export const FailedLogs = ({
+ failedTasks,
+}: {
+ readonly failedTasks: TaskInstanceCollectionResponse | undefined;
+}) => {
+ const defaultWrap = Boolean(useConfig("default_wrap"));
+ const [wrap, setWrap] = useState(defaultWrap);
+
+ const taskLogs = failedTasks?.task_instances.slice(0, 5);
+
+ const toggleWrap = () => setWrap(!wrap);
+
+ if (taskLogs === undefined || taskLogs.length <= 0) {
+ return undefined;
+ }
+
+ return (
+ <Flex flexDirection="column" gap={3}>
+ <Flex alignItems="center" justifyContent="space-between">
+ <Heading size="md">Recent Failed Task Logs</Heading>
+ <Button
+ aria-label={wrap ? "Unwrap" : "Wrap"}
+ bg="bg.panel"
+ fontSize="sm"
+ onClick={toggleWrap}
+ size="sm"
+ variant="outline"
+ >
+ {wrap ? "Unwrap" : "Wrap"}
+ </Button>
+ </Flex>
+ {taskLogs.map((taskInstance) => (
+ <TaskLogPreview key={taskInstance.id} taskInstance={taskInstance}
wrap={wrap} />
+ ))}
+ </Flex>
+ );
+};
diff --git a/airflow/ui/src/pages/Dag/Overview/Overview.tsx
b/airflow/ui/src/pages/Dag/Overview/Overview.tsx
index d1d8a03c8e8..f1fdb62bba8 100644
--- a/airflow/ui/src/pages/Dag/Overview/Overview.tsx
+++ b/airflow/ui/src/pages/Dag/Overview/Overview.tsx
@@ -27,6 +27,8 @@ import TimeRangeSelector from
"src/components/TimeRangeSelector";
import { TrendCountButton } from "src/components/TrendCountButton";
import { isStatePending, useAutoRefresh } from "src/utils";
+import { FailedLogs } from "./FailedLogs";
+
const defaultHour = "168";
export const Overview = () => {
@@ -118,6 +120,7 @@ export const Overview = () => {
)}
</Box>
</SimpleGrid>
+ <FailedLogs failedTasks={failedTasks} />
</Box>
);
};
diff --git a/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
b/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
new file mode 100644
index 00000000000..d2f8ecebeb1
--- /dev/null
+++ b/airflow/ui/src/pages/Dag/Overview/TaskLogPreview.tsx
@@ -0,0 +1,70 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Box, Flex, Link } from "@chakra-ui/react";
+import { Link as RouterLink } from "react-router-dom";
+
+import type { TaskInstanceResponse } from "openapi/requests/types.gen";
+import { ClearTaskInstanceButton } from "src/components/Clear";
+import { StateBadge } from "src/components/StateBadge";
+import Time from "src/components/Time";
+import { TaskLogContent } from "src/pages/TaskInstance/Logs/TaskLogContent";
+import { useLogs } from "src/queries/useLogs";
+import { getTaskInstanceLink } from "src/utils/links";
+
+export const TaskLogPreview = ({
+ taskInstance,
+ wrap,
+}: {
+ readonly taskInstance: TaskInstanceResponse;
+ readonly wrap: boolean;
+}) => {
+ const { data, error, isLoading } = useLogs({
+ dagId: taskInstance.dag_id,
+ logLevelFilters: ["warning", "error", "critical"],
+ taskInstance,
+ tryNumber: taskInstance.try_number,
+ });
+
+ return (
+ <Box borderRadius={4} borderStyle="solid" borderWidth={1}
key={taskInstance.id} width="100%">
+ <Flex alignItems="center" justifyContent="space-between" px={2}>
+ <Box>
+ <StateBadge mr={1} state={taskInstance.state} />
+ {taskInstance.task_display_name}
+ <Time datetime={taskInstance.run_after} ml={1} />
+ </Box>
+ <Flex gap={1}>
+ <ClearTaskInstanceButton taskInstance={taskInstance}
withText={false} />
+ <Link asChild color="fg.info" fontSize="sm">
+ <RouterLink to={getTaskInstanceLink(taskInstance)}>View full
logs</RouterLink>
+ </Link>
+ </Flex>
+ </Flex>
+ <Box maxHeight="100px" overflow="auto">
+ <TaskLogContent
+ error={error}
+ isLoading={isLoading}
+ logError={error}
+ parsedLogs={data.parsedLogs}
+ wrap={wrap}
+ />
+ </Box>
+ </Box>
+ );
+};
diff --git a/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
b/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
index 2134607723f..320f6267007 100644
--- a/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
+++ b/airflow/ui/src/pages/TaskInstance/Logs/TaskLogContent.tsx
@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box, Code, Skeleton, VStack } from "@chakra-ui/react";
+import { Box, Code, VStack } from "@chakra-ui/react";
import type { ReactNode } from "react";
import { ErrorAlert } from "src/components/ErrorAlert";
@@ -33,7 +33,6 @@ type Props = {
export const TaskLogContent = ({ error, isLoading, logError, parsedLogs, wrap
}: Props) => (
<Box>
<ErrorAlert error={error ?? logError} />
- <Skeleton />
<ProgressBar size="xs" visibility={isLoading ? "visible" : "hidden"} />
<Code overflow="auto" py={3} textWrap={wrap ? "pre" : "nowrap"}
width="100%">
<VStack alignItems="flex-start">{parsedLogs}</VStack>
diff --git a/airflow/ui/src/queries/useLogs.tsx
b/airflow/ui/src/queries/useLogs.tsx
index d597bc90179..1b7445859fa 100644
--- a/airflow/ui/src/queries/useLogs.tsx
+++ b/airflow/ui/src/queries/useLogs.tsx
@@ -71,6 +71,7 @@ const renderStructuredLog = (
<Badge
colorPalette={level.toUpperCase() in LogLevel ?
logLevelColorMapping[level as LogLevel] : undefined}
key={1}
+ minH={3}
size="sm"
>
{level.toUpperCase()}