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",
+};