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 afc05322283 Improve DAGs table UI (#42119)
afc05322283 is described below
commit afc053222837f80385dc1aea25312c1f0143e469
Author: Brent Bovenzi <[email protected]>
AuthorDate: Mon Sep 9 23:03:41 2024 -0400
Improve DAGs table UI (#42119)
* Rebase and fix filter pagination
* Add pluralize test
---
.gitignore | 1 +
airflow/ui/package.json | 2 +
airflow/ui/pnpm-lock.yaml | 130 ++++++++++++
airflow/ui/src/app.tsx | 28 +--
.../components/{ => DataTable}/DataTable.test.tsx | 0
.../src/components/{ => DataTable}/DataTable.tsx | 11 +-
.../{theme.ts => components/DataTable/index.tsx} | 24 +--
airflow/ui/src/dagsList.tsx | 230 +++++++++++++++------
airflow/ui/src/main.tsx | 3 +-
airflow/ui/src/theme.ts | 39 ++++
airflow/ui/src/utils/pluralize.test.ts | 85 ++++++++
airflow/ui/src/{theme.ts => utils/pluralize.ts} | 34 +--
12 files changed, 450 insertions(+), 137 deletions(-)
diff --git a/.gitignore b/.gitignore
index 3505a4ed8ab..40845794e3c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -172,6 +172,7 @@ pnpm-debug.log*
.vscode/*
!.vscode/extensions.json
/.vite/
+/.pnpm-store/
# Airflow log files when airflow is run locally
airflow-*.err
diff --git a/airflow/ui/package.json b/airflow/ui/package.json
index 257df8c3dcc..d78e2f1c693 100644
--- a/airflow/ui/package.json
+++ b/airflow/ui/package.json
@@ -15,12 +15,14 @@
"test": "vitest run"
},
"dependencies": {
+ "@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/react": "^2.8.2",
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
"@tanstack/react-query": "^5.52.1",
"@tanstack/react-table": "^8.20.1",
"axios": "^1.7.4",
+ "chakra-react-select": "^4.9.2",
"framer-motion": "^11.3.29",
"react": "^18.3.1",
"react-dom": "^18.3.1",
diff --git a/airflow/ui/pnpm-lock.yaml b/airflow/ui/pnpm-lock.yaml
index 0ff90475d4a..effe48f41ed 100644
--- a/airflow/ui/pnpm-lock.yaml
+++ b/airflow/ui/pnpm-lock.yaml
@@ -25,6 +25,9 @@ importers:
.:
dependencies:
+ '@chakra-ui/anatomy':
+ specifier: ^2.2.2
+ version: 2.2.2
'@chakra-ui/react':
specifier: ^2.8.2
version:
2.8.2(@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))(@types/[email protected])([email protected](@emotion/[email protected])([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])
@@ -43,6 +46,9 @@ importers:
axios:
specifier: ^1.7.4
version: 1.7.4
+ chakra-react-select:
+ specifier: ^4.9.2
+ version: 4.9.2(ygqhzpuo3vwx3we5k6j4i32nqi)
framer-motion:
specifier: ^11.3.29
version:
11.3.29(@emotion/[email protected])([email protected]([email protected]))([email protected])
@@ -879,6 +885,15 @@ packages:
resolution: {integrity:
sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+ '@floating-ui/[email protected]':
+ resolution: {integrity:
sha512-yDzVT/Lm101nQ5TCVeK65LtdN7Tj4Qpr9RTXJ2vPFLqtLxwOrpoxAHAJI8J3yYWUc40J0BDBheaitK5SJmno2g==}
+
+ '@floating-ui/[email protected]':
+ resolution: {integrity:
sha512-fskgCFv8J8OamCmyun8MfjB1Olfn+uZKjOKZ0vhYF3gRmEUXcGOjxWL8bBr7i4kIuPZ2KD2S3EUIOxnjC8kl2A==}
+
+ '@floating-ui/[email protected]':
+ resolution: {integrity:
sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==}
+
'@hey-api/[email protected]':
resolution: {integrity:
sha512-DA3Zf5ONxMK1PUkK88lAuYbXMgn5BvU5sjJdTAO2YOn6Eu/9ovilBztMzvu8pyY44PmL3n4ex4+f+XIwvgfhvw==}
engines: {node: ^18.0.0 || >=20.0.0}
@@ -1171,6 +1186,9 @@ packages:
'@types/[email protected]':
resolution: {integrity:
sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==}
+ '@types/[email protected]':
+ resolution: {integrity:
sha512-RM05tAniPZ5DZPzzNFP+DmrcOdD0efDUxMy3145oljWSl3x9ZV5vhme98gTxFrj2lhXvmGNnUiuDyJgY9IKkNA==}
+
'@types/[email protected]':
resolution: {integrity:
sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==}
@@ -1436,6 +1454,20 @@ packages:
resolution: {integrity:
sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==}
engines: {node: '>=12'}
+ [email protected]:
+ resolution: {integrity:
sha512-uhvKAJ1I2lbIwdn+wx0YvxX5rtQVI0gXL0apx0CXm3blIxk7qf6YuCh2TnGuGKst8gj8jUFZyhYZiGlcvgbBRQ==}
+ peerDependencies:
+ '@chakra-ui/form-control': ^2.0.0
+ '@chakra-ui/icon': ^3.0.0
+ '@chakra-ui/layout': ^2.0.0
+ '@chakra-ui/media-query': ^3.0.0
+ '@chakra-ui/menu': ^2.0.0
+ '@chakra-ui/spinner': ^2.0.0
+ '@chakra-ui/system': ^2.0.0
+ '@emotion/react': ^11.8.1
+ react: ^18.0.0
+ react-dom: ^18.0.0
+
[email protected]:
resolution: {integrity:
sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@@ -1593,6 +1625,9 @@ packages:
[email protected]:
resolution: {integrity:
sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==}
+ [email protected]:
+ resolution: {integrity:
sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
[email protected]:
resolution: {integrity:
sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
engines: {node: '>=12'}
@@ -2174,6 +2209,9 @@ packages:
[email protected]:
resolution: {integrity:
sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==}
+ [email protected]:
+ resolution: {integrity:
sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==}
+
[email protected]:
resolution: {integrity:
sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==}
@@ -2468,6 +2506,12 @@ packages:
'@types/react':
optional: true
+ [email protected]:
+ resolution: {integrity:
sha512-TfjLDo58XrhP6VG5M/Mi56Us0Yt8X7xD6cDybC7yoRMUNm7BGO7qk8J0TLQOua/prb8vUOtsfnXZwfm30HGsAA==}
+ peerDependencies:
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
+
[email protected]:
resolution: {integrity:
sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@@ -2478,6 +2522,12 @@ packages:
'@types/react':
optional: true
+ [email protected]:
+ resolution: {integrity:
sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+ peerDependencies:
+ react: '>=16.6.0'
+ react-dom: '>=16.6.0'
+
[email protected]:
resolution: {integrity:
sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==}
engines: {node: '>=0.10.0'}
@@ -2768,6 +2818,15 @@ packages:
'@types/react':
optional: true
+ [email protected]:
+ resolution: {integrity:
sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
[email protected]:
resolution: {integrity:
sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
engines: {node: '>=10'}
@@ -3871,6 +3930,17 @@ snapshots:
'@eslint/[email protected]': {}
+ '@floating-ui/[email protected]':
+ dependencies:
+ '@floating-ui/utils': 0.2.7
+
+ '@floating-ui/[email protected]':
+ dependencies:
+ '@floating-ui/core': 1.6.7
+ '@floating-ui/utils': 0.2.7
+
+ '@floating-ui/[email protected]': {}
+
'@hey-api/[email protected]([email protected])':
dependencies:
'@apidevtools/json-schema-ref-parser': 11.6.4
@@ -4115,6 +4185,10 @@ snapshots:
dependencies:
'@types/react': 18.3.4
+ '@types/[email protected]':
+ dependencies:
+ '@types/react': 18.3.4
+
'@types/[email protected]':
dependencies:
'@types/prop-types': 15.7.12
@@ -4465,6 +4539,23 @@ snapshots:
loupe: 3.1.1
pathval: 2.0.0
+ [email protected](ygqhzpuo3vwx3we5k6j4i32nqi):
+ dependencies:
+ '@chakra-ui/form-control':
2.2.0(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected])
+ '@chakra-ui/icon':
3.2.0(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected])
+ '@chakra-ui/layout':
2.3.1(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected])
+ '@chakra-ui/media-query':
3.3.0(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected])
+ '@chakra-ui/menu':
2.2.1(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected](@emotion/[email protected])([email protected]([email protected]))([email protected]))([email protected])
+ '@chakra-ui/spinner':
2.1.0(@chakra-ui/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected]))([email protected])
+ '@chakra-ui/system':
2.6.2(@emotion/[email protected](@types/[email protected])([email protected]))(@emotion/[email protected](@emotion/[email protected](@types/[email protected])([email protected]))(@types/[email protected])([email protected]))([email protected])
+ '@emotion/react': 11.13.3(@types/[email protected])([email protected])
+ react: 18.3.1
+ react-dom: 18.3.1([email protected])
+ react-select:
5.8.0(@types/[email protected])([email protected]([email protected]))([email protected])
+ transitivePeerDependencies:
+ - '@types/react'
+ - supports-color
+
[email protected]:
dependencies:
ansi-styles: 3.2.1
@@ -4619,6 +4710,11 @@ snapshots:
[email protected]: {}
+ [email protected]:
+ dependencies:
+ '@babel/runtime': 7.25.4
+ csstype: 3.1.3
+
[email protected]: {}
[email protected]: {}
@@ -5295,6 +5391,8 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.0
+ [email protected]: {}
+
[email protected]: {}
[email protected]: {}
@@ -5565,6 +5663,23 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.4
+
[email protected](@types/[email protected])([email protected]([email protected]))([email protected]):
+ dependencies:
+ '@babel/runtime': 7.25.4
+ '@emotion/cache': 11.13.1
+ '@emotion/react': 11.13.3(@types/[email protected])([email protected])
+ '@floating-ui/dom': 1.6.10
+ '@types/react-transition-group': 4.4.11
+ memoize-one: 6.0.0
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1([email protected])
+ react-transition-group:
4.4.5([email protected]([email protected]))([email protected])
+ use-isomorphic-layout-effect: 1.1.2(@types/[email protected])([email protected])
+ transitivePeerDependencies:
+ - '@types/react'
+ - supports-color
+
[email protected](@types/[email protected])([email protected]):
dependencies:
get-nonce: 1.0.1
@@ -5574,6 +5689,15 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.4
+ [email protected]([email protected]([email protected]))([email protected]):
+ dependencies:
+ '@babel/runtime': 7.25.4
+ dom-helpers: 5.2.1
+ loose-envify: 1.4.0
+ prop-types: 15.8.1
+ react: 18.3.1
+ react-dom: 18.3.1([email protected])
+
[email protected]:
dependencies:
loose-envify: 1.4.0
@@ -5912,6 +6036,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.4
+ [email protected](@types/[email protected])([email protected]):
+ dependencies:
+ react: 18.3.1
+ optionalDependencies:
+ '@types/react': 18.3.4
+
[email protected](@types/[email protected])([email protected]):
dependencies:
detect-node-es: 1.1.0
diff --git a/airflow/ui/src/app.tsx b/airflow/ui/src/app.tsx
index 2f1bc556793..ab2789cefb1 100644
--- a/airflow/ui/src/app.tsx
+++ b/airflow/ui/src/app.tsx
@@ -17,40 +17,16 @@
* under the License.
*/
-import { useState } from "react";
-import { Box, Spinner } from "@chakra-ui/react";
-import { PaginationState } from "@tanstack/react-table";
-
-import { useDagServiceGetDags } from "openapi/queries";
+import { Box } from "@chakra-ui/react";
import { DagsList } from "src/dagsList";
import { Nav } from "src/nav";
export const App = () => {
- // TODO: Change this to be taken from airflow.cfg
- const pageSize = 50;
- const [pagination, setPagination] = useState<PaginationState>({
- pageIndex: 0,
- pageSize: pageSize,
- });
-
- const { data, isLoading } = useDagServiceGetDags({
- limit: pagination.pageSize,
- offset: pagination.pageIndex * pagination.pageSize,
- });
-
return (
<div>
<Nav />
<Box p={3} ml={24}>
- {isLoading && <Spinner />}
- {!isLoading && !!data?.dags && (
- <DagsList
- data={data.dags}
- total={data.total_entries}
- pagination={pagination}
- setPagination={setPagination}
- />
- )}
+ <DagsList />
</Box>
</div>
);
diff --git a/airflow/ui/src/components/DataTable.test.tsx
b/airflow/ui/src/components/DataTable/DataTable.test.tsx
similarity index 100%
rename from airflow/ui/src/components/DataTable.test.tsx
rename to airflow/ui/src/components/DataTable/DataTable.test.tsx
diff --git a/airflow/ui/src/components/DataTable.tsx
b/airflow/ui/src/components/DataTable/DataTable.tsx
similarity index 95%
rename from airflow/ui/src/components/DataTable.tsx
rename to airflow/ui/src/components/DataTable/DataTable.tsx
index fbdd59a1b90..4b4b1251f84 100644
--- a/airflow/ui/src/components/DataTable.tsx
+++ b/airflow/ui/src/components/DataTable/DataTable.tsx
@@ -17,8 +17,6 @@
* under the License.
*/
-"use client";
-
import {
ColumnDef,
Table as TanStackTable,
@@ -41,6 +39,7 @@ import {
Th,
Thead,
Tr,
+ useColorModeValue,
} from "@chakra-ui/react";
import React, { Fragment } from "react";
@@ -143,10 +142,12 @@ export function DataTable<TData>({
},
});
+ const theadBg = useColorModeValue("white", "gray.800");
+
return (
- <TableContainer>
- <ChakraTable variant="striped">
- <Thead>
+ <TableContainer overflowY="auto" maxH="calc(100vh - 10rem)">
+ <ChakraTable colorScheme="blue">
+ <Thead position="sticky" top={0} bg={theadBg}>
{table.getHeaderGroups().map((headerGroup) => (
<Tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
diff --git a/airflow/ui/src/theme.ts
b/airflow/ui/src/components/DataTable/index.tsx
similarity index 69%
copy from airflow/ui/src/theme.ts
copy to airflow/ui/src/components/DataTable/index.tsx
index 03247b8cc75..495cf4e4f38 100644
--- a/airflow/ui/src/theme.ts
+++ b/airflow/ui/src/components/DataTable/index.tsx
@@ -17,26 +17,4 @@
* under the License.
*/
-import { extendTheme } from "@chakra-ui/react";
-
-const theme = extendTheme({
- config: {
- useSystemColorMode: true,
- },
- styles: {
- global: {
- "*, *::before, &::after": {
- borderColor: "gray.200",
- },
- },
- },
- components: {
- Tooltip: {
- baseStyle: {
- fontSize: "md",
- },
- },
- },
-});
-
-export default theme;
+export * from "./DataTable";
diff --git a/airflow/ui/src/dagsList.tsx b/airflow/ui/src/dagsList.tsx
index e8f06545de8..b6c4e6949b9 100644
--- a/airflow/ui/src/dagsList.tsx
+++ b/airflow/ui/src/dagsList.tsx
@@ -17,40 +17,64 @@
* under the License.
*/
+import { useState } from "react";
+import { ColumnDef, PaginationState } from "@tanstack/react-table";
import {
- ColumnDef,
- Row,
- OnChangeFn,
- PaginationState,
-} from "@tanstack/react-table";
-import { MdExpandMore } from "react-icons/md";
-import { Box, Code } from "@chakra-ui/react";
+ Badge,
+ Button,
+ ButtonProps,
+ Checkbox,
+ Heading,
+ HStack,
+ Input,
+ InputGroup,
+ InputGroupProps,
+ InputLeftElement,
+ InputProps,
+ InputRightElement,
+ Select,
+ Spinner,
+ Text,
+ VStack,
+} from "@chakra-ui/react";
+import { Select as ReactSelect } from "chakra-react-select";
+import { FiSearch } from "react-icons/fi";
import { DAG } from "openapi/requests/types.gen";
-import { DataTable } from "src/components/DataTable.tsx";
+import { useDagServiceGetDags } from "openapi/queries";
+import { DataTable } from "./components/DataTable";
+import { pluralize } from "./utils/pluralize";
+
+const SearchBar = ({
+ groupProps,
+ inputProps,
+ buttonProps,
+}: {
+ groupProps?: InputGroupProps;
+ inputProps?: InputProps;
+ buttonProps?: ButtonProps;
+}) => (
+ <InputGroup {...groupProps}>
+ <InputLeftElement pointerEvents="none">
+ <FiSearch />
+ </InputLeftElement>
+ <Input placeholder="Search DAGs" pr={150} {...inputProps} />
+ <InputRightElement width={150}>
+ <Button
+ variant="ghost"
+ colorScheme="blue"
+ width={140}
+ height="1.75rem"
+ fontWeight="normal"
+ {...buttonProps}
+ >
+ Advanced Search
+ </Button>
+ </InputRightElement>
+ </InputGroup>
+);
const columns: ColumnDef<DAG>[] = [
- {
- id: "expander",
- header: () => null,
- cell: ({ row }) => {
- return row.getCanExpand() ? (
- <button
- {...{
- onClick: row.getToggleExpandedHandler(),
- style: { cursor: "pointer" },
- }}
- >
- <Box
- transform={row.getIsExpanded() ? "rotate(-180deg)" : "none"}
- transition="transform 0.2s"
- >
- <MdExpandMore />
- </Box>
- </button>
- ) : null;
- },
- },
{
accessorKey: "dag_display_name",
header: "DAG",
@@ -61,42 +85,130 @@ const columns: ColumnDef<DAG>[] = [
},
{
accessorKey: "timetable_description",
- header: () => "Timetable",
+ header: () => "Schedule",
+ cell: (info) =>
+ info.getValue() !== "Never, external triggers only"
+ ? info.getValue()
+ : undefined,
+ },
+ {
+ accessorKey: "next_dagrun",
+ header: "Next DAG Run",
+ },
+ {
+ accessorKey: "owner",
+ header: () => "Owner",
+ cell: ({ row }) => (
+ <HStack>
+ {row.original.owners?.map((owner) => <Text key={owner}>{owner}</Text>)}
+ </HStack>
+ ),
},
{
- accessorKey: "description",
- header: () => "Description",
+ accessorKey: "tags",
+ header: () => "Tags",
+ cell: ({ row }) => (
+ <HStack>
+ {row.original.tags?.map((tag) => (
+ <Badge key={tag.name}>{tag.name}</Badge>
+ ))}
+ </HStack>
+ ),
},
];
-const renderSubComponent = ({ row }: { row: Row<DAG> }) => {
- return (
- <pre style={{ fontSize: "10px" }}>
- <Code>{JSON.stringify(row.original, null, 2)}</Code>
- </pre>
- );
-};
+const QuickFilterButton = ({ children, ...rest }: ButtonProps) => (
+ <Button
+ borderRadius={20}
+ fontWeight="normal"
+ colorScheme="blue"
+ variant="outline"
+ {...rest}
+ >
+ {children}
+ </Button>
+);
+
+export const DagsList = () => {
+ // TODO: Change this to be taken from airflow.cfg
+ const pageSize = 50;
+ const [pagination, setPagination] = useState<PaginationState>({
+ pageIndex: 0,
+ pageSize: pageSize,
+ });
+ const [showPaused, setShowPaused] = useState(true);
+ const [orderBy, setOrderBy] = useState<string | undefined>();
+
+ const { data, isLoading } = useDagServiceGetDags({
+ limit: pagination.pageSize,
+ offset: pagination.pageIndex * pagination.pageSize,
+ onlyActive: true,
+ paused: showPaused,
+ orderBy,
+ });
-export const DagsList = ({
- data,
- total,
- pagination,
- setPagination,
-}: {
- data: DAG[];
- total: number | undefined;
- pagination: PaginationState;
- setPagination: OnChangeFn<PaginationState>;
-}) => {
return (
- <DataTable
- data={data}
- total={total}
- columns={columns}
- getRowCanExpand={() => true}
- renderSubComponent={renderSubComponent}
- pagination={pagination}
- setPagination={setPagination}
- />
+ <>
+ {isLoading && <Spinner />}
+ {!isLoading && !!data?.dags && (
+ <>
+ <VStack alignItems="none">
+ <SearchBar
+ inputProps={{ isDisabled: true }}
+ buttonProps={{ isDisabled: true }}
+ />
+ <HStack justifyContent="space-between">
+ <HStack>
+ <HStack>
+ <QuickFilterButton isActive>All</QuickFilterButton>
+ <QuickFilterButton isDisabled>Failed</QuickFilterButton>
+ <QuickFilterButton isDisabled>Running</QuickFilterButton>
+ <QuickFilterButton isDisabled>Successful</QuickFilterButton>
+ </HStack>
+ <Checkbox
+ isChecked={showPaused}
+ onChange={() => {
+ setShowPaused(!showPaused);
+ setPagination({
+ ...pagination,
+ pageIndex: 0,
+ });
+ }}
+ >
+ Show Paused DAGs
+ </Checkbox>
+ </HStack>
+ <HStack>
+ <ReactSelect placeholder="Filter by tag" isDisabled />
+ <ReactSelect placeholder="Filter by owner" isDisabled />
+ </HStack>
+ </HStack>
+ <HStack justifyContent="space-between">
+ <Heading size="md">
+ {pluralize("DAG", data.total_entries)}
+ </Heading>
+ <Select
+ placeholder="Sort by…"
+ width="200px"
+ variant="outline"
+ value={orderBy}
+ onChange={(e) => setOrderBy(e.target.value || undefined)}
+ >
+ <option value="dag_id">Sort by DAG ID (A-Z)</option>
+ <option value="-dag_id">Sort by DAG ID (Z-A)</option>
+ </Select>
+ </HStack>
+ </VStack>
+ <DataTable
+ data={data.dags}
+ total={data.total_entries}
+ columns={columns}
+ getRowCanExpand={() => true}
+ pagination={pagination}
+ setPagination={setPagination}
+ />
+ </>
+ )}
+ </>
);
};
diff --git a/airflow/ui/src/main.tsx b/airflow/ui/src/main.tsx
index fa45680d264..be9196defdb 100644
--- a/airflow/ui/src/main.tsx
+++ b/airflow/ui/src/main.tsx
@@ -22,6 +22,7 @@ import { ChakraProvider } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { App } from "src/app.tsx";
import axios, { AxiosResponse } from "axios";
+import theme from "./theme";
const queryClient = new QueryClient({
defaultOptions: {
@@ -57,7 +58,7 @@ axios.interceptors.response.use(
const root = createRoot(document.getElementById("root")!);
root.render(
- <ChakraProvider>
+ <ChakraProvider theme={theme}>
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/theme.ts
index 03247b8cc75..eee148ad09f 100644
--- a/airflow/ui/src/theme.ts
+++ b/airflow/ui/src/theme.ts
@@ -18,6 +18,44 @@
*/
import { extendTheme } from "@chakra-ui/react";
+import { tableAnatomy } from "@chakra-ui/anatomy";
+import { createMultiStyleConfigHelpers } from "@chakra-ui/react";
+
+const { definePartsStyle, defineMultiStyleConfig } =
+ createMultiStyleConfigHelpers(tableAnatomy.keys);
+
+const baseStyle = definePartsStyle((props) => {
+ const { colorScheme: c, colorMode } = props;
+ return {
+ thead: {
+ tr: {
+ th: {
+ borderBottomWidth: 0,
+ },
+ },
+ },
+ tbody: {
+ tr: {
+ "&:nth-of-type(odd)": {
+ "th, td": {
+ borderBottomWidth: "0px",
+ borderColor: colorMode === "light" ? `${c}.50` : `gray.900`,
+ },
+ td: {
+ background: colorMode === "light" ? `${c}.50` : `gray.900`,
+ },
+ },
+ "&:nth-of-type(even)": {
+ "th, td": {
+ borderBottomWidth: "0px",
+ },
+ },
+ },
+ },
+ };
+});
+
+export const tableTheme = defineMultiStyleConfig({ baseStyle });
const theme = extendTheme({
config: {
@@ -36,6 +74,7 @@ const theme = extendTheme({
fontSize: "md",
},
},
+ Table: tableTheme,
},
});
diff --git a/airflow/ui/src/utils/pluralize.test.ts
b/airflow/ui/src/utils/pluralize.test.ts
new file mode 100644
index 00000000000..ead9ff32a04
--- /dev/null
+++ b/airflow/ui/src/utils/pluralize.test.ts
@@ -0,0 +1,85 @@
+/*!
+ * 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 { describe, expect, it } from "vitest";
+
+import { pluralize } from "./pluralize";
+
+type PluralizeTestCase = {
+ in: [string, number, (string | null)?, boolean?];
+ out: string;
+};
+
+const pluralizeTestCases: PluralizeTestCase[] = [
+ { in: ["DAG", 0, undefined, undefined], out: "0 DAGs" },
+ { in: ["DAG", 1, undefined, undefined], out: "1 DAG" },
+ { in: ["DAG", 12000, undefined, undefined], out: "12,000 DAGs" },
+ { in: ["DAG", 12000000, undefined, undefined], out: "12,000,000 DAGs" },
+ { in: ["DAG", 0, undefined, undefined], out: "0 DAGs" },
+ { in: ["DAG", 1, undefined, undefined], out: "1 DAG" },
+ { in: ["DAG", 12000, undefined, undefined], out: "12,000 DAGs" },
+ { in: ["DAG", 12000000, undefined, undefined], out: "12,000,000 DAGs" },
+ // Omit the count.
+ { in: ["DAG", 0, null, true], out: "DAGs" },
+ { in: ["DAG", 1, null, true], out: "DAG" },
+ { in: ["DAG", 12000, null, true], out: "DAGs" },
+ { in: ["DAG", 12000000, null, true], out: "DAGs" },
+ { in: ["DAG", 0, null, true], out: "DAGs" },
+ { in: ["DAG", 1, null, true], out: "DAG" },
+ { in: ["DAG", 12000, null, true], out: "DAGs" },
+ { in: ["DAG", 12000000, null, true], out: "DAGs" },
+ // The casing of the string is preserved.
+ { in: ["goose", 0, "geese", undefined], out: "0 geese" },
+ { in: ["goose", 1, "geese", undefined], out: "1 goose" },
+ // The plural form is different from the singular form.
+ { in: ["Goose", 0, "Geese", undefined], out: "0 Geese" },
+ { in: ["Goose", 1, "Geese", undefined], out: "1 Goose" },
+ { in: ["Goose", 12000, "Geese", undefined], out: "12,000 Geese" },
+ { in: ["Goose", 12000000, "Geese", undefined], out: "12,000,000 Geese" },
+ { in: ["Goose", 0, "Geese", undefined], out: "0 Geese" },
+ { in: ["Goose", 1, "Geese", undefined], out: "1 Goose" },
+ { in: ["Goose", 12000, "Geese", undefined], out: "12,000 Geese" },
+ { in: ["Goose", 12000000, "Geese", undefined], out: "12,000,000 Geese" },
+ // In the case of "Moose", the plural is the same as the singular and you
+ // probably wouldn't elect to use this function at all, but there could be
+ // cases where dynamic data makes it unavoidable.
+ { in: ["Moose", 0, "Moose", undefined], out: "0 Moose" },
+ { in: ["Moose", 1, "Moose", undefined], out: "1 Moose" },
+ { in: ["Moose", 12000, "Moose", undefined], out: "12,000 Moose" },
+ { in: ["Moose", 12000000, "Moose", undefined], out: "12,000,000 Moose" },
+ { in: ["Moose", 0, "Moose", undefined], out: "0 Moose" },
+ { in: ["Moose", 1, "Moose", undefined], out: "1 Moose" },
+ { in: ["Moose", 12000, "Moose", undefined], out: "12,000 Moose" },
+ { in: ["Moose", 12000000, "Moose", undefined], out: "12,000,000 Moose" },
+];
+
+describe("pluralize", () => {
+ it("case", () => {
+ pluralizeTestCases.forEach((testCase) =>
+ expect(
+ pluralize(
+ testCase.in[0],
+ testCase.in[1],
+ testCase.in[2],
+ testCase.in[3]
+ )
+ ).toEqual(testCase.out)
+ );
+ });
+});
diff --git a/airflow/ui/src/theme.ts b/airflow/ui/src/utils/pluralize.ts
similarity index 67%
copy from airflow/ui/src/theme.ts
copy to airflow/ui/src/utils/pluralize.ts
index 03247b8cc75..0fdddb1c69b 100644
--- a/airflow/ui/src/theme.ts
+++ b/airflow/ui/src/utils/pluralize.ts
@@ -17,26 +17,14 @@
* under the License.
*/
-import { extendTheme } from "@chakra-ui/react";
-
-const theme = extendTheme({
- config: {
- useSystemColorMode: true,
- },
- styles: {
- global: {
- "*, *::before, &::after": {
- borderColor: "gray.200",
- },
- },
- },
- components: {
- Tooltip: {
- baseStyle: {
- fontSize: "md",
- },
- },
- },
-});
-
-export default theme;
+export const pluralize = (
+ singularLabel: string,
+ count: number | undefined = 0,
+ pluralLabel?: string | null,
+ omitCount?: boolean
+): string => {
+ const pluralized =
+ count === 1 ? singularLabel : pluralLabel || `${singularLabel}s`;
+ // toLocaleString() will add commas for thousands, millions, etc.
+ return `${omitCount ? "" : `${count.toLocaleString()} `}${pluralized}`;
+};