diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json index 350e3898a39ee..10351910eee16 100644 --- a/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json +++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/components.json @@ -136,6 +136,7 @@ "loading": "Loading Dag information...", "loadingFailed": "Failed to load Dag information. Please try again.", "manualRunDenied": "Manual runs are not allowed for this Dag", + "partitionKeyHelp": "Optional - only applies to partitioned Dags", "runIdHelp": "Optional - will be generated if not provided", "selectDescription": "Trigger a single run of this Dag", "selectLabel": "Single Run", diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.test.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.test.tsx new file mode 100644 index 0000000000000..c8856d0eab90e --- /dev/null +++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.test.tsx @@ -0,0 +1,48 @@ +/*! + * 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 "@testing-library/jest-dom"; +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; + +import type { AssetEventResponse } from "openapi/requests/types.gen"; +import { Wrapper } from "src/utils/Wrapper"; + +import { AssetEvent } from "./AssetEvent"; + +const baseEvent = { + asset_id: 1, + created_dagruns: [], + id: 1, + source_map_index: -1, + timestamp: "2025-01-01T00:00:00Z", +} satisfies Partial as AssetEventResponse; + +describe("AssetEvent", () => { + it("does not render a partition key line when partition_key is null", () => { + render(, { wrapper: Wrapper }); + + expect(screen.queryByText(/dagRun\.partitionKey/u)).not.toBeInTheDocument(); + }); + + it("renders the partition key line when partition_key is set", () => { + render(, { wrapper: Wrapper }); + + expect(screen.getByText("dagRun.partitionKey: foo")).toBeInTheDocument(); + }); +}); diff --git a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx index b7d4b62d265e2..91baa8601f360 100644 --- a/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx +++ b/airflow-core/src/airflow/ui/src/components/Assets/AssetEvent.tsx @@ -95,13 +95,16 @@ export const AssetEvent = ({ - {event.partition_key === undefined ? undefined : ( - - - {rootTranslate("dagRun.partitionKey")}: {event.partition_key} - - - )} + { + // eslint-disable-next-line no-eq-null, eqeqeq + event.partition_key == null ? undefined : ( + + + {rootTranslate("dagRun.partitionKey")}: {event.partition_key} + + + ) + } {Object.keys(extra).length >= 1 ? : undefined} ); diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx index 77d16664e5297..d5512579093f0 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx @@ -25,9 +25,10 @@ import type { DagRunTriggerParams } from "./types"; type TriggerDAGAdvancedOptionsProps = { readonly control: Control; + readonly isPartitioned: boolean; }; -const TriggerDAGAdvancedOptions = ({ control }: TriggerDAGAdvancedOptionsProps) => { +const TriggerDAGAdvancedOptions = ({ control, isPartitioned }: TriggerDAGAdvancedOptionsProps) => { const { t: translate } = useTranslation(["common", "components"]); const { t: rootTranslate } = useTranslation(); @@ -51,22 +52,25 @@ const TriggerDAGAdvancedOptions = ({ control }: TriggerDAGAdvancedOptionsProps) )} /> - ( - - - - {rootTranslate("dagRun.partitionKey")} - - - - - - - )} - /> + {isPartitioned ? ( + ( + + + + {rootTranslate("dagRun.partitionKey")} + + + + + {translate("components:triggerDag.partitionKeyHelp")} + + + )} + /> + ) : undefined} { expect(configJson.value).toContain('"Updated message"'); }); }); + + it("hides the partition key field for non-partitioned Dags", async () => { + render( + , + { wrapper: Wrapper }, + ); + + fireEvent.click(screen.getByText("Advanced Options")); + + await waitFor(() => expect(screen.getByText("runId")).toBeInTheDocument()); + expect(screen.queryByText("dagRun.partitionKey")).not.toBeInTheDocument(); + }); + + it("shows the partition key field for partitioned Dags", async () => { + render( + , + { wrapper: Wrapper }, + ); + + fireEvent.click(screen.getByText("Advanced Options")); + + await waitFor(() => expect(screen.getByText("dagRun.partitionKey")).toBeInTheDocument()); + expect(screen.getByText("components:triggerDag.partitionKeyHelp")).toBeInTheDocument(); + }); }); diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx index 3cac3a7edc113..ccd79c4fc25f9 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx @@ -244,7 +244,7 @@ const TriggerDAGForm = ({ setErrors={setErrors} setFormError={setFormError} > - + diff --git a/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.test.tsx b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.test.tsx new file mode 100644 index 0000000000000..a7e91e64c05f1 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/pages/Asset/CreateAssetEventModal.test.tsx @@ -0,0 +1,222 @@ +/*! + * 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 "@testing-library/jest-dom"; +import { fireEvent, render, screen } from "@testing-library/react"; +import type { ReactNode } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import type * as OpenapiQueries from "openapi/queries"; +import type { AssetResponse, DAGDetailsResponse } from "openapi/requests/types.gen"; +import type { DagRunTriggerParams } from "src/components/TriggerDag/types"; +import type * as Ui from "src/components/ui"; +import { Wrapper } from "src/utils/Wrapper"; + +import { CreateAssetEventModal } from "./CreateAssetEventModal"; + +const materializeSubmitParams = vi.hoisted(() => ({ + conf: "{}", + dagRunId: "", + dataIntervalEnd: "", + dataIntervalMode: "auto", + dataIntervalStart: "", + logicalDate: "", + note: "", + partitionKey: undefined, +})); + +vi.mock("src/components/ui", async (importOriginal) => { + const actual = await importOriginal(); + // Must stay inside the factory: vitest hoists vi.mock above module scope, so an outer-scope + // component cannot be referenced here. + // eslint-disable-next-line unicorn/consistent-function-scoping + const DialogPart = ({ children }: { readonly children?: ReactNode }) =>
{children}
; + + return { + ...actual, + Dialog: { + ...actual.Dialog, + Body: DialogPart, + CloseTrigger: () => undefined, + Content: DialogPart, + Footer: DialogPart, + Header: DialogPart, + Root: ({ children, open }: { readonly children?: ReactNode; readonly open?: boolean }) => + open ?
{children}
: undefined, + }, + }; +}); + +vi.mock("src/components/JsonEditor", () => ({ + JsonEditor: ({ value = "{}" }: { readonly value?: string }) => ( +