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()