diff --git a/airflow-core/docs/core-concepts/params.rst b/airflow-core/docs/core-concepts/params.rst index cdcd150a73b5a..ca3fb6849b31a 100644 --- a/airflow-core/docs/core-concepts/params.rst +++ b/airflow-core/docs/core-concepts/params.rst @@ -409,3 +409,55 @@ Disabling Runtime Param Modification The ability to update params while triggering a Dag depends on the flag ``core.dag_run_conf_overrides_params``. Setting this config to ``False`` will effectively turn your default params into constants. + +Pre-populating Trigger Form via URL +----------------------------------- + +To pre-populate values in the form when publishing a link to the trigger form you can call the trigger URL ``/dags//trigger/single`` or ``/dags//trigger/backfill``, +and add query parameters to the URL. + +There are two trigger form URLs available, each supporting a different set of query parameters: + +* ``/trigger/single``: + - ``conf`` - JSON configuration. + - ``run_id`` - run identifier. + - ``logical_date`` - execution date in ``YYYY-MM-DDTHH:mm:ss.SSS`` format. Defaults to the current timestamp if not provided. + - ``note`` - note attached to the DAG run. + +* ``/trigger/backfill``: + - ``conf`` - JSON configuration, applied to all runs. + - ``from_date`` - start of the backfill window in ``YYYY-MM-DDTHH:mm:ss`` format. + - ``to_date`` - end of the backfill window in ``YYYY-MM-DDTHH:mm:ss`` format. + - ``max_active_runs`` - maximum concurrent runs. Defaults to ``1``. + - ``reprocess_behavior`` - determines how existing runs are reprocessed. Supported values are: + + * ``failed`` - Missing and Errored Runs + * ``completed`` - All Runs + * ``none`` - Missing Runs + + - ``run_backwards`` - if set to true, the backfill is scheduled in reverse order. Defaults to ``false``. + +The trigger form supports two different ways of providing ``conf`` values. The available input methods are summarized in the table below: + +.. list-table:: ``conf`` parameter usage + :header-rows: 1 + :widths: 15 35 55 + + * - Form + - Usage + - Example + * - JSON (explicit) + - Provide the entire configuration as a JSON object. + This form has higher priority if present. + - ``/dags/{dag_id}/trigger/single?conf={"foo":"bar","x":123}`` + * - Key-value (implicit) + - If ``conf`` is not specified, any query parameter that is not a reserved keyword + will be automatically collected into ``conf``. + - ``/dags/{dag_id}/trigger/single?run_id=myrun&foo=bar&x=123`` + results in ``conf={"foo":"bar","x":"123"}`` + +For example, you can pass the pathname and query like below: + +``/dags/{dag_id}/trigger/single?run_id=my_run_dag&logical_date=2025-09-06T12:34:56.789&conf={"foo":"bar"}¬e=run_note`` + +``/dags/{dag_id}/trigger/backfill?from_date=2025-09-01T00:00:00&to_date=2025-09-03T23:59:59&conf={"abc":"loo"}&max_active_runs=2&reprocess_behavior=failed&run_backwards=true`` diff --git a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx index 65adf1bfca302..b8c5210658644 100644 --- a/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/ConfigForm.tsx @@ -35,6 +35,7 @@ type ConfigFormProps = { date?: unknown; }; readonly initialParamsDict: { paramsDict: ParamsSpec }; + readonly openAdvanced?: boolean; readonly setErrors: React.Dispatch< React.SetStateAction<{ conf?: string; @@ -49,6 +50,7 @@ const ConfigForm = ({ control, errors, initialParamsDict, + openAdvanced = false, setErrors, setFormError, }: ConfigFormProps) => { @@ -83,7 +85,9 @@ const ConfigForm = ({ return ( { const [formError, setFormError] = useState(false); const initialParamsDict = useDagParams(dag.dag_id, true); const { conf } = useParamStore(); - const { control, handleSubmit, reset, watch } = useForm({ + const [searchParams] = useSearchParams(); + const reservedKeys = ["from_date", "to_date", "max_active_runs", "reprocess_behavior", "run_backwards"]; + const urlConf = getTriggerConf(searchParams, reservedKeys); + const { control, handleSubmit, reset } = useForm({ defaultValues: { - conf, + conf: urlConf === "{}" ? conf || "{}" : urlConf, dag_id: dag.dag_id, - from_date: "", - max_active_runs: 1, - reprocess_behavior: "none", - run_backwards: false, + from_date: searchParams.get("from_date") ?? "", + max_active_runs: parseInt(searchParams.get("max_active_runs") ?? "1", 10) || 1, + reprocess_behavior: (searchParams.get("reprocess_behavior") ?? "none") as ReprocessBehavior, + run_backwards: searchParams.get("run_backwards") === "true", run_on_latest_version: true, - to_date: "", + to_date: searchParams.get("to_date") ?? "", }, mode: "onBlur", }); @@ -91,16 +101,13 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { if (Boolean(dateValidationError)) { setErrors((prev) => ({ ...prev, date: dateValidationError })); } - }, [dateValidationError]); - useEffect(() => { - if (conf) { - reset((prevValues) => ({ ...prevValues, conf })); + if (Boolean(conf) && urlConf === "{}") { + reset((prev) => ({ ...prev, conf })); } - }, [conf, reset]); - const dataIntervalStart = watch("from_date"); - const dataIntervalEnd = watch("to_date"); - const noDataInterval = !Boolean(dataIntervalStart) || !Boolean(dataIntervalEnd); - const dataIntervalInvalid = dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd)); + }, [dateValidationError, conf, reset, urlConf]); + + const noDataInterval = !Boolean(values.from_date) || !Boolean(values.to_date); + const dataIntervalInvalid = dayjs(values.from_date).isAfter(dayjs(values.to_date)); const onSubmit = (fdata: BackfillFormProps) => { if (unpause && dag.is_paused) { @@ -239,6 +246,10 @@ const RunBackfillForm = ({ dag, onClose }: RunBackfillFormProps) => { control={control} errors={errors} initialParamsDict={initialParamsDict} + openAdvanced={ + urlConf !== "{}" || + ["max_active_runs", "reprocess_behavior", "run_backwards"].some((key) => searchParams.has(key)) + } setErrors={setErrors} setFormError={setFormError} /> 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 b8d3fe2b9f4c5..70c0b2a91b26b 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGAdvancedOptions.tsx @@ -20,8 +20,9 @@ import { Input, Field, Stack } from "@chakra-ui/react"; import { Controller, type Control } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import type { DagRunTriggerParams } from "src/utils/trigger"; + import EditableMarkdown from "./EditableMarkdown"; -import type { DagRunTriggerParams } from "./TriggerDAGForm"; type TriggerDAGAdvancedOptionsProps = { readonly control: Control; 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 cd291acb09e8a..4bad37b767b4d 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGForm.tsx @@ -18,16 +18,26 @@ */ import { Button, Box, Spacer, HStack, Field, Stack, Text, VStack } from "@chakra-ui/react"; import dayjs from "dayjs"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef, useMemo } from "react"; import { Controller, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { FiPlay } from "react-icons/fi"; +import { useSearchParams } from "react-router-dom"; import { useDagParams } from "src/queries/useDagParams"; import { useParamStore } from "src/queries/useParamStore"; import { useTogglePause } from "src/queries/useTogglePause"; import { useTrigger } from "src/queries/useTrigger"; import { DEFAULT_DATETIME_FORMAT } from "src/utils/datetimeUtils"; +import { + getTriggerConf, + mergeUrlParams, + getUpdatedParamsDict, + type DagRunTriggerParams, + dataIntervalModeOptions, + extractParamValues, + type TriggerDAGFormProps, +} from "src/utils/trigger"; import ConfigForm from "../ConfigForm"; import { DateTimeInput } from "../DateTimeInput"; @@ -36,33 +46,6 @@ import { Checkbox } from "../ui/Checkbox"; import { RadioCardItem, RadioCardRoot } from "../ui/RadioCard"; import TriggerDAGAdvancedOptions from "./TriggerDAGAdvancedOptions"; -type TriggerDAGFormProps = { - readonly dagDisplayName: string; - readonly dagId: string; - readonly hasSchedule: boolean; - readonly isPaused: boolean; - readonly onClose: () => void; - readonly open: boolean; -}; - -type DataIntervalMode = "auto" | "manual"; - -export type DagRunTriggerParams = { - conf: string; - dagRunId: string; - dataIntervalEnd: string; - dataIntervalMode: DataIntervalMode; - dataIntervalStart: string; - logicalDate: string; - note: string; - partitionKey: string | undefined; -}; - -const dataIntervalModeOptions: Array<{ label: string; value: DataIntervalMode }> = [ - { label: "components:triggerDag.dataIntervalAuto", value: "auto" }, - { label: "components:triggerDag.dataIntervalManual", value: "manual" }, -]; - const TriggerDAGForm = ({ dagDisplayName, dagId, @@ -76,38 +59,84 @@ const TriggerDAGForm = ({ const [formError, setFormError] = useState(false); const initialParamsDict = useDagParams(dagId, open); const { error: errorTrigger, isPending, triggerDagRun } = useTrigger({ dagId, onSuccessConfirm: onClose }); - const { conf } = useParamStore(); + const { conf, setParamsDict } = useParamStore(); const [unpause, setUnpause] = useState(true); + const [searchParams] = useSearchParams(); + const urlConf = getTriggerConf(searchParams, ["run_id", "logical_date", "note"]); + const urlRunId = searchParams.get("run_id") ?? ""; + const urlDate = searchParams.get("logical_date"); + const urlNote = searchParams.get("note") ?? ""; const { mutate: togglePause } = useTogglePause({ dagId }); - const { control, handleSubmit, reset, watch } = useForm({ + const defaultsRef = useRef(undefined); + const isSyncedRef = useRef(false); + + const cleanInitialParams = useMemo( + () => extractParamValues(initialParamsDict.paramsDict as Record), + [initialParamsDict.paramsDict], + ); + const { control, getValues, handleSubmit, reset, watch } = useForm({ defaultValues: { - conf, - dagRunId: "", + ...initialParamsDict, + conf: urlConf === "{}" ? conf || "{}" : urlConf, + dagRunId: urlRunId, dataIntervalEnd: "", dataIntervalMode: "auto", dataIntervalStart: "", // Default logical date to now, show it in the selected timezone - logicalDate: dayjs().format(DEFAULT_DATETIME_FORMAT), - note: "", + logicalDate: urlDate ?? dayjs().format(DEFAULT_DATETIME_FORMAT), + note: urlNote, + params: cleanInitialParams, partitionKey: undefined, }, }); // Automatically reset form when conf is fetched useEffect(() => { - if (conf) { - reset((prevValues) => ({ - ...prevValues, - conf, - })); + if (defaultsRef.current === undefined && Object.keys(cleanInitialParams).length > 0) { + const current = getValues(); + + defaultsRef.current = { + ...current, + params: cleanInitialParams, + }; } - }, [conf, reset]); + }, [getValues, cleanInitialParams]); - const resetDateError = () => { - setErrors((prev) => ({ ...prev, date: undefined })); - }; + useEffect(() => { + if (defaultsRef.current === undefined) { + return; + } + + if (isSyncedRef.current) { + return; + } + + if (urlConf === "{}") { + if (conf) { + reset((prev) => ({ ...prev, conf })); + } + isSyncedRef.current = true; + + return; + } + + const mergedValues = mergeUrlParams(urlConf, defaultsRef.current.params ?? {}); + + reset({ + ...defaultsRef.current, + conf: JSON.stringify(mergedValues, undefined, 2), + dagRunId: urlRunId || defaultsRef.current.dagRunId, + logicalDate: urlDate ?? defaultsRef.current.logicalDate, + note: urlNote || defaultsRef.current.note, + }); + + setParamsDict(getUpdatedParamsDict(initialParamsDict.paramsDict, mergedValues)); + isSyncedRef.current = true; + }, [urlConf, urlRunId, urlDate, urlNote, initialParamsDict, reset, setParamsDict, conf]); + + const resetDateError = () => setErrors((prev) => ({ ...prev, date: undefined })); const dataIntervalMode = watch("dataIntervalMode"); const dataIntervalStart = watch("dataIntervalStart"); @@ -116,17 +145,17 @@ const TriggerDAGForm = ({ const dataIntervalInvalid = dataIntervalMode === "manual" && (noDataInterval || dayjs(dataIntervalStart).isAfter(dayjs(dataIntervalEnd))); - const onSubmit = (data: DagRunTriggerParams) => { if (unpause && isPaused) { - togglePause({ - dagId, - requestBody: { - is_paused: false, - }, - }); + togglePause({ dagId, requestBody: { is_paused: false } }); } - triggerDagRun(data); + + const finalParams = mergeUrlParams(data.conf, data.params ?? {}); + + triggerDagRun({ + ...data, + conf: JSON.stringify(finalParams), + }); }; return ( @@ -219,6 +248,7 @@ const TriggerDAGForm = ({ control={control} errors={errors} initialParamsDict={initialParamsDict} + openAdvanced={urlConf !== "{}" || Boolean(urlRunId) || Boolean(urlDate) || Boolean(urlNote)} setErrors={setErrors} setFormError={setFormError} > diff --git a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx index 9eb3e00a0e112..aaf9e22db527f 100644 --- a/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx +++ b/airflow-core/src/airflow/ui/src/components/TriggerDag/TriggerDAGModal.tsx @@ -17,8 +17,9 @@ * under the License. */ import { Heading, VStack, HStack, Spinner, Center, Text } from "@chakra-ui/react"; -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { useParams, useNavigate } from "react-router-dom"; import { useDagServiceGetDag } from "openapi/queries"; import { Dialog, Tooltip } from "src/components/ui"; @@ -48,7 +49,11 @@ const TriggerDAGModal: React.FC = ({ open, }) => { const { t: translate } = useTranslation("components"); - const [runMode, setRunMode] = useState(RunMode.SINGLE); + const { mode } = useParams(); + const navigate = useNavigate(); + const [runMode, setRunMode] = useState( + mode === RunMode.BACKFILL ? RunMode.BACKFILL : RunMode.SINGLE, + ); const { data: dag, isError, @@ -63,6 +68,17 @@ const TriggerDAGModal: React.FC = ({ }, ); + useEffect(() => { + if (mode === RunMode.BACKFILL) { + setRunMode(RunMode.BACKFILL); + } else { + setRunMode(RunMode.SINGLE); + } + }, [mode]); + const handleModeChange = (value: string) => { + setRunMode(value as RunMode); + navigate(`/dags/${dagId}/trigger/${value}`, { replace: true }); + }; const hasSchedule = dag?.timetable_summary !== null; const maxDisplayLength = 59; // hard-coded length to prevent dag name overflowing the modal const nameOverflowing = dagDisplayName.length > maxDisplayLength; @@ -98,8 +114,10 @@ const TriggerDAGModal: React.FC = ({ {dag ? ( { - setRunMode((event.target as HTMLInputElement).value as RunMode); + onValueChange={(details) => { + if (details.value !== null) { + handleModeChange(details.value); + } }} value={runMode} > diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx index 8cebcc4ba0b8e..5dd2e2fa9490c 100644 --- a/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx +++ b/airflow-core/src/airflow/ui/src/pages/Dag/Overview/Overview.tsx @@ -20,12 +20,13 @@ import { Box, HStack, Skeleton } from "@chakra-ui/react"; import dayjs from "dayjs"; import { lazy, useState, Suspense } from "react"; import { useTranslation } from "react-i18next"; -import { useParams } from "react-router-dom"; +import { useParams, useNavigate } from "react-router-dom"; import { useLocalStorage } from "usehooks-ts"; import { useAssetServiceGetAssetEvents, useDagRunServiceGetDagRuns, + useDagServiceGetDag, useTaskInstanceServiceGetTaskInstances, } from "openapi/queries"; import { AssetEvents } from "src/components/Assets/AssetEvents"; @@ -33,6 +34,7 @@ import { DurationChart } from "src/components/DurationChart"; import { NeedsReviewButton } from "src/components/NeedsReviewButton"; import TimeRangeSelector from "src/components/TimeRangeSelector"; import { TrendCountButton } from "src/components/TrendCountButton"; +import TriggerDAGModal from "src/components/TriggerDag/TriggerDAGModal"; import { SearchParamsKeys } from "src/constants/searchParams"; import { useGridRuns } from "src/queries/useGridRuns.ts"; @@ -42,7 +44,10 @@ const defaultHour = "24"; export const Overview = () => { const { t: translate } = useTranslation("dag"); - const { dagId } = useParams(); + const { dagId, mode } = useParams(); + const navigate = useNavigate(); + + const { data: dagData } = useDagServiceGetDag({ dagId: dagId ?? "" }); const now = dayjs(); const [startDate, setStartDate] = useState(now.subtract(Number(defaultHour), "hour").toISOString()); @@ -140,6 +145,16 @@ export const Overview = () => { }> + + {dagData ? ( + navigate(`/dags/${dagId}`)} + open={Boolean(mode)} + /> + ) : null} ); }; diff --git a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts index de2baede2482d..f395da3ca1c38 100644 --- a/airflow-core/src/airflow/ui/src/queries/useTrigger.ts +++ b/airflow-core/src/airflow/ui/src/queries/useTrigger.ts @@ -29,8 +29,8 @@ import { UseGridServiceGetGridRunsKeyFn, } from "openapi/queries"; import type { TriggerDagRunResponse } from "openapi/requests/types.gen"; -import type { DagRunTriggerParams } from "src/components/TriggerDag/TriggerDAGForm"; import { toaster } from "src/components/ui"; +import type { DagRunTriggerParams } from "src/utils/trigger"; export const useTrigger = ({ dagId, onSuccessConfirm }: { dagId: string; onSuccessConfirm: () => void }) => { const queryClient = useQueryClient(); @@ -107,7 +107,7 @@ export const useTrigger = ({ dagId, onSuccessConfirm }: { dagId: string; onSucce data_interval_start: formattedDataIntervalStart, logical_date: formattedLogicalDate, note: checkNote, - partition_key: dagRunRequestBody.partitionKey ?? null, + partition_key: dagRunRequestBody.partitionKey ?? undefined, }, }); }; diff --git a/airflow-core/src/airflow/ui/src/router.tsx b/airflow-core/src/airflow/ui/src/router.tsx index 5c3ddc76174e2..faaff2bfa960d 100644 --- a/airflow-core/src/airflow/ui/src/router.tsx +++ b/airflow-core/src/airflow/ui/src/router.tsx @@ -160,6 +160,7 @@ export const routerConfig = [ { children: [ { element: , index: true }, + { element: , path: "trigger/:mode?" }, { element: , path: "runs" }, { element: , path: "tasks" }, { element: , path: "calendar" }, diff --git a/airflow-core/src/airflow/ui/src/utils/trigger.ts b/airflow-core/src/airflow/ui/src/utils/trigger.ts new file mode 100644 index 0000000000000..1fd7c963b91a8 --- /dev/null +++ b/airflow-core/src/airflow/ui/src/utils/trigger.ts @@ -0,0 +1,114 @@ +/*! + * 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. + */ + +// Helper to extract configuration from URL search params +export const getTriggerConf = (searchParams: URLSearchParams, reservedKeys: Array) => { + const confParam = searchParams.get("conf"); + + // 1. If the user provided direct JSON 'conf' param (e.g., ?conf={"foo":"bar"}) + if (confParam !== null) { + try { + const parsed = JSON.parse(confParam) as unknown; + + return JSON.stringify(parsed, undefined, 2); + } catch { + // Ignore parsing errors + } + } + + // 2. If the user provided individual key-value params (e.g., ?foo=bar&run_id=123) + const collected: Record = {}; + + searchParams.forEach((value, key) => { + // Do not include reserved keys (like run_id, date) in the config, as they belong to specific form fields + if (!reservedKeys.includes(key) && key !== "conf") { + collected[key] = value; + } + }); + + return Object.keys(collected).length > 0 ? JSON.stringify(collected, undefined, 2) : "{}"; +}; + +export type DataIntervalMode = "auto" | "manual"; + +export type DagRunTriggerParams = { + conf: string; + dagRunId: string; + dataIntervalEnd: string; + dataIntervalMode: DataIntervalMode; + dataIntervalStart: string; + logicalDate: string; + note: string; + params?: Record; + partitionKey: string | undefined; +}; +export type TriggerDAGFormProps = { + readonly dagDisplayName: string; + readonly dagId: string; + readonly hasSchedule: boolean; + readonly isPaused: boolean; + readonly onClose: () => void; + readonly open: boolean; +}; +export const dataIntervalModeOptions: Array<{ label: string; value: DataIntervalMode }> = [ + { label: "components:triggerDag.dataIntervalAuto", value: "auto" }, + { label: "components:triggerDag.dataIntervalManual", value: "manual" }, +]; + +export const extractParamValues = (obj: Record) => { + const out: Record = {}; + + Object.entries(obj).forEach(([key, val]) => { + if (val !== null && typeof val === "object" && "value" in val) { + out[key] = (val as { value: unknown }).value; + } else if (val !== null && typeof val === "object" && "default" in val) { + out[key] = (val as { default: unknown }).default; + } else { + out[key] = val; + } + }); + + return out; +}; + +export const mergeUrlParams = (urlConf: string, baseParams: Record) => { + try { + const parsed = urlConf === "{}" ? {} : (JSON.parse(urlConf) as Record); + + return { ...baseParams, ...parsed }; + } catch { + return baseParams; + } +}; +export type ParamEntry = { + [key: string]: unknown; + value: unknown; +}; +export const getUpdatedParamsDict = (paramsDict: T, mergedValues: Record): T => { + const updated = structuredClone(paramsDict); + const record = updated as Record; + + Object.entries(mergedValues).forEach(([key, val]) => { + if (record[key] !== undefined) { + record[key].value = val; + } + }); + + return updated; +}; diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index a3e10044b5788..9cd8c459c19cc 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -1523,6 +1523,7 @@ replicaSet repo repos repr +reprocessed req reqs requeue