@@ -13,6 +13,133 @@ import {
1313import { dataSourcePropertiesMap } from "../configuration-data.js" ;
1414import { apiFetch } from "../config.js" ;
1515
16+ /**
17+ * Test a connection and display the result
18+ * @param {Object } connection - The connection object to test
19+ * @param {HTMLElement } button - The button element that was clicked (to show loading state)
20+ */
21+ async function testConnection ( connection , button ) {
22+ const originalText = button . innerText ;
23+ button . innerText = "Testing..." ;
24+ button . disabled = true ;
25+
26+ try {
27+ const response = await apiFetch ( "/connection/test" , {
28+ method : "POST" ,
29+ headers : {
30+ "Content-Type" : "application/json"
31+ } ,
32+ body : JSON . stringify ( connection )
33+ } ) ;
34+
35+ const result = await response . json ( ) ;
36+ // Limit to 100 characters, show ellipsis if truncated
37+ const resultDetails = result . details ? ` - ${ truncateString ( result . details , 100 ) } ` : "" ;
38+
39+ if ( result . success ) {
40+ createToast (
41+ `Test: ${ connection . name } ` ,
42+ `${ result . message } ${ resultDetails } ` ,
43+ "success"
44+ ) ;
45+ } else {
46+ createToast (
47+ `Test: ${ connection . name } ` ,
48+ `${ result . message } ${ resultDetails } ` ,
49+ "fail"
50+ ) ;
51+ }
52+ } catch ( err ) {
53+ // Limit to 100 characters, show ellipsis if truncated
54+ const resultDetails = err . message ? ` - ${ truncateString ( err . message , 100 ) } ` : "" ;
55+ createToast (
56+ `Test: ${ connection . name } ` ,
57+ `Connection test failed: ${ resultDetails } ` ,
58+ "fail"
59+ ) ;
60+ } finally {
61+ button . innerText = originalText ;
62+ button . disabled = false ;
63+ }
64+ }
65+
66+ /**
67+ * Test an existing saved connection by name
68+ * @param {string } connectionName - The name of the saved connection to test
69+ * @param {HTMLElement } button - The button element that was clicked (to show loading state)
70+ */
71+ async function testExistingConnection ( connectionName , button ) {
72+ const originalText = button . innerText ;
73+ button . innerText = "Testing..." ;
74+ button . disabled = true ;
75+
76+ try {
77+ const response = await apiFetch ( `/connection/${ connectionName } /test` , {
78+ method : "POST"
79+ } ) ;
80+
81+ const result = await response . json ( ) ;
82+ // Limit to 100 characters, show ellipsis if truncated
83+ const resultDetails = result . details ? ` - ${ truncateString ( result . details , 100 ) } ` : "" ;
84+ if ( result . success ) {
85+ createToast (
86+ `Test: ${ connectionName } ` ,
87+ `${ result . message } ${ resultDetails } ` ,
88+ "success"
89+ ) ;
90+ } else {
91+ createToast (
92+ `Test: ${ connectionName } ` ,
93+ `${ result . message } ${ resultDetails } ` ,
94+ "fail"
95+ ) ;
96+ }
97+ } catch ( err ) {
98+ // Limit to 100 characters, show ellipsis if truncated
99+ const resultDetails = err . message ? ` - ${ truncateString ( err . message , 100 ) } ` : "" ;
100+ createToast (
101+ `Test: ${ connectionName } ` ,
102+ `Connection test failed: ${ resultDetails } ` ,
103+ "fail"
104+ ) ;
105+ } finally {
106+ button . innerText = originalText ;
107+ button . disabled = false ;
108+ }
109+ }
110+
111+ function truncateString ( str , maxLength ) {
112+ return str . length > maxLength ? str . substring ( 0 , maxLength ) + "..." : str ;
113+ }
114+
115+ /**
116+ * Build a connection object from a data source container element
117+ * @param {HTMLElement } container - The data source container element
118+ * @returns {Object } The connection object
119+ */
120+ function buildConnectionFromContainer ( container ) {
121+ let connection = { } ;
122+ let connectionOptions = { } ;
123+ let inputFields = Array . from ( container . querySelectorAll ( ".input-field" ) . values ( ) ) ;
124+
125+ for ( let inputField of inputFields ) {
126+ let ariaLabel = inputField . getAttribute ( "aria-label" ) ;
127+ if ( ariaLabel ) {
128+ if ( ariaLabel === "Name" ) {
129+ connection [ "name" ] = inputField . value || "test-connection" ;
130+ } else if ( ariaLabel === "Data source" ) {
131+ connection [ "type" ] = inputField . value ;
132+ } else {
133+ if ( inputField . value !== "" ) {
134+ connectionOptions [ ariaLabel ] = inputField . value ;
135+ }
136+ }
137+ }
138+ }
139+ connection [ "options" ] = connectionOptions ;
140+ return connection ;
141+ }
142+
16143const addDataSourceButton = document . getElementById ( "add-data-source-button" ) ;
17144const dataSourceConfigRow = document . getElementById ( "add-data-source-config-row" ) ;
18145const submitConnectionButton = document . getElementById ( "submit-connection" ) ;
@@ -105,17 +232,51 @@ async function getExistingConnections() {
105232 let connections = respJson . connections ;
106233 for ( let connection of connections ) {
107234 numExistingConnections += 1 ;
108- let accordionItem = createAccordionItem ( numExistingConnections , connection . name , "" , syntaxHighlight ( connection ) ) ;
109- // add in button to delete connection
235+
236+ // Check if connection is from config (default connection) or file (user-created)
237+ let isFromConfig = connection . source === "config" ;
238+ let sourceLabel = isFromConfig ? " (default)" : "" ;
239+ let accordionItem = createAccordionItem ( numExistingConnections , connection . name + sourceLabel , "" , syntaxHighlight ( connection ) ) ;
240+
241+ // Add test connection button
242+ let testButton = createButton ( `connection-test-${ connection . name } ` , "Test connection" , "btn btn-info me-2" , "Test" ) ;
243+ testButton . setAttribute ( "title" , "Test this connection" ) ;
244+ testButton . addEventListener ( "click" , async function ( ) {
245+ await testExistingConnection ( connection . name , testButton ) ;
246+ } ) ;
247+
248+ // Add delete connection button
110249 let deleteButton = createButton ( `connection-delete-${ connection . name } ` , "Connection delete" , "btn btn-danger" , "Delete" ) ;
250+
251+ // Disable delete button for config-based connections
252+ if ( isFromConfig ) {
253+ deleteButton . setAttribute ( "disabled" , "true" ) ;
254+ deleteButton . setAttribute ( "title" , "Default connections from application.conf cannot be deleted via UI" ) ;
255+ deleteButton . classList . add ( "btn-secondary" ) ;
256+ deleteButton . classList . remove ( "btn-danger" ) ;
257+ }
111258
112259 deleteButton . addEventListener ( "click" , async function ( ) {
113- await apiFetch ( `/connection/${ connection . name } ` , { method : "DELETE" } ) ;
114- accordionConnections . removeChild ( accordionItem ) ;
115- createToast ( `${ connection . name } ` , `Connection ${ connection . name } deleted!` , "success" ) ;
260+ if ( isFromConfig ) {
261+ createToast ( `${ connection . name } ` , `Cannot delete default connection. Modify application.conf or environment variables to remove.` , "fail" ) ;
262+ return ;
263+ }
264+
265+ try {
266+ const response = await apiFetch ( `/connection/${ connection . name } ` , { method : "DELETE" } ) ;
267+ if ( response . ok ) {
268+ accordionConnections . removeChild ( accordionItem ) ;
269+ createToast ( `${ connection . name } ` , `Connection ${ connection . name } deleted!` , "success" ) ;
270+ } else {
271+ const errorText = await response . text ( ) ;
272+ createToast ( `${ connection . name } ` , `Failed to delete connection: ${ errorText } ` , "fail" ) ;
273+ }
274+ } catch ( err ) {
275+ createToast ( `${ connection . name } ` , `Failed to delete connection: ${ err . message } ` , "fail" ) ;
276+ }
116277 } ) ;
117278
118- let buttonGroup = createButtonGroup ( deleteButton ) ;
279+ let buttonGroup = createButtonGroup ( testButton , deleteButton ) ;
119280 let header = accordionItem . querySelector ( ".accordion-header" ) ;
120281 let divContainer = document . createElement ( "div" ) ;
121282 divContainer . setAttribute ( "class" , "d-flex align-items-center" ) ;
@@ -149,6 +310,8 @@ function createDataSourceElement(index, hr) {
149310 colName . setAttribute ( "class" , "col" ) ;
150311 let colSelect = document . createElement ( "div" ) ;
151312 colSelect . setAttribute ( "class" , "col" ) ;
313+ let colTestButton = document . createElement ( "div" ) ;
314+ colTestButton . setAttribute ( "class" , "col-auto" ) ;
152315
153316 let dataSourceName = createInput ( `data-source-name-${ index } ` , "Name" , "form-control input-field data-source-property" , "text" , `my-data-source-${ index } ` ) ;
154317 let formFloatingName = createFormFloating ( "Name" , dataSourceName ) ;
@@ -181,6 +344,20 @@ function createDataSourceElement(index, hr) {
181344 }
182345 }
183346 }
347+
348+ // Create test connection button for new connections
349+ let testButton = createButton ( `data-source-test-${ index } ` , "Test connection" , "btn btn-info" , "Test Connection" ) ;
350+ testButton . setAttribute ( "title" , "Test this connection before saving" ) ;
351+ testButton . addEventListener ( "click" , async function ( ) {
352+ const connection = buildConnectionFromContainer ( divContainer ) ;
353+ if ( ! connection . type ) {
354+ createToast ( "Test Connection" , "Please select a data source type first" , "fail" ) ;
355+ return ;
356+ }
357+ await testConnection ( connection , testButton ) ;
358+ } ) ;
359+ colTestButton . append ( testButton ) ;
360+
184361 let closeButton = createCloseButton ( divContainer ) ;
185362
186363 createDataSourceOptions ( dataSourceSelect ) ;
@@ -190,7 +367,7 @@ function createDataSourceElement(index, hr) {
190367 if ( hr ) {
191368 divContainer . append ( hr ) ;
192369 }
193- divContainer . append ( colName , colSelect , closeButton ) ;
370+ divContainer . append ( colName , colSelect , colTestButton , closeButton ) ;
194371 return divContainer ;
195372}
196373
0 commit comments