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()}

Reply via email to