Skip to content

Commit ca1ae01

Browse files
fix(admin): stabilize dashboard charts and layout
1 parent 9bcc69f commit ca1ae01

6 files changed

Lines changed: 88 additions & 69 deletions

File tree

internal/admin/dashboard/static/css/dashboard.css

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -106,17 +106,17 @@ body {
106106

107107
/* Sidebar */
108108
.sidebar {
109+
flex: 0 0 var(--sidebar-width);
109110
width: var(--sidebar-width);
110111
background: var(--bg-surface);
111112
border-right: 1px solid var(--border);
112113
display: flex;
113114
flex-direction: column;
114-
position: fixed;
115+
position: sticky;
115116
top: 0;
116-
left: 0;
117-
bottom: 0;
117+
height: 100vh;
118118
z-index: 10;
119-
transition: width 0.2s;
119+
transition: flex-basis 0.2s, width 0.2s;
120120
}
121121

122122
.sidebar-header {
@@ -281,27 +281,27 @@ body {
281281

282282
/* Sidebar toggle handle */
283283
.sidebar-toggle {
284-
position: fixed;
284+
flex: 0 0 6px;
285+
position: sticky;
285286
top: 0;
286-
bottom: 0;
287-
left: var(--sidebar-width);
288287
width: 6px;
288+
height: 100vh;
289289
cursor: w-resize;
290290
z-index: 11;
291-
transition: left 0.2s, background 0.15s;
291+
transition: background 0.15s;
292292
}
293293

294294
.sidebar-toggle:hover {
295295
background: color-mix(in srgb, var(--accent) 15%, transparent);
296296
}
297297

298298
.sidebar-toggle.collapsed {
299-
left: 60px;
300299
cursor: e-resize;
301300
}
302301

303302
/* Collapsed sidebar (desktop) */
304303
.sidebar.sidebar-collapsed {
304+
flex-basis: 60px;
305305
width: 60px;
306306
}
307307

@@ -343,18 +343,13 @@ body {
343343

344344
/* Content */
345345
.content {
346-
flex: 0 1 auto;
346+
flex: 1 1 0;
347347
min-width: 0;
348-
width: min(1200px, calc(100% - var(--sidebar-width)));
349-
margin-left: max(var(--sidebar-width), calc((100% - min(1200px, calc(100% - var(--sidebar-width)))) / 2));
350-
margin-right: auto;
348+
width: 100%;
349+
max-width: 1200px;
350+
margin: 0 auto;
351351
padding: 32px;
352-
transition: margin-left 0.2s, width 0.2s;
353-
}
354-
355-
.content.content-collapsed {
356-
width: min(1200px, calc(100% - 60px));
357-
margin-left: max(60px, calc((100% - min(1200px, calc(100% - 60px))) / 2));
352+
transition: width 0.2s;
358353
}
359354

360355
.page-header {
@@ -2248,7 +2243,7 @@ body.conversation-drawer-open {
22482243

22492244
/* Responsive */
22502245
@media (max-width: 768px) {
2251-
.sidebar { width: 60px; }
2246+
.sidebar { width: 60px; flex-basis: 60px; }
22522247
.sidebar-header { justify-content: center; padding: 16px; }
22532248
.sidebar-header h1, .badge { display: none; }
22542249
.sidebar-nav .nav-item { justify-content: center; padding: 10px; }
@@ -2258,7 +2253,7 @@ body.conversation-drawer-open {
22582253
.sidebar-footer .theme-toggle { display: none; }
22592254
.sidebar-footer .theme-toggle-mobile { display: flex; margin: 0 auto; }
22602255
.sidebar-toggle { display: none; }
2261-
.content { width: calc(100% - 60px); margin-left: 60px; padding: 20px; }
2256+
.content { width: 100%; margin: 0 auto; padding: 20px; }
22622257
.cards { grid-template-columns: repeat(2, 1fr); }
22632258

22642259
/* Page header stacking */

internal/admin/dashboard/static/js/modules/charts.js

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,6 @@
136136
};
137137
},
138138

139-
_updateChartInstance(chart, config) {
140-
chart.data = config.data;
141-
chart.options = config.options;
142-
chart.update('none');
143-
},
144-
145139
fillMissingDays(daily) {
146140
if (this.interval !== 'daily') {
147141
return daily;
@@ -190,13 +184,9 @@
190184
const outputData = filled.map((d) => d.output_tokens);
191185
const config = this._overviewChartConfig(colors, labels, inputData, outputData);
192186

193-
if (this.chart && this.chart.canvas === canvas) {
194-
this._updateChartInstance(this.chart, config);
195-
return;
196-
}
197-
198187
if (this.chart) {
199188
this.chart.destroy();
189+
this.chart = null;
200190
}
201191

202192
this.chart = new Chart(canvas, config);
@@ -275,13 +265,9 @@
275265
const palette = this._barColors();
276266
const config = this._barChartConfig(colors, labels, values, palette);
277267

278-
if (this.usageBarChart && this.usageBarChart.canvas === canvas) {
279-
this._updateChartInstance(this.usageBarChart, config);
280-
return;
281-
}
282-
283268
if (this.usageBarChart) {
284269
this.usageBarChart.destroy();
270+
this.usageBarChart = null;
285271
}
286272

287273
this.usageBarChart = new Chart(canvas, config);

internal/admin/dashboard/static/js/modules/charts.test.js

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ function createChartsContext() {
7171
return { module, canvas };
7272
}
7373

74-
test('renderChart reuses the existing overview chart instance on refresh', () => {
74+
test('renderChart recreates the overview chart instance on refresh', () => {
7575
FakeChart.instances = [];
7676
const { module } = createChartsContext();
7777
module.daily = [
@@ -89,16 +89,16 @@ test('renderChart reuses the existing overview chart instance on refresh', () =>
8989
];
9090
module.renderChart();
9191

92-
assert.strictEqual(module.chart, firstChart);
93-
assert.equal(FakeChart.instances.length, 1);
94-
assert.equal(firstChart.destroyCalls, 0);
95-
assert.equal(JSON.stringify(firstChart.data.labels), JSON.stringify(['2026-03-29']));
96-
assert.equal(JSON.stringify(firstChart.data.datasets[0].data), JSON.stringify([8]));
97-
assert.equal(JSON.stringify(firstChart.data.datasets[1].data), JSON.stringify([13]));
98-
assert.equal(JSON.stringify(firstChart.updateCalls), JSON.stringify(['none']));
92+
assert.notStrictEqual(module.chart, firstChart);
93+
assert.equal(FakeChart.instances.length, 2);
94+
assert.equal(firstChart.destroyCalls, 1);
95+
assert.equal(JSON.stringify(module.chart.data.labels), JSON.stringify(['2026-03-29']));
96+
assert.equal(JSON.stringify(module.chart.data.datasets[0].data), JSON.stringify([8]));
97+
assert.equal(JSON.stringify(module.chart.data.datasets[1].data), JSON.stringify([13]));
98+
assert.equal(JSON.stringify(firstChart.updateCalls), JSON.stringify([]));
9999
});
100100

101-
test('renderBarChart reuses the existing usage bar chart instance on refresh', () => {
101+
test('renderBarChart recreates the usage bar chart instance on refresh', () => {
102102
FakeChart.instances = [];
103103
const { module } = createChartsContext();
104104
module.page = 'usage';
@@ -118,10 +118,10 @@ test('renderBarChart reuses the existing usage bar chart instance on refresh', (
118118
];
119119
module.renderBarChart();
120120

121-
assert.strictEqual(module.usageBarChart, firstChart);
122-
assert.equal(FakeChart.instances.length, 1);
123-
assert.equal(firstChart.destroyCalls, 0);
124-
assert.equal(JSON.stringify(firstChart.data.labels), JSON.stringify(['gpt-5']));
125-
assert.equal(JSON.stringify(firstChart.data.datasets[0].data), JSON.stringify([55]));
126-
assert.equal(JSON.stringify(firstChart.updateCalls), JSON.stringify(['none']));
121+
assert.notStrictEqual(module.usageBarChart, firstChart);
122+
assert.equal(FakeChart.instances.length, 2);
123+
assert.equal(firstChart.destroyCalls, 1);
124+
assert.equal(JSON.stringify(module.usageBarChart.data.labels), JSON.stringify(['gpt-5']));
125+
assert.equal(JSON.stringify(module.usageBarChart.data.datasets[0].data), JSON.stringify([55]));
126+
assert.equal(JSON.stringify(firstChart.updateCalls), JSON.stringify([]));
127127
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert/strict');
3+
const fs = require('node:fs');
4+
const path = require('node:path');
5+
6+
function readFixture(relativePath) {
7+
return fs.readFileSync(path.join(__dirname, relativePath), 'utf8');
8+
}
9+
10+
function readCSSRule(source, selector) {
11+
const escapedSelector = selector.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
12+
const match = source.match(new RegExp(`${escapedSelector}\\s*\\{([\\s\\S]*?)\\n\\}`, 'm'));
13+
assert.ok(match, `Expected CSS rule for ${selector}`);
14+
return match[1];
15+
}
16+
17+
test('sidebar and main content share the flex layout without manual content offsets', () => {
18+
const template = readFixture('../../../templates/layout.html');
19+
const css = readFixture('../../css/dashboard.css');
20+
21+
assert.match(template, /<aside class="sidebar"[\s\S]*<div class="sidebar-toggle"[\s\S]*<main class="content"/);
22+
assert.match(template, /<main class="content" :class="\{ 'content-collapsed': sidebarCollapsed \}">/);
23+
24+
const sidebarRule = readCSSRule(css, '.sidebar');
25+
assert.match(sidebarRule, /flex:\s*0 0 var\(--sidebar-width\)/);
26+
assert.match(sidebarRule, /position:\s*sticky/);
27+
assert.match(sidebarRule, /height:\s*100vh/);
28+
assert.doesNotMatch(sidebarRule, /position:\s*fixed/);
29+
30+
const toggleRule = readCSSRule(css, '.sidebar-toggle');
31+
assert.match(toggleRule, /flex:\s*0 0 6px/);
32+
assert.match(toggleRule, /position:\s*sticky/);
33+
assert.match(toggleRule, /height:\s*100vh/);
34+
assert.doesNotMatch(toggleRule, /left:\s*var\(--sidebar-width\)/);
35+
36+
const contentRule = readCSSRule(css, '.content');
37+
assert.match(contentRule, /flex:\s*1 1 0/);
38+
assert.match(contentRule, /width:\s*100%/);
39+
assert.match(contentRule, /max-width:\s*1200px/);
40+
assert.match(contentRule, /margin:\s*0 auto/);
41+
assert.doesNotMatch(contentRule, /margin-left:\s*max\(/);
42+
43+
const collapsedSidebarRule = readCSSRule(css, '.sidebar.sidebar-collapsed');
44+
assert.match(collapsedSidebarRule, /flex-basis:\s*60px/);
45+
});
46+
47+
test('dashboard layout pins Chart.js to 4.5.0', () => {
48+
const template = readFixture('../../../templates/layout.html');
49+
50+
assert.match(
51+
template,
52+
/<script src="https:\/\/cdn\.jsdelivr\.net\/npm\/chart\.js@4\.5\.0\/dist\/chart\.umd\.min\.js"><\/script>/
53+
);
54+
});

internal/admin/dashboard/static/js/modules/execution-plans-layout.test.js

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -194,19 +194,3 @@ test('endpoint pills use dedicated flush-left icons and tighter right padding',
194194
assert.match(endpointIconRule, /justify-content:\s*flex-start/);
195195
assert.match(endpointIconRule, /padding:\s*0/);
196196
});
197-
198-
test('main content stays offset from the sidebar but centers on wide screens', () => {
199-
const template = readFixture('../../../templates/layout.html');
200-
const css = readFixture('../../css/dashboard.css');
201-
202-
assert.match(template, /<main class="content" :class="\{ 'content-collapsed': sidebarCollapsed \}">/);
203-
204-
const contentRule = readCSSRule(css, '.content');
205-
assert.match(contentRule, /width:\s*min\(1200px,\s*calc\(100%\s*-\s*var\(--sidebar-width\)\)\)/);
206-
assert.match(contentRule, /margin-left:\s*max\(var\(--sidebar-width\),\s*calc\(\(100%\s*-\s*min\(1200px,\s*calc\(100%\s*-\s*var\(--sidebar-width\)\)\)\)\s*\/\s*2\)\)/);
207-
assert.match(contentRule, /margin-right:\s*auto/);
208-
209-
const collapsedRule = readCSSRule(css, '.content.content-collapsed');
210-
assert.match(collapsedRule, /width:\s*min\(1200px,\s*calc\(100%\s*-\s*60px\)\)/);
211-
assert.match(collapsedRule, /margin-left:\s*max\(60px,\s*calc\(\(100%\s*-\s*min\(1200px,\s*calc\(100%\s*-\s*60px\)\)\)\s*\/\s*2\)\)/);
212-
});

internal/admin/dashboard/templates/layout.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
<link rel="preconnect" href="https://fonts.googleapis.com">
99
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1010
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
11+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.0/dist/chart.umd.min.js"></script>
1212
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.8/dist/cdn.min.js"></script>
1313
<script src="https://unpkg.com/htmx.org@2.0.8/dist/htmx.min.js"></script>
1414
<link rel="stylesheet" href="/admin/static/css/dashboard.css">

0 commit comments

Comments
 (0)