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}
/>