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 231c29c1d42 Migrate dags list plaintext to translate files (#51181)
231c29c1d42 is described below
commit 231c29c1d4235909d99ace1daab67068fdd3fe54
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu May 29 12:30:42 2025 -0400
Migrate dags list plaintext to translate files (#51181)
* Migrate dags list plaintext to translate files
* Update SearchBar, DagOwners and fix asset schedule
* Fix test
* Alphabetically sort json
---
.../src/components/DagActions/DeleteDagButton.tsx | 11 +-
.../src/components/DataTable/FilterMenuButton.tsx | 76 +++---
.../src/airflow/ui/src/components/DeleteDialog.tsx | 62 ++---
.../airflow/ui/src/components/SearchBar.test.tsx | 2 +-
.../src/airflow/ui/src/components/SearchBar.tsx | 7 +-
.../src/components/TriggerDag/TriggerDAGButton.tsx | 6 +-
.../src/airflow/ui/src/constants/sortParams.ts | 56 ++--
airflow-core/src/airflow/ui/src/i18n/config.ts | 4 +-
.../src/airflow/ui/src/i18n/locales/en/common.json | 23 +-
.../src/airflow/ui/src/i18n/locales/en/dags.json | 55 ++++
.../src/airflow/ui/src/layouts/DagsLayout.tsx | 27 +-
.../ui/src/pages/DagsList/AssetSchedule.tsx | 5 +-
.../src/airflow/ui/src/pages/DagsList/DagCard.tsx | 8 +-
.../airflow/ui/src/pages/DagsList/DagOwners.tsx | 6 +-
.../airflow/ui/src/pages/DagsList/DagsFilters.tsx | 284 ---------------------
.../src/pages/DagsList/DagsFilters/DagsFilters.tsx | 181 +++++++++++++
.../pages/DagsList/DagsFilters/PausedFilter.tsx | 59 +++++
.../DagsList/DagsFilters/ResetButton.tsx} | 34 ++-
.../pages/DagsList/DagsFilters/StateFilters.tsx | 70 +++++
.../src/pages/DagsList/DagsFilters/TagFilter.tsx | 101 ++++++++
.../DagsList/DagsFilters/index.ts} | 17 +-
.../src/airflow/ui/src/pages/DagsList/DagsList.tsx | 37 +--
.../airflow/ui/src/pages/DagsList/RecentRuns.tsx | 17 +-
.../src/airflow/ui/src/pages/DagsList/Schedule.tsx | 10 +-
.../airflow/ui/src/pages/DagsList/SortSelect.tsx | 48 ++--
25 files changed, 730 insertions(+), 476 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
b/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
index 28645696b15..afddb68436f 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/DeleteDagButton.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import { useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { FiTrash2 } from "react-icons/fi";
import { useNavigate } from "react-router-dom";
@@ -33,7 +34,7 @@ type DeleteDagButtonProps = {
const DeleteDagButton = ({ dagDisplayName, dagId, withText = true }:
DeleteDagButtonProps) => {
const { onClose, onOpen, open } = useDisclosure();
const navigate = useNavigate();
-
+ const { t: translate } = useTranslation("dags");
const { isPending, mutate: deleteDag } = useDeleteDag({
dagId,
onSuccessConfirm: () => {
@@ -45,11 +46,11 @@ const DeleteDagButton = ({ dagDisplayName, dagId, withText
= true }: DeleteDagBu
return (
<>
<ActionButton
- actionName="Delete DAG"
+ actionName={translate("actions.delete")}
colorPalette="red"
icon={<FiTrash2 />}
onClick={onOpen}
- text="Delete DAG"
+ text={translate("actions.delete")}
variant="solid"
withText={withText}
/>
@@ -60,8 +61,8 @@ const DeleteDagButton = ({ dagDisplayName, dagId, withText =
true }: DeleteDagBu
onDelete={() => deleteDag({ dagId })}
open={open}
resourceName={dagDisplayName}
- title="Delete DAG"
- warningText="This will remove all metadata related to the DAG,
including DAG Runs and Tasks."
+ title={translate("actions.delete")}
+ warningText={translate("actions.deleteDagWarning")}
/>
</>
);
diff --git
a/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
b/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
index 6b98ab0df63..189e177200f 100644
--- a/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DataTable/FilterMenuButton.tsx
@@ -18,6 +18,7 @@
*/
import { IconButton } from "@chakra-ui/react";
import { flexRender, type Header, type Table } from "@tanstack/react-table";
+import { useTranslation } from "react-i18next";
import { MdFilterList } from "react-icons/md";
import { Menu } from "src/components/ui";
@@ -27,44 +28,43 @@ type Props<TData> = {
readonly table: Table<TData>;
};
-const FilterMenuButton = <TData,>({ table }: Props<TData>) => (
- <Menu.Root closeOnSelect={false} positioning={{ placement: "bottom" }}>
- <Menu.Trigger asChild>
- <IconButton
- aria-label="Filter table columns"
- margin={1}
- padding={0}
- title="Filter table columns"
- variant="ghost"
- >
- <MdFilterList size="1" />
- </IconButton>
- </Menu.Trigger>
- <Menu.Content>
- {table.getAllLeafColumns().map((column) => {
- const text = flexRender(column.columnDef.header, {
- column,
- header: { column } as Header<TData, unknown>,
- table,
- });
+const FilterMenuButton = <TData,>({ table }: Props<TData>) => {
+ const { t: translate } = useTranslation("common");
+ const filterLabel = translate("table.filterColumns");
- return text?.toString ? (
- <Menu.Item asChild key={column.id} value={column.id}>
- <Checkbox
- checked={column.getIsVisible()}
- // At least one item needs to be visible
- disabled={table.getVisibleFlatColumns().length < 2 &&
column.getIsVisible()}
- onChange={() => {
- column.toggleVisibility();
- }}
- >
- {text}
- </Checkbox>
- </Menu.Item>
- ) : undefined;
- })}
- </Menu.Content>
- </Menu.Root>
-);
+ return (
+ <Menu.Root closeOnSelect={false} positioning={{ placement: "bottom" }}>
+ <Menu.Trigger asChild>
+ <IconButton aria-label={filterLabel} margin={1} padding={0}
title={filterLabel} variant="ghost">
+ <MdFilterList size="1" />
+ </IconButton>
+ </Menu.Trigger>
+ <Menu.Content>
+ {table.getAllLeafColumns().map((column) => {
+ const text = flexRender(column.columnDef.header, {
+ column,
+ header: { column } as Header<TData, unknown>,
+ table,
+ });
+
+ return text?.toString ? (
+ <Menu.Item asChild key={column.id} value={column.id}>
+ <Checkbox
+ checked={column.getIsVisible()}
+ // At least one item needs to be visible
+ disabled={table.getVisibleFlatColumns().length < 2 &&
column.getIsVisible()}
+ onChange={() => {
+ column.toggleVisibility();
+ }}
+ >
+ {text}
+ </Checkbox>
+ </Menu.Item>
+ ) : undefined;
+ })}
+ </Menu.Content>
+ </Menu.Root>
+ );
+};
export default FilterMenuButton;
diff --git a/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
b/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
index 6d8ac6b56fc..b77262838bf 100644
--- a/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DeleteDialog.tsx
@@ -18,6 +18,7 @@
*/
import { Text, Heading, HStack } from "@chakra-ui/react";
import React from "react";
+import { useTranslation } from "react-i18next";
import { FiTrash2 } from "react-icons/fi";
import { Button, Dialog } from "src/components/ui";
@@ -34,7 +35,7 @@ type DeleteDialogProps = {
};
const DeleteDialog: React.FC<DeleteDialogProps> = ({
- deleteButtonText = "Delete",
+ deleteButtonText,
isDeleting,
onClose,
onDelete,
@@ -42,33 +43,36 @@ const DeleteDialog: React.FC<DeleteDialogProps> = ({
resourceName,
title,
warningText,
-}) => (
- <Dialog.Root lazyMount onOpenChange={onClose} open={open} size="md"
unmountOnExit>
- <Dialog.Content backdrop>
- <Dialog.Header>
- <Heading size="lg">{title}</Heading>
- </Dialog.Header>
- <Dialog.CloseTrigger />
- <Dialog.Body>
- <Text>
- Are you sure you want to delete <strong>{resourceName}</strong>?
This action cannot be undone.
- </Text>
- <Text color="red.500" fontWeight="bold" mt={4}>
- {warningText}
- </Text>
- </Dialog.Body>
- <Dialog.Footer>
- <HStack justifyContent="flex-end" width="100%">
- <Button onClick={onClose} variant="outline">
- Cancel
- </Button>
- <Button colorPalette="red" loading={isDeleting} onClick={onDelete}>
- <FiTrash2 style={{ marginRight: "8px" }} /> {deleteButtonText}
- </Button>
- </HStack>
- </Dialog.Footer>
- </Dialog.Content>
- </Dialog.Root>
-);
+}) => {
+ const { t: translate } = useTranslation("common");
+
+ return (
+ <Dialog.Root lazyMount onOpenChange={onClose} open={open} size="md"
unmountOnExit>
+ <Dialog.Content backdrop>
+ <Dialog.Header>
+ <Heading size="lg">{title}</Heading>
+ </Dialog.Header>
+ <Dialog.CloseTrigger />
+ <Dialog.Body>
+ <Text>{translate("modal.delete.confirmation", { resourceName
})}</Text>
+ <Text color="red.500" fontWeight="bold" mt={4}>
+ {warningText}
+ </Text>
+ </Dialog.Body>
+ <Dialog.Footer>
+ <HStack justifyContent="flex-end" width="100%">
+ <Button onClick={onClose} variant="outline">
+ {translate("modal.cancel")}
+ </Button>
+ <Button colorPalette="red" loading={isDeleting} onClick={onDelete}>
+ <FiTrash2 style={{ marginRight: "8px" }} />{" "}
+ {deleteButtonText ?? translate("modal.delete.button")}
+ </Button>
+ </HStack>
+ </Dialog.Footer>
+ </Dialog.Content>
+ </Dialog.Root>
+ );
+};
export default DeleteDialog;
diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
index 264b82c7705..0b0faaf6b4b 100644
--- a/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
+++ b/airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx
@@ -31,7 +31,7 @@ describe("Test SearchBar", () => {
const input = screen.getByTestId("search-dags");
- expect(screen.getByText("Advanced Search")).toBeDefined();
+ expect(screen.getByText("advancedSearch")).toBeDefined();
expect(screen.queryByTestId("clear-search")).toBeNull();
fireEvent.change(input, { target: { value: "search" } });
diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
index 5578aae5415..b70a5655b71 100644
--- a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
+++ b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx
@@ -19,6 +19,7 @@
import { Button, Input, Kbd, type ButtonProps } from "@chakra-ui/react";
import { useState, useRef, type ChangeEvent } from "react";
import { useHotkeys } from "react-hotkeys-hook";
+import { useTranslation } from "react-i18next";
import { FiSearch } from "react-icons/fi";
import { useDebouncedCallback } from "use-debounce";
@@ -51,7 +52,7 @@ export const SearchBar = ({
const searchRef = useRef<HTMLInputElement>(null);
const [value, setValue] = useState(defaultValue);
const metaKey = getMetaKey();
-
+ const { t: translate } = useTranslation(["dags"]);
const onSearchChange = (event: ChangeEvent<HTMLInputElement>) => {
setValue(event.target.value);
handleSearchChange(event.target.value);
@@ -73,7 +74,7 @@ export const SearchBar = ({
<>
{Boolean(value) ? (
<CloseButton
- aria-label="Clear search"
+ aria-label={translate("clearSearch")}
colorPalette="gray"
data-testid="clear-search"
onClick={() => {
@@ -85,7 +86,7 @@ export const SearchBar = ({
) : undefined}
{Boolean(hideAdvanced) ? undefined : (
<Button fontWeight="normal" height="1.75rem" variant="ghost"
width={140} {...buttonProps}>
- Advanced Search
+ {translate("advancedSearch")}
</Button>
)}
{!hotkeyDisabled && <Kbd size="sm">{metaKey}+K</Kbd>}
diff --git
a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
index ce79b04dd2a..07f87d11cb7 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGButton.tsx
@@ -18,6 +18,7 @@
*/
import { Box } from "@chakra-ui/react";
import { useDisclosure } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { FiPlay } from "react-icons/fi";
import type { DAGResponse, DAGWithLatestDagRunsResponse } from
"openapi/requests/types.gen";
@@ -32,15 +33,16 @@ type Props = {
const TriggerDAGButton: React.FC<Props> = ({ dag, withText = true }) => {
const { onClose, onOpen, open } = useDisclosure();
+ const { t: translate } = useTranslation("dags");
return (
<Box>
<ActionButton
- actionName="Trigger Dag"
+ actionName={translate("actions.triggerDag")}
colorPalette="blue"
icon={<FiPlay />}
onClick={onOpen}
- text="Trigger"
+ text={translate("actions.trigger")}
variant="solid"
withText={withText}
/>
diff --git a/airflow-core/src/airflow/ui/src/constants/sortParams.ts
b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
index 3ffc36844e8..39864ba7f95 100644
--- a/airflow-core/src/airflow/ui/src/constants/sortParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/sortParams.ts
@@ -17,22 +17,42 @@
* under the License.
*/
import { createListCollection } from "@chakra-ui/react/collection";
+import type { TFunction } from "i18next";
-export const dagSortOptions = createListCollection({
- items: [
- { label: "Sort by Display Name (A-Z)", value: "dag_display_name" },
- { label: "Sort by Display Name (Z-A)", value: "-dag_display_name" },
- { label: "Sort by Next DAG Run (Earliest-Latest)", value: "next_dagrun" },
- { label: "Sort by Next DAG Run (Latest-Earliest)", value: "-next_dagrun" },
- { label: "Sort by Latest Run State (A-Z)", value: "last_run_state" },
- { label: "Sort by Latest Run State (Z-A)", value: "-last_run_state" },
- {
- label: "Sort by Latest Run Start Date (Earliest-Latest)",
- value: "last_run_start_date",
- },
- {
- label: "Sort by Latest Run Start Date (Latest-Earliest)",
- value: "-last_run_start_date",
- },
- ],
-});
+export const createDagSortOptions = (translate: TFunction) =>
+ createListCollection({
+ items: [
+ {
+ label: translate("sort.displayName.asc"),
+ value: "dag_display_name",
+ },
+ {
+ label: translate("sort.displayName.desc"),
+ value: "-dag_display_name",
+ },
+ {
+ label: translate("sort.nextDagRun.asc"),
+ value: "next_dagrun",
+ },
+ {
+ label: translate("sort.nextDagRun.desc"),
+ value: "-next_dagrun",
+ },
+ {
+ label: translate("sort.lastRunState.asc"),
+ value: "last_run_state",
+ },
+ {
+ label: translate("sort.lastRunState.desc"),
+ value: "-last_run_state",
+ },
+ {
+ label: translate("sort.lastRunStartDate.asc"),
+ value: "last_run_start_date",
+ },
+ {
+ label: translate("sort.lastRunStartDate.desc"),
+ value: "-last_run_start_date",
+ },
+ ],
+ });
diff --git a/airflow-core/src/airflow/ui/src/i18n/config.ts
b/airflow-core/src/airflow/ui/src/i18n/config.ts
index 944ca987bd5..045216407a9 100644
--- a/airflow-core/src/airflow/ui/src/i18n/config.ts
+++ b/airflow-core/src/airflow/ui/src/i18n/config.ts
@@ -23,6 +23,7 @@ import { initReactI18next } from "react-i18next";
import deCommon from "./locales/de/common.json";
import deDashboard from "./locales/de/dashboard.json";
import enCommon from "./locales/en/common.json";
+import enDags from "./locales/en/dags.json";
import enDashboard from "./locales/en/dashboard.json";
import koCommon from "./locales/ko/common.json";
import koDashboard from "./locales/ko/dashboard.json";
@@ -43,7 +44,7 @@ export const supportedLanguages = [
] as const;
export const defaultLanguage = "en";
-export const namespaces = ["common", "dashboard"] as const;
+export const namespaces = ["common", "dashboard", "dags"] as const;
const resources = {
de: {
@@ -52,6 +53,7 @@ const resources = {
},
en: {
common: enCommon,
+ dags: enDags,
dashboard: enDashboard,
},
ko: {
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
b/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
index b1a3a0d0c42..59e26eae483 100644
--- a/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/common.json
@@ -13,6 +13,8 @@
"auditLog": "Audit Log",
"xcoms": "XComs"
},
+ "dag_one": "Dag",
+ "dag_other": "Dags",
"dagRun_one": "Dag Run",
"dagRun_other": "Dag Runs",
"defaultToGraphView": "Default to graph view",
@@ -26,7 +28,11 @@
"logoutConfirmation": "You are about to logout from the application.",
"modal": {
"cancel": "Cancel",
- "confirm": "Confirm"
+ "confirm": "Confirm",
+ "delete": {
+ "button": "Delete",
+ "confirmation": "Are you sure you want to delete {{resourceName}}? This
action cannot be undone."
+ }
},
"nav": {
"admin": "Admin",
@@ -72,6 +78,21 @@
},
"switchToDarkMode": "Switch to Dark Mode",
"switchToLightMode": "Switch to Light Mode",
+ "table": {
+ "filterByTag": "Filter Dags by tag",
+ "filterColumns": "Filter table columns",
+ "filters": {
+ "filter_one": "filter",
+ "filter_other": "filters",
+ "reset": "Reset"
+ },
+ "noTagsFound": "No tags found",
+ "tagMode": {
+ "all": "All",
+ "any": "Any"
+ },
+ "tagPlaceholder": "Filter by tag"
+ },
"taskInstance_one": "Task Instance",
"taskInstance_other": "Task Instances",
"timeRange": {
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json
b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json
new file mode 100644
index 00000000000..cb6db5cc3ce
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json
@@ -0,0 +1,55 @@
+{
+ "actions": {
+ "delete": "Delete Dag",
+ "deleteDagWarning": "This will remove all metadata related to the DAG,
including DAG Runs and Tasks.",
+ "trigger": "Trigger",
+ "triggerDag": "Trigger Dag"
+ },
+ "assetSchedule": "{{count}} of {{total}} assets updated",
+ "filters": {
+ "paused": {
+ "active": "Active",
+ "all": "All",
+ "paused": "Paused"
+ }
+ },
+ "list": {
+ "advancedSearch": "Advanced Search",
+ "clearSearch": "Clear search",
+ "columns": {
+ "dagId": "Dag ID",
+ "lastDagRun": "Last Dag Run",
+ "nextDagRun": "Next Dag Run",
+ "schedule": "Schedule",
+ "tags": "Tags"
+ },
+ "ownerLink": "Owner link for {{owner}}",
+ "runs": {
+ "duration": "Duration",
+ "endDate": "End Date",
+ "runAfter": "Run After",
+ "startDate": "Start Date",
+ "state": "State"
+ },
+ "searchPlaceholder": "Search Dags"
+ },
+ "sort": {
+ "displayName": {
+ "asc": "Sort by Display Name (A-Z)",
+ "desc": "Sort by Display Name (Z-A)"
+ },
+ "lastRunStartDate": {
+ "asc": "Sort by Latest Run Start Date (Earliest-Latest)",
+ "desc": "Sort by Latest Run Start Date (Latest-Earliest)"
+ },
+ "lastRunState": {
+ "asc": "Sort by Latest Run State (A-Z)",
+ "desc": "Sort by Latest Run State (Z-A)"
+ },
+ "nextDagRun": {
+ "asc": "Sort by Next Dag Run (Earliest-Latest)",
+ "desc": "Sort by Next Dag Run (Latest-Earliest)"
+ },
+ "placeholder": "Sort by"
+ }
+}
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
b/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
index 331fd77e68f..3838179e0d4 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
@@ -18,18 +18,23 @@
*/
import { Box } from "@chakra-ui/react";
import type { PropsWithChildren } from "react";
+import { useTranslation } from "react-i18next";
import { NavTabs } from "./Details/NavTabs";
-const tabs = [
- { label: "Dags", value: "/dags" },
- { label: "Runs", value: "/dag_runs" },
- { label: "Task Instances", value: "/task_instances" },
-];
+export const DagsLayout = ({ children }: PropsWithChildren) => {
+ const { t: translate } = useTranslation("common");
-export const DagsLayout = ({ children }: PropsWithChildren) => (
- <Box>
- <NavTabs tabs={tabs} />
- {children}
- </Box>
-);
+ const tabs = [
+ { label: translate("nav.dags"), value: "/dags" },
+ { label: translate("dagRun_other"), value: "/dag_runs" },
+ { label: translate("taskInstance_other"), value: "/task_instances" },
+ ];
+
+ return (
+ <Box>
+ <NavTabs tabs={tabs} />
+ {children}
+ </Box>
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
index c566a5532ac..39a51c4be0c 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/AssetSchedule.tsx
@@ -18,6 +18,7 @@
*/
import { HStack, Text, Link } from "@chakra-ui/react";
import dayjs from "dayjs";
+import { useTranslation } from "react-i18next";
import { FiDatabase } from "react-icons/fi";
import { Link as RouterLink } from "react-router-dom";
@@ -32,6 +33,7 @@ type Props = {
};
export const AssetSchedule = ({ dag }: Props) => {
+ const { t: translate } = useTranslation("dags");
const { data: nextRun, isLoading } = useAssetServiceNextRunAssets({ dagId:
dag.dag_id });
const nextRunEvents = (nextRun?.events ?? []) as Array<NextRunEvent>;
@@ -72,8 +74,7 @@ export const AssetSchedule = ({ dag }: Props) => {
<Popover.Trigger asChild>
<Button loading={isLoading} paddingInline={0} size="sm"
variant="ghost">
<FiDatabase style={{ display: "inline" }} />
- {pendingEvents.length}
- {nextRunEvents.length > 1 ? ` of ${nextRunEvents.length} ` : "
"}assets updated
+ {translate("assetSchedule", { count: pendingEvents.length, total:
nextRunEvents.length })}
</Button>
</Popover.Trigger>
<Popover.Content css={{ "--popover-bg": "colors.bg.emphasized" }}
width="fit-content">
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
index 26b2655c26a..46a9b362a72 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagCard.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import { Box, Flex, HStack, SimpleGrid, Link, Spinner } from
"@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { Link as RouterLink } from "react-router-dom";
import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -37,6 +38,7 @@ type Props = {
};
export const DagCard = ({ dag }: Props) => {
+ const { t: translate } = useTranslation(["dags", "common"]);
const [latestRun] = dag.latest_dag_runs;
const refetchInterval = useAutoRefresh({ isPaused: dag.is_paused });
@@ -64,10 +66,10 @@ export const DagCard = ({ dag }: Props) => {
</HStack>
</Flex>
<SimpleGrid columns={4} gap={1} height={20} px={3} py={1}>
- <Stat label="Schedule">
+ <Stat label={translate("list.columns.schedule")}>
<Schedule dag={dag} />
</Stat>
- <Stat label="Latest Run">
+ <Stat label={translate("list.columns.lastDagRun")}>
{latestRun ? (
<Link asChild color="fg.info">
<RouterLink
to={`/dags/${latestRun.dag_id}/runs/${latestRun.dag_run_id}`}>
@@ -83,7 +85,7 @@ export const DagCard = ({ dag }: Props) => {
</Link>
) : undefined}
</Stat>
- <Stat label="Next Run">
+ <Stat label={translate("list.columns.nextDagRun")}>
{Boolean(dag.next_dagrun_run_after) ? (
<DagRunInfo
logicalDate={dag.next_dagrun_logical_date}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
index 1ba38be2510..70fb6195c0a 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagOwners.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import { Link, Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { LimitedItemsList } from "src/components/LimitedItemsList";
@@ -30,13 +31,16 @@ export const DagOwners = ({
readonly ownerLinks?: Record<string, string> | null;
readonly owners?: Array<string>;
}) => {
+ const { t: translate } = useTranslation("dags");
const items = owners.map((owner) => {
const link = ownerLinks?.[owner];
const hasOwnerLink = link !== undefined;
return hasOwnerLink ? (
<Link
- aria-label={`Owner link for ${owner}`}
+ aria-label={translate("list.ownerLink", { owner })}
+ asChild
+ color="fg.info"
href={link}
key={owner}
rel="noopener noreferrer"
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx
deleted file mode 100644
index c44a87410d8..00000000000
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx
+++ /dev/null
@@ -1,284 +0,0 @@
-/*!
- * 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 {
- Box,
- Button,
- createListCollection,
- Field,
- HStack,
- Text,
- type SelectValueChangeDetails,
-} from "@chakra-ui/react";
-import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
-import { useCallback } from "react";
-import { LuX } from "react-icons/lu";
-import { useSearchParams } from "react-router-dom";
-
-import { useDagServiceGetDagTags } from "openapi/queries";
-import { useTableURLState } from "src/components/DataTable/useTableUrlState";
-import { QuickFilterButton } from "src/components/QuickFilterButton";
-import { StateBadge } from "src/components/StateBadge";
-import { Select, Switch } from "src/components/ui";
-import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
-import { useConfig } from "src/queries/useConfig";
-import { pluralize } from "src/utils";
-
-const {
- LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
- PAUSED: PAUSED_PARAM,
- TAGS: TAGS_PARAM,
- TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM,
-}: SearchParamsKeysType = SearchParamsKeys;
-
-const enabledOptions = createListCollection({
- items: [
- { label: "All", value: "all" },
- { label: "Enabled", value: "false" },
- { label: "Disabled", value: "true" },
- ],
-});
-
-export const DagsFilters = () => {
- const [searchParams, setSearchParams] = useSearchParams();
-
- const showPaused = searchParams.get(PAUSED_PARAM);
- const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
- const selectedTags = searchParams.getAll(TAGS_PARAM);
- const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any";
- const isAll = state === null;
- const isRunning = state === "running";
- const isFailed = state === "failed";
- const isSuccess = state === "success";
-
- const { data } = useDagServiceGetDagTags({
- orderBy: "name",
- });
-
- const hidePausedDagsByDefault =
Boolean(useConfig("hide_paused_dags_by_default"));
- const defaultShowPaused = hidePausedDagsByDefault ? "false" : "all";
-
- const { setTableURLState, tableURLState } = useTableURLState();
- const { pagination, sorting } = tableURLState;
-
- const handlePausedChange = useCallback(
- ({ value }: SelectValueChangeDetails<string>) => {
- const [val] = value;
-
- if (val === undefined || val === "all") {
- searchParams.delete(PAUSED_PARAM);
- } else {
- searchParams.set(PAUSED_PARAM, val);
- }
- setTableURLState({
- pagination: { ...pagination, pageIndex: 0 },
- sorting,
- });
- setSearchParams(searchParams);
- },
- [pagination, searchParams, setSearchParams, setTableURLState, sorting],
- );
-
- const handleStateChange: React.MouseEventHandler<HTMLButtonElement> =
useCallback(
- ({ currentTarget: { value } }) => {
- if (value === "all") {
- searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
- } else {
- searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
- }
- setTableURLState({
- pagination: { ...pagination, pageIndex: 0 },
- sorting,
- });
- setSearchParams(searchParams);
- },
- [pagination, searchParams, setSearchParams, setTableURLState, sorting],
- );
- const handleSelectTagsChange = useCallback(
- (
- tags: MultiValue<{
- label: string;
- value: string;
- }>,
- ) => {
- searchParams.delete(TAGS_PARAM);
- tags.forEach(({ value }) => {
- searchParams.append(TAGS_PARAM, value);
- });
- if (tags.length < 2) {
- searchParams.delete(TAGS_MATCH_MODE_PARAM);
- }
- setSearchParams(searchParams);
- },
- [searchParams, setSearchParams],
- );
-
- const onClearFilters = () => {
- searchParams.delete(PAUSED_PARAM);
- searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
- searchParams.delete(TAGS_PARAM);
- searchParams.delete(TAGS_MATCH_MODE_PARAM);
-
- setSearchParams(searchParams);
- };
-
- let filterCount = 0;
-
- if (state !== null) {
- filterCount += 1;
- }
- if (showPaused !== null) {
- filterCount += 1;
- }
- if (selectedTags.length > 0) {
- filterCount += 1;
- }
-
- const handleTagModeChange = useCallback(
- ({ checked }: { checked: boolean }) => {
- const mode = checked ? "all" : "any";
-
- searchParams.set(TAGS_MATCH_MODE_PARAM, mode);
- setSearchParams(searchParams);
- },
- [searchParams, setSearchParams],
- );
-
- return (
- <HStack justifyContent="space-between">
- <HStack gap={4}>
- <HStack>
- <QuickFilterButton isActive={isAll} onClick={handleStateChange}
value="all">
- All
- </QuickFilterButton>
- <QuickFilterButton
- data-testid="dags-failed-filter"
- isActive={isFailed}
- onClick={handleStateChange}
- value="failed"
- >
- <StateBadge state="failed" />
- Failed
- </QuickFilterButton>
- <QuickFilterButton
- data-testid="dags-running-filter"
- isActive={isRunning}
- onClick={handleStateChange}
- value="running"
- >
- <StateBadge state="running" />
- Running
- </QuickFilterButton>
- <QuickFilterButton
- data-testid="dags-success-filter"
- isActive={isSuccess}
- onClick={handleStateChange}
- value="success"
- >
- <StateBadge state="success" />
- Success
- </QuickFilterButton>
- </HStack>
- <Select.Root
- collection={enabledOptions}
- onValueChange={handlePausedChange}
- value={[showPaused ?? defaultShowPaused]}
- >
- <Select.Trigger colorPalette="blue" isActive={Boolean(showPaused)}>
- <Select.ValueText width={20} />
- </Select.Trigger>
- <Select.Content>
- {enabledOptions.items.map((option) => (
- <Select.Item item={option} key={option.label}>
- {option.label}
- </Select.Item>
- ))}
- </Select.Content>
- </Select.Root>
- <Field.Root>
- <ReactSelect
- aria-label="Filter Dags by tag"
- chakraStyles={{
- clearIndicator: (provided) => ({
- ...provided,
- color: "gray.fg",
- }),
- container: (provided) => ({
- ...provided,
- minWidth: 64,
- }),
- control: (provided) => ({
- ...provided,
- colorPalette: "blue",
- }),
- menu: (provided) => ({
- ...provided,
- zIndex: 2,
- }),
- }}
- isClearable
- isMulti
- noOptionsMessage={() => "No tags found"}
- onChange={handleSelectTagsChange}
- options={
- data?.tags.map((tag) => ({
- label: tag,
- value: tag,
- })) ?? []
- }
- placeholder="Filter by tag"
- value={selectedTags.map((tag) => ({
- label: tag,
- value: tag,
- }))}
- />
- </Field.Root>
- {selectedTags.length >= 2 && (
- <HStack align="center" gap={1}>
- <Text
- color={tagFilterMode === "any" ? "fg.info" : "fg.muted"}
- fontSize="sm"
- fontWeight={tagFilterMode === "any" ? "bold" : "normal"}
- >
- Any
- </Text>
- <Switch
- checked={tagFilterMode === "all"}
- onCheckedChange={handleTagModeChange}
- variant="raised"
- />
- <Text
- color={tagFilterMode === "all" ? "fg.info" : "fg.muted"}
- fontSize="sm"
- fontWeight={tagFilterMode === "all" ? "bold" : "normal"}
- >
- All
- </Text>
- </HStack>
- )}
- </HStack>
- <Box>
- {filterCount > 0 && (
- <Button onClick={onClearFilters} size="sm" variant="outline">
- <LuX /> Reset {pluralize("filter", filterCount)}
- </Button>
- )}
- </Box>
- </HStack>
- );
-};
diff --git
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
new file mode 100644
index 00000000000..d36e2b2f8ef
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx
@@ -0,0 +1,181 @@
+/*!
+ * 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 { Box, HStack } from "@chakra-ui/react";
+import type { MultiValue } from "chakra-react-select";
+import { useCallback } from "react";
+import { useSearchParams } from "react-router-dom";
+
+import { useDagServiceGetDagTags } from "openapi/queries";
+import { useTableURLState } from "src/components/DataTable/useTableUrlState";
+import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
+import { useConfig } from "src/queries/useConfig";
+
+import { PausedFilter } from "./PausedFilter";
+import { ResetButton } from "./ResetButton";
+import { StateFilters } from "./StateFilters";
+import { TagFilter } from "./TagFilter";
+
+const {
+ LAST_DAG_RUN_STATE: LAST_DAG_RUN_STATE_PARAM,
+ PAUSED: PAUSED_PARAM,
+ TAGS: TAGS_PARAM,
+ TAGS_MATCH_MODE: TAGS_MATCH_MODE_PARAM,
+}: SearchParamsKeysType = SearchParamsKeys;
+
+const getFilterCount = (state: string | null, showPaused: string | null,
selectedTags: Array<string>) => {
+ let count = 0;
+
+ if (state !== null) {
+ count += 1;
+ }
+ if (showPaused !== null) {
+ count += 1;
+ }
+ if (selectedTags.length > 0) {
+ count += 1;
+ }
+
+ return count;
+};
+
+export const DagsFilters = () => {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const showPaused = searchParams.get(PAUSED_PARAM);
+ const state = searchParams.get(LAST_DAG_RUN_STATE_PARAM);
+ const selectedTags = searchParams.getAll(TAGS_PARAM);
+ const tagFilterMode = searchParams.get(TAGS_MATCH_MODE_PARAM) ?? "any";
+ const isAll = state === null;
+ const isRunning = state === "running";
+ const isFailed = state === "failed";
+ const isSuccess = state === "success";
+
+ const { data } = useDagServiceGetDagTags({
+ orderBy: "name",
+ });
+
+ const hidePausedDagsByDefault =
Boolean(useConfig("hide_paused_dags_by_default"));
+ const defaultShowPaused = hidePausedDagsByDefault ? "false" : "all";
+
+ const { setTableURLState, tableURLState } = useTableURLState();
+ const { pagination, sorting } = tableURLState;
+
+ const handlePausedChange = useCallback(
+ ({ value }: { value: Array<string> }) => {
+ const [val] = value;
+
+ if (val === undefined || val === "all") {
+ searchParams.delete(PAUSED_PARAM);
+ } else {
+ searchParams.set(PAUSED_PARAM, val);
+ }
+ setTableURLState({
+ pagination: { ...pagination, pageIndex: 0 },
+ sorting,
+ });
+ setSearchParams(searchParams);
+ },
+ [pagination, searchParams, setSearchParams, setTableURLState, sorting],
+ );
+
+ const handleStateChange: React.MouseEventHandler<HTMLButtonElement> =
useCallback(
+ ({ currentTarget: { value } }) => {
+ if (value === "all") {
+ searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
+ } else {
+ searchParams.set(LAST_DAG_RUN_STATE_PARAM, value);
+ }
+ setTableURLState({
+ pagination: { ...pagination, pageIndex: 0 },
+ sorting,
+ });
+ setSearchParams(searchParams);
+ },
+ [pagination, searchParams, setSearchParams, setTableURLState, sorting],
+ );
+
+ const handleSelectTagsChange = useCallback(
+ (
+ tags: MultiValue<{
+ label: string;
+ value: string;
+ }>,
+ ) => {
+ searchParams.delete(TAGS_PARAM);
+ tags.forEach(({ value }) => {
+ searchParams.append(TAGS_PARAM, value);
+ });
+ if (tags.length < 2) {
+ searchParams.delete(TAGS_MATCH_MODE_PARAM);
+ }
+ setSearchParams(searchParams);
+ },
+ [searchParams, setSearchParams],
+ );
+
+ const onClearFilters = () => {
+ searchParams.delete(PAUSED_PARAM);
+ searchParams.delete(LAST_DAG_RUN_STATE_PARAM);
+ searchParams.delete(TAGS_PARAM);
+ searchParams.delete(TAGS_MATCH_MODE_PARAM);
+
+ setSearchParams(searchParams);
+ };
+
+ const handleTagModeChange = useCallback(
+ ({ checked }: { checked: boolean }) => {
+ const mode = checked ? "all" : "any";
+
+ searchParams.set(TAGS_MATCH_MODE_PARAM, mode);
+ setSearchParams(searchParams);
+ },
+ [searchParams, setSearchParams],
+ );
+
+ const filterCount = getFilterCount(state, showPaused, selectedTags);
+
+ return (
+ <HStack justifyContent="space-between">
+ <HStack gap={4}>
+ <StateFilters
+ isAll={isAll}
+ isFailed={isFailed}
+ isRunning={isRunning}
+ isSuccess={isSuccess}
+ onStateChange={handleStateChange}
+ />
+ <PausedFilter
+ defaultShowPaused={defaultShowPaused}
+ onPausedChange={handlePausedChange}
+ showPaused={showPaused}
+ />
+ <TagFilter
+ onSelectTagsChange={handleSelectTagsChange}
+ onTagModeChange={handleTagModeChange}
+ selectedTags={selectedTags}
+ tagFilterMode={tagFilterMode}
+ tags={data?.tags ?? []}
+ />
+ </HStack>
+ <Box>
+ <ResetButton filterCount={filterCount} onClearFilters={onClearFilters}
/>
+ </Box>
+ </HStack>
+ );
+};
diff --git
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx
new file mode 100644
index 00000000000..61f467630b4
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx
@@ -0,0 +1,59 @@
+/*!
+ * 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 { createListCollection, type SelectValueChangeDetails } from
"@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { Select } from "src/components/ui";
+
+type Props = {
+ readonly defaultShowPaused: string;
+ readonly onPausedChange: (details: SelectValueChangeDetails<string>) => void;
+ readonly showPaused: string | null;
+};
+
+export const PausedFilter = ({ defaultShowPaused, onPausedChange, showPaused
}: Props) => {
+ const { t: translate } = useTranslation("dags");
+
+ const enabledOptions = createListCollection({
+ items: [
+ { label: translate("filters.paused.all"), value: "all" },
+ { label: translate("filters.paused.active"), value: "false" },
+ { label: translate("filters.paused.paused"), value: "true" },
+ ],
+ });
+
+ return (
+ <Select.Root
+ collection={enabledOptions}
+ onValueChange={onPausedChange}
+ value={[showPaused ?? defaultShowPaused]}
+ >
+ <Select.Trigger colorPalette="blue" isActive={Boolean(showPaused)}>
+ <Select.ValueText width={20} />
+ </Select.Trigger>
+ <Select.Content>
+ {enabledOptions.items.map((option) => (
+ <Select.Item item={option} key={option.label}>
+ {option.label}
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Root>
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
similarity index 55%
copy from airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
copy to
airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
index 331fd77e68f..32905fc9fcf 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx
@@ -16,20 +16,26 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box } from "@chakra-ui/react";
-import type { PropsWithChildren } from "react";
+import { Button } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+import { LuX } from "react-icons/lu";
-import { NavTabs } from "./Details/NavTabs";
+type Props = {
+ readonly filterCount: number;
+ readonly onClearFilters: () => void;
+};
-const tabs = [
- { label: "Dags", value: "/dags" },
- { label: "Runs", value: "/dag_runs" },
- { label: "Task Instances", value: "/task_instances" },
-];
+export const ResetButton = ({ filterCount, onClearFilters }: Props) => {
+ const { t: translate } = useTranslation("common");
-export const DagsLayout = ({ children }: PropsWithChildren) => (
- <Box>
- <NavTabs tabs={tabs} />
- {children}
- </Box>
-);
+ if (filterCount === 0) {
+ return undefined;
+ }
+
+ return (
+ <Button onClick={onClearFilters} size="sm" variant="outline">
+ <LuX /> {translate("table.filters.reset")}{" "}
+ {filterCount === 1 ? translate("table.filters.filter_one") :
translate("table.filters.filter_other")}
+ </Button>
+ );
+};
diff --git
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
new file mode 100644
index 00000000000..0ec17d809b4
--- /dev/null
+++
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx
@@ -0,0 +1,70 @@
+/*!
+ * 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 { HStack } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
+
+import { QuickFilterButton } from "src/components/QuickFilterButton";
+import { StateBadge } from "src/components/StateBadge";
+
+type Props = {
+ readonly isAll: boolean;
+ readonly isFailed: boolean;
+ readonly isRunning: boolean;
+ readonly isSuccess: boolean;
+ readonly onStateChange: React.MouseEventHandler<HTMLButtonElement>;
+};
+
+export const StateFilters = ({ isAll, isFailed, isRunning, isSuccess,
onStateChange }: Props) => {
+ const { t: translate } = useTranslation(["dags", "common"]);
+
+ return (
+ <HStack>
+ <QuickFilterButton isActive={isAll} onClick={onStateChange} value="all">
+ {translate("filters.paused.all")}
+ </QuickFilterButton>
+ <QuickFilterButton
+ data-testid="dags-failed-filter"
+ isActive={isFailed}
+ onClick={onStateChange}
+ value="failed"
+ >
+ <StateBadge state="failed" />
+ {translate("common:states.failed")}
+ </QuickFilterButton>
+ <QuickFilterButton
+ data-testid="dags-running-filter"
+ isActive={isRunning}
+ onClick={onStateChange}
+ value="running"
+ >
+ <StateBadge state="running" />
+ {translate("common:states.running")}
+ </QuickFilterButton>
+ <QuickFilterButton
+ data-testid="dags-success-filter"
+ isActive={isSuccess}
+ onClick={onStateChange}
+ value="success"
+ >
+ <StateBadge state="success" />
+ {translate("common:states.success")}
+ </QuickFilterButton>
+ </HStack>
+ );
+};
diff --git
a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
new file mode 100644
index 00000000000..57fa2b1089d
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx
@@ -0,0 +1,101 @@
+/*!
+ * 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 { Field, HStack, Text } from "@chakra-ui/react";
+import { Select as ReactSelect, type MultiValue } from "chakra-react-select";
+import { useTranslation } from "react-i18next";
+
+import { Switch } from "src/components/ui";
+
+type Props = {
+ readonly onSelectTagsChange: (tags: MultiValue<{ label: string; value:
string }>) => void;
+ readonly onTagModeChange: ({ checked }: { checked: boolean }) => void;
+ readonly selectedTags: Array<string>;
+ readonly tagFilterMode: string;
+ readonly tags: Array<string>;
+};
+
+export const TagFilter = ({
+ onSelectTagsChange,
+ onTagModeChange,
+ selectedTags,
+ tagFilterMode,
+ tags,
+}: Props) => {
+ const { t: translate } = useTranslation("common");
+
+ return (
+ <>
+ <Field.Root>
+ <ReactSelect
+ aria-label={translate("table.filterByTag")}
+ chakraStyles={{
+ clearIndicator: (provided) => ({
+ ...provided,
+ color: "gray.fg",
+ }),
+ container: (provided) => ({
+ ...provided,
+ minWidth: 64,
+ }),
+ control: (provided) => ({
+ ...provided,
+ colorPalette: "blue",
+ }),
+ menu: (provided) => ({
+ ...provided,
+ zIndex: 2,
+ }),
+ }}
+ isClearable
+ isMulti
+ noOptionsMessage={() => translate("table.noTagsFound")}
+ onChange={onSelectTagsChange}
+ options={tags.map((tag) => ({
+ label: tag,
+ value: tag,
+ }))}
+ placeholder={translate("table.tagPlaceholder")}
+ value={selectedTags.map((tag) => ({
+ label: tag,
+ value: tag,
+ }))}
+ />
+ </Field.Root>
+ {selectedTags.length >= 2 && (
+ <HStack align="center" gap={1}>
+ <Text
+ color={tagFilterMode === "any" ? "fg.info" : "fg.muted"}
+ fontSize="sm"
+ fontWeight={tagFilterMode === "any" ? "bold" : "normal"}
+ >
+ {translate("table.tagMode.any")}
+ </Text>
+ <Switch checked={tagFilterMode === "all"}
onCheckedChange={onTagModeChange} variant="raised" />
+ <Text
+ color={tagFilterMode === "all" ? "fg.info" : "fg.muted"}
+ fontSize="sm"
+ fontWeight={tagFilterMode === "all" ? "bold" : "normal"}
+ >
+ {translate("table.tagMode.all")}
+ </Text>
+ </HStack>
+ )}
+ </>
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
similarity index 66%
copy from airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
copy to airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
index 331fd77e68f..6c88a1f6a1d 100644
--- a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts
@@ -16,20 +16,5 @@
* specific language governing permissions and limitations
* under the License.
*/
-import { Box } from "@chakra-ui/react";
-import type { PropsWithChildren } from "react";
-import { NavTabs } from "./Details/NavTabs";
-
-const tabs = [
- { label: "Dags", value: "/dags" },
- { label: "Runs", value: "/dag_runs" },
- { label: "Task Instances", value: "/task_instances" },
-];
-
-export const DagsLayout = ({ children }: PropsWithChildren) => (
- <Box>
- <NavTabs tabs={tabs} />
- {children}
- </Box>
-);
+export { DagsFilters } from "./DagsFilters";
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 60b45fbaca9..e06098d4dce 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsList.tsx
@@ -26,7 +26,8 @@ import {
Box,
} from "@chakra-ui/react";
import type { ColumnDef } from "@tanstack/react-table";
-import { useCallback, useState } from "react";
+import { useCallback, useState, useMemo } from "react";
+import { useTranslation } from "react-i18next";
import { Link as RouterLink, useSearchParams } from "react-router-dom";
import { useLocalStorage } from "usehooks-ts";
@@ -45,7 +46,6 @@ import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searc
import { DagsLayout } from "src/layouts/DagsLayout";
import { useConfig } from "src/queries/useConfig";
import { useDags } from "src/queries/useDags";
-import { pluralize } from "src/utils";
import { DAGImportErrors } from "../Dashboard/Stats/DAGImportErrors";
import { DagCard } from "./DagCard";
@@ -54,7 +54,9 @@ import { DagsFilters } from "./DagsFilters";
import { Schedule } from "./Schedule";
import { SortSelect } from "./SortSelect";
-const columns: Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
+const createColumns = (
+ translate: (key: string, options?: Record<string, unknown>) => string,
+): Array<ColumnDef<DAGWithLatestDagRunsResponse>> => [
{
accessorKey: "is_paused",
cell: ({ row: { original } }) => (
@@ -77,13 +79,13 @@ const columns:
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
<RouterLink
to={`/dags/${original.dag_id}`}>{original.dag_display_name}</RouterLink>
</Link>
),
- header: "Dag",
+ header: () => translate("list.columns.dagId"),
},
{
accessorKey: "timetable_description",
cell: ({ row: { original } }) => <Schedule dag={original} />,
enableSorting: false,
- header: () => "Schedule",
+ header: () => translate("list.columns.schedule"),
},
{
accessorKey: "next_dagrun",
@@ -94,7 +96,7 @@ const columns: Array<ColumnDef<DAGWithLatestDagRunsResponse>>
= [
runAfter={original.next_dagrun_run_after as string}
/>
) : undefined,
- header: "Next Dag Run",
+ header: () => translate("list.columns.nextDagRun"),
},
{
accessorKey: "last_run_start_date",
@@ -112,7 +114,7 @@ const columns:
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
</RouterLink>
</Link>
) : undefined,
- header: "Last Dag Run",
+ header: () => translate("list.columns.lastDagRun"),
},
{
accessorKey: "tags",
@@ -122,22 +124,18 @@ const columns:
Array<ColumnDef<DAGWithLatestDagRunsResponse>> = [
},
}) => <DagTags hideIcon tags={tags} />,
enableSorting: false,
- header: () => "Tags",
+ header: () => translate("list.columns.tags"),
},
{
accessorKey: "trigger",
- cell: ({ row }) => <TriggerDAGButton dag={row.original} withText={false}
/>,
+ cell: ({ row: { original } }) => <TriggerDAGButton dag={original}
withText={false} />,
enableSorting: false,
header: "",
},
{
accessorKey: "delete",
- cell: ({ row }) => (
- <DeleteDagButton
- dagDisplayName={row.original.dag_display_name}
- dagId={row.original.dag_id}
- withText={false}
- />
+ cell: ({ row: { original } }) => (
+ <DeleteDagButton dagDisplayName={original.dag_display_name}
dagId={original.dag_id} withText={false} />
),
enableSorting: false,
header: "",
@@ -162,6 +160,7 @@ const cardDef: CardDef<DAGWithLatestDagRunsResponse> = {
const DAGS_LIST_DISPLAY = "dags_list_display";
export const DagsList = () => {
+ const { t: translate } = useTranslation(["dags", "common"]);
const [searchParams, setSearchParams] = useSearchParams();
const [display, setDisplay] = useLocalStorage<"card" |
"table">(DAGS_LIST_DISPLAY, "card");
const dagRunsLimit = display === "card" ? 14 : 1;
@@ -185,6 +184,8 @@ export const DagsList = () => {
const [sort] = sorting;
const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` :
"dag_display_name";
+ const columns = useMemo(() => createColumns(translate), [translate]);
+
const handleSearchChange = (value: string) => {
if (value) {
searchParams.set(NAME_PATTERN_PARAM, value);
@@ -241,13 +242,13 @@ export const DagsList = () => {
buttonProps={{ disabled: true }}
defaultValue={dagDisplayNamePattern ?? ""}
onChange={handleSearchChange}
- placeHolder="Search Dags"
+ placeHolder={translate("list.searchPlaceholder")}
/>
<DagsFilters />
<HStack justifyContent="space-between">
<HStack>
<Heading py={3} size="md">
- {pluralize("Dag", data.total_entries)}
+ {`${data.total_entries} ${data.total_entries === 1 ?
translate("common:dag_one") : translate("common:dag_other")}`}
</Heading>
<DAGImportErrors iconOnly />
</HStack>
@@ -266,7 +267,7 @@ export const DagsList = () => {
errorMessage={<ErrorAlert error={error} />}
initialState={tableURLState}
isLoading={isLoading}
- modelName="Dag"
+ modelName={translate("common:dag_one")}
onStateChange={setTableURLState}
skeletonCount={display === "card" ? 5 : undefined}
total={data.total_entries}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
index 5e7def01650..af6c5c1a146 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/RecentRuns.tsx
@@ -19,6 +19,7 @@
import { Flex, Box, Text } from "@chakra-ui/react";
import dayjs from "dayjs";
import duration from "dayjs/plugin/duration";
+import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -35,6 +36,8 @@ export const RecentRuns = ({
}: {
readonly latestRuns: DAGWithLatestDagRunsResponse["latest_dag_runs"];
}) => {
+ const { t: translate } = useTranslation(["dags", "common"]);
+
if (!latestRuns.length) {
return undefined;
}
@@ -55,21 +58,25 @@ export const RecentRuns = ({
<Tooltip
content={
<Box>
- <Text>State: {run.state}</Text>
<Text>
- Run After: <Time datetime={run.run_after} />
+ {translate("list.runs.state")}:
{translate(`common:states.${run.state}`)}
+ </Text>
+ <Text>
+ {translate("list.runs.runAfter")}: <Time
datetime={run.run_after} />
</Text>
{run.start_date === null ? undefined : (
<Text>
- Start Date: <Time datetime={run.start_date} />
+ {translate("list.runs.startDate")}: <Time
datetime={run.start_date} />
</Text>
)}
{run.end_date === null ? undefined : (
<Text>
- End Date: <Time datetime={run.end_date} />
+ {translate("list.runs.endDate")}: <Time
datetime={run.end_date} />
</Text>
)}
- <Text>Duration: {getDuration(run.start_date,
run.end_date)}</Text>
+ <Text>
+ {translate("list.runs.duration")}:
{getDuration(run.start_date, run.end_date)}
+ </Text>
</Box>
}
key={run.dag_run_id}
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
index 9f071cfb437..0810fa743fb 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/Schedule.tsx
@@ -17,6 +17,7 @@
* under the License.
*/
import { Text } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { FiCalendar } from "react-icons/fi";
import type { DAGWithLatestDagRunsResponse } from "openapi/requests/types.gen";
@@ -28,9 +29,11 @@ type Props = {
readonly dag: DAGWithLatestDagRunsResponse;
};
-export const Schedule = ({ dag }: Props) =>
- Boolean(dag.timetable_summary) ? (
- Boolean(dag.asset_expression) || dag.timetable_summary === "Asset" ? (
+export const Schedule = ({ dag }: Props) => {
+ const { t: translate } = useTranslation("dags");
+
+ return Boolean(dag.timetable_summary) ? (
+ Boolean(dag.asset_expression) || dag.timetable_summary ===
translate("schedule.asset") ? (
<AssetSchedule dag={dag} />
) : (
<Tooltip content={dag.timetable_description}>
@@ -40,3 +43,4 @@ export const Schedule = ({ dag }: Props) =>
</Tooltip>
)
) : undefined;
+};
diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
b/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
index 256bc862de3..e0e66b9033f 100644
--- a/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/DagsList/SortSelect.tsx
@@ -17,32 +17,38 @@
* under the License.
*/
import type { SelectValueChangeDetails } from "@chakra-ui/react";
+import { useTranslation } from "react-i18next";
import { Select } from "src/components/ui";
-import { dagSortOptions } from "src/constants/sortParams";
+import { createDagSortOptions } from "src/constants/sortParams";
type Props = {
readonly handleSortChange: ({ value }:
SelectValueChangeDetails<Array<string>>) => void;
readonly orderBy?: string;
};
-export const SortSelect = ({ handleSortChange, orderBy }: Props) => (
- <Select.Root
- collection={dagSortOptions}
- data-testid="sort-by-select"
- onValueChange={handleSortChange}
- value={orderBy === undefined ? undefined : [orderBy]}
- width="310px"
- >
- <Select.Trigger>
- <Select.ValueText placeholder="Sort by" />
- </Select.Trigger>
- <Select.Content>
- {dagSortOptions.items.map((option) => (
- <Select.Item item={option} key={option.value}>
- {option.label}
- </Select.Item>
- ))}
- </Select.Content>
- </Select.Root>
-);
+export const SortSelect = ({ handleSortChange, orderBy }: Props) => {
+ const { t: translate } = useTranslation("dags");
+ const dagSortOptions = createDagSortOptions(translate);
+
+ return (
+ <Select.Root
+ collection={dagSortOptions}
+ data-testid="sort-by-select"
+ onValueChange={handleSortChange}
+ value={orderBy === undefined ? undefined : [orderBy]}
+ width="310px"
+ >
+ <Select.Trigger>
+ <Select.ValueText placeholder={translate("sort.placeholder")} />
+ </Select.Trigger>
+ <Select.Content>
+ {dagSortOptions.items.map((option) => (
+ <Select.Item item={option} key={option.value}>
+ {option.label}
+ </Select.Item>
+ ))}
+ </Select.Content>
+ </Select.Root>
+ );
+};