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 6641d78703b Add latest dag run info to dags list and dag details 
(#43489)
6641d78703b is described below

commit 6641d78703b1a26254144c47e0fc9673efdcdf8f
Author: Brent Bovenzi <[email protected]>
AuthorDate: Wed Oct 30 09:27:05 2024 -0400

    Add latest dag run info to dags list and dag details (#43489)
    
    * Add latest dag run info to dags list and dag details
    
    * Add tooltip offset
---
 airflow/ui/src/layouts/Nav/Nav.tsx             |  7 --
 airflow/ui/src/pages/DagsList/Dag/Dag.tsx      | 23 ++++--
 airflow/ui/src/pages/DagsList/Dag/Header.tsx   |  7 +-
 airflow/ui/src/pages/DagsList/DagCard.test.tsx | 10 ++-
 airflow/ui/src/pages/DagsList/DagCard.tsx      | 34 ++++-----
 airflow/ui/src/pages/DagsList/DagTags.tsx      |  5 +-
 airflow/ui/src/pages/DagsList/DagsFilters.tsx  |  2 +-
 airflow/ui/src/pages/DagsList/DagsList.tsx     | 99 ++++++++++++--------------
 airflow/ui/src/pages/DagsList/LatestRun.tsx    | 41 +++++++++++
 airflow/ui/src/pages/DagsList/RecentRuns.tsx   | 83 +++++++++++++++++++++
 airflow/ui/src/pages/DagsList/Schedule.tsx     | 36 ++++++++++
 airflow/ui/src/queries/useDags.tsx             | 81 +++++++++++++++++++++
 airflow/ui/src/utils/stateColor.ts             | 36 ++++++++++
 13 files changed, 372 insertions(+), 92 deletions(-)

diff --git a/airflow/ui/src/layouts/Nav/Nav.tsx 
b/airflow/ui/src/layouts/Nav/Nav.tsx
index 8177f8065eb..7115c85d1da 100644
--- a/airflow/ui/src/layouts/Nav/Nav.tsx
+++ b/airflow/ui/src/layouts/Nav/Nav.tsx
@@ -19,7 +19,6 @@
 import { Box, Flex, Icon, VStack } from "@chakra-ui/react";
 import { motion } from "framer-motion";
 import {
-  FiBarChart2,
   FiCornerUpLeft,
   FiDatabase,
   FiGlobe,
@@ -70,12 +69,6 @@ export const Nav = () => (
         title="Assets"
         to="assets"
       />
-      <NavButton
-        icon={<FiBarChart2 size="1.75rem" />}
-        isDisabled
-        title="Dag Runs"
-        to="dag_runs"
-      />
       <NavButton
         icon={<FiGlobe size="1.75rem" />}
         isDisabled
diff --git a/airflow/ui/src/pages/DagsList/Dag/Dag.tsx 
b/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
index 52f8934e899..cdee232f78a 100644
--- a/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
+++ b/airflow/ui/src/pages/DagsList/Dag/Dag.tsx
@@ -29,7 +29,10 @@ import {
 import { FiChevronsLeft } from "react-icons/fi";
 import { Link as RouterLink, useParams } from "react-router-dom";
 
-import { useDagServiceGetDagDetails } from "openapi/queries";
+import {
+  useDagServiceGetDagDetails,
+  useDagsServiceRecentDagRuns,
+} from "openapi/queries";
 import { ErrorAlert } from "src/components/ErrorAlert";
 
 import { Header } from "./Header";
@@ -45,6 +48,17 @@ export const Dag = () => {
     dagId: dagId ?? "",
   });
 
+  // TODO: replace with with a list dag runs by dag id request
+  const {
+    data: runsData,
+    error: runsError,
+    isLoading: isLoadingRuns,
+  } = useDagsServiceRecentDagRuns({ dagIdPattern: dagId ?? "" });
+
+  const runs =
+    runsData?.dags.find((dagWithRuns) => dagWithRuns.dag_id === dagId)
+      ?.latest_dag_runs ?? [];
+
   return (
     <Box>
       <Button
@@ -56,18 +70,19 @@ export const Dag = () => {
       >
         Back to all dags
       </Button>
-      <Header dag={dag} dagId={dagId} />
-      <ErrorAlert error={error} />
+      <Header dag={dag} dagId={dagId} latestRun={runs[0]} />
+      <ErrorAlert error={error ?? runsError} />
       <Progress
         isIndeterminate
         size="xs"
-        visibility={isLoading ? "visible" : "hidden"}
+        visibility={isLoading || isLoadingRuns ? "visible" : "hidden"}
       />
       <Tabs>
         <TabList>
           <Tab>Overview</Tab>
           <Tab>Runs</Tab>
           <Tab>Tasks</Tab>
+          <Tab>Events</Tab>
         </TabList>
 
         <TabPanels>
diff --git a/airflow/ui/src/pages/DagsList/Dag/Header.tsx 
b/airflow/ui/src/pages/DagsList/Dag/Header.tsx
index 25d674e1441..f831dd15c6a 100644
--- a/airflow/ui/src/pages/DagsList/Dag/Header.tsx
+++ b/airflow/ui/src/pages/DagsList/Dag/Header.tsx
@@ -30,19 +30,22 @@ import {
 } from "@chakra-ui/react";
 import { FiCalendar, FiPlay } from "react-icons/fi";
 
-import type { DAGResponse } from "openapi/requests/types.gen";
+import type { DAGResponse, DAGRunResponse } from "openapi/requests/types.gen";
 import { DagIcon } from "src/assets/DagIcon";
 import Time from "src/components/Time";
 import { TogglePause } from "src/components/TogglePause";
 
 import { DagTags } from "../DagTags";
+import { LatestRun } from "../LatestRun";
 
 export const Header = ({
   dag,
   dagId,
+  latestRun,
 }: {
   readonly dag?: DAGResponse;
   readonly dagId?: string;
+  readonly latestRun?: DAGRunResponse;
 }) => {
   const grayBg = useColorModeValue("gray.100", "gray.900");
   const grayBorder = useColorModeValue("gray.200", "gray.700");
@@ -74,6 +77,8 @@ export const Header = ({
             <Heading color="gray.500" fontSize="xs">
               Last Run
             </Heading>
+            <LatestRun latestRun={latestRun} />
+            <LatestRun />
           </VStack>
           <VStack align="flex-start" spacing={1}>
             <Heading color="gray.500" fontSize="xs">
diff --git a/airflow/ui/src/pages/DagsList/DagCard.test.tsx 
b/airflow/ui/src/pages/DagsList/DagCard.test.tsx
index c01c627e23f..3e60146baa0 100644
--- a/airflow/ui/src/pages/DagsList/DagCard.test.tsx
+++ b/airflow/ui/src/pages/DagsList/DagCard.test.tsx
@@ -20,8 +20,8 @@
  */
 import { render, screen } from "@testing-library/react";
 import type {
-  DAGResponse,
   DagTagPydantic,
+  DAGWithLatestDagRunsResponse,
 } from "openapi-gen/requests/types.gen";
 import { afterEach, describe, it, vi, expect } from "vitest";
 
@@ -44,6 +44,7 @@ const mockDag = {
   last_expired: null,
   last_parsed_time: "2024-08-22T13:50:10.372238+00:00",
   last_pickled: null,
+  latest_dag_runs: [],
   max_active_runs: 16,
   max_active_tasks: 16,
   max_consecutive_failed_dag_runs: 0,
@@ -56,7 +57,7 @@ const mockDag = {
   tags: [],
   timetable_description: "",
   timetable_summary: "",
-} satisfies DAGResponse;
+} satisfies DAGWithLatestDagRunsResponse;
 
 afterEach(() => {
   vi.restoreAllMocks();
@@ -77,7 +78,10 @@ describe("DagCard", () => {
       { dag_id: "id", name: "tag4" },
     ] satisfies Array<DagTagPydantic>;
 
-    const expandedMockDag = { ...mockDag, tags } satisfies DAGResponse;
+    const expandedMockDag = {
+      ...mockDag,
+      tags,
+    } satisfies DAGWithLatestDagRunsResponse;
 
     render(<DagCard dag={expandedMockDag} />, { wrapper: Wrapper });
     expect(screen.getByTestId("dag-tag")).toBeInTheDocument();
diff --git a/airflow/ui/src/pages/DagsList/DagCard.tsx 
b/airflow/ui/src/pages/DagsList/DagCard.tsx
index e3b933c8ebb..ecb54f86590 100644
--- a/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -22,22 +22,23 @@ import {
   HStack,
   Heading,
   SimpleGrid,
-  Text,
   Tooltip,
   VStack,
   Link,
 } from "@chakra-ui/react";
-import { FiCalendar } from "react-icons/fi";
 import { Link as RouterLink } from "react-router-dom";
 
-import type { DAGResponse } from "openapi/requests/types.gen";
+import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
 import Time from "src/components/Time";
 import { TogglePause } from "src/components/TogglePause";
 
 import { DagTags } from "./DagTags";
+import { LatestRun } from "./LatestRun";
+import { RecentRuns } from "./RecentRuns";
+import { Schedule } from "./Schedule";
 
 type Props = {
-  readonly dag: DAGResponse;
+  readonly dag: DAGWithLatestDagRunsResponse;
 };
 
 export const DagCard = ({ dag }: Props) => (
@@ -73,30 +74,25 @@ export const DagCard = ({ dag }: Props) => (
       </HStack>
     </Flex>
     <SimpleGrid columns={4} height={20} px={3} py={2} spacing={4}>
-      <div />
       <VStack align="flex-start" spacing={1}>
         <Heading color="gray.500" fontSize="xs">
-          Last Run
+          Schedule
         </Heading>
+        <Schedule dag={dag} />
+      </VStack>
+      <VStack align="flex-start" spacing={1}>
+        <Heading color="gray.500" fontSize="xs">
+          Latest Run
+        </Heading>
+        <LatestRun latestRun={dag.latest_dag_runs[0]} />
       </VStack>
       <VStack align="flex-start" spacing={1}>
         <Heading color="gray.500" fontSize="xs">
           Next Run
         </Heading>
-        <Text fontSize="sm">
-          <Time datetime={dag.next_dagrun} />
-        </Text>
-        {Boolean(dag.timetable_summary) ? (
-          <Tooltip hasArrow label={dag.timetable_description}>
-            <Text fontSize="sm">
-              {" "}
-              <FiCalendar style={{ display: "inline" }} />{" "}
-              {dag.timetable_summary}
-            </Text>
-          </Tooltip>
-        ) : undefined}
+        <Time datetime={dag.next_dagrun} />
       </VStack>
-      <div />
+      <RecentRuns latestRuns={dag.latest_dag_runs} />
     </SimpleGrid>
   </Box>
 );
diff --git a/airflow/ui/src/pages/DagsList/DagTags.tsx 
b/airflow/ui/src/pages/DagsList/DagTags.tsx
index 2a3cb3f76be..b384cd67264 100644
--- a/airflow/ui/src/pages/DagsList/DagTags.tsx
+++ b/airflow/ui/src/pages/DagsList/DagTags.tsx
@@ -24,13 +24,14 @@ import type { DagTagPydantic } from 
"openapi/requests/types.gen";
 const MAX_TAGS = 3;
 
 type Props = {
+  readonly hideIcon?: boolean;
   readonly tags: Array<DagTagPydantic>;
 };
 
-export const DagTags = ({ tags }: Props) =>
+export const DagTags = ({ hideIcon = false, tags }: Props) =>
   tags.length ? (
     <Flex alignItems="center" ml={2}>
-      <FiTag data-testid="dag-tag" />
+      {hideIcon ? undefined : <FiTag data-testid="dag-tag" />}
       <Text ml={1}>
         {tags
           .slice(0, MAX_TAGS)
diff --git a/airflow/ui/src/pages/DagsList/DagsFilters.tsx 
b/airflow/ui/src/pages/DagsList/DagsFilters.tsx
index 376b9220d23..9306c54c2ad 100644
--- a/airflow/ui/src/pages/DagsList/DagsFilters.tsx
+++ b/airflow/ui/src/pages/DagsList/DagsFilters.tsx
@@ -133,7 +133,7 @@ export const DagsFilters = () => {
             onClick={handleStateChange}
             value="success"
           >
-            Successful
+            Success
           </QuickFilterButton>
         </HStack>
         <Select
diff --git a/airflow/ui/src/pages/DagsList/DagsList.tsx 
b/airflow/ui/src/pages/DagsList/DagsList.tsx
index cbc09b87c1a..5606843bc25 100644
--- a/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -17,7 +17,6 @@
  * under the License.
  */
 import {
-  Badge,
   Heading,
   HStack,
   Select,
@@ -35,8 +34,10 @@ import {
 import { Link as RouterLink, useSearchParams } from "react-router-dom";
 import { useLocalStorage } from "usehooks-ts";
 
-import { useDagServiceGetDags } from "openapi/queries";
-import type { DAGResponse, DagRunState } from "openapi/requests/types.gen";
+import type {
+  DagRunState,
+  DAGWithLatestDagRunsResponse,
+} from "openapi/requests/types.gen";
 import { DataTable } from "src/components/DataTable";
 import { ToggleTableDisplay } from 
"src/components/DataTable/ToggleTableDisplay";
 import type { CardDef } from "src/components/DataTable/types";
@@ -49,19 +50,20 @@ import {
   SearchParamsKeys,
   type SearchParamsKeysType,
 } from "src/constants/searchParams";
+import { useDags } from "src/queries/useDags";
 import { pluralize } from "src/utils";
 
 import { DagCard } from "./DagCard";
+import { DagTags } from "./DagTags";
 import { DagsFilters } from "./DagsFilters";
+import { LatestRun } from "./LatestRun";
+import { Schedule } from "./Schedule";
 
-const columns: Array<ColumnDef<DAGResponse>> = [
+const columns: Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
   {
     accessorKey: "is_paused",
-    cell: ({ row }) => (
-      <TogglePause
-        dagId={row.original.dag_id}
-        isPaused={row.original.is_paused}
-      />
+    cell: ({ row: { original } }) => (
+      <TogglePause dagId={original.dag_id} isPaused={original.is_paused} />
     ),
     enableSorting: false,
     header: "",
@@ -71,24 +73,21 @@ const columns: Array<ColumnDef<DAGResponse>> = [
   },
   {
     accessorKey: "dag_id",
-    cell: ({ row }) => (
+    cell: ({ row: { original } }) => (
       <Link
         as={RouterLink}
         color="blue.contrast"
         fontWeight="bold"
-        to={`/dags/${row.original.dag_id}`}
+        to={`/dags/${original.dag_id}`}
       >
-        {row.original.dag_display_name}
+        {original.dag_display_name}
       </Link>
     ),
     header: "Dag",
   },
   {
     accessorKey: "timetable_description",
-    cell: (info) =>
-      info.getValue() === "Never, external triggers only"
-        ? undefined
-        : info.getValue(),
+    cell: ({ row: { original } }) => <Schedule dag={original} />,
     enableSorting: false,
     header: () => "Schedule",
   },
@@ -102,15 +101,21 @@ const columns: Array<ColumnDef<DAGResponse>> = [
     header: "Next Dag Run",
   },
   {
-    accessorKey: "tags",
-    cell: ({ row }) => (
-      <HStack>
-        {row.original.tags.map((tag) => (
-          <Badge key={tag.name}>{tag.name}</Badge>
-        ))}
-      </HStack>
+    accessorKey: "latest_dag_runs",
+    cell: ({ row: { original } }) => (
+      <LatestRun latestRun={original.latest_dag_runs[0]} />
     ),
     enableSorting: false,
+    header: "Last Dag Run",
+  },
+  {
+    accessorKey: "tags",
+    cell: ({
+      row: {
+        original: { tags },
+      },
+    }) => <DagTags hideIcon tags={tags} />,
+    enableSorting: false,
     header: () => "Tags",
   },
 ];
@@ -122,7 +127,7 @@ const {
   TAGS: TAGS_PARAM,
 }: SearchParamsKeysType = SearchParamsKeys;
 
-const cardDef: CardDef<DAGResponse> = {
+const cardDef: CardDef<DAGWithLatestDagRunsResponse> = {
   card: ({ row }) => <DagCard dag={row} />,
   meta: {
     customSkeleton: <Skeleton height="120px" width="100%" />,
@@ -170,34 +175,18 @@ export const DagsList = () => {
     setDagDisplayNamePattern(value);
   };
 
-  const { data, error, isFetching, isLoading } = useDagServiceGetDags(
-    {
-      dagDisplayNamePattern: Boolean(dagDisplayNamePattern)
-        ? `%${dagDisplayNamePattern}%`
-        : undefined,
-      lastDagRunState,
-      limit: pagination.pageSize,
-      offset: pagination.pageIndex * pagination.pageSize,
-      onlyActive: true,
-      orderBy,
-      paused: showPaused === null ? undefined : showPaused === "true",
-      tags: selectedTags,
-    },
-    [
-      dagDisplayNamePattern,
-      showPaused,
-      lastDagRunState,
-      pagination,
-      orderBy,
-      selectedTags,
-    ],
-    {
-      refetchOnMount: true,
-      refetchOnReconnect: false,
-      refetchOnWindowFocus: false,
-      staleTime: 5 * 60 * 1000,
-    },
-  );
+  const { data, error, isFetching, isLoading } = useDags({
+    dagDisplayNamePattern: Boolean(dagDisplayNamePattern)
+      ? `%${dagDisplayNamePattern}%`
+      : undefined,
+    lastDagRunState,
+    limit: pagination.pageSize,
+    offset: pagination.pageIndex * pagination.pageSize,
+    onlyActive: true,
+    orderBy,
+    paused: showPaused === null ? undefined : showPaused === "true",
+    tags: selectedTags,
+  });
 
   const handleSortChange = useCallback<ChangeEventHandler<HTMLSelectElement>>(
     ({ currentTarget: { value } }) => {
@@ -224,7 +213,7 @@ export const DagsList = () => {
         <DagsFilters />
         <HStack justifyContent="space-between">
           <Heading py={3} size="md">
-            {pluralize("Dag", data?.total_entries)}
+            {pluralize("Dag", data.total_entries)}
           </Heading>
           {display === "card" ? (
             <Select
@@ -247,7 +236,7 @@ export const DagsList = () => {
       <DataTable
         cardDef={cardDef}
         columns={columns}
-        data={data?.dags ?? []}
+        data={data.dags}
         displayMode={display}
         errorMessage={<ErrorAlert error={error} />}
         initialState={tableURLState}
@@ -256,7 +245,7 @@ export const DagsList = () => {
         modelName="Dag"
         onStateChange={setTableURLState}
         skeletonCount={display === "card" ? 5 : undefined}
-        total={data?.total_entries}
+        total={data.total_entries}
       />
     </>
   );
diff --git a/airflow/ui/src/pages/DagsList/LatestRun.tsx 
b/airflow/ui/src/pages/DagsList/LatestRun.tsx
new file mode 100644
index 00000000000..e2ee60f0a49
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/LatestRun.tsx
@@ -0,0 +1,41 @@
+/*!
+ * 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, HStack, Text } from "@chakra-ui/react";
+
+import type { DAGRunResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { stateColor } from "src/utils/stateColor";
+
+type Props = {
+  readonly latestRun?: DAGRunResponse;
+};
+
+export const LatestRun = ({ latestRun }: Props) =>
+  latestRun ? (
+    <HStack fontSize="sm">
+      <Time datetime={latestRun.logical_date} />
+      <Box
+        bg={stateColor[latestRun.state]}
+        borderRadius="50%"
+        height={2}
+        width={2}
+      />
+      <Text color={stateColor[latestRun.state]}>{latestRun.state}</Text>
+    </HStack>
+  ) : undefined;
diff --git a/airflow/ui/src/pages/DagsList/RecentRuns.tsx 
b/airflow/ui/src/pages/DagsList/RecentRuns.tsx
new file mode 100644
index 00000000000..7d656bc6293
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -0,0 +1,83 @@
+/*!
+ * 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, Box, Tooltip, Text } from "@chakra-ui/react";
+import dayjs from "dayjs";
+import duration from "dayjs/plugin/duration";
+
+import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
+import Time from "src/components/Time";
+import { stateColor } from "src/utils/stateColor";
+
+dayjs.extend(duration);
+
+const BAR_HEIGHT = 60;
+
+export const RecentRuns = ({
+  latestRuns,
+}: {
+  readonly latestRuns: DAGWithLatestDagRunsResponse["latest_dag_runs"];
+}) => {
+  if (!latestRuns.length) {
+    return undefined;
+  }
+
+  const runsWithDuration = latestRuns.map((run) => ({
+    ...run,
+    duration: dayjs
+      .duration(dayjs(run.end_date).diff(run.start_date))
+      .asSeconds(),
+  }));
+
+  const max = Math.max.apply(
+    undefined,
+    runsWithDuration.map((run) => run.duration),
+  );
+
+  return (
+    <Flex alignItems="flex-end" flexDirection="row-reverse">
+      {runsWithDuration.map((run) => (
+        <Tooltip
+          hasArrow
+          key={run.run_id}
+          label={
+            <Box>
+              <Text>State: {run.state}</Text>
+              <Text>
+                Logical Date: <Time datetime={run.logical_date} />
+              </Text>
+              <Text>Duration: {run.duration.toFixed(2)}s</Text>
+            </Box>
+          }
+          offset={[10, 5]}
+          placement="bottom-start"
+        >
+          <Box p={1}>
+            <Box
+              bg={stateColor[run.state]}
+              borderRadius="4px"
+              height={`${(run.duration / max) * BAR_HEIGHT}px`}
+              minHeight={1}
+              width="4px"
+            />
+          </Box>
+        </Tooltip>
+      ))}
+    </Flex>
+  );
+};
diff --git a/airflow/ui/src/pages/DagsList/Schedule.tsx 
b/airflow/ui/src/pages/DagsList/Schedule.tsx
new file mode 100644
index 00000000000..4cf94743b63
--- /dev/null
+++ b/airflow/ui/src/pages/DagsList/Schedule.tsx
@@ -0,0 +1,36 @@
+/*!
+ * 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 { Text, Tooltip } from "@chakra-ui/react";
+import { FiCalendar } from "react-icons/fi";
+
+import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
+
+type Props = {
+  readonly dag: DAGWithLatestDagRunsResponse;
+};
+
+export const Schedule = ({ dag }: Props) =>
+  Boolean(dag.timetable_summary) &&
+  dag.timetable_description !== "Never, external triggers only" ? (
+    <Tooltip hasArrow label={dag.timetable_description}>
+      <Text fontSize="sm">
+        <FiCalendar style={{ display: "inline" }} /> {dag.timetable_summary}
+      </Text>
+    </Tooltip>
+  ) : undefined;
diff --git a/airflow/ui/src/queries/useDags.tsx 
b/airflow/ui/src/queries/useDags.tsx
new file mode 100644
index 00000000000..2255ec06332
--- /dev/null
+++ b/airflow/ui/src/queries/useDags.tsx
@@ -0,0 +1,81 @@
+/*!
+ * 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 {
+  useDagServiceGetDags,
+  useDagsServiceRecentDagRuns,
+} from "openapi/queries";
+import type { DagRunState } from "openapi/requests/types.gen";
+
+const queryOptions = {
+  refetchOnMount: true,
+  refetchOnReconnect: false,
+  refetchOnWindowFocus: false,
+  staleTime: 5 * 60 * 1000,
+};
+
+export const useDags = (
+  searchParams: {
+    dagDisplayNamePattern?: string;
+    dagIdPattern?: string;
+    lastDagRunState?: DagRunState;
+    limit?: number;
+    offset?: number;
+    onlyActive?: boolean;
+    orderBy?: string;
+    owners?: Array<string>;
+    paused?: boolean;
+    tags?: Array<string>;
+  } = {},
+) => {
+  const { data, error, isFetching, isLoading } = useDagServiceGetDags(
+    searchParams,
+    undefined,
+    queryOptions,
+  );
+
+  const { lastDagRunState, orderBy, ...runsParams } = searchParams;
+  const {
+    data: runsData,
+    error: runsError,
+    isFetching: isRunsFetching,
+    isLoading: isRunsLoading,
+  } = useDagsServiceRecentDagRuns(
+    {
+      ...runsParams,
+      dagRunsLimit: 14,
+    },
+    undefined,
+    queryOptions,
+  );
+
+  const dags = (data?.dags ?? []).map((dag) => {
+    const dagWithRuns = runsData?.dags.find(
+      (runsDag) => runsDag.dag_id === dag.dag_id,
+    );
+
+    return dagWithRuns ?? { ...dag, latest_dag_runs: [] };
+  });
+
+  return {
+    data: { dags, total_entries: data?.total_entries ?? 0 },
+    error: error ?? runsError,
+    isFetching: isFetching || isRunsFetching,
+    isLoading: isLoading || isRunsLoading,
+  };
+};
diff --git a/airflow/ui/src/utils/stateColor.ts 
b/airflow/ui/src/utils/stateColor.ts
new file mode 100644
index 00000000000..ed3a79fba4e
--- /dev/null
+++ b/airflow/ui/src/utils/stateColor.ts
@@ -0,0 +1,36 @@
+/*!
+ * 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.
+ */
+
+// TODO: replace this with Airflow config values
+
+export const stateColor = {
+  deferred: "mediumpurple",
+  failed: "red",
+  null: "lightblue",
+  queued: "gray",
+  removed: "lightgrey",
+  restarting: "violet",
+  running: "lime",
+  scheduled: "tan",
+  skipped: "hotpink",
+  success: "green",
+  up_for_reschedule: "turquoise",
+  up_for_retry: "gold",
+  upstream_failed: "orange",
+};

Reply via email to