diff --git a/e2e/trino/connection-form.spec.ts b/e2e/trino/connection-form.spec.ts index 0467eff3..ab7aa778 100644 --- a/e2e/trino/connection-form.spec.ts +++ b/e2e/trino/connection-form.spec.ts @@ -32,7 +32,12 @@ function interceptSaveSuccess(page: import('@playwright/test').Page) { valid: true, posted: true, errors: {}, - data: { connectionUrl: 'http://localhost:8080', authType: 'none' }, + data: { + connectionUrl: 'http://localhost:8080', + authType: 'none', + authUsername: '', + authPassword: '' + }, message: { type: 'success' } }; await route.fulfill({ @@ -90,7 +95,7 @@ test.describe('Connection form (env-configured)', () => { await page.goto('/trino'); await waitForHydration(page); - await expect(page.getByText('Connection', { exact: true })).not.toBeVisible(); + await expect(page.getByText('Edit connection', { exact: true })).not.toBeVisible(); await expect(page.locator('input[type="url"]')).not.toBeVisible(); }); }); @@ -117,25 +122,27 @@ test.describe('Connection form (manual mode)', () => { await page.goto('/trino'); await waitForHydration(page); - await expect(page.getByText('Connection', { exact: true })).toBeVisible(); + await expect(page.getByText('Edit connection', { exact: true })).toBeVisible(); }); - test('expanding the form reveals URL input', async ({ page }) => { + test('form is expanded initially when there is no connection', async ({ page }) => { await page.goto('/trino'); await waitForHydration(page); - // Click the collapse toggle to open the form. - await page.getByLabel('Connection').check(); - + await expect(page.getByLabel('Edit connection')).toBeChecked(); await expect(page.getByLabel('URL')).toBeVisible(); await expect(page.getByRole('button', { name: 'Save' })).toBeVisible(); + + // The collapse toggle still hides the form. + await page.getByLabel('Edit connection').uncheck(); + await expect(page.getByLabel('URL')).not.toBeVisible(); }); test('auth type toggle shows credential fields for basic auth', async ({ page }) => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); // Initially no credential fields (auth type defaults to "none"). await expect(page.getByLabel('Username')).not.toBeVisible(); @@ -152,7 +159,7 @@ test.describe('Connection form (manual mode)', () => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); // Switch to basic, then back to no auth. await page.getByRole('radio', { name: 'Basic' }).click(); @@ -167,7 +174,7 @@ test.describe('Connection form (manual mode)', () => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); // Submit with empty URL. await page.getByRole('button', { name: 'Save' }).click(); @@ -176,11 +183,25 @@ test.describe('Connection form (manual mode)', () => { await expect(page.locator('.text-error')).toBeVisible(); }); + test('invalid URL shows a translated validation message', async ({ page }) => { + await page.goto('/trino'); + await waitForHydration(page); + + await page.getByLabel('Edit connection').check(); + + await page.getByLabel('URL').fill('not a url'); + await page.getByRole('button', { name: 'Save' }).click(); + + await expect( + page.getByText('Enter a valid URL, e.g. https://trino.example.com:8443.') + ).toBeVisible(); + }); + test('basic auth requires username and password', async ({ page }) => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); // Fill URL but leave credentials empty with basic auth selected. await page.getByLabel('URL').fill('http://localhost:8080'); @@ -197,7 +218,7 @@ test.describe('Connection form (manual mode)', () => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); const testUrl = 'http://localhost:8080'; await page.getByLabel('URL').fill(testUrl); @@ -217,7 +238,7 @@ test.describe('Connection form (manual mode)', () => { await page.goto('/trino'); await waitForHydration(page); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); // The URL field should be pre-filled from localStorage. await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080'); @@ -236,6 +257,41 @@ test.describe('Connection form (manual mode)', () => { await expect(page.getByText('localhost:8080')).toBeVisible(); }); + test('failed reconnect of a stored connection opens the form with the error', async ({ + page + }) => { + await page.addInitScript(() => { + localStorage.setItem('trino_url', 'http://localhost:8080'); + localStorage.setItem('trino_auth_type', 'none'); + }); + + // The server is env-configured, so the automatic reconnect is rejected. + await page.goto('/trino'); + await waitForHydration(page); + + await expect( + page.getByText('The connection is managed via environment variables.') + ).toBeVisible(); + await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080'); + }); + + test('network error during reconnect opens the form with the error', async ({ page }) => { + await page.addInitScript(() => { + localStorage.setItem('trino_url', 'http://localhost:8080'); + localStorage.setItem('trino_auth_type', 'none'); + }); + await page.route( + (url) => url.pathname === '/trino' && url.search === '?/save', + (route) => route.abort() + ); + + await page.goto('/trino'); + await waitForHydration(page); + + await expect(page.getByText('Could not save the connection. Please try again.')).toBeVisible(); + await expect(page.getByLabel('URL')).toHaveValue('http://localhost:8080'); + }); + test('saving a connection clears previous query results', async ({ page }) => { await setTabSql(page, 'SELECT id, name FROM users'); interceptSaveSuccess(page); @@ -249,7 +305,7 @@ test.describe('Connection form (manual mode)', () => { await waitForQueryComplete(page); await expect(page.getByRole('table', { name: 'Query results' })).toBeVisible(); - await page.getByLabel('Connection').check(); + await page.getByLabel('Edit connection').check(); await page.getByLabel('URL').fill('http://localhost:8080'); await page.getByRole('button', { name: 'Save' }).click(); diff --git a/messages/de.json b/messages/de.json index 6173dea8..8431a757 100644 --- a/messages/de.json +++ b/messages/de.json @@ -58,7 +58,7 @@ "trino_prev_page": "Vorherige Seite", "trino_next_page": "Nächste Seite", "trino_last_page": "Letzte Seite", - "trino_connection_label": "Verbindung", + "trino_connection_label": "Verbindung bearbeiten", "trino_connection_error": "Bitte konfigurieren Sie eine gültige Verbindungs-URL.", "trino_connection_url": "URL", "trino_connection_auth": "Authentifizierung", @@ -68,6 +68,13 @@ "trino_auth_password": "Passwort", "trino_save_connection": "Speichern", "trino_connection_saved": "Verbindung gespeichert.", + "trino_connection_url_invalid": "Bitte eine gültige URL eingeben, z. B. https://trino.example.com:8443.", + "trino_auth_username_required": "Für Basic-Authentifizierung ist ein Benutzername erforderlich", + "trino_auth_password_required": "Für Basic-Authentifizierung ist ein Passwort erforderlich", + "trino_connection_test_timeout": "Zeitüberschreitung beim Verbindungstest", + "trino_connection_test_failed": "Verbindung zu Trino fehlgeschlagen – bitte URL und Zugangsdaten prüfen.", + "trino_connection_save_failed": "Die Verbindung konnte nicht gespeichert werden. Bitte erneut versuchen.", + "trino_connection_env_managed": "Die Verbindung wird über Umgebungsvariablen verwaltet.", "trino_cancel_query": "Abbrechen", "trino_state_submitting": "Wird eingereicht", "trino_state_queued": "Warteschlange", diff --git a/messages/en.json b/messages/en.json index b9216ce0..92a586a0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -58,7 +58,7 @@ "trino_prev_page": "Previous page", "trino_next_page": "Next page", "trino_last_page": "Last page", - "trino_connection_label": "Connection", + "trino_connection_label": "Edit connection", "trino_connection_error": "Please configure a valid connection URL.", "trino_connection_url": "URL", "trino_connection_auth": "Auth", @@ -68,6 +68,13 @@ "trino_auth_password": "Password", "trino_save_connection": "Save", "trino_connection_saved": "Connection saved.", + "trino_connection_url_invalid": "Enter a valid URL, e.g. https://trino.example.com:8443.", + "trino_auth_username_required": "Username is required for basic authentication", + "trino_auth_password_required": "Password is required for basic authentication", + "trino_connection_test_timeout": "Connection test timed out", + "trino_connection_test_failed": "Could not connect to Trino — check the URL and credentials.", + "trino_connection_save_failed": "Could not save the connection. Please try again.", + "trino_connection_env_managed": "The connection is managed via environment variables.", "trino_cancel_query": "Cancel", "trino_state_submitting": "Submitting", "trino_state_queued": "Queued", diff --git a/src/routes/(app)/trino/+page.server.ts b/src/routes/(app)/trino/+page.server.ts index 997eafe0..c6c00ad1 100644 --- a/src/routes/(app)/trino/+page.server.ts +++ b/src/routes/(app)/trino/+page.server.ts @@ -16,6 +16,7 @@ import { } from '$lib/server/trino/user-clients.js'; import { ConnectionSchema, type ConnectionMessage } from './validation.js'; import type { Actions, PageServerLoad } from './$types'; +import * as m from '$lib/paraglide/messages.js'; export const load: PageServerLoad = async ({ locals }) => { locals.logger.debug('loading Trino page'); @@ -30,12 +31,17 @@ export const actions: Actions = { save: async ({ request, locals }) => { const log = locals.logger; + const form = await superValidate(request, zod(ConnectionSchema)); + if (trinoConfigured) { - return fail(400, { error: 'Connection is managed via environment variables' }); + log.debug('connection save rejected, Trino is configured via environment variables'); + return message( + form, + { type: 'error', message: m.trino_connection_env_managed() } satisfies ConnectionMessage, + { status: 400 } + ); } - const form = await superValidate(request, zod(ConnectionSchema)); - if (!form.valid) { log.debug({ errors: form.errors }, 'connection form validation failed'); return fail(400, { form }); @@ -64,8 +70,8 @@ export const actions: Actions = { const name = (err as { name?: string })?.name; const reason = name === 'TimeoutError' || name === 'AbortError' - ? 'Connection test timed out' - : 'Could not connect to Trino — check the URL and credentials.'; + ? m.trino_connection_test_timeout() + : m.trino_connection_test_failed(); log.info({ err, trino_url: connectionUrl }, 'connection test failed'); return message(form, { type: 'error', message: reason } satisfies ConnectionMessage, { status: 400 diff --git a/src/routes/(app)/trino/+page.svelte b/src/routes/(app)/trino/+page.svelte index e61e2d51..ae4e4434 100644 --- a/src/routes/(app)/trino/+page.svelte +++ b/src/routes/(app)/trino/+page.svelte @@ -1,5 +1,5 @@