This is an automated email from the ASF dual-hosted git repository.
vincbeck 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 a190f11596f Add team selector in connection form (#60237)
a190f11596f is described below
commit a190f11596f26e753595c64b3a6d1348a39be886
Author: Vincent <[email protected]>
AuthorDate: Fri Jan 9 20:24:20 2026 +0100
Add team selector in connection form (#60237)
---
.../api_fastapi/core_api/datamodels/ui/config.py | 1 +
.../api_fastapi/core_api/openapi/_private_ui.yaml | 4 ++
.../api_fastapi/core_api/routes/ui/config.py | 1 +
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 6 +-
.../airflow/ui/openapi-gen/requests/types.gen.ts | 1 +
.../ui/public/i18n/locales/en/components.json | 7 ++
.../src/airflow/ui/src/components/TeamSelector.tsx | 76 ++++++++++++++++++++++
.../src/airflow/ui/src/mocks/handlers/config.ts | 1 +
.../src/pages/Connections/AddConnectionButton.tsx | 1 +
.../ui/src/pages/Connections/ConnectionForm.tsx | 26 ++++----
.../ui/src/pages/Connections/Connections.tsx | 1 +
.../src/pages/Connections/EditConnectionButton.tsx | 1 +
.../src/airflow/ui/src/queries/useAddConnection.ts | 2 +
.../api_fastapi/core_api/routes/ui/test_config.py | 1 +
14 files changed, 116 insertions(+), 13 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
index 045030269ce..88832c61e3d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/config.py
@@ -35,3 +35,4 @@ class ConfigResponse(BaseModel):
show_external_log_redirect: bool
external_log_name: str | None = None
theme: Theme | None
+ multi_team: bool
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 31e2a351036..3eae26be31d 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -1355,6 +1355,9 @@ components:
anyOf:
- $ref: '#/components/schemas/Theme'
- type: 'null'
+ multi_team:
+ type: boolean
+ title: Multi Team
type: object
required:
- page_size
@@ -1368,6 +1371,7 @@ components:
- dashboard_alert
- show_external_log_redirect
- theme
+ - multi_team
title: ConfigResponse
description: configuration serializer.
ConnectionHookFieldBehavior:
diff --git a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
index 28009f8343e..e0cd3591bd4 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/routes/ui/config.py
@@ -61,6 +61,7 @@ def get_configs() -> ConfigResponse:
"show_external_log_redirect": task_log_reader.supports_external_link,
"external_log_name": getattr(task_log_reader.log_handler, "log_name",
None),
"theme": loads(conf.get("api", "theme", fallback="{}")) or None,
+ "multi_team": conf.getboolean("core", "multi_team"),
}
config.update({key: value for key, value in additional_config.items()})
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index c71112cc1a6..b44c2883a95 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -7162,10 +7162,14 @@ export const $ConfigResponse = {
type: 'null'
}
]
+ },
+ multi_team: {
+ type: 'boolean',
+ title: 'Multi Team'
}
},
type: 'object',
- required: ['page_size', 'auto_refresh_interval',
'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui',
'require_confirmation_dag_change', 'default_wrap', 'test_connection',
'dashboard_alert', 'show_external_log_redirect', 'theme'],
+ required: ['page_size', 'auto_refresh_interval',
'hide_paused_dags_by_default', 'instance_name', 'enable_swagger_ui',
'require_confirmation_dag_change', 'default_wrap', 'test_connection',
'dashboard_alert', 'show_external_log_redirect', 'theme', 'multi_team'],
title: 'ConfigResponse',
description: 'configuration serializer.'
} as const;
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 69aeeda532f..945062547ad 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -1773,6 +1773,7 @@ export type ConfigResponse = {
show_external_log_redirect: boolean;
external_log_name?: string | null;
theme: Theme | null;
+ multi_team: boolean;
};
/**
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 6668a88c1b4..606117451ec 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
@@ -111,6 +111,13 @@
"sortedUnsorted": "unsorted",
"taskTries": "Task Tries",
"taskTryPlaceholder": "Task Try",
+ "team": {
+ "selector": {
+ "helperText": "Optional. Restrict usage to a specific team.",
+ "label": "Team",
+ "placeHolder": "Select a team"
+ }
+ },
"toggleCardView": "Show card view",
"toggleTableView": "Show table view",
"triggerDag": {
diff --git a/airflow-core/src/airflow/ui/src/components/TeamSelector.tsx
b/airflow-core/src/airflow/ui/src/components/TeamSelector.tsx
new file mode 100644
index 00000000000..b5a84e26abd
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/components/TeamSelector.tsx
@@ -0,0 +1,76 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import { Field, Stack, Spinner } from "@chakra-ui/react";
+import { Select } from "chakra-react-select";
+import { useMemo } from "react";
+import { type Control, Controller, type FieldValues, type Path } from
"react-hook-form";
+import { useTranslation } from "react-i18next";
+
+import { useTeamsServiceListTeams } from "openapi/queries";
+
+type Props<T extends FieldValues = FieldValues> = {
+ readonly control: Control<T>;
+};
+
+export const TeamSelector = <T extends FieldValues = FieldValues>({ control }:
Props<T>) => {
+ const { data, isLoading } = useTeamsServiceListTeams({ orderBy: ["name"] });
+ const options = useMemo(
+ () =>
+ (data?.teams ?? []).map((team: { name: string }) => ({
+ label: team.name,
+ value: team.name,
+ })),
+ [data],
+ );
+
+ const { t: translate } = useTranslation("components");
+
+ return (
+ <Controller
+ control={control}
+ name={"team_name" as Path<T>}
+ render={({ field: { onChange, value }, fieldState }) => (
+ <Field.Root invalid={Boolean(fieldState.error)}
orientation="horizontal">
+ <Stack>
+ <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+ {translate("team.selector.label")}
+ </Field.Label>
+ </Stack>
+ <Stack css={{ flexBasis: "70%" }}>
+ <Stack>
+ {isLoading ? (
+ <Spinner size="sm" style={{ left: "60%", position: "absolute",
top: "20%" }} />
+ ) : undefined}
+ <Select
+ {...Field}
+ isClearable
+ isDisabled={isLoading}
+ onChange={(val) => onChange(val?.value)}
+ options={options}
+ placeholder={translate("team.selector.placeHolder")}
+ value={options.find((option) => option.value === value)}
+ />
+ </Stack>
+
<Field.HelperText>{translate("team.selector.helperText")}</Field.HelperText>
+ </Stack>
+ </Field.Root>
+ )}
+ />
+ );
+};
diff --git a/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts
b/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts
index 3caa610c125..82ad9987e11 100644
--- a/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts
+++ b/airflow-core/src/airflow/ui/src/mocks/handlers/config.ts
@@ -26,6 +26,7 @@ export const handlers: Array<HttpHandler> = [
enable_swagger_ui: true,
hide_paused_dags_by_default: false,
instance_name: "Airflow",
+ multi_team: false,
page_size: 15,
require_confirmation_dag_change: false,
test_connection: "Disabled",
diff --git
a/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
index 5ae35a4101f..ce039d8fa96 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
@@ -42,6 +42,7 @@ const AddConnectionButton = () => {
password: "",
port: "",
schema: "",
+ team_name: "",
};
return (
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 dbecdf2ecf7..ec791fef400 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
@@ -26,7 +26,9 @@ import { FiSave } from "react-icons/fi";
import { ErrorAlert } from "src/components/ErrorAlert";
import { FlexibleForm } from "src/components/FlexibleForm";
import { JsonEditor } from "src/components/JsonEditor";
+import { TeamSelector } from "src/components/TeamSelector.tsx";
import { Accordion } from "src/components/ui";
+import { useConfig } from "src/queries/useConfig.tsx";
import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
import type { ParamsSpec } from "src/queries/useDagParams";
import { useParamStore } from "src/queries/useParamStore";
@@ -61,7 +63,7 @@ const ConnectionForm = ({
control,
formState: { isDirty, isValid },
handleSubmit,
- reset,
+ setValue,
watch,
} = useForm<ConnectionBody>({
defaultValues: initialConnection,
@@ -74,23 +76,21 @@ const ConnectionForm = ({
const paramsDic = { paramsDict:
connectionTypeMeta[selectedConnType]?.extra_fields ?? ({} as ParamsSpec) };
const [formErrors, setFormErrors] = useState(false);
+ const multiTeamEnabled = Boolean(useConfig("multi_team"));
useEffect(() => {
- reset((prevValues) => ({
- ...initialConnection,
- conn_type: selectedConnType,
- connection_id: prevValues.connection_id,
- }));
+ setValue("conn_type", selectedConnType, {
+ shouldDirty: true,
+ });
setConf(JSON.stringify(JSON.parse(initialConnection.extra), undefined, 2));
- }, [selectedConnType, reset, initialConnection, setConf]);
+ }, [selectedConnType, initialConnection, setConf, setValue]);
// Automatically reset form when conf is fetched
useEffect(() => {
- reset((prevValues) => ({
- ...prevValues, // Retain existing form values
- extra,
- }));
- }, [extra, reset, setConf]);
+ setValue("extra", extra, {
+ shouldDirty: true,
+ });
+ }, [extra, setValue]);
const onSubmit = (data: ConnectionBody) => {
mutateConnection(data);
@@ -257,6 +257,8 @@ const ConnectionForm = ({
</Accordion.Item>
</Accordion.Root>
) : undefined}
+
+ {multiTeamEnabled ? <TeamSelector control={control} /> : undefined}
</VStack>
<ErrorAlert error={error} />
<Box as="footer" display="flex" justifyContent="flex-end" mr={3} mt={4}>
diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
index 771ee24b0a5..d666f843911 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
@@ -53,6 +53,7 @@ export type ConnectionBody = {
password: string;
port: string;
schema: string;
+ team_name: string;
};
const getColumns = ({
diff --git
a/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
index f7c8c204ed0..21ff0d564f3 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
@@ -46,6 +46,7 @@ const EditConnectionButton = ({ connection, disabled }:
Props) => {
password: connection.password ?? "",
port: connection.port?.toString() ?? "",
schema: connection.schema ?? "",
+ team_name: connection.team_name ?? "",
};
const { editConnection, error, isPending, setError } =
useEditConnection(initialConnectionValue, {
onSuccessConfirm: onClose,
diff --git a/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
b/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
index b0172ee4507..062eb1cef49 100644
--- a/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
@@ -66,6 +66,7 @@ export const useAddConnection = ({ onSuccessConfirm }: {
onSuccessConfirm: () =>
const port = requestBody.port === "" ? undefined :
Number(requestBody.port);
const schema = requestBody.schema === "" ? undefined : requestBody.schema;
const extra = requestBody.extra === "{}" ? undefined : requestBody.extra;
+ const teamName = requestBody.team_name === "" ? undefined :
requestBody.team_name;
mutate({
requestBody: {
@@ -78,6 +79,7 @@ export const useAddConnection = ({ onSuccessConfirm }: {
onSuccessConfirm: () =>
password,
port,
schema,
+ team_name: teamName,
},
});
};
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
index 6973ef97e42..437e82d1a5f 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_config.py
@@ -58,6 +58,7 @@ expected_config_response = {
"show_external_log_redirect": False,
"external_log_name": None,
"theme": THEME,
+ "multi_team": False,
}