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 };
+};