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 ab3f0eca2a8 Migrate all plaintext to i18n (#51635) ab3f0eca2a8 is described below commit ab3f0eca2a809b7d6f8a7d499538d30ce6d69d09 Author: Brent Bovenzi <br...@astronomer.io> AuthorDate: Thu Jun 12 12:41:47 2025 -0400 Migrate all plaintext to i18n (#51635) Fix tryNumber key Fix run details merge issues Replace all plaintext and change eslint to ERROR Fix zh-tw assets file --- airflow-core/src/airflow/ui/rules/i18next.js | 8 +- .../src/airflow/ui/src/components/ConfigForm.tsx | 4 +- .../src/airflow/ui/src/components/DagVersion.tsx | 5 +- .../airflow/ui/src/components/DagVersionSelect.tsx | 4 +- .../airflow/ui/src/components/DurationChart.tsx | 4 +- .../airflow/ui/src/i18n/locales/de/components.json | 4 +- .../src/airflow/ui/src/i18n/locales/en/assets.json | 31 ++- .../src/airflow/ui/src/i18n/locales/en/browse.json | 5 +- .../src/airflow/ui/src/i18n/locales/en/common.json | 17 ++ .../airflow/ui/src/i18n/locales/en/components.json | 12 +- .../src/airflow/ui/src/i18n/locales/en/dag.json | 26 ++- .../airflow/ui/src/i18n/locales/pl/components.json | 4 +- .../airflow/ui/src/i18n/locales/zh-TW/assets.json | 12 +- .../airflow/ui/src/layouts/Details/Gantt/Gantt.tsx | 21 -- .../airflow/ui/src/layouts/Details/Gantt/index.ts | 20 -- .../src/airflow/ui/src/pages/Asset/AssetLayout.tsx | 4 +- .../ui/src/pages/Asset/CreateAssetEvent.tsx | 6 +- .../ui/src/pages/Asset/CreateAssetEventModal.tsx | 36 ++-- .../src/airflow/ui/src/pages/Asset/Header.tsx | 9 +- .../airflow/ui/src/pages/AssetsList/AssetsList.tsx | 10 +- .../ui/src/pages/Connections/ConnectionForm.tsx | 4 +- airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx | 26 +-- airflow-core/src/airflow/ui/src/pages/Error.tsx | 12 +- .../src/airflow/ui/src/pages/Events/Events.tsx | 4 +- .../src/airflow/ui/src/pages/Run/Details.tsx | 213 ++++++++++----------- .../src/airflow/ui/src/pages/Run/Header.tsx | 20 +- airflow-core/src/airflow/ui/src/pages/Run/Run.tsx | 18 +- .../src/airflow/ui/src/pages/Task/Header.tsx | 45 +++-- .../ui/src/pages/Task/Overview/Overview.tsx | 6 +- .../src/airflow/ui/src/pages/Task/Task.tsx | 16 +- .../ui/src/pages/TaskInstance/BlockingDeps.tsx | 8 +- .../airflow/ui/src/pages/TaskInstance/Details.tsx | 61 +++--- .../ui/src/pages/TaskInstance/ExtraLinks.tsx | 4 +- .../airflow/ui/src/pages/TaskInstance/Header.tsx | 26 ++- .../pages/TaskInstance/Logs/ExternalLogLink.tsx | 4 +- .../ui/src/pages/TaskInstance/Logs/Logs.tsx | 4 +- .../ui/src/pages/TaskInstance/TaskInstance.tsx | 30 +-- .../ui/src/pages/TaskInstance/TriggererInfo.tsx | 75 ++++---- .../src/airflow/ui/src/pages/XCom/XCom.tsx | 7 +- airflow-core/src/airflow/ui/src/utils/TrimText.tsx | 8 +- airflow-core/src/airflow/ui/src/utils/index.ts | 1 - .../src/airflow/ui/src/utils/pluralize.test.ts | 77 -------- airflow-core/src/airflow/ui/src/utils/pluralize.ts | 28 --- 43 files changed, 457 insertions(+), 482 deletions(-) diff --git a/airflow-core/src/airflow/ui/rules/i18next.js b/airflow-core/src/airflow/ui/rules/i18next.js index f255bc1cc03..1ba9fc3ded7 100644 --- a/airflow-core/src/airflow/ui/rules/i18next.js +++ b/airflow-core/src/airflow/ui/rules/i18next.js @@ -22,7 +22,7 @@ */ import i18nextPlugin from "eslint-plugin-i18next"; -import { WARN } from "./levels.js"; +import { ERROR } from "./levels.js"; const allExtensions = "*.{j,t}s{x,}"; @@ -37,6 +37,10 @@ export const i18nextRules = /** @type {const} @satisfies {FlatConfig.Config} */ // Check files in the ui/src directory `src/**/${allExtensions}`, ], + ignores: [ + // Ignore test files + "src/**/*.test.tsx", + ], plugins: { i18next: i18nextPlugin, }, @@ -56,7 +60,7 @@ export const i18nextRules = /** @type {const} @satisfies {FlatConfig.Config} */ * @see [i18next/no-literal-string](https://github.com/edvardchen/eslint-plugin-i18next#no-literal-string) */ "i18next/no-literal-string": [ - WARN, + ERROR, { markupOnly: true, }, diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx index 2f9f408e244..1977d25236d 100644 --- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx @@ -51,7 +51,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({ setErrors, setFormError, }: ConfigFormProps<T>) => { - const { t: translate } = useTranslation("components"); + const { t: translate } = useTranslation(["components", "common"]); const { conf, setConf } = useParamStore(); const validateAndPrettifyJson = (value: string) => { @@ -68,7 +68,7 @@ const ConfigForm = <T extends FieldValues = FieldValues>({ return formattedJson; } catch (error) { - const errorMessage = error instanceof Error ? error.message : translate("configForm.unkownError"); + const errorMessage = error instanceof Error ? error.message : translate("common:error.unknown"); setErrors((prev) => ({ ...prev, diff --git a/airflow-core/src/airflow/ui/src/components/DagVersion.tsx b/airflow-core/src/airflow/ui/src/components/DagVersion.tsx index 68174bfceae..2c65c3c09eb 100644 --- a/airflow-core/src/airflow/ui/src/components/DagVersion.tsx +++ b/airflow-core/src/airflow/ui/src/components/DagVersion.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Text } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import type { DagVersionResponse } from "openapi/requests/types.gen"; @@ -24,13 +25,15 @@ import Time from "./Time"; import { Tooltip } from "./ui"; export const DagVersion = ({ version }: { readonly version: DagVersionResponse | null | undefined }) => { + const { t: translate } = useTranslation("components"); + if (version === null || version === undefined) { return undefined; } return ( <Tooltip content={<Time datetime={version.created_at} />}> - <Text as="span">v{version.version_number}</Text> + <Text as="span">{translate("versionSelect.versionCode", { versionCode: version.version_number })}</Text> </Tooltip> ); }; diff --git a/airflow-core/src/airflow/ui/src/components/DagVersionSelect.tsx b/airflow-core/src/airflow/ui/src/components/DagVersionSelect.tsx index b0f88ed9a46..9ea751d2ed2 100644 --- a/airflow-core/src/airflow/ui/src/components/DagVersionSelect.tsx +++ b/airflow-core/src/airflow/ui/src/components/DagVersionSelect.tsx @@ -91,7 +91,9 @@ export const DagVersionSelect = ({ showLabel = true }: { readonly showLabel?: bo <Select.Content> {versionOptions.items.map((option) => ( <Select.Item item={option} key={option.version.version_number}> - <Text>v{option.version.version_number}</Text> + <Text> + {translate("versionSelect.versionCode", { versionCode: option.version.version_number })} + </Text> <Time datetime={option.version.created_at} /> </Select.Item> ))} diff --git a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx index bf8fb803db7..5d09441284f 100644 --- a/airflow-core/src/airflow/ui/src/components/DurationChart.tsx +++ b/airflow-core/src/airflow/ui/src/components/DurationChart.tsx @@ -102,10 +102,10 @@ export const DurationChart = ({ {entries.length > 1 ? kind === "Dag Run" ? translate("durationChart.lastDagRun_other", { count: entries.length }) - : translate("durationChart.lasttaskInstance_other", { count: entries.length }) + : translate("durationChart.lastTaskInstance_other", { count: entries.length }) : kind === "Dag Run" ? translate("durationChart.lastDagRun_one") - : translate("durationChart.lasttaskInstance_one")} + : translate("durationChart.lastTaskInstance_one")} </Heading> <Bar data={{ diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/de/components.json b/airflow-core/src/airflow/ui/src/i18n/locales/de/components.json index 57f2717f4b6..845a7a62d27 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/de/components.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/de/components.json @@ -41,8 +41,8 @@ "duration": "Laufzeit (Sekunden)", "lastDagRun_one": "Letzter Dag Lauf", "lastDagRun_other": "Letzte {{count}} Dag Läufe", - "lasttaskInstance_one": "Letzte Task Instanz", - "lasttaskInstance_other": "Letzte {{count}} Task Instanzen", + "lastTaskInstance_one": "Letzte Task Instanz", + "lastTaskInstance_other": "Letzte {{count}} Task Instanzen", "queuedDuration": "Zeit in der Warteschlange", "runAfter": "Lauf ab", "runDuration": "Laufzeit" diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/assets.json b/airflow-core/src/airflow/ui/src/i18n/locales/en/assets.json index b927d6891ea..e82019096c8 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/assets.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/assets.json @@ -1,10 +1,29 @@ { - "columns": { - "consumingDags": "Consuming Dags", - "group": "Group", - "lastAssetEvent": "Last Asset Event", - "name": "Name", - "producingTasks": "Producing Tasks" + "consumingDags": "Consuming Dags", + "createEvent": { + "button": "Create Event", + "manual": { + "description": "Directly create an Asset Event", + "extra": "Asset Event Extra", + "label": "Manual" + }, + "materialize": { + "description": "Trigger the Dag upstream of this asset", + "descriptionWithDag": "Trigger the Dag upstream of this asset: {{dagName}}", + "label": "Materialize", + "unpauseDag": "Unpause {{dagName}} on trigger" + }, + "success": { + "manualDescription": "Manual asset event creation was successful.", + "manualTitle": "Asset Event Created", + "materializeDescription": "Upstream Dag {{dagId}} was triggered successfully.", + "materializeTitle": "Materializing Asset" + }, + "title": "Create Asset Event for {{name}}" }, + "group": "Group", + "lastAssetEvent": "Last Asset Event", + "name": "Name", + "producingTasks": "Producing Tasks", "searchPlaceholder": "Search Assets" } diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/browse.json b/airflow-core/src/airflow/ui/src/i18n/locales/en/browse.json index 56d89a8dc5c..8bdbec5aa9c 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/browse.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/browse.json @@ -10,13 +10,14 @@ "user": "User", "when": "When" }, - "title": "Audit Log Events" + "title": "Audit Log" }, "xcom":{ "columns":{ "dag": "Dag", "key": "Key", "value": "Value" - } + }, + "title": "XCom" } } 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 8908acf24e5..92ed06fc380 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 @@ -53,6 +53,7 @@ "queuedAt": "Queued At", "runAfter": "Run After", "runType": "Run Type", + "sourceAssetEvent": "Source Asset Event", "triggeredBy": "Triggered By" }, "dagRun_one": "Dag Run", @@ -68,6 +69,13 @@ }, "duration": "Duration", "endDate": "End Date", + "error": { + "back": "Back", + "defaultMessage": "An unexpected error occurred", + "home": "Home", + "notFound": "Page Not Found", + "title": "Error" + }, "expression": { "all": "All", "and": "AND", @@ -169,6 +177,7 @@ "to": "To" }, "task": { + "documentation": "Task Documentation", "lastInstance": "Last Instance", "operator": "Operator", "triggerRule": "Trigger Rule" @@ -189,6 +198,14 @@ "queue": "Queue", "queuedWhen": "Queued At", "scheduledWhen": "Scheduled At", + "triggerer": { + "assigned": "Assigned triggerer", + "class": "Trigger class", + "createdAt": "Trigger creation time", + "id": "Trigger ID", + "latestHeartbeat": "Latest triggerer heartbeat", + "title": "Triggerer Info" + }, "unixname": "Unix Name" }, "taskInstance_one": "Task Instance", diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/components.json b/airflow-core/src/airflow/ui/src/i18n/locales/en/components.json index 5e1b1abd9d7..d4526010cd1 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/components.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/components.json @@ -28,8 +28,7 @@ "configForm": { "advancedOptions": "Advanced Options", "configJson": "Configuration JSON", - "invalidJson": "Invalid JSON format: {{errorMessage}}", - "unkownError": "Unknown error occurred." + "invalidJson": "Invalid JSON format: {{errorMessage}}" }, "dagWarnings": { "error_one": "1 Error", @@ -41,8 +40,8 @@ "duration": "Duration (seconds)", "lastDagRun_one": "Last Dag Run", "lastDagRun_other": "Last {{count}} Dag Runs", - "lasttaskInstance_one": "Last Task Instance", - "lasttaskInstance_other": "Last {{count}} Task Instances", + "lastTaskInstance_one": "Last Task Instance", + "lastTaskInstance_other": "Last {{count}} Task Instances", "queuedDuration": "Queued Duration", "runAfter": "Run After", "runDuration": "Run Duration" @@ -96,6 +95,11 @@ "title": "Trigger Dag", "unpause": "Unpause {{dagDisplayName}} on trigger" }, + "trimText": { + "details": "Details", + "empty": "Empty", + "noContent": "No content available." + }, "versionDetails": { "bundleLink": "Bundle Link", "bundleName": "Bundle Name", diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/dag.json b/airflow-core/src/airflow/ui/src/i18n/locales/en/dag.json index 4e94582a22d..4a1a3c6f9f6 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/en/dag.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/dag.json @@ -1,10 +1,16 @@ { "allRuns": "All Runs", + "blockingDeps": { + "dependency": "Dependency", + "reason": "Reason", + "title": "Dependencies Blocking Task From Getting Scheduled" + }, "code": { "bundleUrl": "Bundle Url", "noCode": "No Code Found", "parsedAt": "Parsed at:" }, + "extraLinks": "Extra Links", "grid": { "buttons": { "resetToLatest": "Reset to latest", @@ -16,12 +22,18 @@ "dagDocs": "Dag Docs" } }, + "logs": { + "noTryNumber": "No try number", + "viewInExternal": "View logs in {{name}} (attempt {{attempt}})" + }, "overview": { "buttons": { "failedRun_one": "Failed Run", "failedRun_other": "Failed Runs", "failedTask_one": "Failed Task", - "failedTask_other": "Failed Tasks" + "failedTask_other": "Failed Tasks", + "failedTaskInstance_one": "Failed Task Instance", + "failedTaskInstance_other": "Failed Task Instances" }, "charts": { "assetEvent_one": "Created Asset Event", @@ -55,14 +67,20 @@ } }, "tabs": { - "asset_events": "Asset Events", + "assetEvents": "Asset Events", + "auditLog": "Audit Log", "backfills": "Backfills", "code": "Code", "details": "Details", - "events": "Audit Logs", + "logs": "Logs", + "mappedTaskInstances_one": "Task Instance [{{count}}]", + "mappedTaskInstances_other": "Task Instances [{{count}}]", "overview": "Overview", + "renderedTemplates": "Rendered Templates", "runs": "Runs", - "tasks": "Tasks" + "taskInstances": "Task Instances", + "tasks": "Tasks", + "xcom": "XCom" }, "taskGroups": { "collapseAll": "Collapse all task groups", diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/pl/components.json b/airflow-core/src/airflow/ui/src/i18n/locales/pl/components.json index 25526c053ae..363fd119d84 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/pl/components.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/pl/components.json @@ -41,8 +41,8 @@ "duration": "Czas trwania (sekundy)", "lastDagRun_one": "Ostatnie wykonanie Daga", "lastDagRun_other": "Ostatnie {{count}} wykonania Daga", - "lasttaskInstance_one": "Ostatnia instancja zadania", - "lasttaskInstance_other": "Ostatnie {{count}} instancje zadania", + "lastTaskInstance_one": "Ostatnia instancja zadania", + "lastTaskInstance_other": "Ostatnie {{count}} instancje zadania", "queuedDuration": "Czas oczekiwania", "runAfter": "Uruchom po", "runDuration": "Czas trwania wykonania" diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/assets.json b/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/assets.json index 04e46d86abf..133c2177884 100644 --- a/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/assets.json +++ b/airflow-core/src/airflow/ui/src/i18n/locales/zh-TW/assets.json @@ -1,10 +1,8 @@ { - "columns": { - "consumingDags": "消費者 Dags", - "group": "群組", - "lastAssetEvent": "最後資源事件", - "name": "名稱", - "producingTasks": "生產任務" - }, + "consumingDags": "消費者 Dags", + "group": "群組", + "lastAssetEvent": "最後資源事件", + "name": "名稱", + "producingTasks": "生產任務", "searchPlaceholder": "搜尋資源" } diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx deleted file mode 100644 index 90530c4b7b0..00000000000 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/Gantt.tsx +++ /dev/null @@ -1,21 +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 } from "@chakra-ui/react"; - -export const Gantt = () => <Box>gantt</Box>; diff --git a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/index.ts b/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/index.ts deleted file mode 100644 index 24f6dabe4cf..00000000000 --- a/airflow-core/src/airflow/ui/src/layouts/Details/Gantt/index.ts +++ /dev/null @@ -1,20 +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. - */ - -export * from "./Gantt"; diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx index c41be956098..e136bf18ce5 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/AssetLayout.tsx @@ -19,6 +19,7 @@ import { HStack, Box } from "@chakra-ui/react"; import { useReactFlow } from "@xyflow/react"; import { useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { PanelGroup, Panel, PanelResizeHandle } from "react-resizable-panels"; import { useParams } from "react-router-dom"; @@ -33,6 +34,7 @@ import { CreateAssetEvent } from "./CreateAssetEvent"; import { Header } from "./Header"; export const AssetLayout = () => { + const { t: translate } = useTranslation(["assets", "common"]); const { assetId } = useParams(); const { setTableURLState, tableURLState } = useTableURLState(); @@ -51,7 +53,7 @@ export const AssetLayout = () => { const links = [ { label: asset?.name, - title: "Asset", + title: translate("common:asset_one"), value: `/assets/${assetId}`, }, ]; diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx index 6310e6de50d..10b00c9310a 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEvent.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 { AssetResponse } from "openapi/requests/types.gen"; @@ -32,16 +33,17 @@ type Props = { export const CreateAssetEvent = ({ asset, withText = true }: Props) => { const { onClose, onOpen, open } = useDisclosure(); + const { t: translate } = useTranslation("assets"); return ( <Box> <ActionButton - actionName="Create Asset Event" + actionName={translate("createEvent.button")} colorPalette="blue" disabled={asset === undefined} icon={<FiPlay />} onClick={onOpen} - text="Create Asset Event" + text={translate("createEvent.button")} variant="solid" withText={withText} /> diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx index f54e87182d6..212e9d6a187 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.tsx @@ -19,6 +19,7 @@ import { Button, Field, Heading, HStack, VStack, Text } from "@chakra-ui/react"; import { useQueryClient } from "@tanstack/react-query"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { FiPlay } from "react-icons/fi"; import { @@ -52,6 +53,7 @@ type Props = { }; export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { + const { t: translate } = useTranslation(["assets", "components"]); const [eventType, setEventType] = useState("manual"); const [extraError, setExtraError] = useState<string | undefined>(); const [unpause, setUnpause] = useState(true); @@ -82,7 +84,7 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { setExtra(formattedJson); // Update only if the value is different } } catch (error) { - const errorMessage = error instanceof Error ? error.message : "Unknown error occurred."; + const errorMessage = error instanceof Error ? error.message : translate("common:error.unknown"); setExtraError(errorMessage); } @@ -107,14 +109,14 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { ]; toaster.create({ - description: `Upstream Dag ${response.dag_id} was triggered successfully.`, - title: "Materializing Asset", + description: translate("createEvent.success.materializeDescription", { dagId: response.dag_id }), + title: translate("createEvent.success.materializeTitle"), type: "success", }); } else { toaster.create({ - description: "Manual asset event creation was successful.", - title: "Asset Event Created", + description: translate("createEvent.success.manualDescription"), + title: translate("createEvent.success.manualTitle"), type: "success", }); } @@ -164,7 +166,7 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { <Dialog.Content backdrop> <Dialog.Header paddingBottom={0}> <VStack align="start" gap={4}> - <Heading size="xl">Create Asset Event for {asset.name}</Heading> + <Heading size="xl">{translate("createEvent.title", { name: asset.name })}</Heading> </VStack> </Dialog.Header> @@ -180,24 +182,34 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { > <HStack align="stretch"> <RadioCardItem - description={`Trigger the Dag upstream of this asset${upstreamDagId === undefined ? "" : `: ${dag?.dag_display_name ?? upstreamDagId}`}`} + description={ + upstreamDagId === undefined + ? translate("createEvent.materialize.description") + : translate("createEvent.materialize.descriptionWithDag", { + dagName: dag?.dag_display_name ?? upstreamDagId, + }) + } disabled={!hasUpstreamDag} - label="Materialize" + label={translate("createEvent.materialize.label")} value="materialize" /> - <RadioCardItem description="Directly create an Asset Event" label="Manual" value="manual" /> + <RadioCardItem + description={translate("createEvent.manual.description")} + label={translate("createEvent.manual.label")} + value="manual" + /> </HStack> </RadioCardRoot> {eventType === "manual" ? ( <Field.Root mt={6}> - <Field.Label fontSize="md">Asset Event Extra</Field.Label> + <Field.Label fontSize="md">{translate("createEvent.manual.extra")}</Field.Label> <JsonEditor onChange={validateAndPrettifyJson} value={extra} /> <Text color="fg.error">{extraError}</Text> </Field.Root> ) : undefined} {eventType === "materialize" && dag?.is_paused ? ( <Checkbox checked={unpause} colorPalette="blue" onChange={() => setUnpause(!unpause)}> - Unpause {dag.dag_display_name} on trigger + {translate("createEvent.materialize.unpauseDag", { dagName: dag.dag_display_name })} </Checkbox> ) : undefined} <ErrorAlert error={eventType === "manual" ? manualError : materializeError} /> @@ -209,7 +221,7 @@ export const CreateAssetEventModal = ({ asset, onClose, open }: Props) => { loading={isPending || isMaterializePending} onClick={handleSubmit} > - <FiPlay /> Create Event + <FiPlay /> {translate("createEvent.button")} </Button> </Dialog.Footer> </Dialog.Content> diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx index b7fdfa46043..0ab20fa3e73 100644 --- a/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Asset/Header.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { useTranslation } from "react-i18next"; import { FiDatabase } from "react-icons/fi"; import type { AssetResponse } from "openapi/requests/types.gen"; @@ -30,14 +31,16 @@ export const Header = ({ readonly asset?: AssetResponse; readonly isRefreshing?: boolean; }) => { + const { t: translate } = useTranslation("assets"); + const stats = [ - { label: "Group", value: asset?.group }, + { label: translate("group"), value: asset?.group }, { - label: "Producing Tasks", + label: translate("producingTasks"), value: <DependencyPopover dependencies={asset?.producing_tasks ?? []} type="Task" />, }, { - label: "Consuming Dags", + label: translate("consumingDags"), value: <DependencyPopover dependencies={asset?.consuming_dags ?? []} type="Dag" />, }, ]; diff --git a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx index d05404f7ff5..51767172c67 100644 --- a/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx +++ b/airflow-core/src/airflow/ui/src/pages/AssetsList/AssetsList.tsx @@ -44,7 +44,7 @@ const createColumns = (translate: (key: string) => string): Array<ColumnDef<Asse <RouterLink to={`/assets/${original.id}`}>{original.name}</RouterLink> </Link> ), - header: () => translate("columns.name"), + header: () => translate("name"), }, { accessorKey: "last_asset_event", @@ -59,12 +59,12 @@ const createColumns = (translate: (key: string) => string): Array<ColumnDef<Asse return <Time datetime={timestamp} />; }, enableSorting: false, - header: () => translate("columns.lastAssetEvent"), + header: () => translate("lastAssetEvent"), }, { accessorKey: "group", enableSorting: false, - header: () => translate("columns.group"), + header: () => translate("group"), }, { accessorKey: "consuming_dags", @@ -73,7 +73,7 @@ const createColumns = (translate: (key: string) => string): Array<ColumnDef<Asse <DependencyPopover dependencies={original.consuming_dags} type="Dag" /> ) : undefined, enableSorting: false, - header: () => translate("columns.consumingDags"), + header: () => translate("consumingDags"), }, { accessorKey: "producing_tasks", @@ -82,7 +82,7 @@ const createColumns = (translate: (key: string) => string): Array<ColumnDef<Asse <DependencyPopover dependencies={original.producing_tasks} type="Task" /> ) : undefined, enableSorting: false, - header: () => translate("columns.producingTasks"), + header: () => translate("producingTasks"), }, { accessorKey: "trigger", diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx index 2badcd37966..bf921a1de3c 100644 --- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx @@ -66,7 +66,7 @@ const ConnectionForm = ({ mode: "onBlur", }); - const { t: translate } = useTranslation("admin"); + const { t: translate } = useTranslation(["admin", "common"]); const selectedConnType = watch("conn_type"); // Get the selected connection type const standardFields = connectionTypeMeta[selectedConnType]?.standard_fields ?? {}; const paramsDic = { paramsDict: connectionTypeMeta[selectedConnType]?.extra_fields ?? ({} as ParamsSpec) }; @@ -107,7 +107,7 @@ const ConnectionForm = ({ return formattedJson; } catch (error_) { - const errorMessage = error_ instanceof Error ? error_.message : "Unknown error occurred."; + const errorMessage = error_ instanceof Error ? error_.message : translate("common:error.unknown"); setErrors((prev) => ({ ...prev, diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx index 5190a357853..cca182140b8 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Dag.tsx @@ -34,20 +34,20 @@ import { isStatePending, useAutoRefresh } from "src/utils"; import { Header } from "./Header"; -const getTabs = (translate: (key: string) => string) => [ - { icon: <LuChartColumn />, label: translate("tabs.overview"), value: "" }, - { icon: <FiBarChart />, label: translate("tabs.runs"), value: "runs" }, - { icon: <TaskIcon />, label: translate("tabs.tasks"), value: "tasks" }, - { icon: <RiArrowGoBackFill />, label: translate("tabs.backfills"), value: "backfills" }, - { icon: <MdOutlineEventNote />, label: translate("tabs.events"), value: "events" }, - { icon: <FiCode />, label: translate("tabs.code"), value: "code" }, - { icon: <MdDetails />, label: translate("tabs.details"), value: "details" }, -]; - export const Dag = () => { const { t: translate } = useTranslation("dag"); const { dagId = "" } = useParams(); + const tabs = [ + { icon: <LuChartColumn />, label: translate("tabs.overview"), value: "" }, + { icon: <FiBarChart />, label: translate("tabs.runs"), value: "runs" }, + { icon: <TaskIcon />, label: translate("tabs.tasks"), value: "tasks" }, + { icon: <RiArrowGoBackFill />, label: translate("tabs.backfills"), value: "backfills" }, + { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: "events" }, + { icon: <FiCode />, label: translate("tabs.code"), value: "code" }, + { icon: <MdDetails />, label: translate("tabs.details"), value: "details" }, + ]; + const { data: dag, error, @@ -89,15 +89,15 @@ export const Dag = () => { } satisfies DAGWithLatestDagRunsResponse; } + const displayTabs = tabs.filter((tab) => !(dag?.timetable_summary === null && tab.value === "backfills")); + return ( <ReactFlowProvider> <DetailsLayout dag={dag} error={error ?? runsError} isLoading={isLoading || isLoadingRuns} - tabs={getTabs(translate).filter( - (tab) => !(dag?.timetable_summary === null && tab.value === "backfills"), - )} + tabs={displayTabs} > <Header dag={dag} diff --git a/airflow-core/src/airflow/ui/src/pages/Error.tsx b/airflow-core/src/airflow/ui/src/pages/Error.tsx index 525cbb9e764..f486c9e7306 100644 --- a/airflow-core/src/airflow/ui/src/pages/Error.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Error.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Box, VStack, Heading, Text, Button, Container, HStack, Code } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useNavigate, useRouteError, isRouteErrorResponse } from "react-router-dom"; import { AirflowPin } from "src/assets/AirflowPin"; @@ -24,15 +25,16 @@ import { AirflowPin } from "src/assets/AirflowPin"; export const ErrorPage = () => { const navigate = useNavigate(); const error = useRouteError(); + const { t: translate } = useTranslation(); - let errorMessage = "An unexpected error occurred"; + let errorMessage = translate("error.defaultMessage"); let statusCode = ""; if (isRouteErrorResponse(error)) { statusCode = String(error.status); errorMessage = ((error as unknown as Error).message || (error as { statusText?: string }).statusText) ?? - "Page Not Found"; + translate("error.notFound"); } else if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === "string") { @@ -49,7 +51,7 @@ export const ErrorPage = () => { <AirflowPin height="50px" width="50px" /> <VStack gap={4}> - <Heading>{statusCode || "Error"}</Heading> + <Heading>{statusCode || translate("error.title")}</Heading> <Text fontSize="lg">{errorMessage}</Text> {error instanceof Error && isDev ? ( <Code borderRadius="md" fontSize="sm" p={3} width="full"> @@ -60,10 +62,10 @@ export const ErrorPage = () => { <HStack gap={4}> <Button colorPalette="blue" onClick={() => navigate(-1)} size="lg"> - Back + {translate("error.back")} </Button> <Button colorPalette="blue" onClick={() => navigate("/")} size="lg" variant="outline"> - Home + {translate("error.home")} </Button> </HStack> </VStack> diff --git a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx index a3b306a4af3..f71e57b6419 100644 --- a/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Events/Events.tsx @@ -167,7 +167,9 @@ export const Events = () => { return ( <Box> <Flex alignItems="center" justifyContent="space-between"> - <Heading>{translate("auditLog.title")}</Heading> + {dagId === undefined && runId === undefined && taskId === undefined ? ( + <Heading size="md">{translate("auditLog.title")}</Heading> + ) : undefined} <ButtonGroup attached mt="1" size="sm" variant="surface"> <IconButton aria-label={translate("auditLog.actions.expandAllExtra")} diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx index 75860092487..23d525b0edf 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/Details.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, HStack, StackSeparator, Table, Text, VStack } from "@chakra-ui/react"; +import { Flex, HStack, StackSeparator, Table, Text, VStack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { useDagRunServiceGetDagRun } from "openapi/queries"; @@ -29,6 +30,7 @@ import { ClipboardRoot, ClipboardIconButton } from "src/components/ui"; import { getDuration, isStatePending, useAutoRefresh } from "src/utils"; export const Details = () => { + const { t: translate } = useTranslation(["common", "components"]); const { dagId = "", runId = "" } = useParams(); const refetchInterval = useAutoRefresh({ dagId }); @@ -42,112 +44,109 @@ export const Details = () => { { refetchInterval: (query) => (isStatePending(query.state.data?.state) ? refetchInterval : false) }, ); - // TODO : Render DagRun configuration object + if (!dagRun) { + return undefined; + } + return ( - <Box p={2}> - {dagRun === undefined ? ( - <div /> - ) : ( - <Table.Root striped> - <Table.Body> - <Table.Row> - <Table.Cell>State</Table.Cell> - <Table.Cell> - <Flex gap={1}> - <StateBadge state={dagRun.state} /> - {dagRun.state} - </Flex> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Run ID</Table.Cell> - <Table.Cell> - <HStack> - {dagRun.dag_run_id} - <ClipboardRoot value={dagRun.dag_run_id}> - <ClipboardIconButton /> - </ClipboardRoot> - </HStack> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Run Type</Table.Cell> - <Table.Cell> - <HStack> - <RunTypeIcon runType={dagRun.run_type} /> - <Text>{dagRun.run_type}</Text> - </HStack> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Run Duration</Table.Cell> - <Table.Cell>{getDuration(dagRun.start_date, dagRun.end_date)}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Last Scheduling Decision</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.last_scheduling_decision} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Queued at</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.queued_at} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Start Date</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.start_date} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>End Date</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.end_date} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Data Interval Start</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.data_interval_start} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Data Interval End</Table.Cell> - <Table.Cell> - <Time datetime={dagRun.data_interval_end} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Trigger Source</Table.Cell> - <Table.Cell>{dagRun.triggered_by}</Table.Cell> - </Table.Row> - {dagRun.bundle_version !== null && ( - <Table.Row> - <Table.Cell>Bundle Version</Table.Cell> - <Table.Cell>{dagRun.bundle_version}</Table.Cell> - </Table.Row> - )} - <Table.Row> - <Table.Cell>Dag Version(s)</Table.Cell> - <Table.Cell> - <VStack separator={<StackSeparator />}> - {dagRun.dag_versions.map((dagVersion) => ( - <DagVersionDetails dagVersion={dagVersion} key={dagVersion.id} /> - ))} - </VStack> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Run Config</Table.Cell> - <Table.Cell> - <RenderedJsonField content={dagRun.conf ?? {}} /> - </Table.Cell> - </Table.Row> - </Table.Body> - </Table.Root> - )} - </Box> + <Table.Root striped> + <Table.Body> + <Table.Row> + <Table.Cell>{translate("state")}</Table.Cell> + <Table.Cell> + <Flex gap={1}> + <StateBadge state={dagRun.state} /> + {dagRun.state} + </Flex> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("runId")}</Table.Cell> + <Table.Cell> + <HStack> + {dagRun.dag_run_id} + <ClipboardRoot value={dagRun.dag_run_id}> + <ClipboardIconButton /> + </ClipboardRoot> + </HStack> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.runType")}</Table.Cell> + <Table.Cell> + <HStack> + <RunTypeIcon runType={dagRun.run_type} /> + <Text>{dagRun.run_type}</Text> + </HStack> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("duration")}</Table.Cell> + <Table.Cell>{getDuration(dagRun.start_date, dagRun.end_date)}</Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.lastSchedulingDecision")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.last_scheduling_decision} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.queuedAt")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.queued_at} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("startDate")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.start_date} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("endDate")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.end_date} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.dataIntervalStart")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.data_interval_start} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.dataIntervalEnd")}</Table.Cell> + <Table.Cell> + <Time datetime={dagRun.data_interval_end} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.triggeredBy")}</Table.Cell> + <Table.Cell>{dagRun.triggered_by}</Table.Cell> + </Table.Row> + {dagRun.bundle_version !== null && ( + <Table.Row> + <Table.Cell>{translate("components:versionDetails.bundleVersion")}</Table.Cell> + <Table.Cell>{dagRun.bundle_version}</Table.Cell> + </Table.Row> + )} + <Table.Row> + <Table.Cell>{translate("dagRun.dagVersions")}</Table.Cell> + <Table.Cell> + <VStack separator={<StackSeparator />}> + {dagRun.dag_versions.map((dagVersion) => ( + <DagVersionDetails dagVersion={dagVersion} key={dagVersion.id} /> + ))} + </VStack> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("dagRun.conf")}</Table.Cell> + <Table.Cell> + <RenderedJsonField content={dagRun.conf ?? {}} /> + </Table.Cell> + </Table.Row> + </Table.Body> + </Table.Root> ); }; diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx index c412da36b9d..2a338baf3a1 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/Header.tsx @@ -18,6 +18,7 @@ */ import { HStack, Text, Box } from "@chakra-ui/react"; import { useCallback, useState, useRef } from "react"; +import { useTranslation } from "react-i18next"; import { FiBarChart, FiMessageSquare } from "react-icons/fi"; import type { DAGRunResponse } from "openapi/requests/types.gen"; @@ -39,6 +40,7 @@ export const Header = ({ readonly dagRun: DAGRunResponse; readonly isRefreshing?: boolean; }) => { + const { t: translate } = useTranslation(); const [note, setNote] = useState<string | null>(dagRun.note); const dagId = dagRun.dag_id; @@ -67,14 +69,14 @@ export const Header = ({ actions={ <> <EditableMarkdownButton - header="Dag Run Note" + header={translate("note.dagRun")} icon={<FiMessageSquare />} isPending={isPending} mdContent={note} onConfirm={onConfirm} - placeholder="Add a note..." + placeholder={translate("note.placeholder")} setMdContent={setNote} - text={Boolean(dagRun.note) ? "Note" : "Add a note"} + text={Boolean(dagRun.note) ? translate("note.label") : translate("note.add")} withText={containerWidth > 700} /> <ClearRunButton dagRun={dagRun} isHotkeyEnabled withText={containerWidth > 700} /> @@ -89,12 +91,12 @@ export const Header = ({ ? [] : [ { - label: "Logical Date", + label: translate("logicalDate"), value: <Time datetime={dagRun.logical_date} />, }, ]), { - label: "Run Type", + label: translate("dagRun.runType"), value: ( <HStack> <RunTypeIcon runType={dagRun.run_type} /> @@ -102,11 +104,11 @@ export const Header = ({ </HStack> ), }, - { label: "Start", value: <Time datetime={dagRun.start_date} /> }, - { label: "End", value: <Time datetime={dagRun.end_date} /> }, - { label: "Duration", value: getDuration(dagRun.start_date, dagRun.end_date) }, + { label: translate("startDate"), value: <Time datetime={dagRun.start_date} /> }, + { label: translate("endDate"), value: <Time datetime={dagRun.end_date} /> }, + { label: translate("duration"), value: getDuration(dagRun.start_date, dagRun.end_date) }, { - label: "Dag Version(s)", + label: translate("dagRun.dagVersions"), value: ( <LimitedItemsList items={dagRun.dag_versions.map((version) => ( diff --git a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx index 5b4dfffd3e5..0b9d8c9db94 100644 --- a/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Run/Run.tsx @@ -17,6 +17,7 @@ * under the License. */ import { ReactFlowProvider } from "@xyflow/react"; +import { useTranslation } from "react-i18next"; import { FiCode, FiDatabase } from "react-icons/fi"; import { MdDetails, MdOutlineEventNote, MdOutlineTask } from "react-icons/md"; import { useParams } from "react-router-dom"; @@ -27,17 +28,18 @@ import { isStatePending, useAutoRefresh } from "src/utils"; import { Header } from "./Header"; -const tabs = [ - { icon: <MdOutlineTask />, label: "Task Instances", value: "" }, - { icon: <MdOutlineEventNote />, label: "Audit Logs", value: "events" }, - { icon: <FiCode />, label: "Code", value: "code" }, - { icon: <MdDetails />, label: "Details", value: "details" }, - { icon: <FiDatabase />, label: "Asset Events", value: "asset_events" }, -]; - export const Run = () => { + const { t: translate } = useTranslation("dag"); const { dagId = "", runId = "" } = useParams(); + const tabs = [ + { icon: <MdOutlineTask />, label: translate("tabs.taskInstances"), value: "" }, + { icon: <FiDatabase />, label: translate("tabs.assetEvents"), value: "asset_events" }, + { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: "events" }, + { icon: <FiCode />, label: translate("tabs.code"), value: "code" }, + { icon: <MdDetails />, label: translate("tabs.details"), value: "details" }, + ]; + const refetchInterval = useAutoRefresh({ dagId }); const { diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Header.tsx b/airflow-core/src/airflow/ui/src/pages/Task/Header.tsx index e90793c2f12..2681dd52e39 100644 --- a/airflow-core/src/airflow/ui/src/pages/Task/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Task/Header.tsx @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +import { useTranslation } from "react-i18next"; import { FiBookOpen } from "react-icons/fi"; import type { TaskResponse } from "openapi/requests/types.gen"; @@ -23,23 +24,27 @@ import { TaskIcon } from "src/assets/TaskIcon"; import DisplayMarkdownButton from "src/components/DisplayMarkdownButton"; import { HeaderCard } from "src/components/HeaderCard"; -export const Header = ({ task }: { readonly task: TaskResponse }) => ( - <HeaderCard - actions={ - task.doc_md === null ? undefined : ( - <DisplayMarkdownButton - header="Task Documentation" - icon={<FiBookOpen />} - mdContent={task.doc_md} - text="Task Docs" - /> - ) - } - icon={<TaskIcon />} - stats={[ - { label: "Operator", value: task.operator_name }, - { label: "Trigger Rule", value: task.trigger_rule }, - ]} - title={`${task.task_display_name}${task.is_mapped ? " [ ]" : ""}`} - /> -); +export const Header = ({ task }: { readonly task: TaskResponse }) => { + const { t: translate } = useTranslation(); + + return ( + <HeaderCard + actions={ + task.doc_md === null ? undefined : ( + <DisplayMarkdownButton + header={translate("task.documentation")} + icon={<FiBookOpen />} + mdContent={task.doc_md} + text={translate("docs.documentation")} + /> + ) + } + icon={<TaskIcon />} + stats={[ + { label: translate("task.operator"), value: task.operator_name }, + { label: translate("task.triggerRule"), value: task.trigger_rule }, + ]} + title={`${task.task_display_name}${task.is_mapped ? " [ ]" : ""}`} + /> + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx index e36a613eb61..79b39d21b9f 100644 --- a/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Task/Overview/Overview.tsx @@ -19,6 +19,7 @@ import { Box, HStack, Skeleton, SimpleGrid } from "@chakra-ui/react"; import dayjs from "dayjs"; import { useState } from "react"; +import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { useTaskInstanceServiceGetTaskInstances } from "openapi/queries"; @@ -31,6 +32,7 @@ const defaultHour = "24"; export const Overview = () => { const { dagId = "", groupId, taskId } = useParams(); + const { t: translate } = useTranslation("dag"); const now = dayjs(); const [startDate, setStartDate] = useState(now.subtract(Number(defaultHour), "hour").toISOString()); @@ -86,7 +88,9 @@ export const Overview = () => { timestamp: ti.start_date ?? ti.logical_date, }))} isLoading={isFailedTaskInstancesLoading} - label="Failed Task Instance" + label={translate("overview.buttons.failedTaskInstance", { + count: failedTaskInstances?.total_entries ?? 0, + })} route={{ pathname: "task_instances", search: "state=failed", diff --git a/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx b/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx index 0dc3dce538c..1095642b3d2 100644 --- a/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Task/Task.tsx @@ -17,6 +17,7 @@ * under the License. */ import { ReactFlowProvider } from "@xyflow/react"; +import { useTranslation } from "react-i18next"; import { LuChartColumn } from "react-icons/lu"; import { MdOutlineEventNote, MdOutlineTask } from "react-icons/md"; import { useParams } from "react-router-dom"; @@ -28,16 +29,17 @@ import { getGroupTask } from "src/utils/groupTask"; import { GroupTaskHeader } from "./GroupTaskHeader"; import { Header } from "./Header"; -const tabs = [ - { icon: <LuChartColumn />, label: "Overview", value: "" }, - { icon: <MdOutlineTask />, label: "Task Instances", value: "task_instances" }, - { icon: <MdOutlineEventNote />, label: "Audit Logs", value: "events" }, -]; - export const Task = () => { + const { t: translate } = useTranslation("dag"); const { dagId = "", groupId, taskId } = useParams(); - const displayTabs = groupId === undefined ? tabs : tabs.filter((tab) => tab.label !== "Events"); + const tabs = [ + { icon: <LuChartColumn />, label: translate("tabs.overview"), value: "" }, + { icon: <MdOutlineTask />, label: translate("tabs.taskInstances"), value: "task_instances" }, + { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: "events" }, + ]; + + const displayTabs = groupId === undefined ? tabs : tabs.filter((tab) => tab.value !== "events"); const { data: task, diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/BlockingDeps.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/BlockingDeps.tsx index e14502b7475..2ed76613df7 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/BlockingDeps.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/BlockingDeps.tsx @@ -17,11 +17,13 @@ * under the License. */ import { Box, Table, Heading } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useTaskInstanceServiceGetTaskInstanceDependencies } from "openapi/queries"; import type { TaskInstanceResponse } from "openapi/requests/types.gen"; export const BlockingDeps = ({ taskInstance }: { readonly taskInstance: TaskInstanceResponse }) => { + const { t: translate } = useTranslation(); const { data } = useTaskInstanceServiceGetTaskInstanceDependencies({ dagId: taskInstance.dag_id, dagRunId: taskInstance.dag_run_id, @@ -36,13 +38,13 @@ export const BlockingDeps = ({ taskInstance }: { readonly taskInstance: TaskInst return ( <Box flexGrow={1} mt={3}> <Heading py={2} size="sm"> - Dependencies Blocking Task From Getting Scheduled + {translate("dag.blockingDeps.title")} </Heading> <Table.Root striped> <Table.Body> <Table.Row> - <Table.ColumnHeader>Dependency</Table.ColumnHeader> - <Table.ColumnHeader>Reason</Table.ColumnHeader> + <Table.ColumnHeader>{translate("dag.blockingDeps.dependency")}</Table.ColumnHeader> + <Table.ColumnHeader>{translate("dag.blockingDeps.reason")}</Table.ColumnHeader> </Table.Row> {data.dependencies.map((dep) => ( <Table.Row key={dep.name}> diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx index 9fed186008f..62ef8472e0d 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Details.tsx @@ -16,7 +16,8 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Flex, HStack, Table, Heading } from "@chakra-ui/react"; +import { Box, Flex, HStack, Table } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useParams, useSearchParams } from "react-router-dom"; import { @@ -35,6 +36,7 @@ import { ExtraLinks } from "./ExtraLinks"; import { TriggererInfo } from "./TriggererInfo"; export const Details = () => { + const { t: translate } = useTranslation(); const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); @@ -94,22 +96,19 @@ export const Details = () => { {taskInstance !== undefined && (taskInstance.trigger ?? taskInstance.triggerer_job) ? ( <TriggererInfo taskInstance={taskInstance} /> ) : undefined} - <Heading py={2} size="sm"> - Task Instance Info - </Heading> <Table.Root striped> <Table.Body> <Table.Row> - <Table.Cell>State</Table.Cell> + <Table.Cell>{translate("state")}</Table.Cell> <Table.Cell> <Flex gap={1}> <StateBadge state={tryInstance?.state} /> - {tryInstance?.state ?? "no status"} + {tryInstance?.state ?? translate("states.no_status")} </Flex> </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Task ID</Table.Cell> + <Table.Cell>{translate("taskId")}</Table.Cell> <Table.Cell> <HStack> {tryInstance?.task_id} @@ -120,7 +119,7 @@ export const Details = () => { </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Run ID</Table.Cell> + <Table.Cell>{translate("runId")}</Table.Cell> <Table.Cell> <HStack> {tryInstance?.dag_run_id} @@ -131,15 +130,15 @@ export const Details = () => { </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Map Index</Table.Cell> + <Table.Cell>{translate("mapIndex")}</Table.Cell> <Table.Cell>{tryInstance?.map_index}</Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Operator</Table.Cell> + <Table.Cell>{translate("task.operator")}</Table.Cell> <Table.Cell>{tryInstance?.operator}</Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Duration</Table.Cell> + <Table.Cell>{translate("duration")}</Table.Cell> <Table.Cell> {Boolean(tryInstance?.start_date) // eslint-disable-next-line unicorn/no-null ? getDuration(tryInstance?.start_date ?? null, tryInstance?.end_date ?? null) @@ -147,25 +146,25 @@ export const Details = () => { </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Started</Table.Cell> + <Table.Cell>{translate("startDate")}</Table.Cell> <Table.Cell> <Time datetime={tryInstance?.start_date} /> </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Ended</Table.Cell> + <Table.Cell>{translate("endDate")}</Table.Cell> <Table.Cell> <Time datetime={tryInstance?.end_date} /> </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Dag Version</Table.Cell> + <Table.Cell>{translate("taskInstance.dagVersion")}</Table.Cell> <Table.Cell> <DagVersionDetails dagVersion={taskInstance?.dag_version} /> </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Process ID (PID)</Table.Cell> + <Table.Cell>{translate("taskInstance.pid")}</Table.Cell> <Table.Cell> <HStack> {tryInstance?.pid} @@ -176,7 +175,7 @@ export const Details = () => { </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Hostname</Table.Cell> + <Table.Cell>{translate("taskInstance.hostname")}</Table.Cell> <Table.Cell> <HStack> {tryInstance?.hostname} @@ -187,37 +186,25 @@ export const Details = () => { </Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Pool</Table.Cell> - <Table.Cell>{tryInstance?.pool}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Pool Slots</Table.Cell> - <Table.Cell>{tryInstance?.pool_slots}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Executor</Table.Cell> - <Table.Cell>{tryInstance?.executor}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Executor Config</Table.Cell> - <Table.Cell>{tryInstance?.executor_config}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Unix Name</Table.Cell> + <Table.Cell>{translate("taskInstance.unixname")}</Table.Cell> <Table.Cell>{tryInstance?.unixname}</Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Max Tries</Table.Cell> - <Table.Cell>{tryInstance?.max_tries}</Table.Cell> + <Table.Cell>{translate("taskInstance.pool")}</Table.Cell> + <Table.Cell>{tryInstance?.pool}</Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Queue</Table.Cell> + <Table.Cell>{translate("taskInstance.queue")}</Table.Cell> <Table.Cell>{tryInstance?.queue}</Table.Cell> </Table.Row> <Table.Row> - <Table.Cell>Priority Weight</Table.Cell> + <Table.Cell>{translate("taskInstance.priorityWeight")}</Table.Cell> <Table.Cell>{tryInstance?.priority_weight}</Table.Cell> </Table.Row> + <Table.Row> + <Table.Cell>{translate("taskInstance.executor")}</Table.Cell> + <Table.Cell>{tryInstance?.executor_config}</Table.Cell> + </Table.Row> </Table.Body> </Table.Root> </Box> diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/ExtraLinks.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/ExtraLinks.tsx index efad2007b4e..167346175eb 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/ExtraLinks.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/ExtraLinks.tsx @@ -17,11 +17,13 @@ * under the License. */ import { Box, Button, Heading, HStack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { useTaskInstanceServiceGetExtraLinks } from "openapi/queries"; export const ExtraLinks = () => { + const { t: translate } = useTranslation(); const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams(); const { data } = useTaskInstanceServiceGetExtraLinks({ @@ -33,7 +35,7 @@ export const ExtraLinks = () => { return data && Object.keys(data.extra_links).length > 0 ? ( <Box py={1}> - <Heading size="sm">Extra Links</Heading> + <Heading size="sm">{translate("dag.extraLinks")}</Heading> <HStack gap={2} py={2}> {Object.entries(data.extra_links).map(([key, value], _) => value === null ? undefined : ( diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx index 702060ae285..34e80d3716a 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Header.tsx @@ -18,6 +18,7 @@ */ import { Box } from "@chakra-ui/react"; import { useCallback, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; import { FiMessageSquare } from "react-icons/fi"; import { MdOutlineTask } from "react-icons/md"; @@ -38,20 +39,25 @@ export const Header = ({ readonly isRefreshing?: boolean; readonly taskInstance: TaskInstanceResponse; }) => { + const { t: translate } = useTranslation(); const containerRef = useRef<HTMLDivElement>(); const containerWidth = useContainerWidth(containerRef); const stats = [ - { label: "Operator", value: taskInstance.operator }, - ...(taskInstance.map_index > -1 ? [{ label: "Map Index", value: taskInstance.rendered_map_index }] : []), - ...(taskInstance.try_number > 1 ? [{ label: "Try Number", value: taskInstance.try_number }] : []), - { label: "Start", value: <Time datetime={taskInstance.start_date} /> }, - { label: "End", value: <Time datetime={taskInstance.end_date} /> }, + { label: translate("task.operator"), value: taskInstance.operator }, + ...(taskInstance.map_index > -1 + ? [{ label: translate("mapIndex"), value: taskInstance.rendered_map_index }] + : []), + ...(taskInstance.try_number > 1 + ? [{ label: translate("tryNumber"), value: taskInstance.try_number }] + : []), + { label: translate("startDate"), value: <Time datetime={taskInstance.start_date} /> }, + { label: translate("endDate"), value: <Time datetime={taskInstance.end_date} /> }, ...(Boolean(taskInstance.start_date) - ? [{ label: "Duration", value: getDuration(taskInstance.start_date, taskInstance.end_date) }] + ? [{ label: translate("duration"), value: getDuration(taskInstance.start_date, taskInstance.end_date) }] : []), { - label: "DAG Version", + label: translate("taskInstance.dagVersion"), value: <DagVersion version={taskInstance.dag_version} />, }, ]; @@ -88,14 +94,14 @@ export const Header = ({ actions={ <> <EditableMarkdownButton - header="Task Instance Note" + header={translate("note.taskInstance")} icon={<FiMessageSquare />} isPending={isPending} mdContent={note} onConfirm={onConfirm} - placeholder="Add a note..." + placeholder={translate("note.placeholder")} setMdContent={setNote} - text={Boolean(taskInstance.note) ? "Note" : "Add a note"} + text={Boolean(taskInstance.note) ? translate("note.label") : translate("note.add")} withText={containerWidth > 700} /> <ClearTaskInstanceButton diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/ExternalLogLink.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/ExternalLogLink.tsx index e54d2bbd873..50aafc5d773 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/ExternalLogLink.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/ExternalLogLink.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Button } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; import { useTaskInstanceServiceGetExternalLogUrl } from "openapi/queries"; @@ -29,6 +30,7 @@ type Props = { }; export const ExternalLogLink = ({ externalLogName, taskInstance, tryNumber }: Props) => { + const { t: translate } = useTranslation("dag"); const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams(); const { @@ -57,7 +59,7 @@ export const ExternalLogLink = ({ externalLogName, taskInstance, tryNumber }: Pr return ( <Button asChild colorScheme="blue" variant="outline"> <a href={externalLogData.url} rel="noopener noreferrer" target="_blank"> - View logs in {externalLogName} (attempt {tryNumber}) + {translate("logs.viewInExternal", { attempt: tryNumber, name: externalLogName })} </a> </Button> ); diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx index 25fce170dd0..96e9d06fccc 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/Logs/Logs.tsx @@ -19,6 +19,7 @@ import { Box, Heading, VStack } from "@chakra-ui/react"; import { useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; +import { useTranslation } from "react-i18next"; import { useParams, useSearchParams } from "react-router-dom"; import { useTaskInstanceServiceGetMappedTaskInstance } from "openapi/queries"; @@ -34,6 +35,7 @@ import { TaskLogHeader } from "./TaskLogHeader"; export const Logs = () => { const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams(); const [searchParams, setSearchParams] = useSearchParams(); + const { t: translate } = useTranslation("dag"); const tryNumberParam = searchParams.get(SearchParamsKeys.TRY_NUMBER); const logLevelFilters = searchParams.getAll(SearchParamsKeys.LOG_LEVEL); @@ -104,7 +106,7 @@ export const Logs = () => { /> {showExternalLogRedirect && externalLogName && taskInstance ? ( tryNumber === undefined ? ( - <p>No try number</p> + <p>{translate("logs.noTryNumber")}</p> ) : ( <ExternalLogLink externalLogName={externalLogName} diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx index c11fce76741..c7fa69d6811 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TaskInstance.tsx @@ -17,6 +17,7 @@ * under the License. */ import { ReactFlowProvider } from "@xyflow/react"; +import { useTranslation } from "react-i18next"; import { FiCode, FiDatabase } from "react-icons/fi"; import { MdDetails, MdOutlineEventNote, MdOutlineTask, MdReorder, MdSyncAlt } from "react-icons/md"; import { PiBracketsCurlyBold } from "react-icons/pi"; @@ -32,19 +33,24 @@ import { isStatePending, useAutoRefresh } from "src/utils"; import { Header } from "./Header"; -const tabs = [ - { icon: <MdReorder />, label: "Logs", value: "" }, - { icon: <PiBracketsCurlyBold />, label: "Rendered Templates", value: "rendered_templates" }, - { icon: <MdSyncAlt />, label: "XCom", value: "xcom" }, - { icon: <MdOutlineEventNote />, label: "Audit Logs", value: "events" }, - { icon: <FiCode />, label: "Code", value: "code" }, - { icon: <MdDetails />, label: "Details", value: "details" }, - { icon: <FiDatabase />, label: "Asset Events", value: "asset_events" }, -]; - export const TaskInstance = () => { + const { t: translate } = useTranslation("dag"); const { dagId = "", mapIndex = "-1", runId = "", taskId = "" } = useParams(); + const tabs = [ + { icon: <MdReorder />, label: translate("tabs.logs"), value: "" }, + { + icon: <PiBracketsCurlyBold />, + label: translate("tabs.renderedTemplates"), + value: "rendered_templates", + }, + { icon: <MdSyncAlt />, label: translate("tabs.xcom"), value: "xcom" }, + { icon: <FiDatabase />, label: translate("tabs.assetEvents"), value: "asset_events" }, + { icon: <MdOutlineEventNote />, label: translate("tabs.auditLog"), value: "events" }, + { icon: <FiCode />, label: translate("tabs.code"), value: "code" }, + { icon: <MdDetails />, label: translate("tabs.details"), value: "details" }, + ]; + const refetchInterval = useAutoRefresh({ dagId }); const { @@ -98,7 +104,9 @@ export const TaskInstance = () => { ...tabs.slice(0, 1), { icon: <MdOutlineTask />, - label: `Task Instances [${mappedTaskInstance?.task_count ?? ""}]`, + label: translate("tabs.mappedTaskInstances_other", { + count: Number(mappedTaskInstance?.task_count ?? 0), + }), value: "task_instances", }, ...tabs.slice(1), diff --git a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TriggererInfo.tsx b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TriggererInfo.tsx index cc718810d58..9787635b38a 100644 --- a/airflow-core/src/airflow/ui/src/pages/TaskInstance/TriggererInfo.tsx +++ b/airflow-core/src/airflow/ui/src/pages/TaskInstance/TriggererInfo.tsx @@ -17,42 +17,47 @@ * under the License. */ import { Box, Table, Heading } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import type { TaskInstanceResponse } from "openapi/requests/types.gen"; import Time from "src/components/Time"; -export const TriggererInfo = ({ taskInstance }: { readonly taskInstance: TaskInstanceResponse }) => ( - <Box py={1}> - <Heading py={1} size="sm"> - Triggerer Info - </Heading> - <Table.Root striped> - <Table.Body> - <Table.Row> - <Table.Cell>Trigger class</Table.Cell> - <Table.Cell>{taskInstance.trigger?.classpath}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Trigger ID</Table.Cell> - <Table.Cell>{taskInstance.trigger?.id}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Trigger creation time</Table.Cell> - <Table.Cell> - <Time datetime={taskInstance.trigger?.created_date} /> - </Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Assigned triggerer</Table.Cell> - <Table.Cell>{taskInstance.triggerer_job?.hostname}</Table.Cell> - </Table.Row> - <Table.Row> - <Table.Cell>Latest triggerer heartbeat</Table.Cell> - <Table.Cell> - <Time datetime={taskInstance.triggerer_job?.latest_heartbeat} /> - </Table.Cell> - </Table.Row> - </Table.Body> - </Table.Root> - </Box> -); +export const TriggererInfo = ({ taskInstance }: { readonly taskInstance: TaskInstanceResponse }) => { + const { t: translate } = useTranslation(); + + return ( + <Box py={1}> + <Heading py={1} size="sm"> + {translate("taskInstance.triggerer.title")} + </Heading> + <Table.Root striped> + <Table.Body> + <Table.Row> + <Table.Cell>{translate("taskInstance.triggerer.class")}</Table.Cell> + <Table.Cell>{taskInstance.trigger?.classpath}</Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("taskInstance.triggerer.id")}</Table.Cell> + <Table.Cell>{taskInstance.trigger?.id}</Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("taskInstance.triggerer.createdAt")}</Table.Cell> + <Table.Cell> + <Time datetime={taskInstance.trigger?.created_date} /> + </Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("taskInstance.triggerer.assigned")}</Table.Cell> + <Table.Cell>{taskInstance.triggerer_job?.hostname}</Table.Cell> + </Table.Row> + <Table.Row> + <Table.Cell>{translate("taskInstance.triggerer.latestHeartbeat")}</Table.Cell> + <Table.Cell> + <Time datetime={taskInstance.triggerer_job?.latest_heartbeat} /> + </Table.Cell> + </Table.Row> + </Table.Body> + </Table.Root> + </Box> + ); +}; diff --git a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx index 1fe894d3edf..842bd7ac7d4 100644 --- a/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx +++ b/airflow-core/src/airflow/ui/src/pages/XCom/XCom.tsx @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { Box, Link } from "@chakra-ui/react"; +import { Box, Heading, Link } from "@chakra-ui/react"; import type { ColumnDef } from "@tanstack/react-table"; import { useTranslation } from "react-i18next"; import { Link as RouterLink, useParams } from "react-router-dom"; @@ -119,6 +119,9 @@ export const XCom = () => { return ( <Box> + {dagId === "~" && runId === "~" && taskId === "~" ? ( + <Heading size="md">{translate("xcom.title")}</Heading> + ) : undefined} <ErrorAlert error={error} /> <DataTable columns={columns(translate)} @@ -127,7 +130,7 @@ export const XCom = () => { initialState={tableURLState} isFetching={isFetching} isLoading={isLoading} - modelName="XCom" + modelName={translate("xcom.title")} onStateChange={setTableURLState} skeletonCount={undefined} total={data ? data.total_entries : 0} diff --git a/airflow-core/src/airflow/ui/src/utils/TrimText.tsx b/airflow-core/src/airflow/ui/src/utils/TrimText.tsx index 39d8993bad8..3ae671d529a 100644 --- a/airflow-core/src/airflow/ui/src/utils/TrimText.tsx +++ b/airflow-core/src/airflow/ui/src/utils/TrimText.tsx @@ -17,6 +17,7 @@ * under the License. */ import { Text, Box, useDisclosure, Heading, Stack } from "@chakra-ui/react"; +import { useTranslation } from "react-i18next"; import { Dialog, Tooltip } from "src/components/ui"; @@ -44,6 +45,7 @@ export const TrimText = ({ showTooltip = false, text, }: TrimTextProps) => { + const { t: translate } = useTranslation(["components"]); const safeText = text ?? ""; const { isTrimmed, trimmedText } = trimText(safeText, charLimit); @@ -70,7 +72,7 @@ export const TrimText = ({ <Dialog.Root onOpenChange={onClose} open={isClickable ? open : undefined} size="xl"> <Dialog.Content backdrop> <Dialog.Header> - <Heading size="xl">Details</Heading> + <Heading size="xl">{translate("trimText.details")}</Heading> </Dialog.Header> <Dialog.CloseTrigger /> @@ -100,14 +102,14 @@ export const TrimText = ({ color={isEmpty ? "gray.emphasized" : undefined} fontWeight={isEmpty ? "bold" : "normal"} > - {isEmpty ? "Empty" : String(value)} + {isEmpty ? translate("trimText.empty") : String(value)} </Text> </Box> </Box> ); }) ) : ( - <Text>No content available.</Text> + <Text>{translate("trimText.noContent")}</Text> )} </Stack> </Dialog.Body> diff --git a/airflow-core/src/airflow/ui/src/utils/index.ts b/airflow-core/src/airflow/ui/src/utils/index.ts index 49924fabcad..d1badfb8f00 100644 --- a/airflow-core/src/airflow/ui/src/utils/index.ts +++ b/airflow-core/src/airflow/ui/src/utils/index.ts @@ -18,7 +18,6 @@ */ export { capitalize } from "./capitalize"; -export { pluralize } from "./pluralize"; export { getDuration, renderDuration } from "./datetimeUtils"; export { getMetaKey } from "./getMetaKey"; export { useContainerWidth } from "./useContainerWidth"; diff --git a/airflow-core/src/airflow/ui/src/utils/pluralize.test.ts b/airflow-core/src/airflow/ui/src/utils/pluralize.test.ts deleted file mode 100644 index 541caf62318..00000000000 --- a/airflow-core/src/airflow/ui/src/utils/pluralize.test.ts +++ /dev/null @@ -1,77 +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 { describe, expect, it } from "vitest"; - -import { pluralize } from "./pluralize"; - -type PluralizeTestCase = { - in: [string, number, string?, boolean?]; - out: string; -}; - -const pluralizeTestCases = [ - { in: ["DAG", 0, undefined, undefined], out: "0 DAGs" }, - { in: ["DAG", 1, undefined, undefined], out: "1 DAG" }, - { in: ["DAG", 12_000, undefined, undefined], out: "12,000 DAGs" }, - { in: ["DAG", 12_000_000, undefined, undefined], out: "12,000,000 DAGs" }, - { in: ["DAG", 0, undefined, undefined], out: "0 DAGs" }, - { in: ["DAG", 1, undefined, undefined], out: "1 DAG" }, - { in: ["DAG", 12_000, undefined, undefined], out: "12,000 DAGs" }, - { in: ["DAG", 12_000_000, undefined, undefined], out: "12,000,000 DAGs" }, - // Omit the count. - { in: ["DAG", 0, undefined, true], out: "DAGs" }, - { in: ["DAG", 1, undefined, true], out: "DAG" }, - { in: ["DAG", 12_000, undefined, true], out: "DAGs" }, - { in: ["DAG", 12_000_000, undefined, true], out: "DAGs" }, - { in: ["DAG", 0, undefined, true], out: "DAGs" }, - { in: ["DAG", 1, undefined, true], out: "DAG" }, - { in: ["DAG", 12_000, undefined, true], out: "DAGs" }, - { in: ["DAG", 12_000_000, undefined, true], out: "DAGs" }, - // The casing of the string is preserved. - { in: ["goose", 0, "geese", undefined], out: "0 geese" }, - { in: ["goose", 1, "geese", undefined], out: "1 goose" }, - // The plural form is different from the singular form. - { in: ["Goose", 0, "Geese", undefined], out: "0 Geese" }, - { in: ["Goose", 1, "Geese", undefined], out: "1 Goose" }, - { in: ["Goose", 12_000, "Geese", undefined], out: "12,000 Geese" }, - { in: ["Goose", 12_000_000, "Geese", undefined], out: "12,000,000 Geese" }, - { in: ["Goose", 0, "Geese", undefined], out: "0 Geese" }, - { in: ["Goose", 1, "Geese", undefined], out: "1 Goose" }, - { in: ["Goose", 12_000, "Geese", undefined], out: "12,000 Geese" }, - { in: ["Goose", 12_000_000, "Geese", undefined], out: "12,000,000 Geese" }, - // In the case of "Moose", the plural is the same as the singular and you - // probably wouldn't elect to use this function at all, but there could be - // cases where dynamic data makes it unavoidable. - { in: ["Moose", 0, "Moose", undefined], out: "0 Moose" }, - { in: ["Moose", 1, "Moose", undefined], out: "1 Moose" }, - { in: ["Moose", 12_000, "Moose", undefined], out: "12,000 Moose" }, - { in: ["Moose", 12_000_000, "Moose", undefined], out: "12,000,000 Moose" }, - { in: ["Moose", 0, "Moose", undefined], out: "0 Moose" }, - { in: ["Moose", 1, "Moose", undefined], out: "1 Moose" }, - { in: ["Moose", 12_000, "Moose", undefined], out: "12,000 Moose" }, - { in: ["Moose", 12_000_000, "Moose", undefined], out: "12,000,000 Moose" }, -] as const satisfies Array<PluralizeTestCase>; - -describe("pluralize", () => { - it("case", () => { - pluralizeTestCases.forEach((testCase) => - expect(pluralize(testCase.in[0], testCase.in[1], testCase.in[2], testCase.in[3])).toEqual(testCase.out), - ); - }); -}); diff --git a/airflow-core/src/airflow/ui/src/utils/pluralize.ts b/airflow-core/src/airflow/ui/src/utils/pluralize.ts deleted file mode 100644 index b8e13dc84a8..00000000000 --- a/airflow-core/src/airflow/ui/src/utils/pluralize.ts +++ /dev/null @@ -1,28 +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. - */ - -export const pluralize = ( - singularLabel: string, - count = 0, - pluralLabel = `${singularLabel}s`, - omitCount = false, - // eslint-disable-next-line @typescript-eslint/max-params -) => - // toLocaleString() will add commas for thousands, millions, etc. - `${omitCount ? "" : `${count.toLocaleString()} `}${count === 1 ? singularLabel : pluralLabel}`;