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 e2237bfc7ee UI: Guard the remaining RadioCardRoot values instead of
casting (#74743)
e2237bfc7ee is described below
commit e2237bfc7ee7636e9d68df2958873fee1a425733
Author: Brent Bovenzi <[email protected]>
AuthorDate: Thu Oct 15 10:33:01 2026 -0400
UI: Guard the remaining RadioCardRoot values instead of casting (#74743)
The RadioCardRoot sites converted in #73985 left three behind. Two pass
react-hook-form's field.onChange straight to onChange, which works only
because react-hook-form reads target.value off the event, and the third
casts the callback's value to its union. Both forms bypass the checks
that the rest of the radio groups now get.
Each of these groups was also seeded with defaultValue while mirroring
the value into state, so the group and the state could disagree if a
value were ever rejected; they are controlled now.
The ReprocessBehavior values are shared between the backfill form and
the backfill list, so the guard lives beside the options list it is
derived from and cannot drift from the values actually rendered.
---
.../ui/src/components/DagActions/RunBackfillForm.tsx | 11 +++++++++--
.../ui/src/components/TriggerDag/TriggerDAGForm.tsx | 16 ++++++++++++++--
.../src/airflow/ui/src/components/TriggerDag/types.ts | 5 ++++-
.../airflow/ui/src/constants/reprocessBehaviourParams.ts | 8 +++++++-
.../src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx | 11 +++--------
.../ui/src/pages/TaskStateStore/TaskStateStoreModal.tsx | 9 ++++++++-
6 files changed, 45 insertions(+), 15 deletions(-)
diff --git
a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
index 8834d2c9f15..ffc8fc8540f 100644
--- a/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/DagActions/RunBackfillForm.tsx
@@ -30,7 +30,7 @@ import { Alert, Checkbox, RadioCardItem, RadioCardLabel,
RadioCardRoot } from "s
import { useRerunWithLatestVersion } from
"src/components/Clear/useRerunWithLatestVersion";
-import { reprocessBehaviors } from "src/constants/reprocessBehaviourParams";
+import { isReprocessBehavior, reprocessBehaviors } from
"src/constants/reprocessBehaviourParams";
import { useCreateBackfill } from "src/queries/useCreateBackfill";
import { useCreateBackfillDryRun } from "src/queries/useCreateBackfillDryRun";
import { useDagParams } from "src/queries/useDagParams";
@@ -204,7 +204,14 @@ const RunBackfillForm = ({ dag, disabled = false, onClose
}: RunBackfillFormProp
control={control}
name="reprocess_behavior"
render={({ field }) => (
- <RadioCardRoot defaultValue={field.value}
onChange={field.onChange}>
+ <RadioCardRoot
+ onValueChange={(details) => {
+ if (isReprocessBehavior(details.value)) {
+ field.onChange(details.value);
+ }
+ }}
+ value={field.value}
+ >
<RadioCardLabel fontSize="md" fontWeight="semibold" mb={3}>
{translate("backfill.reprocessBehavior")}
</RadioCardLabel>
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 e1a7487026e..5f40f57f485 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx
@@ -37,7 +37,12 @@ import { ErrorAlert, type ExpandedApiError } from
"../ErrorAlert";
import PausedDagOptions from "./PausedDagOptions";
import RecentConfigSelect from "./RecentConfigSelect";
import TriggerDAGAdvancedOptions from "./TriggerDAGAdvancedOptions";
-import { dataIntervalModeOptions, type DagRunTriggerParams, type
PausedDagAction } from "./types";
+import {
+ dataIntervalModeOptions,
+ isDataIntervalMode,
+ type DagRunTriggerParams,
+ type PausedDagAction,
+} from "./types";
type TriggerDAGFormProps = {
readonly dagId: string;
@@ -209,7 +214,14 @@ const TriggerDAGForm = ({
control={control}
name="dataIntervalMode"
render={({ field }) => (
- <RadioCardRoot defaultValue={String(field.value)}
onChange={field.onChange}>
+ <RadioCardRoot
+ onValueChange={(details) => {
+ if (isDataIntervalMode(details.value)) {
+ field.onChange(details.value);
+ }
+ }}
+ value={field.value}
+ >
<HStack align="stretch">
{dataIntervalModeOptions.map((mode) => (
<RadioCardItem
diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
b/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
index aa0b89c6aba..9040e701802 100644
--- a/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
+++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/types.ts
@@ -16,6 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
+import { isOneOf } from "src/utils";
export type DataIntervalMode = "auto" | "manual";
@@ -36,4 +37,6 @@ export type DagRunTriggerParams = {
export const dataIntervalModeOptions = [
{ label: "components:triggerDag.dataIntervalAuto", value: "auto" },
{ label: "components:triggerDag.dataIntervalManual", value: "manual" },
-] as const satisfies Array<{ label: string; value: DataIntervalMode }>;
+] as const satisfies ReadonlyArray<{ label: string; value: DataIntervalMode }>;
+
+export const isDataIntervalMode = isOneOf(dataIntervalModeOptions.map((option)
=> option.value));
diff --git
a/airflow-core/src/airflow/ui/src/constants/reprocessBehaviourParams.ts
b/airflow-core/src/airflow/ui/src/constants/reprocessBehaviourParams.ts
index 957f22050d8..65e553b79e0 100644
--- a/airflow-core/src/airflow/ui/src/constants/reprocessBehaviourParams.ts
+++ b/airflow-core/src/airflow/ui/src/constants/reprocessBehaviourParams.ts
@@ -16,8 +16,14 @@
* specific language governing permissions and limitations
* under the License.
*/
+import type { ReprocessBehavior } from "openapi/requests/types.gen";
+
+import { isOneOf } from "src/utils";
+
export const reprocessBehaviors = [
{ label: "components:backfill.missingRuns", value: "none" },
{ label: "components:backfill.missingAndErroredRuns", value: "failed" },
{ label: "components:backfill.allRuns", value: "completed" },
-];
+] as const satisfies ReadonlyArray<{ label: string; value: ReprocessBehavior
}>;
+
+export const isReprocessBehavior = isOneOf(reprocessBehaviors.map((behavior)
=> behavior.value));
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
index 398394ebee2..3ba435b104e 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Backfills/Backfills.tsx
@@ -23,7 +23,7 @@ import { useTranslation } from "react-i18next";
import { useLocation, useNavigate, useParams, useSearchParams } from
"react-router-dom";
import { useBackfillServiceListBackfillsUi } from "openapi/queries";
-import type { BackfillResponse, ReprocessBehavior } from
"openapi/requests/types.gen";
+import type { BackfillResponse } from "openapi/requests/types.gen";
import { DataTable } from "src/components/DataTable";
import type { DataTableFeatures } from "src/components/DataTable/features";
@@ -31,8 +31,9 @@ import { useTableURLState } from
"src/components/DataTable/useTableUrlState";
import { ErrorAlert } from "src/components/ErrorAlert";
import Time from "src/components/Time";
+import { isReprocessBehavior } from "src/constants/reprocessBehaviourParams";
import { SearchParamsKeys, type SearchParamsKeysType } from
"src/constants/searchParams";
-import { type DurationFormat, isOneOf, useDurationFormat } from "src/utils";
+import { type DurationFormat, useDurationFormat } from "src/utils";
import { BackfillDagRunsModal } from "./BackfillDagRunsModal";
import { BackfillsFilters } from "./BackfillsFilters";
@@ -53,12 +54,6 @@ const {
TO_DATE_LTE: TO_DATE_LTE_PARAM,
}: SearchParamsKeysType = SearchParamsKeys;
-const isReprocessBehavior = isOneOf([
- "failed",
- "completed",
- "none",
-] as const satisfies ReadonlyArray<ReprocessBehavior>);
-
type ColumnProps = {
readonly onSelectBackfill: (backfillId: number) => void;
readonly translate: TFunction;
diff --git
a/airflow-core/src/airflow/ui/src/pages/TaskStateStore/TaskStateStoreModal.tsx
b/airflow-core/src/airflow/ui/src/pages/TaskStateStore/TaskStateStoreModal.tsx
index 60ad38c2028..03946bf0b09 100644
---
a/airflow-core/src/airflow/ui/src/pages/TaskStateStore/TaskStateStoreModal.tsx
+++
b/airflow-core/src/airflow/ui/src/pages/TaskStateStore/TaskStateStoreModal.tsx
@@ -38,6 +38,9 @@ import { JsonEditor } from "src/components/JsonEditor";
import { useTimezone } from "src/context/timezone";
import { useTaskInstanceCoordinates } from
"src/hooks/useTaskInstanceCoordinates";
import { useStoreMutation } from "src/queries/useStoreMutation";
+import { isOneOf } from "src/utils";
+
+const isExpiresAtMode = isOneOf(["custom", "default", "never"] as const);
dayjs.extend(tz);
@@ -195,7 +198,11 @@ export const TaskStateStoreModal = ({
{translate("dag:taskStateStore.expiresAt.label")}
</Text>
<RadioCard.Root
- onValueChange={(ev) => setExpiresAt(ev.value as "custom" |
"default" | "never")}
+ onValueChange={(details) => {
+ if (isExpiresAtMode(details.value)) {
+ setExpiresAt(details.value);
+ }
+ }}
value={expiresAt}
>
<Flex gap={2}>