From 7e4e00ef3d0b578220883f1a46f163d4f1348562 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Wed, 28 May 2025 18:02:18 -0400 Subject: [PATCH 1/4] Migrate dags list plaintext to translate files --- .../components/DagActions/DeleteDagButton.tsx | 11 +- .../components/DataTable/FilterMenuButton.tsx | 76 ++--- .../ui/src/components/DeleteDialog.tsx | 62 ++-- .../TriggerDag/TriggerDAGButton.tsx | 6 +- .../airflow/ui/src/constants/sortParams.ts | 56 ++-- .../src/airflow/ui/src/i18n/config.ts | 4 +- .../ui/src/i18n/locales/en/common.json | 23 +- .../airflow/ui/src/i18n/locales/en/dags.json | 52 ++++ .../src/airflow/ui/src/layouts/DagsLayout.tsx | 27 +- .../ui/src/pages/DagsList/AssetSchedule.tsx | 5 +- .../airflow/ui/src/pages/DagsList/DagCard.tsx | 8 +- .../ui/src/pages/DagsList/DagsFilters.tsx | 284 ------------------ .../DagsList/DagsFilters/DagsFilters.tsx | 181 +++++++++++ .../DagsList/DagsFilters/PausedFilter.tsx | 59 ++++ .../DagsList/DagsFilters/ResetButton.tsx | 41 +++ .../DagsList/DagsFilters/StateFilters.tsx | 70 +++++ .../pages/DagsList/DagsFilters/TagFilter.tsx | 101 +++++++ .../src/pages/DagsList/DagsFilters/index.ts | 20 ++ .../ui/src/pages/DagsList/DagsList.tsx | 37 +-- .../ui/src/pages/DagsList/RecentRuns.tsx | 17 +- .../ui/src/pages/DagsList/Schedule.tsx | 10 +- .../ui/src/pages/DagsList/SortSelect.tsx | 48 +-- 22 files changed, 757 insertions(+), 441 deletions(-) create mode 100644 airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json delete mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/DagsFilters.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/PausedFilter.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/StateFilters.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/TagFilter.tsx create mode 100644 airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts 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 28645696b1593..afddb68436ffb 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 ( <> } 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 6b98ab0df63ae..189e177200ff3 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 = { readonly table: Table; }; -const FilterMenuButton = ({ table }: Props) => ( - - - - - - - - {table.getAllLeafColumns().map((column) => { - const text = flexRender(column.columnDef.header, { - column, - header: { column } as Header, - table, - }); +const FilterMenuButton = ({ table }: Props) => { + const { t: translate } = useTranslation("common"); + const filterLabel = translate("table.filterColumns"); - return text?.toString ? ( - - { - column.toggleVisibility(); - }} - > - {text} - - - ) : undefined; - })} - - -); + return ( + + + + + + + + {table.getAllLeafColumns().map((column) => { + const text = flexRender(column.columnDef.header, { + column, + header: { column } as Header, + table, + }); + + return text?.toString ? ( + + { + column.toggleVisibility(); + }} + > + {text} + + + ) : undefined; + })} + + + ); +}; 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 6d8ac6b56fc91..b77262838bf54 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 = ({ - deleteButtonText = "Delete", + deleteButtonText, isDeleting, onClose, onDelete, @@ -42,33 +43,36 @@ const DeleteDialog: React.FC = ({ resourceName, title, warningText, -}) => ( - - - - {title} - - - - - Are you sure you want to delete {resourceName}? This action cannot be undone. - - - {warningText} - - - - - - - - - - -); +}) => { + const { t: translate } = useTranslation("common"); + + return ( + + + + {title} + + + + {translate("modal.delete.confirmation", { resourceName })} + + {warningText} + + + + + + + + + + + ); +}; export default DeleteDialog; 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 ce79b04dd2ae3..07f87d11cb747 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 = ({ dag, withText = true }) => { const { onClose, onOpen, open } = useDisclosure(); + const { t: translate } = useTranslation("dags"); return ( } 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 3ffc36844e82e..39864ba7f9589 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 944ca987bd5e0..045216407a9bf 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 b1a3a0d0c42a0..09995e0b6a563 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,26 @@ "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." + } + }, + "table": { + "filterColumns": "Filter table columns", + "filterByTag": "Filter Dags by tag", + "noTagsFound": "No tags found", + "tagPlaceholder": "Filter by tag", + "tagMode": { + "any": "Any", + "all": "All" + }, + "filters": { + "reset": "Reset", + "filter_one": "filter", + "filter_other": "filters" + } }, "nav": { "admin": "Admin", 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 0000000000000..eefaff0a483dd --- /dev/null +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json @@ -0,0 +1,52 @@ +{ + "list": { + "searchPlaceholder": "Search Dags", + "columns": { + "dagId": "Dag ID", + "schedule": "Schedule", + "nextDagRun": "Next Dag Run", + "lastDagRun": "Last Dag Run", + "tags": "Tags" + }, + "runs": { + "state": "State", + "runAfter": "Run After", + "startDate": "Start Date", + "endDate": "End Date", + "duration": "Duration" + } + }, + "actions": { + "triggerDag": "Trigger Dag", + "trigger": "Trigger", + "delete": "Delete Dag", + "deleteDagWarning": "This will remove all metadata related to the DAG, including DAG Runs and Tasks." + }, + "filters": { + "paused": { + "all": "All", + "active": "Active", + "paused": "Paused" + } + }, + "assetSchedule": "{{count}} of {{total}} Asset Events updated", + "sort": { + "placeholder": "Sort by", + "displayName": { + "asc": "Sort by Display Name (A-Z)", + "desc": "Sort by Display Name (Z-A)" + }, + "nextDagRun": { + "asc": "Sort by Next Dag Run (Earliest-Latest)", + "desc": "Sort by Next Dag Run (Latest-Earliest)" + }, + "lastRunState": { + "asc": "Sort by Latest Run State (A-Z)", + "desc": "Sort by Latest Run State (Z-A)" + }, + "lastRunStartDate": { + "asc": "Sort by Latest Run Start Date (Earliest-Latest)", + "desc": "Sort by Latest Run Start Date (Latest-Earliest)" + } + } +} diff --git a/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx b/airflow-core/src/airflow/ui/src/layouts/DagsLayout.tsx index 331fd77e68f32..3838179e0d4bc 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) => ( - - - {children} - -); + const tabs = [ + { label: translate("nav.dags"), value: "/dags" }, + { label: translate("dagRun_other"), value: "/dag_runs" }, + { label: translate("taskInstance_other"), value: "/task_instances" }, + ]; + + return ( + + + {children} + + ); +}; 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 c566a5532ace6..39a51c4be0ca1 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; @@ -72,8 +74,7 @@ export const AssetSchedule = ({ dag }: Props) => { 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 26b2655c26aa2..46a9b362a72b1 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) => { - + - + {latestRun ? ( @@ -83,7 +85,7 @@ export const DagCard = ({ dag }: Props) => { ) : undefined} - + {Boolean(dag.next_dagrun_run_after) ? ( { - 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) => { - 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 = 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 ( - - - - - All - - - - Failed - - - - Running - - - - Success - - - - - - - - {enabledOptions.items.map((option) => ( - - {option.label} - - ))} - - - - ({ - ...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, - }))} - /> - - {selectedTags.length >= 2 && ( - - - Any - - - - All - - - )} - - - {filterCount > 0 && ( - - )} - - - ); -}; 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 0000000000000..d36e2b2f8ef46 --- /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) => { + 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 }) => { + 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 = 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 ( + + + + + + + + + + + ); +}; 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 0000000000000..61f467630b444 --- /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) => 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 ( + + + + + + {enabledOptions.items.map((option) => ( + + {option.label} + + ))} + + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx new file mode 100644 index 0000000000000..32905fc9fcff5 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/ResetButton.tsx @@ -0,0 +1,41 @@ +/*! + * 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 { Button } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; +import { LuX } from "react-icons/lu"; + +type Props = { + readonly filterCount: number; + readonly onClearFilters: () => void; +}; + +export const ResetButton = ({ filterCount, onClearFilters }: Props) => { + const { t: translate } = useTranslation("common"); + + if (filterCount === 0) { + return undefined; + } + + return ( + + ); +}; 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 0000000000000..0ec17d809b49f --- /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; +}; + +export const StateFilters = ({ isAll, isFailed, isRunning, isSuccess, onStateChange }: Props) => { + const { t: translate } = useTranslation(["dags", "common"]); + + return ( + + + {translate("filters.paused.all")} + + + + {translate("common:states.failed")} + + + + {translate("common:states.running")} + + + + {translate("common:states.success")} + + + ); +}; 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 0000000000000..57fa2b1089de1 --- /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; + readonly tagFilterMode: string; + readonly tags: Array; +}; + +export const TagFilter = ({ + onSelectTagsChange, + onTagModeChange, + selectedTags, + tagFilterMode, + tags, +}: Props) => { + const { t: translate } = useTranslation("common"); + + return ( + <> + + ({ + ...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, + }))} + /> + + {selectedTags.length >= 2 && ( + + + {translate("table.tagMode.any")} + + + + {translate("table.tagMode.all")} + + + )} + + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts new file mode 100644 index 0000000000000..6c88a1f6a1d73 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/DagsList/DagsFilters/index.ts @@ -0,0 +1,20 @@ +/*! + * 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. + */ + +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 60b45fbaca99b..e06098d4dce01 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> = [ +const createColumns = ( + translate: (key: string, options?: Record) => string, +): Array> => [ { accessorKey: "is_paused", cell: ({ row: { original } }) => ( @@ -77,13 +79,13 @@ const columns: Array> = [ {original.dag_display_name} ), - header: "Dag", + header: () => translate("list.columns.dagId"), }, { accessorKey: "timetable_description", cell: ({ row: { original } }) => , enableSorting: false, - header: () => "Schedule", + header: () => translate("list.columns.schedule"), }, { accessorKey: "next_dagrun", @@ -94,7 +96,7 @@ const columns: Array> = [ 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> = [ ) : undefined, - header: "Last Dag Run", + header: () => translate("list.columns.lastDagRun"), }, { accessorKey: "tags", @@ -122,22 +124,18 @@ const columns: Array> = [ }, }) => , enableSorting: false, - header: () => "Tags", + header: () => translate("list.columns.tags"), }, { accessorKey: "trigger", - cell: ({ row }) => , + cell: ({ row: { original } }) => , enableSorting: false, header: "", }, { accessorKey: "delete", - cell: ({ row }) => ( - + cell: ({ row: { original } }) => ( + ), enableSorting: false, header: "", @@ -162,6 +160,7 @@ const cardDef: CardDef = { 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")} /> - {pluralize("Dag", data.total_entries)} + {`${data.total_entries} ${data.total_entries === 1 ? translate("common:dag_one") : translate("common:dag_other")}`} @@ -266,7 +267,7 @@ export const DagsList = () => { errorMessage={} 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 5e7def016502b..af6c5c1a14655 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 = ({ - State: {run.state} - Run After: + + {translate("list.runs.runAfter")}: {run.start_date === null ? undefined : ( - Start Date: )} {run.end_date === null ? undefined : ( - End Date: )} - Duration: {getDuration(run.start_date, run.end_date)} + + {translate("list.runs.duration")}: {getDuration(run.start_date, run.end_date)} + } 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 9f071cfb43735..0810fa743fb73 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") ? ( ) : ( @@ -40,3 +43,4 @@ export const Schedule = ({ dag }: Props) => ) ) : 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 256bc862de3b3..e0e66b9033f96 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>) => void; readonly orderBy?: string; }; -export const SortSelect = ({ handleSortChange, orderBy }: Props) => ( - - - - - - {dagSortOptions.items.map((option) => ( - - {option.label} - - ))} - - -); +export const SortSelect = ({ handleSortChange, orderBy }: Props) => { + const { t: translate } = useTranslation("dags"); + const dagSortOptions = createDagSortOptions(translate); + + return ( + + + + + + {dagSortOptions.items.map((option) => ( + + {option.label} + + ))} + + + ); +}; From e1444a6f08ad7ed4ca767b8a50c5a76fce7ddea1 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Thu, 29 May 2025 10:34:28 -0400 Subject: [PATCH 2/4] Update SearchBar, DagOwners and fix asset schedule --- airflow-core/src/airflow/ui/src/components/SearchBar.tsx | 7 ++++--- airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json | 7 +++++-- .../src/airflow/ui/src/pages/DagsList/DagOwners.tsx | 6 +++++- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/airflow-core/src/airflow/ui/src/components/SearchBar.tsx b/airflow-core/src/airflow/ui/src/components/SearchBar.tsx index 5578aae541523..b70a5655b7127 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(null); const [value, setValue] = useState(defaultValue); const metaKey = getMetaKey(); - + const { t: translate } = useTranslation(["dags"]); const onSearchChange = (event: ChangeEvent) => { setValue(event.target.value); handleSearchChange(event.target.value); @@ -73,7 +74,7 @@ export const SearchBar = ({ <> {Boolean(value) ? ( { @@ -85,7 +86,7 @@ export const SearchBar = ({ ) : undefined} {Boolean(hideAdvanced) ? undefined : ( )} {!hotkeyDisabled && {metaKey}+K} 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 index eefaff0a483dd..12a256c82ea28 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json @@ -1,6 +1,8 @@ { "list": { "searchPlaceholder": "Search Dags", + "clearSearch": "Clear search", + "advancedSearch": "Advanced Search", "columns": { "dagId": "Dag ID", "schedule": "Schedule", @@ -14,7 +16,8 @@ "startDate": "Start Date", "endDate": "End Date", "duration": "Duration" - } + }, + "ownerLink": "Owner link for {{owner}}" }, "actions": { "triggerDag": "Trigger Dag", @@ -29,7 +32,7 @@ "paused": "Paused" } }, - "assetSchedule": "{{count}} of {{total}} Asset Events updated", + "assetSchedule": "{{count}} of {{total}} assets updated", "sort": { "placeholder": "Sort by", "displayName": { 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 1ba38be25105c..70fb6195c0a1f 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 | null; readonly owners?: Array; }) => { + const { t: translate } = useTranslation("dags"); const items = owners.map((owner) => { const link = ownerLinks?.[owner]; const hasOwnerLink = link !== undefined; return hasOwnerLink ? ( Date: Thu, 29 May 2025 11:14:03 -0400 Subject: [PATCH 3/4] Fix test --- airflow-core/src/airflow/ui/src/components/SearchBar.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 264b82c770580..0b0faaf6b4bb5 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" } }); From a7e33be8f2fcb79254ff9012b8ab578403c82896 Mon Sep 17 00:00:00 2001 From: Brent Bovenzi Date: Thu, 29 May 2025 12:10:56 -0400 Subject: [PATCH 4/4] Alphabetically sort json --- .../ui/src/i18n/locales/en/common.json | 30 +++++----- .../airflow/ui/src/i18n/locales/en/dags.json | 60 +++++++++---------- 2 files changed, 45 insertions(+), 45 deletions(-) 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 09995e0b6a563..59e26eae483ff 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 @@ -34,21 +34,6 @@ "confirmation": "Are you sure you want to delete {{resourceName}}? This action cannot be undone." } }, - "table": { - "filterColumns": "Filter table columns", - "filterByTag": "Filter Dags by tag", - "noTagsFound": "No tags found", - "tagPlaceholder": "Filter by tag", - "tagMode": { - "any": "Any", - "all": "All" - }, - "filters": { - "reset": "Reset", - "filter_one": "filter", - "filter_other": "filters" - } - }, "nav": { "admin": "Admin", "assets": "Assets", @@ -93,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 index 12a256c82ea28..cb6db5cc3cec7 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dags.json @@ -1,55 +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": { - "searchPlaceholder": "Search Dags", - "clearSearch": "Clear search", "advancedSearch": "Advanced Search", + "clearSearch": "Clear search", "columns": { "dagId": "Dag ID", - "schedule": "Schedule", - "nextDagRun": "Next Dag Run", "lastDagRun": "Last Dag Run", + "nextDagRun": "Next Dag Run", + "schedule": "Schedule", "tags": "Tags" }, + "ownerLink": "Owner link for {{owner}}", "runs": { - "state": "State", + "duration": "Duration", + "endDate": "End Date", "runAfter": "Run After", "startDate": "Start Date", - "endDate": "End Date", - "duration": "Duration" + "state": "State" }, - "ownerLink": "Owner link for {{owner}}" - }, - "actions": { - "triggerDag": "Trigger Dag", - "trigger": "Trigger", - "delete": "Delete Dag", - "deleteDagWarning": "This will remove all metadata related to the DAG, including DAG Runs and Tasks." - }, - "filters": { - "paused": { - "all": "All", - "active": "Active", - "paused": "Paused" - } + "searchPlaceholder": "Search Dags" }, - "assetSchedule": "{{count}} of {{total}} assets updated", "sort": { - "placeholder": "Sort by", "displayName": { "asc": "Sort by Display Name (A-Z)", "desc": "Sort by Display Name (Z-A)" }, - "nextDagRun": { - "asc": "Sort by Next Dag Run (Earliest-Latest)", - "desc": "Sort by Next Dag Run (Latest-Earliest)" + "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)" }, - "lastRunStartDate": { - "asc": "Sort by Latest Run Start Date (Earliest-Latest)", - "desc": "Sort by Latest Run Start Date (Latest-Earliest)" - } + "nextDagRun": { + "asc": "Sort by Next Dag Run (Earliest-Latest)", + "desc": "Sort by Next Dag Run (Latest-Earliest)" + }, + "placeholder": "Sort by" } }