diff --git a/airflow-core/src/airflow/ui/dev/index.html b/airflow-core/src/airflow/ui/dev/index.html
index f77a08175d1cd..46be06afe20e0 100644
--- a/airflow-core/src/airflow/ui/dev/index.html
+++ b/airflow-core/src/airflow/ui/dev/index.html
@@ -3,7 +3,7 @@
-
+
-
diff --git a/airflow-core/src/airflow/ui/package.json b/airflow-core/src/airflow/ui/package.json
index 9eef5758807fb..d5e437f4070fc 100644
--- a/airflow-core/src/airflow/ui/package.json
+++ b/airflow-core/src/airflow/ui/package.json
@@ -110,5 +110,6 @@
"esbuild",
"msw"
]
- }
+ },
+ "packageManager": "pnpm@10.16.1+sha512.0e155aa2629db8672b49e8475da6226aa4bdea85fdcdfdc15350874946d4f3c91faaf64cbdc4a5d1ab8002f473d5c3fcedcd197989cf0390f9badd3c04678706"
}
diff --git a/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx b/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
index 7b27877d87037..59e9b1c9410cd 100644
--- a/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Connections/TestConnectionButton.tsx
@@ -24,6 +24,7 @@ import type { ConnectionResponse, ConnectionBody } from "openapi/requests/types.
import ActionButton from "src/components/ui/ActionButton";
import { useConfig } from "src/queries/useConfig";
import { useTestConnection } from "src/queries/useTestConnection";
+import { Tooltip } from "src/components/ui";
type TestConnectionOption = "Disabled" | "Enabled" | "Hidden";
type Props = {
@@ -37,6 +38,7 @@ const disconnectedIcon = ;
const TestConnectionButton = ({ connection }: Props) => {
const { t: translate } = useTranslation("admin");
const [icon, setIcon] = useState(defaultIcon);
+ const [message, setMessage] = useState(undefined);
const testConnection = useConfig("test_connection");
let option: TestConnectionOption;
@@ -63,28 +65,59 @@ const TestConnectionButton = ({ connection }: Props) => {
const { isPending, mutate } = useTestConnection((result) => {
if (result === undefined) {
setIcon(defaultIcon);
+ setMessage(undefined);
} else if (result === true) {
setIcon(connectedIcon);
+ // Message will be set by the hook's onSuccess callback
} else {
setIcon(disconnectedIcon);
+ // Message will be set by the hook's onSuccess callback
}
+ }, (newMessage) => {
+ setMessage(newMessage);
});
+ const tooltipContent = message ? message : translate("connections.test");
+
return (
- {
- mutate({ requestBody: connectionBody });
- }}
- text={translate("connections.test")}
- withText={false}
- />
+
+
+ {
+ // Reset message when starting a new test
+ setMessage(undefined);
+ mutate({ requestBody: connectionBody });
+ }}
+ text={translate("connections.test")}
+ withText={false}
+ />
+
+ {message && (
+
+ {message}
+
+ )}
+
);
};
diff --git a/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts b/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
index f68b3c5d5626c..1ad614a52c1da 100644
--- a/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
+++ b/airflow-core/src/airflow/ui/src/queries/useTestConnection.ts
@@ -22,7 +22,10 @@ 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>) => {
+export const useTestConnection = (
+ setConnected: Dispatch>,
+ setMessage: Dispatch>
+) => {
const queryClient = useQueryClient();
const onSuccess = async (res: ConnectionTestResponse) => {
@@ -30,10 +33,43 @@ export const useTestConnection = (setConnected: Dispatch {
+ const onError = (error: any) => {
setConnected(false);
+
+ // Extract error message from different possible error structures
+ let errorMessage = "Connection test failed";
+
+ // Try different error message extraction strategies
+ if (error?.body?.detail) {
+ errorMessage = error.body.detail;
+ } else if (error?.body?.message) {
+ errorMessage = error.body.message;
+ } else if (error?.response?.data?.detail) {
+ errorMessage = error.response.data.detail;
+ } else if (error?.response?.data?.message) {
+ errorMessage = error.response.data.message;
+ } else if (error?.message) {
+ errorMessage = error.message;
+ } else if (typeof error?.body === 'string') {
+ errorMessage = error.body;
+ } else if (error?.body && typeof error.body === 'object') {
+ // Try to extract message from nested error object
+ const bodyStr = JSON.stringify(error.body);
+ if (bodyStr.includes('detail')) {
+ try {
+ const parsed = JSON.parse(bodyStr);
+ errorMessage = parsed.detail || parsed.message || errorMessage;
+ } catch (e) {
+ // If parsing fails, use the string representation
+ errorMessage = bodyStr;
+ }
+ }
+ }
+
+ setMessage(errorMessage);
};
return useConnectionServiceTestConnection({
diff --git a/airflow-core/src/airflow/ui/vite.config.ts b/airflow-core/src/airflow/ui/vite.config.ts
index 7e49f32a1c822..c16a16550ddf1 100644
--- a/airflow-core/src/airflow/ui/vite.config.ts
+++ b/airflow-core/src/airflow/ui/vite.config.ts
@@ -37,6 +37,33 @@ export default defineConfig({
resolve: { alias: { openapi: "/openapi-gen", src: "/src" } },
server: {
cors: true, // Only used by the dev server.
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/ui': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/config': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/auth': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ },
+ '/login': {
+ target: 'http://localhost:8080',
+ changeOrigin: true,
+ secure: false,
+ },
+ },
},
test: {
coverage: {
diff --git a/setup_complete_airflow.sh b/setup_complete_airflow.sh
new file mode 100755
index 0000000000000..eddab36db4db0
--- /dev/null
+++ b/setup_complete_airflow.sh
@@ -0,0 +1,97 @@
+#!/bin/bash
+
+echo "๐ Setting up complete Airflow environment for PR #55680..."
+
+# Set environment variables
+export PATH="/Users/alphaskynet/Library/Python/3.9/bin:$PATH"
+export AIRFLOW_HOME=/tmp/airflow_test
+export AIRFLOW__CORE__TEST_CONNECTION=Enabled
+export AIRFLOW__CORE__LOAD_EXAMPLES=False
+export AIRFLOW__CORE__EXECUTOR=SequentialExecutor
+export AIRFLOW__CORE__DAGS_FOLDER=/tmp/airflow_test/dags
+export AIRFLOW__CORE__SQL_ALCHEMY_CONN=sqlite:////tmp/airflow_test/airflow.db
+
+# Clean up previous runs
+echo "๐งน Cleaning up previous runs..."
+rm -rf /tmp/airflow_test
+mkdir -p /tmp/airflow_test/dags
+
+# Install HTTP provider
+echo "๐ฆ Installing HTTP provider..."
+pip install -U apache-airflow-providers-http
+
+# Initialize Airflow database
+echo "๐๏ธ Initializing Airflow database..."
+cd /Users/alphaskynet/Downloads/Github\ Contributions/airflow
+airflow db init
+
+# Create test connections
+echo "๐ Creating test connections..."
+python3 -c "
+import os
+import sys
+from airflow.models import Connection
+from airflow.utils.session import create_session
+
+def create_test_connections():
+ with create_session() as session:
+ # Delete existing test connections
+ session.query(Connection).filter(Connection.conn_id.in_(['test_connection_success', 'test_connection_failure'])).delete()
+
+ # Create success connection
+ success_conn = Connection(
+ conn_id='test_connection_success',
+ conn_type='http',
+ host='https://httpbin.org/anything',
+ port=443,
+ schema='https',
+ )
+
+ # Create failure connection
+ failure_conn = Connection(
+ conn_id='test_connection_failure',
+ conn_type='http',
+ host='https://invalid.invalid',
+ port=443,
+ schema='https',
+ )
+
+ session.add_all([success_conn, failure_conn])
+ session.commit()
+ print('โ
Test connections created successfully!')
+ print(f' - {success_conn.conn_id}: {success_conn.host} (should work)')
+ print(f' - {failure_conn.conn_id}: {failure_conn.host} (should fail)')
+
+create_test_connections()
+"
+
+# Start Airflow backend
+echo "๐ Starting Airflow backend..."
+airflow standalone &
+AIRFLOW_PID=$!
+
+# Wait for Airflow to start
+echo "โณ Waiting for Airflow to start..."
+sleep 30
+
+# Get the generated password
+echo "๐ Getting login credentials..."
+if [ -f "/tmp/airflow_test/simple_auth_manager_passwords.json.generated" ]; then
+ PASSWORD=$(cat /tmp/airflow_test/simple_auth_manager_passwords.json.generated | grep -o '"admin": "[^"]*"' | cut -d'"' -f4)
+ echo "โ
Airflow is ready!"
+ echo "๐ Backend UI: http://localhost:8080/"
+ echo "๐ค Username: admin"
+ echo "๐ Password: $PASSWORD"
+ echo ""
+ echo "๐ธ Ready to test your PR #55680!"
+ echo "1. Go to http://localhost:8080/"
+ echo "2. Login with admin / $PASSWORD"
+ echo "3. Navigate to Admin โ Connections"
+ echo "4. Test the connections and take screenshots"
+else
+ echo "โ Failed to get password. Check Airflow logs."
+fi
+
+echo "โ
Complete setup finished!"
+echo "Backend PID: $AIRFLOW_PID"
+
diff --git a/test_connection_fix.py b/test_connection_fix.py
new file mode 100755
index 0000000000000..be6dc7b4e4db4
--- /dev/null
+++ b/test_connection_fix.py
@@ -0,0 +1,150 @@
+#!/usr/bin/env python3
+"""
+Test script to verify the test connection response display fix.
+
+This script helps test the fix by:
+1. Starting the Airflow development server
+2. Opening the connections page
+3. Testing various connection scenarios
+4. Verifying that error/success messages are displayed
+
+Usage:
+ python test_connection_fix.py
+"""
+
+import subprocess
+import time
+import webbrowser
+import os
+import sys
+from pathlib import Path
+
+def run_command(cmd, cwd=None):
+ """Run a command and return the result."""
+ print(f"Running: {cmd}")
+ try:
+ result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"Error: {result.stderr}")
+ return False
+ print(f"Success: {result.stdout}")
+ return True
+ except Exception as e:
+ print(f"Exception: {e}")
+ return False
+
+def check_dependencies():
+ """Check if required dependencies are installed."""
+ print("Checking dependencies...")
+
+ # Check if we're in the right directory
+ if not os.path.exists("airflow-core"):
+ print("Error: Please run this script from the airflow root directory")
+ return False
+
+ # Check if uv is installed
+ if not run_command("which uv"):
+ print("Error: uv is not installed. Please install it first.")
+ return False
+
+ return True
+
+def setup_environment():
+ """Set up the development environment."""
+ print("Setting up environment...")
+
+ # Navigate to airflow-core directory
+ os.chdir("airflow-core")
+
+ # Install dependencies
+ print("Installing dependencies...")
+ if not run_command("uv sync"):
+ print("Error: Failed to install dependencies")
+ return False
+
+ return True
+
+def start_airflow():
+ """Start the Airflow development server."""
+ print("Starting Airflow...")
+
+ # Set environment variables
+ env = os.environ.copy()
+ env["AIRFLOW__CORE__TEST_CONNECTION"] = "Enabled"
+ env["AIRFLOW__CORE__LOAD_EXAMPLES"] = "False"
+ env["AIRFLOW__DATABASE__SQL_ALCHEMY_CONN"] = "sqlite:///airflow.db"
+
+ # Start Airflow webserver
+ print("Starting Airflow webserver...")
+ webserver_process = subprocess.Popen(
+ ["uv", "run", "airflow", "webserver", "--port", "8080"],
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+
+ # Wait a bit for the server to start
+ print("Waiting for server to start...")
+ time.sleep(10)
+
+ return webserver_process
+
+def test_connections():
+ """Test the connections functionality."""
+ print("Testing connections...")
+
+ # Open the connections page
+ url = "http://localhost:8080/connections"
+ print(f"Opening {url}")
+ webbrowser.open(url)
+
+ print("\n" + "="*50)
+ print("TESTING INSTRUCTIONS:")
+ print("="*50)
+ print("1. The connections page should open in your browser")
+ print("2. Look for the 'Test' button next to any connection")
+ print("3. Click the 'Test' button and observe:")
+ print(" - The button should show a loading state")
+ print(" - After completion, hover over the button to see the tooltip")
+ print(" - You should see a message overlay below the button")
+ print(" - The button icon should change (green for success, red for failure)")
+ print("4. Try testing different connections:")
+ print(" - Valid connections should show success messages")
+ print(" - Invalid connections should show error messages")
+ print("5. Check the browser console for any error messages")
+ print("\nExpected behavior:")
+ print("- Success: Green wifi icon + success message in tooltip/overlay")
+ print("- Failure: Red wifi-off icon + error message in tooltip/overlay")
+ print("- Messages should be visible both in tooltip and as overlay")
+ print("="*50)
+
+def main():
+ """Main function."""
+ print("Test Connection Response Display Fix")
+ print("===================================")
+
+ if not check_dependencies():
+ sys.exit(1)
+
+ if not setup_environment():
+ sys.exit(1)
+
+ try:
+ webserver_process = start_airflow()
+ test_connections()
+
+ print("\nPress Ctrl+C to stop the server and exit...")
+ try:
+ webserver_process.wait()
+ except KeyboardInterrupt:
+ print("\nStopping server...")
+ webserver_process.terminate()
+ webserver_process.wait()
+ print("Server stopped.")
+
+ except Exception as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()
diff --git a/test_connections.py b/test_connections.py
new file mode 100755
index 0000000000000..47c90e29fd392
--- /dev/null
+++ b/test_connections.py
@@ -0,0 +1,44 @@
+#!/usr/bin/env python3
+"""
+Test script to verify the test connection functionality works
+"""
+import requests
+import json
+import time
+
+def test_connection_endpoint():
+ """Test the connection test endpoint directly"""
+ base_url = "http://localhost:8080"
+
+ # Test success connection
+ print("๐งช Testing success connection...")
+ success_response = requests.post(
+ f"{base_url}/api/v2/connections/test",
+ params={"connection_id": "test_connection_success"},
+ headers={"Content-Type": "application/json"}
+ )
+
+ if success_response.status_code == 200:
+ success_data = success_response.json()
+ print(f"โ
Success connection: {success_data}")
+ else:
+ print(f"โ Success connection failed: {success_response.status_code} - {success_response.text}")
+
+ # Test failure connection
+ print("\n๐งช Testing failure connection...")
+ failure_response = requests.post(
+ f"{base_url}/api/v2/connections/test",
+ params={"connection_id": "test_connection_failure"},
+ headers={"Content-Type": "application/json"}
+ )
+
+ if failure_response.status_code == 200:
+ failure_data = failure_response.json()
+ print(f"โ
Failure connection: {failure_data}")
+ else:
+ print(f"โ Failure connection failed: {failure_response.status_code} - {failure_response.text}")
+
+if __name__ == "__main__":
+ print("๐ Testing Airflow connection endpoints...")
+ test_connection_endpoint()
+
diff --git a/test_ui_fix.py b/test_ui_fix.py
new file mode 100755
index 0000000000000..ec606829c3eb4
--- /dev/null
+++ b/test_ui_fix.py
@@ -0,0 +1,107 @@
+#!/usr/bin/env python3
+"""
+Simple test script to verify the test connection response display fix.
+
+This script starts the UI development server so you can test the fix manually.
+"""
+
+import subprocess
+import time
+import webbrowser
+import os
+import sys
+from pathlib import Path
+
+def run_command(cmd, cwd=None):
+ """Run a command and return the result."""
+ print(f"Running: {cmd}")
+ try:
+ result = subprocess.run(cmd, shell=True, cwd=cwd, capture_output=True, text=True)
+ if result.returncode != 0:
+ print(f"Error: {result.stderr}")
+ return False
+ print(f"Success: {result.stdout}")
+ return True
+ except Exception as e:
+ print(f"Exception: {e}")
+ return False
+
+def main():
+ """Main function."""
+ print("Test Connection Response Display Fix - UI Testing")
+ print("================================================")
+
+ # Navigate to the UI directory
+ ui_dir = Path("airflow-core/src/airflow/ui")
+ if not ui_dir.exists():
+ print("Error: UI directory not found")
+ sys.exit(1)
+
+ print(f"Changing to directory: {ui_dir}")
+ os.chdir(ui_dir)
+
+ # Check if node_modules exists
+ if not Path("node_modules").exists():
+ print("Installing dependencies...")
+ if not run_command("pnpm install"):
+ print("Error: Failed to install dependencies")
+ sys.exit(1)
+
+ # Start the development server
+ print("Starting UI development server...")
+ print("This will start the Vite development server on http://localhost:5173")
+ print("You can then test the test connection functionality.")
+
+ try:
+ # Start the dev server
+ dev_server = subprocess.Popen(
+ ["pnpm", "dev"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE
+ )
+
+ # Wait a bit for the server to start
+ print("Waiting for server to start...")
+ time.sleep(5)
+
+ # Open the browser
+ url = "http://localhost:5173"
+ print(f"Opening {url}")
+ webbrowser.open(url)
+
+ print("\n" + "="*60)
+ print("TESTING INSTRUCTIONS:")
+ print("="*60)
+ print("1. The UI should open in your browser")
+ print("2. Navigate to the Connections page")
+ print("3. Look for the 'Test' button next to any connection")
+ print("4. Click the 'Test' button and observe:")
+ print(" - The button should show a loading state")
+ print(" - After completion, hover over the button to see the tooltip")
+ print(" - You should see a message overlay below the button")
+ print(" - The button icon should change (green for success, red for failure)")
+ print("5. Try testing different connections:")
+ print(" - Valid connections should show success messages")
+ print(" - Invalid connections should show error messages")
+ print("6. Check the browser console for any error messages")
+ print("\nExpected behavior:")
+ print("- Success: Green wifi icon + success message in tooltip/overlay")
+ print("- Failure: Red wifi-off icon + error message in tooltip/overlay")
+ print("- Messages should be visible both in tooltip and as overlay")
+ print("="*60)
+
+ print("\nPress Ctrl+C to stop the server and exit...")
+ try:
+ dev_server.wait()
+ except KeyboardInterrupt:
+ print("\nStopping server...")
+ dev_server.terminate()
+ dev_server.wait()
+ print("Server stopped.")
+
+ except Exception as e:
+ print(f"Error: {e}")
+ sys.exit(1)
+
+if __name__ == "__main__":
+ main()