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,
 }
 
 

Reply via email to