From 20a3973fccf63a61a10a2cf8c755d6d9090535e1 Mon Sep 17 00:00:00 2001 From: F3l1x1vo Date: Thu, 30 Jul 2026 08:58:28 +0200 Subject: [PATCH 1/4] add iframe modularization to apps --- TECH_DEBT.md | 34 +- src/hooks.server.ts | 41 +- .../trino/query-runner.svelte.ts | 0 src/{routes/(app) => lib}/trino/validation.ts | 0 src/routes/(app)/api/trino/query/+server.ts | 2 +- src/routes/(app)/trino/+page.server.ts | 2 +- src/routes/(app)/trino/+page.svelte | 4 +- src/routes/embed/+layout.svelte | 26 + src/routes/embed/storage/+error.svelte | 66 ++ src/routes/embed/storage/+layout.server.ts | 15 + src/routes/embed/storage/+layout.svelte | 39 + src/routes/embed/storage/+layout.ts | 44 + src/routes/embed/storage/+page.server.ts | 65 ++ src/routes/embed/storage/+page.svelte | 37 + .../[bucket]/[...prefix]/+page.server.ts | 7 + .../storage/[bucket]/[...prefix]/+page.svelte | 84 ++ .../storage/[bucket]/[...prefix]/+page.ts | 50 + .../embed/storage/connections/+page.server.ts | 8 + .../embed/storage/connections/+page.svelte | 195 ++++ .../storage/connections/[id]/+layout.svelte | 22 + .../connections/[id]/edit/+page.server.ts | 64 ++ .../connections/[id]/edit/+page.svelte | 366 +++++++ src/routes/embed/trino/+page.server.ts | 55 ++ src/routes/embed/trino/+page.svelte | 910 ++++++++++++++++++ static/embed-demo.html | 473 +++++++++ 25 files changed, 2595 insertions(+), 14 deletions(-) rename src/{routes/(app) => lib}/trino/query-runner.svelte.ts (100%) rename src/{routes/(app) => lib}/trino/validation.ts (100%) create mode 100644 src/routes/embed/+layout.svelte create mode 100644 src/routes/embed/storage/+error.svelte create mode 100644 src/routes/embed/storage/+layout.server.ts create mode 100644 src/routes/embed/storage/+layout.svelte create mode 100644 src/routes/embed/storage/+layout.ts create mode 100644 src/routes/embed/storage/+page.server.ts create mode 100644 src/routes/embed/storage/+page.svelte create mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.server.ts create mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.svelte create mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.ts create mode 100644 src/routes/embed/storage/connections/+page.server.ts create mode 100644 src/routes/embed/storage/connections/+page.svelte create mode 100644 src/routes/embed/storage/connections/[id]/+layout.svelte create mode 100644 src/routes/embed/storage/connections/[id]/edit/+page.server.ts create mode 100644 src/routes/embed/storage/connections/[id]/edit/+page.svelte create mode 100644 src/routes/embed/trino/+page.server.ts create mode 100644 src/routes/embed/trino/+page.svelte create mode 100644 static/embed-demo.html diff --git a/TECH_DEBT.md b/TECH_DEBT.md index 63591e87..991b352c 100644 --- a/TECH_DEBT.md +++ b/TECH_DEBT.md @@ -90,7 +90,7 @@ Completed query snapshots (including result rows) are cleaned up after `STACKABL ### Client-side row accumulation has no memory bound -**File:** `src/routes/(app)/trino/query-runner.svelte.ts` +**File:** `src/lib/trino/query-runner.svelte.ts` The client accumulates all result rows in memory up to `MAX_CLIENT_ROWS` (10,000). For wide result sets this could consume significant browser memory. Consider implementing streaming/virtual scrolling for large results. @@ -98,7 +98,7 @@ The client accumulates all result rows in memory up to `MAX_CLIENT_ROWS` (10,000 ### Displayed results not cleared on connection change -**File:** `src/routes/(app)/trino/+page.svelte` +**File:** `src/lib/trino/query-runner.svelte.ts`, `src/routes/(app)/trino/+page.svelte`, `src/routes/embed/trino/+page.svelte` After saving a new connection, the previous query results remain visible until a new query is run. Consider calling `runner.reset()` when the connection changes. @@ -106,7 +106,7 @@ After saving a new connection, the previous query results remain visible until a ### No validation that the connection target is a Trino instance -**File:** `src/routes/(app)/trino/+page.server.ts` +**File:** `src/routes/(app)/trino/+page.server.ts`, `src/routes/embed/trino/+page.server.ts` The query action sends whatever SQL the user provides to the configured connection URL without first verifying that the endpoint is actually a Trino instance. A user could point the URL at any HTTP server, and the app would blindly POST to it. We should validate new connections (e.g. by calling Trino's `/v1/info` endpoint) and reject URLs that do not respond as a Trino server. @@ -122,6 +122,34 @@ Mobile viewport tests (393×851, touch-enabled) are excluded from CI runs to red ## Infrastructure +### Session cookie not configured for cross-origin iframe embedding + +**File:** `src/lib/server/auth.ts` + +The better-auth session cookie currently uses `SameSite=Lax` (the browser default when no `SameSite` attribute is set). Browsers do not send `SameSite=Lax` cookies when a page is loaded inside an ` + + + +
+ +
+ + + + + From b4e7fe42aa8464693684daf279fba667a4d3c74f Mon Sep 17 00:00:00 2001 From: F3l1x1vo Date: Thu, 30 Jul 2026 10:18:55 +0200 Subject: [PATCH 2/4] fix shared components being shown in iframes --- .../storage/landing/BucketGrid.svelte | 10 +- .../storage/landing/RecentItems.svelte | 12 +- .../storage/sidebar/BucketList.svelte | 16 +- .../sidebar/StorageConnectionSidebar.svelte | 13 +- src/lib/storage/display-helpers.ts | 36 +- src/lib/storage/route-context.ts | 46 ++ src/routes/(app)/storage/+layout.svelte | 2 + src/routes/embed/storage/+layout.svelte | 2 + static/embed-demo.html | 537 +++++++----------- 9 files changed, 320 insertions(+), 354 deletions(-) create mode 100644 src/lib/storage/route-context.ts diff --git a/src/lib/components/storage/landing/BucketGrid.svelte b/src/lib/components/storage/landing/BucketGrid.svelte index 41cad839..9e507a9c 100644 --- a/src/lib/components/storage/landing/BucketGrid.svelte +++ b/src/lib/components/storage/landing/BucketGrid.svelte @@ -1,13 +1,15 @@ {#if buckets.length > 0} @@ -21,11 +23,9 @@ > {#each buckets as bucket (bucket)}
+ @@ -97,7 +99,7 @@
{locationName(loc)} @@ -146,7 +148,7 @@ @@ -161,7 +163,7 @@
import { page } from '$app/state'; - import { resolve } from '$app/paths'; import { beforeNavigate } from '$app/navigation'; import IconClose from 'virtual:icons/material-symbols/close'; import IconMoreHoriz from 'virtual:icons/material-symbols/more-horiz'; @@ -12,12 +11,14 @@ import Modal from '$lib/components/Modal.svelte'; import * as m from '$lib/paraglide/messages.js'; import { getStorageState } from '$lib/storage/context.js'; + import { getStorageRouteBase } from '$lib/storage/route-context.js'; import type { PinnedLocation, StorageLocation } from '$lib/storage/types.js'; import { pinnedLabel, pinnedHref } from '$lib/storage/display-helpers.js'; import { createResizablePanel } from './resizable-panel.svelte.js'; import ResizeHandle from './ResizeHandle.svelte'; const storage = getStorageState(); + const routes = getStorageRouteBase(); const resize = createResizablePanel({ storageKey: 'storage_sidebar_width', @@ -40,7 +41,7 @@ // with optional chaining to avoid crashing the layout and escalating the // error to the root fallback handler. const activeBucket = $derived.by(() => { - const match = page.url?.pathname?.match(/^\/storage\/([^/]+)/); + const match = page.url?.pathname?.match(routes.activeBucketRegex); return match ? decodeURIComponent(match[1]) : null; }); const activePrefix = $derived(page.params.prefix ? page.params.prefix + '/' : ''); @@ -157,7 +158,7 @@ > -
+ - - - - - - - -
- - - -
-
- - - -
-

- Welcome to My Portal -

-

- This page simulates a host frontend that embeds the - Trino SQL Editor and the Storage Browser from - Stackable Cockpit via <iframe>. Click a tab or sidebar link to open - an embedded app. -

+ Pipelines + + Admin + Users + Settings + + +
+ +
+
+ Acme Corp Data Platform + Select a section from the sidebar. The SQL Query and + Object Storage panels are powered by Stackable Cockpit embedded via + <iframe>.
-
- -
- -
+ +
+ +
- -
- + +
+ +
-
+
From 2760dfd0d24bed1d407eab28db469b797d4d7213 Mon Sep 17 00:00:00 2001 From: F3l1x1vo Date: Thu, 30 Jul 2026 13:43:11 +0200 Subject: [PATCH 3/4] fix unit tests --- .../explorer/__tests__/ContextWrapper.svelte | 6 +- .../storage/landing/BucketGrid.svelte.spec.ts | 22 +++---- .../landing/StorageConnectForm.svelte.spec.ts | 62 +++++++++---------- .../__tests__/BucketGridWrapper.svelte | 14 +++++ .../__tests__/RecentItemsWrapper.svelte | 6 +- .../StorageConnectFormWrapper.svelte | 11 ++++ .../__tests__/BucketListWrapper.svelte | 6 +- 7 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 src/lib/components/storage/landing/__tests__/BucketGridWrapper.svelte create mode 100644 src/lib/components/storage/landing/__tests__/StorageConnectFormWrapper.svelte diff --git a/src/lib/components/storage/explorer/__tests__/ContextWrapper.svelte b/src/lib/components/storage/explorer/__tests__/ContextWrapper.svelte index 4e200081..72c30a22 100644 --- a/src/lib/components/storage/explorer/__tests__/ContextWrapper.svelte +++ b/src/lib/components/storage/explorer/__tests__/ContextWrapper.svelte @@ -1,6 +1,7 @@ {@render children()} diff --git a/src/lib/components/storage/landing/BucketGrid.svelte.spec.ts b/src/lib/components/storage/landing/BucketGrid.svelte.spec.ts index fb093e5f..eccddff5 100644 --- a/src/lib/components/storage/landing/BucketGrid.svelte.spec.ts +++ b/src/lib/components/storage/landing/BucketGrid.svelte.spec.ts @@ -2,22 +2,22 @@ import { page } from 'vitest/browser'; import { describe, expect, it } from 'vitest'; import { render } from 'vitest-browser-svelte'; import { faker } from '@faker-js/faker'; -import BucketGrid from './BucketGrid.svelte'; +import BucketGridWrapper from './__tests__/BucketGridWrapper.svelte'; describe('BucketGrid', () => { it('should show empty message when no buckets provided', async () => { - render(BucketGrid); + render(BucketGridWrapper); await expect.element(page.getByText('No buckets found')).toBeInTheDocument(); }); it('should show empty message when buckets array is empty', async () => { - render(BucketGrid, { buckets: [] }); + render(BucketGridWrapper, { buckets: [] }); await expect.element(page.getByText('No buckets found')).toBeInTheDocument(); }); it('should render bucket links when buckets are provided', async () => { const buckets = ['my-bucket', 'other-bucket']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); const links = page.getByRole('link'); await expect.element(links.nth(0)).toBeInTheDocument(); @@ -26,7 +26,7 @@ describe('BucketGrid', () => { it('should display bucket name in each link', async () => { const buckets = ['data-lake', 'backups']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); await expect.element(page.getByText('data-lake')).toBeInTheDocument(); await expect.element(page.getByText('backups')).toBeInTheDocument(); @@ -34,7 +34,7 @@ describe('BucketGrid', () => { it('should encode bucket names in href', async () => { const buckets = ['bucket with spaces']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); const link = page.getByRole('link'); await expect @@ -44,7 +44,7 @@ describe('BucketGrid', () => { it('should handle special characters in bucket names', async () => { const buckets = ['my-bucket/special', 'name+plus', 'unicode-ñ-ü']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); await expect.element(page.getByText('my-bucket/special')).toBeInTheDocument(); await expect.element(page.getByText('name+plus')).toBeInTheDocument(); @@ -53,7 +53,7 @@ describe('BucketGrid', () => { it('should have tooltip with bucket name', async () => { const buckets = ['tooltip-bucket']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); const tooltip = page.getByText('tooltip-bucket').element().closest('[data-tip]'); expect(tooltip?.getAttribute('data-tip')).toBe('tooltip-bucket'); @@ -61,7 +61,7 @@ describe('BucketGrid', () => { it('should render a large number of buckets', async () => { const buckets = Array.from({ length: 100 }, () => faker.string.alphanumeric(10)); - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); const links = page.getByRole('link'); await expect.element(links.nth(99)).toBeInTheDocument(); @@ -69,14 +69,14 @@ describe('BucketGrid', () => { it('should handle very long bucket names', async () => { const longName = faker.string.alphanumeric(200); - render(BucketGrid, { buckets: [longName] }); + render(BucketGridWrapper, { buckets: [longName] }); await expect.element(page.getByText(longName)).toBeInTheDocument(); }); it('should handle unicode bucket names', async () => { const buckets = ['日本語バケット', '中文桶', 'émojis-🪣']; - render(BucketGrid, { buckets }); + render(BucketGridWrapper, { buckets }); await expect.element(page.getByText('日本語バケット')).toBeInTheDocument(); await expect.element(page.getByText('中文桶')).toBeInTheDocument(); diff --git a/src/lib/components/storage/landing/StorageConnectForm.svelte.spec.ts b/src/lib/components/storage/landing/StorageConnectForm.svelte.spec.ts index 3655e81f..e3c6558c 100644 --- a/src/lib/components/storage/landing/StorageConnectForm.svelte.spec.ts +++ b/src/lib/components/storage/landing/StorageConnectForm.svelte.spec.ts @@ -2,10 +2,10 @@ import { page } from 'vitest/browser'; import { describe, expect, it, vi, beforeEach } from 'vitest'; import { render } from 'vitest-browser-svelte'; import type { ComponentProps } from 'svelte'; -import StorageConnectForm from './StorageConnectForm.svelte'; +import StorageConnectFormWrapper from './__tests__/StorageConnectFormWrapper.svelte'; import type { SavedConnection } from '$lib/storage/connection-storage.js'; -type ConnectionFormProp = ComponentProps['connectionForm']; +type ConnectionFormProp = ComponentProps['connectionForm']; /** Build a minimal StoredConnection fixture with a stable UUID. */ function makeConn( @@ -117,14 +117,14 @@ describe('StorageConnectForm', () => { }); it('should render the form title and subtitle', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('Connect to Storage')).toBeInTheDocument(); await expect.element(page.getByText('Enter your connection details')).toBeInTheDocument(); }); it('should render all form fields with labels', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByLabelText('Backend type')).toBeInTheDocument(); await expect.element(page.getByLabelText('Host')).toBeInTheDocument(); @@ -137,7 +137,7 @@ describe('StorageConnectForm', () => { }); it('should render backend type as a select with S3 option', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const select = page.getByLabelText('Backend type'); await expect.element(select).toBeInTheDocument(); @@ -146,28 +146,28 @@ describe('StorageConnectForm', () => { }); it('should render host as text input', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const input = page.getByLabelText('Host'); await expect.element(input).toHaveAttribute('type', 'text'); }); it('should render port as number input', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const input = page.getByLabelText('Port'); await expect.element(input).toHaveAttribute('type', 'number'); }); it('should render TLS as a checkbox toggle', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const toggle = page.getByLabelText('Use TLS'); await expect.element(toggle).toHaveAttribute('type', 'checkbox'); }); it('should show verify certificate toggle when TLS is enabled', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ data: { tls: { verification: 'Full' } } }) }); @@ -175,7 +175,7 @@ describe('StorageConnectForm', () => { }); it('should hide verify certificate toggle when TLS is disabled', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ data: { tls: undefined } }) }); @@ -183,20 +183,20 @@ describe('StorageConnectForm', () => { }); it('should render secret key as password input', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const input = page.getByLabelText('Secret key'); await expect.element(input).toHaveAttribute('type', 'password'); }); it('should render connect button', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByRole('button', { name: 'Connect' })).toBeInTheDocument(); }); it('should show no saved connections message when none exist', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('No saved connections yet')).toBeInTheDocument(); }); @@ -212,7 +212,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('minio.example.com:9000').first()).toBeInTheDocument(); }); @@ -227,7 +227,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect .element(page.getByRole('button', { name: 'Forget s3.test.io' })) @@ -244,7 +244,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const forgetBtn = page.getByRole('button', { name: 'Forget s3.remove.io' }); await forgetBtn.click(); @@ -282,7 +282,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('first.example.com').first()).toBeInTheDocument(); await expect.element(page.getByText('second.example.com:9000').first()).toBeInTheDocument(); @@ -299,20 +299,20 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByRole('list').first()).toBeInTheDocument(); await expect.element(page.getByRole('listitem').first()).toBeInTheDocument(); }); it('should render port hint text', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('Leave blank for default port')).toBeInTheDocument(); }); it('should render form with POST method', async () => { - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); const form = page.getByRole('button', { name: 'Connect' }).element().closest('form'); expect(form?.getAttribute('method')).toBe('POST'); @@ -328,7 +328,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await page.getByRole('button', { name: 'Forget s3.cancel.io' }).click(); await expect @@ -351,7 +351,7 @@ describe('StorageConnectForm', () => { }); vi.mocked(loadAllConnectionsLocally).mockReturnValue([conn]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await page.getByRole('button', { name: 'Forget s3.forget.io' }).click(); // After clicking forget, mock returns empty list @@ -363,7 +363,7 @@ describe('StorageConnectForm', () => { }); it('should display host error when form has host errors', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ errors: { host: ['Host is required'] } }) @@ -376,7 +376,7 @@ describe('StorageConnectForm', () => { }); it('should display region error when form has region errors', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ errors: { region: { name: ['Region name is required'] } } }) @@ -388,7 +388,7 @@ describe('StorageConnectForm', () => { }); it('should display access key error', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ errors: { credentials: { accessKey: ['Access key is required when secret key is provided'] } @@ -404,7 +404,7 @@ describe('StorageConnectForm', () => { }); it('should display secret key error', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ errors: { credentials: { secretKey: ['Secret key is required when access key is provided'] } @@ -420,7 +420,7 @@ describe('StorageConnectForm', () => { }); it('should display server message when present', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ message: 'Connection refused: unable to reach endpoint' }) @@ -444,7 +444,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); // Click the saved connection button (first match — mobile card) await page.getByRole('button', { name: 's3.select.io:9000', exact: true }).first().click(); @@ -466,13 +466,13 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await expect.element(page.getByText('Saved connections').first()).toBeInTheDocument(); }); it('should display multiple field errors simultaneously', async () => { - render(StorageConnectForm, { + render(StorageConnectFormWrapper, { connectionForm: createMockForm({ errors: { host: ['Host is required'], @@ -503,7 +503,7 @@ describe('StorageConnectForm', () => { }) ]); - render(StorageConnectForm, { connectionForm: createMockForm() }); + render(StorageConnectFormWrapper, { connectionForm: createMockForm() }); await page.getByRole('button', { name: 'Forget s3.backdrop.io' }).click(); await expect diff --git a/src/lib/components/storage/landing/__tests__/BucketGridWrapper.svelte b/src/lib/components/storage/landing/__tests__/BucketGridWrapper.svelte new file mode 100644 index 00000000..b894957c --- /dev/null +++ b/src/lib/components/storage/landing/__tests__/BucketGridWrapper.svelte @@ -0,0 +1,14 @@ + + + diff --git a/src/lib/components/storage/landing/__tests__/RecentItemsWrapper.svelte b/src/lib/components/storage/landing/__tests__/RecentItemsWrapper.svelte index 4f3cf0eb..b9532216 100644 --- a/src/lib/components/storage/landing/__tests__/RecentItemsWrapper.svelte +++ b/src/lib/components/storage/landing/__tests__/RecentItemsWrapper.svelte @@ -1,6 +1,7 @@ diff --git a/src/lib/components/storage/landing/__tests__/StorageConnectFormWrapper.svelte b/src/lib/components/storage/landing/__tests__/StorageConnectFormWrapper.svelte new file mode 100644 index 00000000..202ae05b --- /dev/null +++ b/src/lib/components/storage/landing/__tests__/StorageConnectFormWrapper.svelte @@ -0,0 +1,11 @@ + + + diff --git a/src/lib/components/storage/sidebar/__tests__/BucketListWrapper.svelte b/src/lib/components/storage/sidebar/__tests__/BucketListWrapper.svelte index 622b879f..03cbd8cb 100644 --- a/src/lib/components/storage/sidebar/__tests__/BucketListWrapper.svelte +++ b/src/lib/components/storage/sidebar/__tests__/BucketListWrapper.svelte @@ -1,6 +1,7 @@ From 255490f4239035a87df621adc5dd66eb4e29e128 Mon Sep 17 00:00:00 2001 From: F3l1x1vo Date: Fri, 31 Jul 2026 10:38:57 +0200 Subject: [PATCH 4/4] Replace iframe route duplication with embed query parameter Previously the trino and storage browser applications were duplicated under /embed/* routes just to hide the app shell. Instead, a single `?embed=1` query parameter now hides the sidebar and header via the (app) layout, keeping the embed mode sticky across navigations. The /embed route tree, the storage route-context indirection and the trino helper duplication are removed; the embed response headers are now keyed on the query parameter instead of the path prefix. --- TECH_DEBT.md | 6 +- src/hooks.server.ts | 12 +- .../explorer/__tests__/ContextWrapper.svelte | 6 +- .../storage/landing/BucketGrid.svelte | 10 +- .../storage/landing/BucketGrid.svelte.spec.ts | 22 +- .../storage/landing/RecentItems.svelte | 12 +- .../landing/StorageConnectForm.svelte.spec.ts | 62 +- .../__tests__/BucketGridWrapper.svelte | 14 - .../__tests__/RecentItemsWrapper.svelte | 6 +- .../StorageConnectFormWrapper.svelte | 11 - .../storage/sidebar/BucketList.svelte | 16 +- .../sidebar/StorageConnectionSidebar.svelte | 13 +- .../__tests__/BucketListWrapper.svelte | 6 +- src/lib/storage/display-helpers.ts | 36 +- src/lib/storage/route-context.ts | 46 - src/routes/(app)/+layout.svelte | 79 +- src/routes/(app)/api/trino/query/+server.ts | 2 +- src/routes/(app)/storage/+layout.svelte | 2 - src/routes/(app)/trino/+page.server.ts | 2 +- src/routes/(app)/trino/+page.svelte | 4 +- .../(app)}/trino/query-runner.svelte.ts | 0 src/{lib => routes/(app)}/trino/validation.ts | 0 src/routes/embed/+layout.svelte | 26 - src/routes/embed/storage/+error.svelte | 66 -- src/routes/embed/storage/+layout.server.ts | 15 - src/routes/embed/storage/+layout.svelte | 41 - src/routes/embed/storage/+layout.ts | 44 - src/routes/embed/storage/+page.server.ts | 65 -- src/routes/embed/storage/+page.svelte | 37 - .../[bucket]/[...prefix]/+page.server.ts | 7 - .../storage/[bucket]/[...prefix]/+page.svelte | 84 -- .../storage/[bucket]/[...prefix]/+page.ts | 50 - .../embed/storage/connections/+page.server.ts | 8 - .../embed/storage/connections/+page.svelte | 195 ---- .../storage/connections/[id]/+layout.svelte | 22 - .../connections/[id]/edit/+page.server.ts | 64 -- .../connections/[id]/edit/+page.svelte | 366 ------- src/routes/embed/trino/+page.server.ts | 55 -- src/routes/embed/trino/+page.svelte | 910 ------------------ static/embed-demo.html | 8 +- 40 files changed, 164 insertions(+), 2266 deletions(-) delete mode 100644 src/lib/components/storage/landing/__tests__/BucketGridWrapper.svelte delete mode 100644 src/lib/components/storage/landing/__tests__/StorageConnectFormWrapper.svelte delete mode 100644 src/lib/storage/route-context.ts rename src/{lib => routes/(app)}/trino/query-runner.svelte.ts (100%) rename src/{lib => routes/(app)}/trino/validation.ts (100%) delete mode 100644 src/routes/embed/+layout.svelte delete mode 100644 src/routes/embed/storage/+error.svelte delete mode 100644 src/routes/embed/storage/+layout.server.ts delete mode 100644 src/routes/embed/storage/+layout.svelte delete mode 100644 src/routes/embed/storage/+layout.ts delete mode 100644 src/routes/embed/storage/+page.server.ts delete mode 100644 src/routes/embed/storage/+page.svelte delete mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.server.ts delete mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.svelte delete mode 100644 src/routes/embed/storage/[bucket]/[...prefix]/+page.ts delete mode 100644 src/routes/embed/storage/connections/+page.server.ts delete mode 100644 src/routes/embed/storage/connections/+page.svelte delete mode 100644 src/routes/embed/storage/connections/[id]/+layout.svelte delete mode 100644 src/routes/embed/storage/connections/[id]/edit/+page.server.ts delete mode 100644 src/routes/embed/storage/connections/[id]/edit/+page.svelte delete mode 100644 src/routes/embed/trino/+page.server.ts delete mode 100644 src/routes/embed/trino/+page.svelte diff --git a/TECH_DEBT.md b/TECH_DEBT.md index 991b352c..b18ff77a 100644 --- a/TECH_DEBT.md +++ b/TECH_DEBT.md @@ -98,7 +98,7 @@ The client accumulates all result rows in memory up to `MAX_CLIENT_ROWS` (10,000 ### Displayed results not cleared on connection change -**File:** `src/lib/trino/query-runner.svelte.ts`, `src/routes/(app)/trino/+page.svelte`, `src/routes/embed/trino/+page.svelte` +**File:** `src/routes/(app)/trino/query-runner.svelte.ts`, `src/routes/(app)/trino/+page.svelte` After saving a new connection, the previous query results remain visible until a new query is run. Consider calling `runner.reset()` when the connection changes. @@ -106,7 +106,7 @@ After saving a new connection, the previous query results remain visible until a ### No validation that the connection target is a Trino instance -**File:** `src/routes/(app)/trino/+page.server.ts`, `src/routes/embed/trino/+page.server.ts` +**File:** `src/routes/(app)/trino/+page.server.ts` The query action sends whatever SQL the user provides to the configured connection URL without first verifying that the endpoint is actually a Trino instance. A user could point the URL at any HTTP server, and the app would blindly POST to it. We should validate new connections (e.g. by calling Trino's `/v1/info` endpoint) and reject URLs that do not respond as a Trino server. @@ -126,7 +126,7 @@ Mobile viewport tests (393×851, touch-enabled) are excluded from CI runs to red **File:** `src/lib/server/auth.ts` -The better-auth session cookie currently uses `SameSite=Lax` (the browser default when no `SameSite` attribute is set). Browsers do not send `SameSite=Lax` cookies when a page is loaded inside an `
- +