This is an automated email from the ASF dual-hosted git repository.

shubhamraj 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 9b421936318 Add Edit connection button on list connection page (#48102)
9b421936318 is described below

commit 9b421936318db64c241f465ad34d49f9cddb8e2d
Author: Shubham Raj <[email protected]>
AuthorDate: Mon Mar 24 01:33:32 2025 +0530

    Add Edit connection button on list connection page (#48102)
    
    * Edit connection form
    
    * Reviews
    
    * Fix extra fields
---
 .../src/pages/Connections/AddConnectionButton.tsx  |  40 +++---
 .../ui/src/pages/Connections/ConnectionForm.tsx    | 149 +++++++--------------
 .../pages/Connections/ConnectionStandardFields.tsx |  98 ++++++++++++++
 .../ui/src/pages/Connections/Connections.tsx       |  17 +++
 .../src/pages/Connections/EditConnectionButton.tsx |  92 +++++++++++++
 .../src/airflow/ui/src/queries/useAddConnection.ts |   6 +-
 .../ui/src/queries/useConnectionTypeMeta.ts        |   6 +-
 .../src/airflow/ui/src/queries/useDagParams.ts     |   2 +-
 .../airflow/ui/src/queries/useEditConnection.tsx   | 101 ++++++++++++++
 9 files changed, 384 insertions(+), 127 deletions(-)

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 09ff52d11d0..be9de6ce449 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/AddConnectionButton.tsx
@@ -16,39 +16,38 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Box, Heading, Spinner, VStack } from "@chakra-ui/react";
+import { Box, Heading, VStack } from "@chakra-ui/react";
 import { useDisclosure } from "@chakra-ui/react";
 import { FiPlusCircle } from "react-icons/fi";
 
 import { Dialog } from "src/components/ui";
 import ActionButton from "src/components/ui/ActionButton";
-import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
+import { useAddConnection } from "src/queries/useAddConnection";
 
 import ConnectionForm from "./ConnectionForm";
-
-export type AddConnectionParams = {
-  conf: string;
-  conn_type: string;
-  connection_id: string;
-  description: string;
-  host: string;
-  login: string;
-  password: string;
-  port: string;
-  schema: string;
-};
+import type { ConnectionBody } from "./Connections";
 
 const AddConnectionButton = () => {
   const { onClose, onOpen, open } = useDisclosure();
-  const { formattedData: connectionTypeMeta, isPending, keysList: 
connectionTypes } = useConnectionTypeMeta();
+  const { addConnection, error, isPending } = useAddConnection({ 
onSuccessConfirm: onClose });
+  const initialConnection: ConnectionBody = {
+    conn_type: "",
+    connection_id: "",
+    description: "",
+    extra: "{}",
+    host: "",
+    login: "",
+    password: "",
+    port: "",
+    schema: "",
+  };
 
   return (
     <Box>
       <ActionButton
         actionName="Add Connection"
         colorPalette="blue"
-        disabled={isPending}
-        icon={isPending ? <Spinner size="sm" /> : <FiPlusCircle />}
+        icon={<FiPlusCircle />}
         onClick={onOpen}
         text="Add Connection"
         variant="solid"
@@ -66,9 +65,10 @@ const AddConnectionButton = () => {
 
           <Dialog.Body>
             <ConnectionForm
-              connectionTypeMeta={connectionTypeMeta}
-              connectionTypes={connectionTypes}
-              onClose={onClose}
+              error={error}
+              initialConnection={initialConnection}
+              isPending={isPending}
+              mutateConnection={addConnection}
             />
           </Dialog.Body>
         </Dialog.Content>
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 d4993f7b576..f2a47c47b15 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionForm.tsx
@@ -16,52 +16,51 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import { Input, Button, Box, Spacer, HStack, Field, Stack, VStack, Textarea } 
from "@chakra-ui/react";
+import { Input, Button, Box, Spacer, HStack, Field, Stack, VStack, Spinner } 
from "@chakra-ui/react";
 import { Select } from "chakra-react-select";
 import { useEffect, useState } from "react";
 import { useForm, Controller } from "react-hook-form";
-import { FiEye, FiEyeOff, FiSave } from "react-icons/fi";
+import { FiSave } from "react-icons/fi";
 
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { FlexibleForm, flexibleFormExtraFieldSection } from 
"src/components/FlexibleForm";
 import { JsonEditor } from "src/components/JsonEditor";
 import { Accordion } from "src/components/ui";
-import { useAddConnection } from "src/queries/useAddConnection";
-import type { ConnectionMetaEntry } from "src/queries/useConnectionTypeMeta";
+import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
 import type { ParamsSpec } from "src/queries/useDagParams";
 import { useParamStore } from "src/queries/useParamStore";
 
-import type { AddConnectionParams } from "./AddConnectionButton";
+import StandardFields from "./ConnectionStandardFields";
+import type { ConnectionBody } from "./Connections";
 
 type AddConnectionFormProps = {
-  readonly connectionTypeMeta: Record<string, ConnectionMetaEntry>;
-  readonly connectionTypes: Array<string>;
-  readonly onClose: () => void;
+  readonly error: unknown;
+  readonly initialConnection: ConnectionBody;
+  readonly isPending: boolean;
+  readonly mutateConnection: (requestBody: ConnectionBody) => void;
 };
 
-const ConnectionForm = ({ connectionTypeMeta, connectionTypes, onClose }: 
AddConnectionFormProps) => {
+const ConnectionForm = ({
+  error,
+  initialConnection,
+  isPending,
+  mutateConnection,
+}: AddConnectionFormProps) => {
   const [errors, setErrors] = useState<{ conf?: string }>({});
-  const { addConnection, error, isPending } = useAddConnection({ 
onSuccessConfirm: onClose });
-  const { conf, setConf } = useParamStore();
-  const [showPassword, setShowPassword] = useState(false);
+  const {
+    formattedData: connectionTypeMeta,
+    isPending: isMetaPending,
+    keysList: connectionTypes,
+  } = useConnectionTypeMeta();
+  const { conf: extra, setConf } = useParamStore();
   const {
     control,
     formState: { isValid },
     handleSubmit,
     reset,
     watch,
-  } = useForm<AddConnectionParams>({
-    defaultValues: {
-      conf,
-      conn_type: "",
-      connection_id: "",
-      description: "",
-      host: "",
-      login: "",
-      password: "",
-      port: "",
-      schema: "",
-    },
+  } = useForm<ConnectionBody>({
+    defaultValues: initialConnection,
     mode: "onBlur",
   });
 
@@ -71,27 +70,23 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
 
   useEffect(() => {
     reset((prevValues) => ({
-      ...prevValues,
+      ...initialConnection,
       conn_type: selectedConnType,
-      description: "",
-      host: "",
-      login: "",
-      password: "",
-      port: "",
-      schema: "",
+      connection_id: prevValues.connection_id,
     }));
-  }, [selectedConnType, reset]);
+    setConf(JSON.stringify(JSON.parse(initialConnection.extra), undefined, 2));
+  }, [selectedConnType, reset, initialConnection, setConf]);
 
   // Automatically reset form when conf is fetched
   useEffect(() => {
     reset((prevValues) => ({
       ...prevValues, // Retain existing form values
-      conf,
+      extra,
     }));
-  }, [conf, reset, setConf]);
+  }, [extra, reset, setConf]);
 
-  const onSubmit = (data: AddConnectionParams) => {
-    addConnection(data);
+  const onSubmit = (data: ConnectionBody) => {
+    mutateConnection(data);
   };
 
   const validateAndPrettifyJson = (value: string) => {
@@ -101,7 +96,7 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
       setErrors((prev) => ({ ...prev, conf: undefined }));
       const formattedJson = JSON.stringify(parsedJson, undefined, 2);
 
-      if (formattedJson !== conf) {
+      if (formattedJson !== extra) {
         setConf(formattedJson); // Update only if the value is different
       }
 
@@ -137,7 +132,7 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
                 </Field.Label>
               </Stack>
               <Stack css={{ flexBasis: "70%" }}>
-                <Input {...field} required size="sm" />
+                <Input {...field} 
disabled={Boolean(initialConnection.connection_id)} required size="sm" />
                 {fieldState.error ? 
<Field.ErrorText>{fieldState.error.message}</Field.ErrorText> : undefined}
               </Stack>
             </Field.Root>
@@ -159,13 +154,19 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
                 </Field.Label>
               </Stack>
               <Stack css={{ flexBasis: "70%" }}>
-                <Select
-                  {...Field}
-                  onChange={(val) => onChange(val?.value)}
-                  options={connTypesOptions}
-                  placeholder="Select Connection Type"
-                  value={connTypesOptions.find((type) => type.value === value)}
-                />
+                <Stack>
+                  {isMetaPending ? (
+                    <Spinner size="sm" style={{ left: "60%", position: 
"absolute", top: "20%" }} />
+                  ) : undefined}
+                  <Select
+                    {...Field}
+                    isDisabled={isMetaPending}
+                    onChange={(val) => onChange(val?.value)}
+                    options={connTypesOptions}
+                    placeholder="Select Connection Type"
+                    value={connTypesOptions.find((type) => type.value === 
value)}
+                  />
+                </Stack>
                 <Field.HelperText>
                   Connection type missing? Make sure you have installed the 
corresponding Airflow Providers
                   Package.
@@ -188,60 +189,9 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
             variant="enclosed"
           >
             <Accordion.Item key="standardFields" value="standardFields">
-              <Accordion.ItemTrigger cursor="button">Standard 
Fields</Accordion.ItemTrigger>
+              <Accordion.ItemTrigger>Standard Fields</Accordion.ItemTrigger>
               <Accordion.ItemContent>
-                <Stack pb={3} pl={3} pr={3}>
-                  {Object.entries(standardFields).map(([key, fields]) => {
-                    if (Boolean(fields.hidden)) {
-                      return undefined;
-                    } // Skip hidden fields
-
-                    return (
-                      <Controller
-                        control={control}
-                        key={key}
-                        name={key as keyof AddConnectionParams}
-                        render={({ field }) => (
-                          <Field.Root mt={3} orientation="horizontal">
-                            <Stack>
-                              <Field.Label fontSize="md" style={{ flexBasis: 
"30%" }}>
-                                {fields.title ?? key}
-                              </Field.Label>
-                            </Stack>
-                            <Stack css={{ flexBasis: "70%", position: 
"relative" }}>
-                              {key === "description" ? (
-                                <Textarea {...field} 
placeholder={fields.placeholder ?? ""} />
-                              ) : (
-                                <div style={{ position: "relative", width: 
"100%" }}>
-                                  <Input
-                                    {...field}
-                                    placeholder={fields.placeholder ?? ""}
-                                    type={key === "password" && !showPassword 
? "password" : "text"}
-                                  />
-                                  {key === "password" && (
-                                    <button
-                                      onClick={() => 
setShowPassword(!showPassword)}
-                                      style={{
-                                        cursor: "pointer",
-                                        position: "absolute",
-                                        right: "10px",
-                                        top: "50%",
-                                        transform: "translateY(-50%)",
-                                      }}
-                                      type="button"
-                                    >
-                                      {showPassword ? <FiEye size={15} /> : 
<FiEyeOff size={15} />}
-                                    </button>
-                                  )}
-                                </div>
-                              )}
-                            </Stack>
-                          </Field.Root>
-                        )}
-                      />
-                    );
-                  })}
-                </Stack>
+                <StandardFields control={control} 
standardFields={standardFields} />
               </Accordion.ItemContent>
             </Accordion.Item>
             <FlexibleForm
@@ -254,7 +204,7 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
               <Accordion.ItemContent>
                 <Controller
                   control={control}
-                  name="conf"
+                  name="extra"
                   render={({ field }) => (
                     <Field.Root invalid={Boolean(errors.conf)}>
                       <JsonEditor
@@ -286,7 +236,6 @@ const ConnectionForm = ({ connectionTypeMeta, 
connectionTypes, onClose }: AddCon
         </HStack>
       </Box>
     </>
-    // eslint-disable-next-line max-lines
   );
 };
 
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionStandardFields.tsx
 
b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionStandardFields.tsx
new file mode 100644
index 00000000000..0a47bdc450b
--- /dev/null
+++ 
b/airflow-core/src/airflow/ui/src/pages/Connections/ConnectionStandardFields.tsx
@@ -0,0 +1,98 @@
+/*!
+ * 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, Textarea, Input } from "@chakra-ui/react";
+import { useState } from "react";
+import { type Control, Controller } from "react-hook-form";
+import { FiEye, FiEyeOff } from "react-icons/fi";
+
+import type { StandardFieldSpec } from "src/queries/useConnectionTypeMeta";
+
+import type { ConnectionBody } from "./Connections";
+
+type StandardFieldsProps = {
+  readonly control: Control<ConnectionBody, unknown>;
+  readonly standardFields: StandardFieldSpec;
+};
+
+const StandardFields = ({ control, standardFields }: StandardFieldsProps) => {
+  const [showPassword, setShowPassword] = useState(false);
+
+  return (
+    <Stack pb={3} pl={3} pr={3}>
+      {Object.entries(standardFields).map(([key, fields]) => {
+        if (Boolean(fields.hidden)) {
+          return undefined;
+        }
+
+        return (
+          <Controller
+            control={control}
+            key={key}
+            name={key as keyof ConnectionBody}
+            render={({ field }) => (
+              <Field.Root mt={3} orientation="horizontal">
+                <Stack>
+                  <Field.Label fontSize="md" style={{ flexBasis: "30%" }}>
+                    {fields.title ?? key}
+                  </Field.Label>
+                </Stack>
+                <Stack css={{ flexBasis: "70%", position: "relative" }}>
+                  {key === "description" ? (
+                    <Textarea {...field} placeholder={fields.placeholder ?? 
""} />
+                  ) : (
+                    <div style={{ position: "relative", width: "100%" }}>
+                      <Input
+                        {...field}
+                        placeholder={fields.placeholder ?? ""}
+                        type={
+                          key === "password" && !showPassword
+                            ? "password"
+                            : key === "port"
+                              ? "number"
+                              : "text"
+                        }
+                      />
+                      {key === "password" && (
+                        <button
+                          onClick={() => setShowPassword(!showPassword)}
+                          style={{
+                            cursor: "pointer",
+                            position: "absolute",
+                            right: "10px",
+                            top: "50%",
+                            transform: "translateY(-50%)",
+                          }}
+                          type="button"
+                        >
+                          {showPassword ? <FiEye size={15} /> : <FiEyeOff 
size={15} />}
+                        </button>
+                      )}
+                    </div>
+                  )}
+                </Stack>
+              </Field.Root>
+            )}
+          />
+        );
+      })}
+    </Stack>
+  );
+};
+
+export default StandardFields;
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 57aee6e4c4c..17c3bae337b 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
@@ -28,9 +28,23 @@ import { useTableURLState } from 
"src/components/DataTable/useTableUrlState";
 import { ErrorAlert } from "src/components/ErrorAlert";
 import { SearchBar } from "src/components/SearchBar";
 import { SearchParamsKeys, type SearchParamsKeysType } from 
"src/constants/searchParams";
+import { useConnectionTypeMeta } from "src/queries/useConnectionTypeMeta";
 
 import AddConnectionButton from "./AddConnectionButton";
 import DeleteConnectionButton from "./DeleteConnectionButton";
+import EditConnectionButton from "./EditConnectionButton";
+
+export type ConnectionBody = {
+  conn_type: string;
+  connection_id: string;
+  description: string;
+  extra: string;
+  host: string;
+  login: string;
+  password: string;
+  port: string;
+  schema: string;
+};
 
 const columns: Array<ColumnDef<ConnectionResponse>> = [
   {
@@ -57,6 +71,7 @@ const columns: Array<ColumnDef<ConnectionResponse>> = [
     accessorKey: "actions",
     cell: ({ row: { original } }) => (
       <Flex justifyContent="end">
+        <EditConnectionButton connection={original} disabled={false} />
         <DeleteConnectionButton connectionId={original.connection_id} 
disabled={false} />
         {/* For now disabled is set as false, will depend on selected rows 
once multi action PR merges */}
       </Flex>
@@ -76,6 +91,8 @@ export const Connections = () => {
   const [connectionIdPattern, setConnectionIdPattern] = useState(
     searchParams.get(NAME_PATTERN_PARAM) ?? undefined,
   );
+
+  useConnectionTypeMeta(); // Pre-fetch connection type metadata
   const { pagination, sorting } = tableURLState;
   const [sort] = sorting;
   const orderBy = sort ? `${sort.desc ? "-" : ""}${sort.id}` : 
"-connection_id";
diff --git 
a/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx 
b/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
new file mode 100644
index 00000000000..c4d9c263263
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/EditConnectionButton.tsx
@@ -0,0 +1,92 @@
+/*!
+ * 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 { Heading, useDisclosure } from "@chakra-ui/react";
+import { FiEdit } from "react-icons/fi";
+
+import type { ConnectionResponse } from "openapi/requests/types.gen";
+import { Dialog } from "src/components/ui";
+import ActionButton from "src/components/ui/ActionButton";
+import { useEditConnection } from "src/queries/useEditConnection";
+
+import ConnectionForm from "./ConnectionForm";
+import type { ConnectionBody } from "./Connections";
+
+type Props = {
+  readonly connection: ConnectionResponse;
+  readonly disabled: boolean;
+};
+
+const EditConnectionButton = ({ connection, disabled }: Props) => {
+  const { onClose, onOpen, open } = useDisclosure();
+  const initialConnectionValue: ConnectionBody = {
+    conn_type: connection.conn_type,
+    connection_id: connection.connection_id,
+    description: connection.description ?? "",
+    extra: connection.extra ?? "{}",
+    host: connection.host ?? "",
+    login: connection.login ?? "",
+    password: connection.password ?? "",
+    port: connection.port?.toString() ?? "",
+    schema: connection.schema ?? "",
+  };
+  const { editConnection, error, isPending, setError } = 
useEditConnection(initialConnectionValue, {
+    onSuccessConfirm: onClose,
+  });
+
+  const handleClose = () => {
+    setError(undefined);
+    onClose();
+  };
+
+  return (
+    <>
+      <ActionButton
+        actionName="Edit Connection"
+        disabled={disabled}
+        icon={<FiEdit />}
+        onClick={() => {
+          onOpen();
+        }}
+        text="Edit Connection"
+        withText={false}
+      />
+
+      <Dialog.Root onOpenChange={handleClose} open={open} size="xl">
+        <Dialog.Content backdrop>
+          <Dialog.Header>
+            <Heading size="xl">Edit Variable</Heading>
+          </Dialog.Header>
+
+          <Dialog.CloseTrigger />
+
+          <Dialog.Body>
+            <ConnectionForm
+              error={error}
+              initialConnection={initialConnectionValue}
+              isPending={isPending}
+              mutateConnection={editConnection}
+            />
+          </Dialog.Body>
+        </Dialog.Content>
+      </Dialog.Root>
+    </>
+  );
+};
+
+export default EditConnectionButton;
diff --git a/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts 
b/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
index 624f84c7800..626dd10afd3 100644
--- a/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useAddConnection.ts
@@ -21,7 +21,7 @@ import { useState } from "react";
 
 import { useConnectionServiceGetConnectionsKey, 
useConnectionServicePostConnection } from "openapi/queries";
 import { toaster } from "src/components/ui";
-import type { AddConnectionParams } from 
"src/pages/Connections/AddConnectionButton";
+import type { ConnectionBody } from "src/pages/Connections/Connections";
 
 export const useAddConnection = ({ onSuccessConfirm }: { onSuccessConfirm: () 
=> void }) => {
   const queryClient = useQueryClient();
@@ -49,14 +49,14 @@ export const useAddConnection = ({ onSuccessConfirm }: { 
onSuccessConfirm: () =>
     onSuccess,
   });
 
-  const addConnection = (requestBody: AddConnectionParams) => {
+  const addConnection = (requestBody: ConnectionBody) => {
     const description = requestBody.description === "" ? undefined : 
requestBody.description;
     const host = requestBody.host === "" ? undefined : requestBody.host;
     const login = requestBody.login === "" ? undefined : requestBody.login;
     const password = requestBody.password === "" ? undefined : 
requestBody.password;
     const port = requestBody.port === "" ? undefined : 
Number(requestBody.port);
     const schema = requestBody.schema === "" ? undefined : requestBody.schema;
-    const extra = requestBody.conf === "" ? undefined : requestBody.conf;
+    const extra = requestBody.extra === "{}" ? undefined : requestBody.extra;
 
     mutate({
       requestBody: {
diff --git a/airflow-core/src/airflow/ui/src/queries/useConnectionTypeMeta.ts 
b/airflow-core/src/airflow/ui/src/queries/useConnectionTypeMeta.ts
index b4c82e052d7..ebad7192507 100644
--- a/airflow-core/src/airflow/ui/src/queries/useConnectionTypeMeta.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useConnectionTypeMeta.ts
@@ -21,14 +21,14 @@ import { toaster } from "src/components/ui";
 
 import type { ParamsSpec } from "./useDagParams";
 
-type StandardFieldSpec = Record<string, StandardFieldSchema>;
-
 type StandardFieldSchema = {
   hidden?: boolean | undefined;
   placeholder?: string | undefined;
   title?: string | undefined;
 };
 
+export type StandardFieldSpec = Record<string, StandardFieldSchema>;
+
 export type ConnectionMetaEntry = {
   connection_type: string;
   default_conn_name: string | undefined;
@@ -48,7 +48,7 @@ export const useConnectionTypeMeta = () => {
     const errorDescription =
       typeof error === "object" && error !== null
         ? JSON.stringify(error, undefined, 2) // Safely stringify the object 
with pretty-printing
-        : String(error ?? ""); // Convert other types (e.g., numbers, strings) 
to string
+        : String(Boolean(error) ? error : ""); // Convert other types (e.g., 
numbers, strings) to string
 
     toaster.create({
       description: `Connection Type Meta request failed. Error: 
${errorDescription}`,
diff --git a/airflow-core/src/airflow/ui/src/queries/useDagParams.ts 
b/airflow-core/src/airflow/ui/src/queries/useDagParams.ts
index ec72ece27e7..64a7f0915af 100644
--- a/airflow-core/src/airflow/ui/src/queries/useDagParams.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useDagParams.ts
@@ -58,7 +58,7 @@ export const useDagParams = (dagId: string, open: boolean) => 
{
     const errorDescription =
       typeof error === "object" && error !== null
         ? JSON.stringify(error, undefined, 2) // Safely stringify the object 
with pretty-printing
-        : String(error ?? ""); // Convert other types (e.g., numbers, strings) 
to string
+        : String(Boolean(error) ? error : ""); // Convert other types (e.g., 
numbers, strings) to string
 
     toaster.create({
       description: `Dag params request failed. Error: ${errorDescription}`,
diff --git a/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx 
b/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
new file mode 100644
index 00000000000..49a864e6244
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useEditConnection.tsx
@@ -0,0 +1,101 @@
+/*!
+ * 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 { useQueryClient } from "@tanstack/react-query";
+import { useState } from "react";
+
+import { useConnectionServiceGetConnectionsKey, 
useConnectionServicePatchConnection } from "openapi/queries";
+import { toaster } from "src/components/ui";
+import type { ConnectionBody } from "src/pages/Connections/Connections";
+
+export const useEditConnection = (
+  initialConnection: ConnectionBody,
+  {
+    onSuccessConfirm,
+  }: {
+    onSuccessConfirm: () => void;
+  },
+) => {
+  const queryClient = useQueryClient();
+  const [error, setError] = useState<unknown>(undefined);
+
+  const onSuccess = async () => {
+    await queryClient.invalidateQueries({
+      queryKey: [useConnectionServiceGetConnectionsKey],
+    });
+
+    toaster.create({
+      description: "Connection has been edited successfully",
+      title: "Connection Edit Request Submitted",
+      type: "success",
+    });
+
+    onSuccessConfirm();
+  };
+
+  const onError = (_error: unknown) => {
+    setError(_error);
+  };
+
+  const { isPending, mutate } = useConnectionServicePatchConnection({
+    onError,
+    onSuccess,
+  });
+
+  const editConnection = (requestBody: ConnectionBody) => {
+    const updateMask: Array<string> = [];
+
+    if (requestBody.extra !== initialConnection.extra) {
+      updateMask.push("extra");
+    }
+    if (requestBody.conn_type !== initialConnection.conn_type) {
+      updateMask.push("conn_type");
+    }
+    if (requestBody.description !== initialConnection.description) {
+      updateMask.push("description");
+    }
+    if (requestBody.host !== initialConnection.host) {
+      updateMask.push("host");
+    }
+    if (requestBody.login !== initialConnection.login) {
+      updateMask.push("login");
+    }
+    if (requestBody.password !== initialConnection.password) {
+      updateMask.push("password");
+    }
+    if (requestBody.port !== initialConnection.port) {
+      updateMask.push("port");
+    }
+    if (requestBody.schema !== initialConnection.schema) {
+      updateMask.push("schema");
+    }
+
+    mutate({
+      connectionId: initialConnection.connection_id,
+      requestBody: {
+        ...requestBody,
+        conn_type: requestBody.conn_type,
+        connection_id: initialConnection.connection_id,
+        port: Number(requestBody.port),
+      },
+      updateMask,
+    });
+  };
+
+  return { editConnection, error, isPending, setError };
+};

Reply via email to