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}`;
+};

Reply via email to