This is an automated email from the ASF dual-hosted git repository.

eladkal 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 94f6fcc6c5 Upgrade react-table package. Use with Audit Log table 
(#38092)
94f6fcc6c5 is described below

commit 94f6fcc6c555f106059f57e04afaf8bbf6b18a83
Author: Brent Bovenzi <[email protected]>
AuthorDate: Mon Mar 18 01:39:47 2024 -0700

    Upgrade react-table package. Use with Audit Log table (#38092)
---
 airflow/www/package.json                           |   1 +
 airflow/www/static/js/api/useEventLogs.tsx         |   1 +
 .../www/static/js/components/NewTable/NewCells.tsx |  33 +++
 .../www/static/js/components/NewTable/NewTable.tsx | 269 +++++++++++++++++++++
 .../js/components/NewTable/createSkeleton.tsx      |  47 ++++
 .../js/components/NewTable/searchParams.test.ts    |  63 +++++
 .../static/js/components/NewTable/searchParams.ts  | 126 ++++++++++
 .../js/components/NewTable/useTableUrlState.ts     |  69 ++++++
 airflow/www/static/js/dag/details/AuditLog.tsx     | 102 ++++----
 airflow/www/static/js/dag/useSelection.ts          |  23 ++
 airflow/www/yarn.lock                              |  12 +
 11 files changed, 695 insertions(+), 51 deletions(-)

diff --git a/airflow/www/package.json b/airflow/www/package.json
index e6149c8bd1..0f6fb546c9 100644
--- a/airflow/www/package.json
+++ b/airflow/www/package.json
@@ -103,6 +103,7 @@
     "@emotion/cache": "^11.9.3",
     "@emotion/react": "^11.9.3",
     "@emotion/styled": "^11",
+    "@tanstack/react-table": "^8.13.2",
     "@visx/group": "^2.10.0",
     "@visx/shape": "^2.12.2",
     "ansi_up": "^6.0.2",
diff --git a/airflow/www/static/js/api/useEventLogs.tsx 
b/airflow/www/static/js/api/useEventLogs.tsx
index 409fd930b0..3a04eac393 100644
--- a/airflow/www/static/js/api/useEventLogs.tsx
+++ b/airflow/www/static/js/api/useEventLogs.tsx
@@ -67,6 +67,7 @@ export default function useEventLogs({
     },
     {
       refetchInterval: isRefreshOn && (autoRefreshInterval || 1) * 1000,
+      keepPreviousData: true,
     }
   );
 }
diff --git a/airflow/www/static/js/components/NewTable/NewCells.tsx 
b/airflow/www/static/js/components/NewTable/NewCells.tsx
new file mode 100644
index 0000000000..40cf40a53c
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/NewCells.tsx
@@ -0,0 +1,33 @@
+/*!
+ * 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 React from "react";
+import { Code } from "@chakra-ui/react";
+
+import Time from "src/components/Time";
+
+export const TimeCell = ({ getValue }: any) => {
+  const value = getValue();
+  return <Time dateTime={value} />;
+};
+
+export const CodeCell = ({ getValue }: any) => {
+  const value = getValue();
+  return value ? <Code>{JSON.stringify(value)}</Code> : null;
+};
diff --git a/airflow/www/static/js/components/NewTable/NewTable.tsx 
b/airflow/www/static/js/components/NewTable/NewTable.tsx
new file mode 100644
index 0000000000..1b45aaa8ea
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/NewTable.tsx
@@ -0,0 +1,269 @@
+/*!
+ * 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.
+ */
+
+/*
+ * Custom wrapper of react-table using Chakra UI components
+ */
+
+import React, { useRef, useCallback } from "react";
+import {
+  Table as ChakraTable,
+  Thead,
+  Tbody,
+  Tr,
+  Th,
+  Td,
+  useColorModeValue,
+  TableProps as ChakraTableProps,
+  HStack,
+  Box,
+  ButtonGroup,
+  Button,
+  Spinner,
+} from "@chakra-ui/react";
+import {
+  useReactTable,
+  getCoreRowModel,
+  getPaginationRowModel,
+  getSortedRowModel,
+  PaginationState,
+  SortingState,
+  flexRender,
+  ColumnDef,
+  TableState as ReactTableState,
+  RowData,
+  OnChangeFn,
+  Updater,
+  Table as ReactTable,
+} from "@tanstack/react-table";
+import { MdKeyboardArrowLeft, MdKeyboardArrowRight } from "react-icons/md";
+import {
+  TiArrowUnsorted,
+  TiArrowSortedDown,
+  TiArrowSortedUp,
+} from "react-icons/ti";
+import { isEqual, pick } from "lodash";
+
+import createSkeleton from "./createSkeleton";
+
+export interface TableProps<TData extends RowData> extends ChakraTableProps {
+  data: TData[];
+  columns: ColumnDef<TData, any>[];
+  initialState?: TableState;
+  onStateChange?: (state: TableState) => void;
+  resultCount?: number;
+  isLoading?: boolean;
+  isFetching?: boolean;
+  onRowClicked?: (row: any, e: unknown) => void;
+  skeletonCount?: number;
+}
+
+const normalizeState = (state: ReactTableState): TableState =>
+  pick(state, "pagination", "sorting", "globalFilter");
+
+export interface TableState {
+  pagination: PaginationState;
+  sorting: SortingState;
+}
+
+export const NewTable = <TData extends RowData>({
+  data,
+  columns,
+  initialState,
+  onStateChange,
+  resultCount,
+  isLoading = false,
+  isFetching = false,
+  onRowClicked,
+  skeletonCount = 10,
+}: TableProps<TData>) => {
+  // ref current table instance so that we can "compute" the state
+  const ref = useRef<{ tableRef: ReactTable<TData> | undefined }>({
+    tableRef: undefined,
+  });
+
+  const oddColor = useColorModeValue("gray.50", "gray.900");
+  const hoverColor = useColorModeValue("gray.100", "gray.700");
+
+  const handleStateChange = useCallback<OnChangeFn<ReactTableState>>(
+    (updater: Updater<ReactTableState>) => {
+      // this is why we need ref, the state is NOT pass out as plain object 
but a updater function.
+      if (ref.current.tableRef && onStateChange) {
+        // nah, this is a ugly piece of code.
+        const current = ref.current.tableRef.getState();
+        const nextState =
+          typeof updater === "function" ? updater(current) : updater;
+        const old = normalizeState(current);
+        const updated = normalizeState(nextState);
+        // react table call onStateChange on every update even the state does 
not change.
+        // so we need to do a quick compare here.
+        if (!isEqual(old, updated)) onStateChange(updated);
+      }
+    },
+    [onStateChange]
+  );
+
+  const tableInstance = useReactTable({
+    columns,
+    data,
+    initialState: {
+      ...initialState,
+      pagination: {
+        pageIndex: 0,
+        pageSize: 25, // default page size to 25
+        ...initialState?.pagination,
+      },
+    },
+    ...(isLoading ? createSkeleton(skeletonCount, columns) : {}),
+    rowCount: resultCount,
+    manualPagination: true,
+    manualSorting: true,
+    getCoreRowModel: getCoreRowModel(),
+    getPaginationRowModel: getPaginationRowModel(),
+    getSortedRowModel: getSortedRowModel(),
+    onStateChange: handleStateChange,
+  });
+
+  ref.current.tableRef = tableInstance;
+
+  const total = resultCount || data.length;
+  const { pageIndex, pageSize } = tableInstance.getState().pagination;
+  const lowerCount = Math.min(pageIndex * pageSize + 1, total);
+  const upperCount = Math.min(
+    (pageIndex + 1) * pageSize,
+    pageIndex * pageSize + data.length,
+    total
+  );
+  const canPrevious = tableInstance.getCanPreviousPage();
+  const canNext = tableInstance.getCanNextPage() && data.length === pageSize;
+  const handlePrevious = () => {
+    tableInstance.previousPage();
+  };
+  const handleNext = () => {
+    tableInstance.nextPage();
+  };
+
+  return (
+    <>
+      <ChakraTable>
+        <Thead>
+          {tableInstance.getHeaderGroups().map((headerGroup) => (
+            <Tr key={headerGroup.id}>
+              {headerGroup.headers.map(
+                ({ id, colSpan, column, isPlaceholder, getContext }) => {
+                  const sort = column.getIsSorted();
+                  const canSort = column.getCanSort();
+                  return (
+                    <Th
+                      key={id}
+                      colSpan={colSpan}
+                      whiteSpace="nowrap"
+                      cursor={column.getCanSort() ? "pointer" : undefined}
+                      onClick={column.getToggleSortingHandler()}
+                    >
+                      {isPlaceholder
+                        ? null
+                        : flexRender(column.columnDef.header, getContext())}
+                      {canSort && !sort && (
+                        <TiArrowUnsorted
+                          aria-label="unsorted"
+                          style={{ display: "inline" }}
+                          size="1em"
+                        />
+                      )}
+                      {canSort &&
+                        sort &&
+                        (sort === "desc" ? (
+                          <TiArrowSortedDown
+                            aria-label="sorted descending"
+                            style={{ display: "inline" }}
+                            size="1em"
+                          />
+                        ) : (
+                          <TiArrowSortedUp
+                            aria-label="sorted ascending"
+                            style={{ display: "inline" }}
+                            size="1em"
+                          />
+                        ))}
+                    </Th>
+                  );
+                }
+              )}
+            </Tr>
+          ))}
+        </Thead>
+        <Tbody>
+          {tableInstance.getRowModel().rows.map((row) => (
+            <Tr
+              key={row.id}
+              _odd={{ backgroundColor: oddColor }}
+              _hover={
+                onRowClicked && {
+                  backgroundColor: hoverColor,
+                  cursor: "pointer",
+                }
+              }
+              onClick={
+                onRowClicked ? (e: unknown) => onRowClicked(row, e) : undefined
+              }
+            >
+              {row.getVisibleCells().map((cell) => (
+                <Td key={cell.id} py={3}>
+                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
+                </Td>
+              ))}
+            </Tr>
+          ))}
+        </Tbody>
+      </ChakraTable>
+      <HStack spacing={4} mt={4}>
+        {upperCount && (
+          <Box fontSize="sm">
+            {lowerCount}-{upperCount}
+            {upperCount !== total && ` out of ${total} total`}
+          </Box>
+        )}
+        {(canPrevious || canNext) && (
+          <ButtonGroup size="sm" isAttached variant="outline">
+            <Button
+              leftIcon={<MdKeyboardArrowLeft />}
+              colorScheme="gray"
+              aria-label="Previous Page"
+              isDisabled={!canPrevious}
+              onClick={handlePrevious}
+            >
+              Prev
+            </Button>
+            <Button
+              rightIcon={<MdKeyboardArrowRight />}
+              colorScheme="gray"
+              aria-label="Next Page"
+              isDisabled={!canNext}
+              onClick={handleNext}
+            >
+              Next
+            </Button>
+            {isFetching && <Spinner ml={2} mt={4} size="sm" />}
+          </ButtonGroup>
+        )}
+      </HStack>
+    </>
+  );
+};
diff --git a/airflow/www/static/js/components/NewTable/createSkeleton.tsx 
b/airflow/www/static/js/components/NewTable/createSkeleton.tsx
new file mode 100644
index 0000000000..cb0a228c76
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/createSkeleton.tsx
@@ -0,0 +1,47 @@
+/*!
+ * 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 React from "react";
+
+import { Skeleton } from "@chakra-ui/react";
+import type { ColumnDef } from "@tanstack/react-table";
+
+function createSkeleton<TData, TValue>(
+  skeletonCount: number,
+  columnDefs: ColumnDef<TData, TValue>[]
+) {
+  const colDefs = columnDefs.map((colDef) => ({
+    ...colDef,
+    cell: () => (
+      <Skeleton
+        data-testid="skeleton"
+        // @ts-ignore
+        width={colDef.meta?.skeletonWidth || 200}
+        height="10px"
+        display="inline-block"
+      />
+    ),
+  }));
+
+  const data = [...Array(skeletonCount)].map(() => ({} as TData));
+
+  return { columns: colDefs, data };
+}
+
+export default createSkeleton;
diff --git a/airflow/www/static/js/components/NewTable/searchParams.test.ts 
b/airflow/www/static/js/components/NewTable/searchParams.test.ts
new file mode 100644
index 0000000000..edff0cc0d5
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/searchParams.test.ts
@@ -0,0 +1,63 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import type { TableState } from "./NewTable";
+import { searchParamsToState, stateToSearchParams } from "./searchParams";
+
+describe("searchParams", () => {
+  describe("stateToSearchParams", () => {
+    it("can serialize table state to search params", () => {
+      const state: TableState = {
+        pagination: {
+          pageIndex: 1,
+          pageSize: 20,
+        },
+        sorting: [{ id: "name", desc: false }],
+      };
+      expect(stateToSearchParams(state).toString()).toEqual(
+        "limit=20&offset=1&sort.name=asc"
+      );
+    });
+  });
+  describe("searchParamsToState", () => {
+    it("can parse search params back to table state", () => {
+      expect(
+        searchParamsToState(
+          new URLSearchParams("limit=20&offset=0&sort.name=asc&sort.age=desc"),
+          {
+            pagination: {
+              pageIndex: 1,
+              pageSize: 5,
+            },
+            sorting: [],
+          }
+        )
+      ).toEqual({
+        pagination: {
+          pageIndex: 0,
+          pageSize: 20,
+        },
+        sorting: [
+          { id: "name", desc: false },
+          { id: "age", desc: true },
+        ],
+      });
+    });
+  });
+});
diff --git a/airflow/www/static/js/components/NewTable/searchParams.ts 
b/airflow/www/static/js/components/NewTable/searchParams.ts
new file mode 100644
index 0000000000..ad01c50b9e
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/searchParams.ts
@@ -0,0 +1,126 @@
+/*!
+ * 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 type { SortingState } from "@tanstack/react-table";
+import { isEqual } from "lodash";
+
+import type { TableState } from "./NewTable";
+
+export const LIMIT_PARAM = "limit";
+export const OFFSET_PARAM = "offset";
+export const SORT_PARAM = "sort.";
+
+export const stateToSearchParams = (
+  state: TableState,
+  defaultTableState?: TableState
+): URLSearchParams => {
+  const queryParams = new URLSearchParams(window.location.search);
+  if (isEqual(state.pagination, defaultTableState?.pagination)) {
+    queryParams.delete(LIMIT_PARAM);
+    queryParams.delete(OFFSET_PARAM);
+  } else if (state.pagination) {
+    queryParams.set(LIMIT_PARAM, `${state.pagination.pageSize}`);
+    queryParams.set(OFFSET_PARAM, `${state.pagination.pageIndex}`);
+  }
+
+  if (isEqual(state.sorting, defaultTableState?.sorting)) {
+    state.sorting.forEach(({ id }) => {
+      queryParams.delete(`${SORT_PARAM}${id}`);
+    });
+  } else if (state.sorting) {
+    state.sorting.forEach(({ id, desc }) => {
+      queryParams.set(`${SORT_PARAM}${id}`, desc ? "desc" : "asc");
+    });
+  }
+
+  return queryParams;
+};
+
+export const searchParamsToState = (
+  searchParams: URLSearchParams,
+  defaultState: TableState
+) => {
+  let urlState: Partial<TableState> = {};
+  const pageIndex = searchParams.get(OFFSET_PARAM);
+  const pageSize = searchParams.get(LIMIT_PARAM);
+  if (pageIndex && pageSize) {
+    urlState = {
+      ...urlState,
+      pagination: {
+        pageIndex: parseInt(pageIndex, 10),
+        pageSize: parseInt(pageSize, 10),
+      },
+    };
+  }
+  const sorting: SortingState = [];
+  searchParams.forEach((v, k) => {
+    if (k.startsWith(SORT_PARAM)) {
+      sorting.push({
+        id: k.replace(SORT_PARAM, ""),
+        desc: v === "desc",
+      });
+    }
+  });
+  if (sorting.length) {
+    urlState = {
+      ...urlState,
+      sorting,
+    };
+  }
+  return {
+    ...defaultState,
+    ...urlState,
+  };
+};
+
+interface CoreServiceQueryParams {
+  offset?: number;
+  limit?: number;
+  sorts?: any[];
+  search?: string;
+}
+
+/**
+ * helper function that build query params that core services rely on
+ * @param state
+ * @returns
+ */
+export const buildQueryParams = (
+  state?: Partial<TableState>
+): CoreServiceQueryParams => {
+  let queryParams = {};
+  if (state?.pagination) {
+    const { pageIndex, pageSize } = state.pagination;
+    queryParams = {
+      ...queryParams,
+      limit: pageSize,
+      offset: pageIndex * pageSize,
+    };
+  }
+  if (state?.sorting) {
+    const sorts = state.sorting.map(
+      ({ id, desc }) => `${id}:${desc ? "desc" : "asc"}`
+    ) as any[];
+    queryParams = {
+      ...queryParams,
+      sorts,
+    };
+  }
+  return queryParams;
+};
diff --git a/airflow/www/static/js/components/NewTable/useTableUrlState.ts 
b/airflow/www/static/js/components/NewTable/useTableUrlState.ts
new file mode 100644
index 0000000000..8f1caeb406
--- /dev/null
+++ b/airflow/www/static/js/components/NewTable/useTableUrlState.ts
@@ -0,0 +1,69 @@
+/*!
+ * 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 { useCallback, useMemo } from "react";
+
+import { useSearchParams } from "react-router-dom";
+import type { TableState } from "./NewTable";
+import {
+  buildQueryParams,
+  searchParamsToState,
+  stateToSearchParams,
+} from "./searchParams";
+
+export const defaultTableState: TableState = {
+  pagination: {
+    pageIndex: 0,
+    pageSize: 25,
+  },
+  sorting: [],
+};
+
+export const useTableURLState = (defaultState?: Partial<TableState>) => {
+  const [searchParams, setSearchParams] = useSearchParams();
+
+  const handleStateChange = useCallback(
+    (state: TableState) => {
+      setSearchParams(stateToSearchParams(state, defaultTableState), {
+        replace: true,
+      });
+    },
+    [setSearchParams]
+  );
+
+  const tableURLState = useMemo(
+    () =>
+      searchParamsToState(searchParams, {
+        ...defaultTableState,
+        ...defaultState,
+      }),
+    [searchParams, defaultState]
+  );
+
+  const requestParams = useMemo(
+    () => buildQueryParams(tableURLState),
+    [tableURLState]
+  );
+
+  return {
+    tableURLState,
+    requestParams,
+    setTableURLState: handleStateChange,
+  };
+};
diff --git a/airflow/www/static/js/dag/details/AuditLog.tsx 
b/airflow/www/static/js/dag/details/AuditLog.tsx
index 0e4a0f0474..15f0900741 100644
--- a/airflow/www/static/js/dag/details/AuditLog.tsx
+++ b/airflow/www/static/js/dag/details/AuditLog.tsx
@@ -19,7 +19,7 @@
 
 /* global moment */
 
-import React, { useMemo, useRef, useState } from "react";
+import React, { useMemo, useRef } from "react";
 import {
   Box,
   Flex,
@@ -29,14 +29,17 @@ import {
   Input,
   HStack,
 } from "@chakra-ui/react";
-import type { SortingRule } from "react-table";
+import { createColumnHelper } from "@tanstack/react-table";
 import { snakeCase } from "lodash";
 
-import { CodeCell, Table, TimeCell } from "src/components/Table";
 import { useEventLogs } from "src/api";
 import { getMetaValue, useOffsetTop } from "src/utils";
 import type { DagRun } from "src/types";
 import LinkButton from "src/components/LinkButton";
+import type { EventLog } from "src/types/api-generated";
+import { NewTable } from "src/components/NewTable/NewTable";
+import { useTableURLState } from "src/components/NewTable/useTableUrlState";
+import { CodeCell, TimeCell } from "src/components/NewTable/NewCells";
 
 interface Props {
   taskId?: string;
@@ -45,57 +48,61 @@ interface Props {
 
 const dagId = getMetaValue("dag_id") || undefined;
 
+const columnHelper = createColumnHelper<EventLog>();
+
 const AuditLog = ({ taskId, run }: Props) => {
   const logRef = useRef<HTMLDivElement>(null);
   const offsetTop = useOffsetTop(logRef);
-  const limit = 25;
-  const [offset, setOffset] = useState(0);
-  const [sortBy, setSortBy] = useState<SortingRule<object>[]>([
-    { id: "when", desc: true },
-  ]);
+  const { tableURLState, setTableURLState } = useTableURLState({
+    sorting: [{ id: "when", desc: true }],
+  });
 
-  const sort = sortBy[0];
+  const sort = tableURLState.sorting[0];
   const orderBy = sort ? `${sort.desc ? "-" : ""}${snakeCase(sort.id)}` : "";
 
-  const { data, isLoading } = useEventLogs({
+  const { data, isLoading, isFetching } = useEventLogs({
     dagId,
     taskId,
     runId: run?.runId || undefined,
     before: run?.lastSchedulingDecision || undefined,
     after: run?.queuedAt || undefined,
     orderBy,
-    limit,
-    offset,
+    limit: tableURLState.pagination.pageSize,
+    offset:
+      tableURLState.pagination.pageIndex * tableURLState.pagination.pageSize,
   });
 
   const columns = useMemo(() => {
-    const when = {
-      Header: "When",
-      accessor: "when",
-      Cell: TimeCell,
-    };
-    const task = {
-      Header: "Task ID",
-      accessor: "taskId",
-    };
-    const runId = {
-      Header: "Run ID",
-      accessor: "runId",
-    };
-    const rest = [
-      {
-        Header: "Event",
-        accessor: "event",
-      },
-      {
-        Header: "Owner",
-        accessor: "owner",
-      },
-      {
-        Header: "Extra",
-        accessor: "extra",
-        Cell: CodeCell,
+    const when = columnHelper.accessor("when", {
+      header: "When",
+      cell: TimeCell,
+    });
+    const task = columnHelper.accessor("taskId", {
+      header: "Task Id",
+      meta: {
+        skeletonWidth: 20,
       },
+    });
+    const runId = columnHelper.accessor("runId", {
+      header: "Run Id",
+    });
+    const rest = [
+      columnHelper.accessor("event", {
+        header: "Event",
+        meta: {
+          skeletonWidth: 40,
+        },
+      }),
+      columnHelper.accessor("owner", {
+        header: "Owner",
+        meta: {
+          skeletonWidth: 20,
+        },
+      }),
+      columnHelper.accessor("extra", {
+        header: "Extra",
+        cell: CodeCell,
+      }),
     ];
     return [
       when,
@@ -106,7 +113,6 @@ const AuditLog = ({ taskId, run }: Props) => {
   }, [taskId, run]);
 
   const memoData = useMemo(() => data?.eventLogs, [data?.eventLogs]);
-  const memoSort = useMemo(() => sortBy, [sortBy]);
 
   return (
     <Box
@@ -162,21 +168,15 @@ const AuditLog = ({ taskId, run }: Props) => {
           <FormHelperText />
         </FormControl>
       </HStack>
-      <Table
+      <NewTable
+        key={`${taskId}-${run?.runId}`}
         data={memoData || []}
         columns={columns}
         isLoading={isLoading}
-        manualPagination={{
-          offset,
-          setOffset,
-          totalEntries: data?.totalEntries || 0,
-        }}
-        manualSort={{
-          setSortBy,
-          sortBy,
-          initialSortBy: memoSort,
-        }}
-        pageSize={limit}
+        isFetching={isFetching}
+        initialState={tableURLState}
+        onStateChange={setTableURLState}
+        resultCount={data?.totalEntries}
       />
     </Box>
   );
diff --git a/airflow/www/static/js/dag/useSelection.ts 
b/airflow/www/static/js/dag/useSelection.ts
index 7464c254ae..00ead40c52 100644
--- a/airflow/www/static/js/dag/useSelection.ts
+++ b/airflow/www/static/js/dag/useSelection.ts
@@ -19,6 +19,11 @@
 
 import { createContext, useContext } from "react";
 import { useSearchParams } from "react-router-dom";
+import {
+  LIMIT_PARAM,
+  OFFSET_PARAM,
+  SORT_PARAM,
+} from "src/components/NewTable/searchParams";
 
 export const RUN_ID = "dag_run_id";
 const TASK_ID = "task_id";
@@ -40,9 +45,18 @@ const useSelection = () => {
 
   // Clear selection, but keep other search params
   const clearSelection = () => {
+    const params = new URLSearchParams(window.location.search);
     searchParams.delete(RUN_ID);
     searchParams.delete(TASK_ID);
     searchParams.delete(MAP_INDEX);
+    [...searchParams.keys()].forEach((key) => {
+      if (
+        key === OFFSET_PARAM ||
+        key === LIMIT_PARAM ||
+        key.includes(SORT_PARAM)
+      )
+        params.delete(key);
+    });
     setSearchParams(searchParams);
   };
 
@@ -50,6 +64,15 @@ const useSelection = () => {
     // Check the window, in case params have changed since this hook was loaded
     const params = new URLSearchParams(window.location.search);
 
+    [...searchParams.keys()].forEach((key) => {
+      if (
+        key === OFFSET_PARAM ||
+        key === LIMIT_PARAM ||
+        key.includes(SORT_PARAM)
+      )
+        params.delete(key);
+    });
+
     if (runId) params.set(RUN_ID, runId);
     else params.delete(RUN_ID);
 
diff --git a/airflow/www/yarn.lock b/airflow/www/yarn.lock
index 25693116bb..62dd14d358 100644
--- a/airflow/www/yarn.lock
+++ b/airflow/www/yarn.lock
@@ -2905,6 +2905,18 @@
   dependencies:
     "@sinonjs/commons" "^1.7.0"
 
+"@tanstack/react-table@^8.13.2":
+  version "8.13.2"
+  resolved 
"https://registry.yarnpkg.com/@tanstack/react-table/-/react-table-8.13.2.tgz#a3aa737ae464abc651f68daa7e82dca17813606c";
+  integrity 
sha512-b6mR3mYkjRtJ443QZh9sc7CvGTce81J35F/XMr0OoWbx0KIM7TTTdyNP2XKObvkLpYnLpCrYDwI3CZnLezWvpg==
+  dependencies:
+    "@tanstack/table-core" "8.13.2"
+
+"@tanstack/[email protected]":
+  version "8.13.2"
+  resolved 
"https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.13.2.tgz#2512574dd3d20dc94b7db1f9f48090f0c18b5c85";
+  integrity 
sha512-/2saD1lWBUV6/uNAwrsg2tw58uvMJ07bO2F1IWMxjFRkJiXKQRuc3Oq2aufeobD3873+4oIM/DRySIw7+QsPPw==
+
 "@testing-library/dom@^8.5.0":
   version "8.13.0"
   resolved 
"https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.13.0.tgz#bc00bdd64c7d8b40841e27a70211399ad3af46f5";

Reply via email to