Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,10 @@
"@typescript-eslint/eslint-plugin": "^8.49.0",
"@typescript-eslint/parser": "^8.49.0",
"@typescript-eslint/utils": "^8.49.0",
"@vitejs/plugin-react": "^5.1.2",
"@vitejs/plugin-react-swc": "^4.0.1",
"@vitest/coverage-v8": "^3.2.4",
"babel-plugin-react-compiler": "^1.0.0",
"eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-i18next": "^6.1.3",
Expand All @@ -102,7 +104,7 @@
"eslint-plugin-perfectionist": "^4.12.3",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.20",
"eslint-plugin-unicorn": "^55.0.0",
"globals": "^15.15.0",
Expand Down
368 changes: 354 additions & 14 deletions airflow-core/src/airflow/ui/pnpm-lock.yaml

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions airflow-core/src/airflow/ui/rules/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ export const reactRules = /** @type {const} @satisfies {FlatConfig.Config} */ ({
*/
[`${reactHooksNamespace}/rules-of-hooks`]: ERROR,

// https://github.com/facebook/react/blob/3640f38/compiler/packages/babel-plugin-react-compiler/src/CompilerError.ts#L807-L1111
[`${reactHooksNamespace}/todo`]: ERROR,

/**
* Prevent usage of button elements without an explicit type attribute.
*
Expand Down Expand Up @@ -471,13 +474,6 @@ export const reactRules = /** @type {const} @satisfies {FlatConfig.Config} */ ({
*/
[`${reactNamespace}/jsx-no-comment-textnodes`]: ERROR,

/**
* Prevent react contexts from taking non-stable values.
*
* @see [react/jsx-no-constructed-context-values](https://github.com/jsx-eslint/eslint-plugin-react/blob/HEAD/docs/rules/jsx-no-constructed-context-values.md)
*/
[`${reactNamespace}/jsx-no-constructed-context-values`]: ERROR,

/**
* Disallow problematic leaked values from being rendered.
*
Expand Down Expand Up @@ -619,5 +615,5 @@ export const reactRules = /** @type {const} @satisfies {FlatConfig.Config} */ ({
*/
[`${reactRefreshNamespace}/only-export-components`]: [WARN, { allowConstantExport: true }],
},
settings: { react: { version: "18" } },
settings: { react: { version: "19" } },
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
*/
import { Box, Editable, Text, VStack } from "@chakra-ui/react";
import type { ChangeEvent } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";

import type { DAGRunResponse, TaskInstanceCollectionResponse } from "openapi/requests/types.gen";
Expand All @@ -40,7 +39,7 @@ const ActionAccordion = ({ affectedTasks, note, setNote }: Props) => {
const showTaskSection = affectedTasks !== undefined;
const { t: translate } = useTranslation();

const columns = useMemo(() => getColumns(translate), [translate]);
const columns = getColumns(translate);

return (
<Accordion.Root
Expand Down
10 changes: 5 additions & 5 deletions airflow-core/src/airflow/ui/src/components/BasicTooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
*/
import { Box, Portal } from "@chakra-ui/react";
import type { ReactElement, ReactNode } from "react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";

type Props = {
readonly children: ReactNode;
Expand All @@ -34,23 +34,23 @@ export const BasicTooltip = ({ children, content }: Props): ReactElement => {
const [showOnTop, setShowOnTop] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout | null>(null);

const handleMouseEnter = useCallback(() => {
const handleMouseEnter = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
timeoutRef.current = setTimeout(() => {
setIsOpen(true);
}, 500);
}, []);
};

const handleMouseLeave = useCallback(() => {
const handleMouseLeave = () => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
// eslint-disable-next-line unicorn/no-null
timeoutRef.current = null;
}
setIsOpen(false);
}, []);
};

// Calculate position based on actual tooltip height before paint
useLayoutEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
* under the License.
*/
import { Button, Icon, Spinner, Text, VStack } from "@chakra-ui/react";
import { useCallback, useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { GoAlertFill } from "react-icons/go";

Expand Down Expand Up @@ -73,13 +73,6 @@ const ClearTaskInstanceConfirmationDialog = ({

const [isReady, setIsReady] = useState(false);

const handleConfirm = useCallback(() => {
if (onConfirm) {
onConfirm();
}
onClose();
}, [onConfirm, onClose]);

const taskInstances = data?.task_instances ?? [];
const [firstInstance] = taskInstances;
const taskCurrentState = firstInstance?.state;
Expand All @@ -89,12 +82,13 @@ const ClearTaskInstanceConfirmationDialog = ({
const isInTriggeringState = taskCurrentState === "queued" || taskCurrentState === "scheduled";

if (!preventRunningTask || !isInTriggeringState) {
handleConfirm();
onConfirm?.();
onClose();
} else {
setIsReady(true);
}
}
}, [isFetching, data, open, handleConfirm, taskCurrentState, preventRunningTask]);
}, [isFetching, data, open, taskCurrentState, preventRunningTask, onConfirm, onClose]);

return (
<Dialog.Root lazyMount onOpenChange={onClose} open={open}>
Expand Down
28 changes: 11 additions & 17 deletions airflow-core/src/airflow/ui/src/components/DagVersionSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
* under the License.
*/
import { createListCollection, Flex, Select, type SelectValueChangeDetails, Text } from "@chakra-ui/react";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import { useParams, useSearchParams } from "react-router-dom";

Expand All @@ -40,22 +39,17 @@ export const DagVersionSelect = ({ showLabel = true }: { readonly showLabel?: bo
const [searchParams, setSearchParams] = useSearchParams();
const selectedVersionNumber = useSelectedVersion();
const selectedVersion = data?.dag_versions.find((dv) => dv.version_number === selectedVersionNumber);
const versionOptions = useMemo(
() =>
createListCollection({
items: (data?.dag_versions ?? []).map((dv) => ({ value: dv.version_number, version: dv })),
}),
[data],
);
const handleStateChange = useCallback(
({ items }: SelectValueChangeDetails<VersionSelected>) => {
if (items[0]) {
searchParams.set(SearchParamsKeys.VERSION_NUMBER, items[0].value.toString());
setSearchParams(searchParams);
}
},
[searchParams, setSearchParams],
);

const versionOptions = createListCollection({
items: (data?.dag_versions ?? []).map((dv) => ({ value: dv.version_number, version: dv })),
});

const handleStateChange = ({ items }: SelectValueChangeDetails<VersionSelected>) => {
if (items[0]) {
searchParams.set(SearchParamsKeys.VERSION_NUMBER, items[0].value.toString());
setSearchParams(searchParams);
}
};

return (
<Select.Root
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,19 @@
* under the License.
*/
import { Box, SimpleGrid, Skeleton } from "@chakra-ui/react";
import { flexRender, type Table as TanStackTable } from "@tanstack/react-table";
import { flexRender, type Row } from "@tanstack/react-table";

import type { CardDef } from "./types";

type DataTableProps<TData> = {
readonly cardDef: CardDef<TData>;
readonly isLoading?: boolean;
readonly table: TanStackTable<TData>;
readonly rows: Array<Row<TData>>;
};

export const CardList = <TData,>({ cardDef, isLoading, table }: DataTableProps<TData>) => (
export const CardList = <TData,>({ cardDef, isLoading, rows }: DataTableProps<TData>) => (
<SimpleGrid data-testid="card-list" {...{ column: { base: 1 }, gap: 2, ...cardDef.gridProps }}>
{table.getRowModel().rows.map((row) => (
{rows.map((row) => (
<Box key={row.id}>
{Boolean(isLoading) &&
(cardDef.meta?.customSkeleton ?? (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import {
type Table as TanStackTable,
type Updater,
} from "@tanstack/react-table";
import React, { type ReactNode, useCallback, useRef, useState } from "react";
import React, { type ReactNode, useRef, useState, useCallback } from "react";
import { useTranslation } from "react-i18next";

import { CardList } from "src/components/DataTable/CardList";
Expand Down Expand Up @@ -76,10 +76,13 @@ export const DataTable = <TData,>({
skeletonCount = 10,
total = 0,
}: DataTableProps<TData>) => {
"use no memo"; // remove if https://github.com/TanStack/table/issues/5567 is resolved

const { t: translate } = useTranslation(["common"]);
const ref = useRef<{ tableRef: TanStackTable<TData> | undefined }>({
tableRef: undefined,
});

const handleStateChange = useCallback<OnChangeFn<ReactTableState>>(
(updater: Updater<ReactTableState>) => {
if (ref.current.tableRef && onStateChange) {
Expand All @@ -98,6 +101,7 @@ export const DataTable = <TData,>({
},
[onStateChange],
);

const [columnVisibility, setColumnVisibility] = useState<VisibilityState>(
initialState?.columnVisibility ?? {},
);
Expand Down Expand Up @@ -125,12 +129,15 @@ export const DataTable = <TData,>({

const { rows } = table.getRowModel();

const rowCount = table.getRowCount();
const { setPageIndex } = table;

const { pagination } = table.getState();
const { pageIndex, pageSize } = pagination;

const display = displayMode === "card" && Boolean(cardDef) ? "card" : "table";
const hasRows = rows.length > 0;
const hasPagination =
initialState?.pagination !== undefined &&
(table.getState().pagination.pageIndex !== 0 ||
(table.getState().pagination.pageIndex === 0 && rows.length !== total));
const hasPagination = initialState?.pagination !== undefined && (pageIndex !== 0 || rows.length !== total);

// Default to show columns filter only if there are actually many columns displayed
const showColumnsFilter = allowFiltering ?? columns.length > 5;
Expand All @@ -144,7 +151,7 @@ export const DataTable = <TData,>({
<TableList allowFiltering={showColumnsFilter} table={table} />
) : undefined}
{hasRows && display === "card" && cardDef !== undefined ? (
<CardList cardDef={cardDef} isLoading={isLoading} table={table} />
<CardList cardDef={cardDef} isLoading={isLoading} rows={rows} />
) : undefined}
{!hasRows && !Boolean(isLoading) && (
<Text as="div" pl={4} pt={1}>
Expand All @@ -153,11 +160,11 @@ export const DataTable = <TData,>({
)}
{hasPagination ? (
<Pagination.Root
count={table.getRowCount()}
count={rowCount}
my={2}
onPageChange={(page) => table.setPageIndex(page.page - 1)}
page={table.getState().pagination.pageIndex + 1}
pageSize={table.getState().pagination.pageSize}
onPageChange={(page) => setPageIndex(page.page - 1)}
page={pageIndex + 1}
pageSize={pageSize}
siblingCount={1}
>
<HStack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ type Props<TData> = {
};

const FilterMenuButton = <TData,>({ table }: Props<TData>) => {
"use no memo"; // remove if https://github.com/TanStack/table/issues/5567 is resolved

const { t: translate } = useTranslation("common");
const filterLabel = translate("table.filterColumns");

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type DataTableProps<TData> = {
};

export const TableList = <TData,>({ allowFiltering, renderSubComponent, table }: DataTableProps<TData>) => {
"use no memo"; // remove if https://github.com/TanStack/table/issues/5567 is resolved
const { t: translate } = useTranslation("components");

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { useState, useCallback, useMemo } from "react";
import { useState } from "react";

type UseRowSelectionProps<T> = {
data?: Array<T>;
Expand All @@ -33,7 +33,7 @@ export type GetColumnsParams = {
export const useRowSelection = <T>({ data = [], getKey }: UseRowSelectionProps<T>) => {
const [selectedRows, setSelectedRows] = useState<Map<string, boolean>>(new Map());

const handleRowSelect = useCallback((key: string, isChecked: boolean) => {
const handleRowSelect = (key: string, isChecked: boolean) => {
setSelectedRows((prev) => {
const isAlreadySelected = prev.has(key);

Expand All @@ -49,33 +49,27 @@ export const useRowSelection = <T>({ data = [], getKey }: UseRowSelectionProps<T

return prev;
});
}, []);
};

const handleSelectAll = useCallback(
(isChecked: boolean) => {
setSelectedRows((prev) => {
const newMap = new Map(prev);
const handleSelectAll = (isChecked: boolean) => {
setSelectedRows((prev) => {
const newMap = new Map(prev);

if (isChecked) {
data.forEach((item) => newMap.set(getKey(item), true));
} else {
data.forEach((item) => newMap.delete(getKey(item)));
}
if (isChecked) {
data.forEach((item) => newMap.set(getKey(item), true));
} else {
data.forEach((item) => newMap.delete(getKey(item)));
}

return newMap;
});
},
[data, getKey],
);
return newMap;
});
};

const allRowsSelected = useMemo(
() => data.length > 0 && data.every((item) => selectedRows.has(getKey(item))),
[data, selectedRows, getKey],
);
const allRowsSelected = data.length > 0 && data.every((item) => selectedRows.has(getKey(item)));

const clearSelections = useCallback(() => {
const clearSelections = () => {
setSelectedRows(new Map());
}, []);
};

return {
allRowsSelected,
Expand Down
Loading
Loading