diff --git a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx index 9725a4ef9e31e..e2f5b274a4c10 100644 --- a/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx +++ b/airflow-core/src/airflow/ui/src/components/ActionAccordion/ActionAccordion.tsx @@ -63,7 +63,7 @@ const ActionAccordion = ({ affectedTasks, note, setNote }: Props) => { columns={columns} data={affectedTasks.task_instances} displayMode="table" - modelName={translate("common:taskInstance_other")} + modelName="common:taskInstance" noRowsMessage={translate("dags:runAndTaskActions.affectedTasks.noItemsFound")} total={affectedTasks.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx index c5b5c5adf8c3d..18156d7081155 100644 --- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx +++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvents.tsx @@ -113,7 +113,7 @@ export const AssetEvents = ({ displayMode="card" initialState={tableUrlState} isLoading={isLoading} - modelName={translate("common:assetEvent_one")} + modelName="common:assetEvent" noRowsMessage={translate("noAssetEvents")} onStateChange={setTableUrlState} skeletonCount={5} diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx index e38bcc2de8a89..a4713410f364b 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx +++ b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.test.tsx @@ -51,6 +51,7 @@ describe("DataTable", () => { columns={columns} data={data} initialState={{ pagination, sorting: [] }} + modelName="task" onStateChange={onStateChange} total={2} />, @@ -69,6 +70,7 @@ describe("DataTable", () => { columns={columns} data={[{ name: "John Doe" }]} initialState={{ pagination, sorting: [] }} + modelName="task" onStateChange={onStateChange} total={2} />, @@ -89,6 +91,7 @@ describe("DataTable", () => { pagination: { pageIndex: 0, pageSize: 10 }, sorting: [], }} + modelName="task" onStateChange={onStateChange} total={2} />, @@ -106,6 +109,7 @@ describe("DataTable", () => { columns={columns} data={data} initialState={{ pagination, sorting: [] }} + modelName="task" onStateChange={onStateChange} total={2} />, @@ -119,7 +123,7 @@ describe("DataTable", () => { }); it("when isLoading renders skeleton columns", () => { - render(, { + render(, { wrapper: ChakraWrapper, }); @@ -127,7 +131,7 @@ describe("DataTable", () => { }); it("still displays table if mode is card but there is no cardDef", () => { - render(, { + render(, { wrapper: ChakraWrapper, }); @@ -135,9 +139,12 @@ describe("DataTable", () => { }); it("displays cards if mode is card and there is cardDef", () => { - render(, { - wrapper: ChakraWrapper, - }); + render( + , + { + wrapper: ChakraWrapper, + }, + ); expect(screen.getByText("My name is John Doe.")).toBeInTheDocument(); }); @@ -150,6 +157,7 @@ describe("DataTable", () => { data={data} displayMode="card" isLoading + modelName="task" skeletonCount={5} />, { @@ -159,4 +167,86 @@ describe("DataTable", () => { expect(screen.getAllByTestId("skeleton")).toHaveLength(5); }); + + it("renders row count heading by default when total > 0", () => { + render( + , + { wrapper: ChakraWrapper }, + ); + + expect(screen.getByRole("heading")).toHaveTextContent("2 task"); + }); + + it("does not render row count heading when showRowCountHeading is false", () => { + render( + , + { wrapper: ChakraWrapper }, + ); + + expect(screen.queryByRole("heading")).toBeNull(); + }); + + it("uses translated zero-count model name in empty state", () => { + render( + , + { wrapper: ChakraWrapper }, + ); + + expect(screen.getByText(/noitemsFound/iu)).toBeInTheDocument(); + }); + + it("renders display toggle when showDisplayToggle and onDisplayToggleChange are provided", () => { + const handleToggle = vi.fn(); + + render( + , + { wrapper: ChakraWrapper }, + ); + + expect(screen.getByLabelText(/toggleTableView/iu)).toBeInTheDocument(); + }); + + it("does not render display toggle without showDisplayToggle", () => { + render( + , + { wrapper: ChakraWrapper }, + ); + + expect(screen.queryByLabelText(/toggleTableView/iu)).toBeNull(); + }); }); diff --git a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx index e776f8fd4c642..5ad64589b092f 100644 --- a/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx +++ b/airflow-core/src/airflow/ui/src/components/DataTable/DataTable.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { HStack, Text } from "@chakra-ui/react"; +import { Heading, HStack, Text } from "@chakra-ui/react"; import { getCoreRowModel, getExpandedRowModel, @@ -34,6 +34,7 @@ import { useTranslation } from "react-i18next"; import { CardList } from "src/components/DataTable/CardList"; import { TableList } from "src/components/DataTable/TableList"; +import { ToggleTableDisplay } from "src/components/DataTable/ToggleTableDisplay"; import { createSkeletonMock } from "src/components/DataTable/skeleton"; import type { CardDef, MetaColumn, TableState } from "src/components/DataTable/types"; import { ProgressBar, Pagination, Toaster } from "src/components/ui"; @@ -49,10 +50,13 @@ type DataTableProps = { readonly initialState?: TableState; readonly isFetching?: boolean; readonly isLoading?: boolean; - readonly modelName?: string; + readonly modelName: string; readonly noRowsMessage?: ReactNode; + readonly onDisplayToggleChange?: (mode: "card" | "table") => void; readonly onStateChange?: (state: TableState) => void; readonly renderSubComponent?: (props: { row: Row }) => React.ReactElement; + readonly showDisplayToggle?: boolean; + readonly showRowCountHeading?: boolean; readonly skeletonCount?: number; readonly total?: number; }; @@ -72,7 +76,10 @@ export const DataTable = ({ isLoading, modelName, noRowsMessage, + onDisplayToggleChange, onStateChange, + showDisplayToggle, + showRowCountHeading = true, skeletonCount = 10, total = 0, }: DataTableProps) => { @@ -142,11 +149,30 @@ export const DataTable = ({ // Default to show columns filter only if there are actually many columns displayed const showColumnsFilter = allowFiltering ?? columns.length > 5; + const translateModelName = useCallback( + (count: number) => translate(modelName, { count }), + [modelName, translate], + ); + const showRowCount = Boolean( + showRowCountHeading && !Boolean(isLoading) && !Boolean(isFetching) && total > 0, + ); + const noRowsModelName = translateModelName(0); + + const rowCountHeading = showRowCount ? ( + + {`${total} ${translateModelName(total)}`} + + ) : undefined; + return ( <> + {showDisplayToggle && onDisplayToggleChange ? ( + + ) : undefined} {errorMessage} + {rowCountHeading} {hasRows && display === "table" ? ( ) : undefined} @@ -155,7 +181,7 @@ export const DataTable = ({ ) : undefined} {!hasRows && !Boolean(isLoading) && ( - {noRowsMessage ?? translate("noItemsFound", { modelName })} + {noRowsMessage ?? translate("noItemsFound", { modelName: noRowsModelName })} )} {hasPagination ? ( diff --git a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx index 77a76c6f0eaa3..3f033cc8adcbb 100644 --- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx @@ -161,7 +161,6 @@ export const AssetsList = () => { {data?.total_entries} {translate("common:asset", { count: data?.total_entries })} - { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("common:asset_one")} + modelName="common:asset" onStateChange={setTableURLState} + showRowCountHeading={false} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx index d1144737aaae9..9c12e10a49e63 100644 --- a/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Configs/Configs.tsx @@ -67,7 +67,7 @@ export const Configs = () => { {translate("config.title")} {error === null ? ( - + ) : ( )} diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx index 6103b1292c911..595e89b46c4c6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx @@ -194,7 +194,7 @@ export const Connections = () => { initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} - modelName={translate("common:admin.Connections")} + modelName="admin:connections.connection" noRowsMessage={} onStateChange={setTableURLState} total={data?.total_entries ?? 0} diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx index fe77ed444b15b..c4ffbc8e20f68 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx @@ -130,7 +130,7 @@ export const Backfills = () => { data={data ? data.backfills : []} isFetching={isFetching} isLoading={isLoading} - modelName={translate("backfill_one")} + modelName="common:backfill" onStateChange={setTableURLState} total={data ? data.total_entries : 0} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx index e378b5c63ae3c..c74723ce775c6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Tasks/Tasks.tsx @@ -17,7 +17,6 @@ * under the License. */ import { Skeleton, Box } from "@chakra-ui/react"; -import { useTranslation } from "react-i18next"; import { useParams, useSearchParams } from "react-router-dom"; import { useTaskServiceGetTasks } from "openapi/queries"; @@ -40,7 +39,6 @@ const cardDef = (dagId: string): CardDef => ({ export const Tasks = () => { const { dagId = "" } = useParams(); const { MAPPED, NAME_PATTERN, OPERATOR, RETRIES, TRIGGER_RULE } = SearchParamsKeys; - const { t: translate } = useTranslation(); const [searchParams] = useSearchParams(); const selectedOperators = searchParams.getAll(OPERATOR); const selectedTriggerRules = searchParams.getAll(TRIGGER_RULE); @@ -100,7 +98,7 @@ export const Tasks = () => { displayMode="card" isFetching={isFetching} isLoading={isLoading} - modelName={translate("task_one")} + modelName="common:task" total={data ? data.total_entries : 0} /> diff --git a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx index b837cbee7c5e3..19ad60613a2aa 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagRuns.tsx @@ -248,7 +248,7 @@ export const DagRuns = () => { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("common:dagRun_other")} + modelName="common:dagRun" onStateChange={setTableURLState} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx index dc4789dbdf173..c17b39ec85f92 100644 --- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx @@ -37,7 +37,6 @@ import DeleteDagButton from "src/components/DagActions/DeleteDagButton"; import { FavoriteDagButton } from "src/components/DagActions/FavoriteDagButton"; import DagRunInfo from "src/components/DagRunInfo"; import { DataTable } from "src/components/DataTable"; -import { ToggleTableDisplay } from "src/components/DataTable/ToggleTableDisplay"; import type { CardDef } from "src/components/DataTable/types"; import { useTableURLState } from "src/components/DataTable/useTableUrlState"; import { ErrorAlert } from "src/components/ErrorAlert"; @@ -284,6 +283,8 @@ export const DagsList = () => { }); }; + const totalEntries = data?.total_entries ?? 0; + return ( @@ -296,7 +297,7 @@ export const DagsList = () => { - {`${data?.total_entries ?? 0} ${translate("dag", { count: data?.total_entries ?? 0 })}`} + {`${totalEntries} ${translate("dag", { count: totalEntries })}`} @@ -305,7 +306,6 @@ export const DagsList = () => { ) : undefined} - { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("dag_one")} + modelName="common:dag" + onDisplayToggleChange={setDisplay} onStateChange={setTableURLState} + showDisplayToggle + showRowCountHeading={false} skeletonCount={display === "card" ? 5 : undefined} - total={data?.total_entries ?? 0} + total={totalEntries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx index 4b4bb50e2af76..fca0d6e3d94ca 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx @@ -232,8 +232,9 @@ export const Events = () => { initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} - modelName={translate("auditLog.columns.event")} + modelName="browse:auditLog.columns.event" onStateChange={setTableURLState} + showRowCountHeading={false} skeletonCount={undefined} total={data?.total_entries ?? 0} /> diff --git a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx index 455d149888033..7733ac675c4b3 100644 --- a/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx +++ b/airflow-core/src/airflow/ui/src/pages/HITLTaskInstances/HITLTaskInstances.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Heading, Link, VStack } from "@chakra-ui/react"; +import { Link, VStack } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; import type { TFunction } from "i18next"; import { useTranslation } from "react-i18next"; @@ -248,11 +248,6 @@ export const HITLTaskInstances = () => { return ( - {!Boolean(dagId) && !Boolean(runId) && !Boolean(taskId) ? ( - - {data?.total_entries} {translate("requiredAction", { count: data?.total_entries })} - - ) : undefined} { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("requiredAction_other")} + modelName="hitl:requiredAction" onStateChange={setTableURLState} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Plugins.tsx b/airflow-core/src/airflow/ui/src/pages/Plugins.tsx index 085cf5a349603..e14bb772aedc4 100644 --- a/airflow-core/src/airflow/ui/src/pages/Plugins.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Plugins.tsx @@ -52,7 +52,8 @@ export const Plugins = () => { columns={columns} data={data?.plugins ?? []} errorMessage={} - modelName={translate("common:admin.Plugins")} + modelName="common:admin.Plugins" + showRowCountHeading={false} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx b/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx index 132cb3ed2986e..234e7292a3809 100644 --- a/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Pools/Pools.tsx @@ -127,7 +127,7 @@ export const Pools = () => { displayMode="card" initialState={tableURLState} isLoading={isLoading} - modelName={translate("common:admin.Pools")} + modelName="admin:pools.pool" noRowsMessage={translate("pools.noPoolsFound")} onStateChange={setTableURLState} total={data ? data.total_entries : 0} diff --git a/airflow-core/src/airflow/ui/src/pages/Providers.tsx b/airflow-core/src/airflow/ui/src/pages/Providers.tsx index 890f41acd92d5..e8e762fcb9bc6 100644 --- a/airflow-core/src/airflow/ui/src/pages/Providers.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Providers.tsx @@ -96,8 +96,9 @@ export const Providers = () => { data={data?.providers ?? []} errorMessage={} initialState={tableURLState} - modelName={translate("common:admin.Providers")} + modelName="common:admin.Providers" onStateChange={setTableURLState} + showRowCountHeading={false} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx index f97c941c70ef8..c45bf30d38a20 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstances/TaskInstances.tsx @@ -307,7 +307,7 @@ export const TaskInstances = () => { errorMessage={} initialState={tableURLState} isLoading={isLoading} - modelName={translate("common:taskInstance_other")} + modelName="common:taskInstance" onStateChange={setTableURLState} total={data?.total_entries} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx index 2c9c461b0ccf9..cd3d9d1ad62f5 100644 --- a/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Variables/Variables.tsx @@ -180,7 +180,7 @@ export const Variables = () => { initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} - modelName={translate("common:admin.Variables")} + modelName="admin:variables.variable" noRowsMessage={translate("variables.noRowsMessage")} onStateChange={setTableURLState} total={data?.total_entries ?? 0} diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx index f469c9f69abf8..3c7315e3662cd 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx @@ -221,8 +221,9 @@ export const XCom = () => { initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} - modelName={translate("xcom.title")} + modelName="browse:xcom.title" onStateChange={setTableURLState} + showRowCountHeading={false} skeletonCount={undefined} total={data ? data.total_entries : 0} />