Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ auto-generated per-PR notes; this file is the curated, human-readable history.

## [Unreleased]

### Fixed
- **In-place authentication recovery no longer compresses the mounted
workspace** (#512). The preserved editor or Dashboard now stays behind a
blocking viewport overlay, rather than being pushed below a second,
independently scrolling login region.

### Added
- **OAuth reauthentication now preserves dirty document work across its page
redirect** (#512 phase 3). Before navigation, a versioned, expiring
Expand Down
21 changes: 11 additions & 10 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -432,22 +432,23 @@ h1, h2, h3, h4, h5, h6 {
}
.login-screen .mono { font-family: var(--mono); }
.auth-host {
flex: 0 1 auto;
max-height: 48vh;
/* Authentication recovery must retain the mounted document session without
becoming part of its layout. Keeping this host in the flex column squeezed
the Dashboard beneath the card and created two competing vertical
scrollers. It is a blocking recovery overlay instead. */
position: fixed; inset: 0; z-index: 120;
display: flex; align-items: center; justify-content: center;
overflow: auto;
padding: 14px;
border-bottom: 1px solid var(--border);
background:
radial-gradient(620px 220px at 50% -30%, color-mix(in oklab, var(--accent) 12%, transparent), transparent 72%),
var(--bg);
box-sizing: border-box; padding: 24px;
background: var(--scrim);
backdrop-filter: blur(4px);
}
.auth-host[hidden] { display: none; }
.login-inline {
display: flex;
justify-content: center;
flex: 0 0 auto;
}
.login-inline .login-card {
box-shadow: var(--shadow-popover);
box-shadow: var(--shadow-dialog);
}
.login-card {
width: 380px; padding: 40px 36px 36px;
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e/dashboard-kpi-move.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<script type="module">
import { createApp } from '/src/ui/app.js';
import { renderDashboard } from '/src/ui/dashboard.js';
import { createAuthenticatedExecutionScope } from '/src/application/authenticated-execution-scope.js';

const seed = {
storageVersion: 5, id: 'workspace', key: 'workspace', name: 'Workspace',
Expand All @@ -50,6 +51,12 @@
};
const root = document.querySelector('#surface');
const app = createApp({ root, broadcastChannel: () => null });
// #524: Dashboard refreshes now require the same authenticated execution
// scope production installs after sign-in. This direct-render fixture has no
// bootstrap/login route, so provide its open, inert test epoch explicitly.
const scope = createAuthenticatedExecutionScope({ epoch: 1, cancelRemote: () => {} });
app.executionScope = () => scope;
app.requireAuthenticatedExecution = () => scope;
app.conn.ensureFreshToken = async () => true;
app.conn.chCtx.onSignedOut = () => {};
app.exec.executeRead = async (result, opts) => {
Expand Down
7 changes: 7 additions & 0 deletions tests/e2e/dashboard-membership.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import { createApp } from '/src/ui/app.js';
import { renderSavedHistory } from '/src/ui/saved-history.js';
import { renderDashboard } from '/src/ui/dashboard.js';
import { createAuthenticatedExecutionScope } from '/src/application/authenticated-execution-scope.js';

// #427: a Library query plus a Dashboard whose tile owns its OWN dedicated
// copy. The star no longer creates membership, so a tile has to be seeded to
Expand All @@ -58,6 +59,12 @@
};
const root = document.querySelector('#surface');
const app = createApp({ root, broadcastChannel: () => null });
// #524: this fixture calls `renderDashboard()` without the production login
// bootstrap, so it owns a minimal open authenticated epoch for its stubbed
// reads instead of allowing the new auth gate to render Login.
const scope = createAuthenticatedExecutionScope({ epoch: 1, cancelRemote: () => {} });
app.executionScope = () => scope;
app.requireAuthenticatedExecution = () => scope;
app.conn.ensureFreshToken = async () => true;
app.conn.chCtx.onSignedOut = () => {};
app.exec.executeRead = async (result) => {
Expand Down
4 changes: 4 additions & 0 deletions tests/e2e/dashboard-mobile.html
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,10 @@
conn: {
host: () => 'clickhouse.example.internal',
email: () => 'me@example.com',
// #523: the shared header now projects the authoritative connection
// lifecycle rather than inferring it from metadata. This hand-built
// layout harness supplies the connected presentation it exercises.
connection: { value: { kind: 'connected', epoch: 1 } },
},
actions: {
openUserMenu: () => {},
Expand Down
13 changes: 13 additions & 0 deletions tests/e2e/dashboard-tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import { createApp } from '/src/ui/app.js';
import { mountAppShell } from '/src/ui/app-shell.js';
import { startDrag } from '/src/ui/splitters.js';
import { createAuthenticatedExecutionScope } from '/src/application/authenticated-execution-scope.js';

const query = (id, name, sql = 'SELECT 1', over = {}) => ({
id, sql, specVersion: 1,
Expand Down Expand Up @@ -79,6 +80,12 @@

const root = document.querySelector('#root');
const app = createApp({ root, broadcastChannel: () => null });
// #524: expanding a schema row enters authenticated work. The fixture
// mounts the shell directly (without normal sign-in bootstrap), so keep an
// explicit open test epoch for those otherwise stubbed requests.
const scope = createAuthenticatedExecutionScope({ epoch: 1, cancelRemote: () => {} });
app.executionScope = () => scope;
app.requireAuthenticatedExecution = () => scope;
app.conn.ensureFreshToken = async () => true;
app.conn.chCtx.onSignedOut = () => {};
// The schema catalog is stubbed: this fixture is about the ROLE SWITCH, and a
Expand All @@ -89,7 +96,13 @@
{ db: 'system', comment: '', tables: [] },
];
};
// The expansion assertion is about preserving the row, not fetching its
// columns. Keep that lazy request within the fixture's no-network seam.
app.catalog.loadColumns = async () => {};
app.catalog.loadReference = async () => {};
// Expanding the database also offers the production schema-graph action;
// this tree-only fixture neither mounts nor asserts that graph.
app.actions.showSchemaGraph = () => {};

// Record navigation instead of performing it: this fixture verifies the TREE,
// not the Dashboard renderer (which has its own fixtures).
Expand Down
6 changes: 6 additions & 0 deletions tests/e2e/oauth-document-recovery.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ test.describe('OAuth document recovery redirect (#512)', () => {
));
await page.evaluate(() => window.__oauthRecoveryTrigger401());
await authLossResponse;
// Recovery retains the document session, but it must not remain in the
// shell's flex flow: that used to push the workspace below a second scroll
// region. The authentication form is a blocking viewport overlay instead.
const recovery = page.locator('.auth-host');
await expect(recovery).toHaveCSS('position', 'fixed');
await expect(recovery).toHaveCSS('z-index', '120');
const sso = page.locator('.login-inline .login-sso .login-btn');
await expect(sso).toHaveText('Continue with Fixture SSO');

Expand Down
11 changes: 11 additions & 0 deletions tests/e2e/tile-open-workbench.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
import { createCodeMirrorEditor } from '/src/editor/codemirror-adapter.js';
import { createSpecEditor } from '/src/editor/spec-editor.js';
import { createCodeViewer } from '/src/editor/code-viewer.js';
import { createAuthenticatedExecutionScope } from '/src/application/authenticated-execution-scope.js';
import { libraryQueries } from '/src/dashboard/model/query-ownership.js';

// `table` rather than a chart panel: the Chart.js seam is deliberately NOT
Expand Down Expand Up @@ -117,10 +118,20 @@
SpecEditor: (a) => createSpecEditor(a),
CodeViewer: createCodeViewer,
});
// #524: this full-surface fixture bypasses sign-in but still executes
// Dashboard and Workbench work, so provide the authenticated epoch that
// production boot installs after a successful connection.
const scope = createAuthenticatedExecutionScope({ epoch: 1, cancelRemote: () => {} });
app.executionScope = () => scope;
app.requireAuthenticatedExecution = () => scope;
app.conn.ensureFreshToken = async () => true;
app.conn.chCtx.onSignedOut = () => {};
app.catalog.loadSchema = async () => { app.state.schema.value = []; };
app.catalog.loadReference = async () => {};
// `renderApp()` starts this best-effort metadata probe after mounting.
// Keep the fixture fully offline rather than letting the probe report an
// authentication loss over its deliberately stubbed Dashboard reads.
app.catalog.loadVersion = async () => {};
// Tiles must reach 'ready' for the KPI band to render its member content — a
// real run would need a server.
app.exec.executeRead = async (result) => {
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/connection-lifecycle-boundaries.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,14 @@ describe('connection lifecycle architecture', () => {
const css = read('src/styles.css');
expect(css).not.toMatch(/\.connection-chip\s*\{\s*display:\s*none\s*;\s*\}/);
});

it('keeps in-place authentication recovery out of the mounted workspace layout', () => {
const css = read('src/styles.css');
const authHost = /\.auth-host\s*\{([\s\S]*?)\n\}/.exec(css)?.[1] || '';
expect(authHost).toMatch(/position:\s*fixed/);
expect(authHost).toMatch(/inset:\s*0/);
expect(authHost).toMatch(/z-index:\s*120/);
expect(authHost).toMatch(/background:\s*var\(--scrim\)/);
expect(authHost).not.toMatch(/max-height/);
});
});