This is an automated email from the ASF dual-hosted git repository.
bbovenzi 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 23a9a99e93b feat: add test connection button to ui (#51055)
23a9a99e93b is described below
commit 23a9a99e93b40e6a20bab3ad8b1d393f6550b9b3
Author: Zhen-Lun (Kevin) Hong <[email protected]>
AuthorDate: Fri May 30 22:46:54 2025 +0800
feat: add test connection button to ui (#51055)
* feat: add test connection button to ui
* fix: determine test connection access based on configuration settings
* fix: simplify button design
* refactor: small changes on test connection button
* fix: remove toasts
* fix: add loading prop to the button
* feat: support i18n in test connection button
---
airflow-core/src/airflow/ui/src/i18n/config.ts | 4 +-
.../ui/src/i18n/locales/en/connections.json | 4 +
.../ui/src/pages/Connections/Connections.tsx | 2 +
.../src/pages/Connections/TestConnectionButton.tsx | 89 ++++++++++++++++++++++
.../airflow/ui/src/queries/useTestConnection.ts | 43 +++++++++++
5 files changed, 141 insertions(+), 1 deletion(-)
diff --git a/airflow-core/src/airflow/ui/src/i18n/config.ts
b/airflow-core/src/airflow/ui/src/i18n/config.ts
index 2af9d4c7acc..4161febba45 100644
--- a/airflow-core/src/airflow/ui/src/i18n/config.ts
+++ b/airflow-core/src/airflow/ui/src/i18n/config.ts
@@ -23,6 +23,7 @@ import { initReactI18next } from "react-i18next";
import deCommon from "./locales/de/common.json";
import deDashboard from "./locales/de/dashboard.json";
import enCommon from "./locales/en/common.json";
+import enConnections from "./locales/en/connections.json";
import enDags from "./locales/en/dags.json";
import enDashboard from "./locales/en/dashboard.json";
import koCommon from "./locales/ko/common.json";
@@ -48,7 +49,7 @@ export const supportedLanguages = [
] as const;
export const defaultLanguage = "en";
-export const namespaces = ["common", "dashboard", "dags"] as const;
+export const namespaces = ["common", "dashboard", "dags", "connections"] as
const;
const resources = {
de: {
@@ -57,6 +58,7 @@ const resources = {
},
en: {
common: enCommon,
+ connections: enConnections,
dags: enDags,
dashboard: enDashboard,
},
diff --git a/airflow-core/src/airflow/ui/src/i18n/locales/en/connections.json
b/airflow-core/src/airflow/ui/src/i18n/locales/en/connections.json
new file mode 100644
index 00000000000..c50cd75a2c7
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/i18n/locales/en/connections.json
@@ -0,0 +1,4 @@
+{
+ "test": "Test Connection",
+ "testDisabled": "Testing connections disabled. Contact your admin to
enable it."
+}
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 68d96d82097..0f4ff93f391 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/Connections.tsx
@@ -38,6 +38,7 @@ import AddConnectionButton from "./AddConnectionButton";
import DeleteConnectionButton from "./DeleteConnectionButton";
import DeleteConnectionsButton from "./DeleteConnectionsButton";
import EditConnectionButton from "./EditConnectionButton";
+import TestConnectionButton from "./TestConnectionButton";
export type ConnectionBody = {
conn_type: string;
@@ -104,6 +105,7 @@ const getColumns = ({
accessorKey: "actions",
cell: ({ row: { original } }) => (
<Flex justifyContent="end">
+ <TestConnectionButton connection={original} />
<EditConnectionButton connection={original}
disabled={selectedRows.size > 0} />
<DeleteConnectionButton connectionId={original.connection_id}
disabled={selectedRows.size > 0} />
</Flex>
diff --git
a/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
b/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
new file mode 100644
index 00000000000..3fddfb62e8b
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
@@ -0,0 +1,89 @@
+/*!
+ * 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 { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { FiActivity, FiWifi, FiWifiOff } from "react-icons/fi";
+
+import type { ConnectionResponse, ConnectionBody } from
"openapi/requests/types.gen";
+import ActionButton from "src/components/ui/ActionButton";
+import { useConfig } from "src/queries/useConfig";
+import { useTestConnection } from "src/queries/useTestConnection";
+
+type TestConnectionOption = "Disabled" | "Enabled" | "Hidden";
+type Props = {
+ readonly connection: ConnectionResponse;
+};
+
+const defaultIcon = <FiActivity />;
+const connectedIcon = <FiWifi color="green" />;
+const disconnectedIcon = <FiWifiOff color="red" />;
+
+const TestConnectionButton = ({ connection }: Props) => {
+ const { t: translate } = useTranslation("connections");
+ const [icon, setIcon] = useState(defaultIcon);
+ const testConnection = useConfig("test_connection");
+ let option: TestConnectionOption;
+
+ if (testConnection === "Enabled") {
+ option = "Enabled";
+ } else if (testConnection === "Hidden") {
+ option = "Hidden";
+ } else {
+ option = "Disabled";
+ }
+
+ const connectionBody: ConnectionBody = {
+ conn_type: connection.conn_type,
+ connection_id: connection.connection_id,
+ description: connection.description ?? "",
+ extra: connection.extra === "" || connection.extra === null ? "{}" :
connection.extra,
+ host: connection.host ?? "",
+ login: connection.login ?? "",
+ password: connection.password ?? "",
+ port: Number(connection.port),
+ schema: connection.schema ?? "",
+ };
+
+ const { isPending, mutate } = useTestConnection((result) => {
+ if (result === undefined) {
+ setIcon(defaultIcon);
+ } else if (result === true) {
+ setIcon(connectedIcon);
+ } else {
+ setIcon(disconnectedIcon);
+ }
+ });
+
+ return (
+ <ActionButton
+ actionName={option === "Enabled" ? translate("test") :
translate("testDisabled")}
+ disabled={option === "Disabled"}
+ display={option === "Hidden" ? "none" : "flex"}
+ icon={icon}
+ loading={isPending}
+ onClick={() => {
+ mutate({ requestBody: connectionBody });
+ }}
+ text={translate("test")}
+ withText={false}
+ />
+ );
+};
+
+export default TestConnectionButton;
diff --git a/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
b/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
new file mode 100644
index 00000000000..f68b3c5d562
--- /dev/null
+++ b/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
@@ -0,0 +1,43 @@
+/*!
+ * 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 type { Dispatch, SetStateAction } from "react";
+
+import { useConnectionServiceTestConnection,
useConnectionServiceGetConnectionsKey } from "openapi/queries";
+import type { ConnectionTestResponse } from "openapi/requests/types.gen";
+
+export const useTestConnection = (setConnected:
Dispatch<SetStateAction<boolean | undefined>>) => {
+ const queryClient = useQueryClient();
+
+ const onSuccess = async (res: ConnectionTestResponse) => {
+ await queryClient.invalidateQueries({
+ queryKey: [useConnectionServiceGetConnectionsKey],
+ });
+ setConnected(res.status);
+ };
+
+ const onError = () => {
+ setConnected(false);
+ };
+
+ return useConnectionServiceTestConnection({
+ onError,
+ onSuccess,
+ });
+};