Skip to content

Commit 824a135

Browse files
perf(admin): improve dashboard rendering performance (#222)
* perf(admin): reduce audit log expansion work * perf(admin): lazy mount dashboard sections * feat(admin): show models loading state
1 parent 4533e7b commit 824a135

13 files changed

Lines changed: 303 additions & 124 deletions

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

Lines changed: 30 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1208,6 +1208,31 @@ td.col-price {
12081208
font-size: 14px;
12091209
}
12101210

1211+
.loading-state {
1212+
display: flex;
1213+
align-items: center;
1214+
justify-content: center;
1215+
gap: 10px;
1216+
min-height: 64px;
1217+
color: var(--text-muted);
1218+
font-size: 14px;
1219+
}
1220+
1221+
.loading-spinner {
1222+
width: 16px;
1223+
height: 16px;
1224+
border: 2px solid var(--border);
1225+
border-top-color: var(--accent);
1226+
border-radius: 50%;
1227+
animation: loading-spin 0.8s linear infinite;
1228+
}
1229+
1230+
@keyframes loading-spin {
1231+
to {
1232+
transform: rotate(360deg);
1233+
}
1234+
}
1235+
12111236
.table-action-btn {
12121237
display: inline-flex;
12131238
align-items: center;
@@ -2016,14 +2041,14 @@ td.col-price {
20162041
display: none;
20172042
}
20182043

2019-
.audit-entry-main {
2044+
.audit-entry-left {
20202045
display: flex;
20212046
align-items: center;
20222047
gap: 8px;
20232048
min-width: 0;
20242049
}
20252050

2026-
.audit-entry-meta {
2051+
.audit-entry-right {
20272052
display: inline-flex;
20282053
align-items: center;
20292054
gap: 10px;
@@ -2125,19 +2150,10 @@ td.col-price {
21252150
}
21262151

21272152
.audit-entry-details {
2128-
border-top: 1px solid transparent;
2129-
padding: 0 12px;
2153+
border-top: 1px solid var(--border);
2154+
padding: 12px;
21302155
background: var(--bg-surface);
2131-
max-height: 0;
2132-
opacity: 0;
21332156
overflow: hidden;
2134-
transition: max-height 0.12s ease-out, opacity 0.08s linear, padding 0.12s ease-out, border-color 0.12s ease-out;
2135-
}
2136-
2137-
.audit-entry[open] .audit-entry-details {
2138-
border-top-color: var(--border);
2139-
padding: 12px;
2140-
opacity: 1;
21412157
}
21422158

21432159
.audit-request-response {
@@ -3211,7 +3227,7 @@ body.conversation-drawer-open {
32113227
flex-direction: column;
32123228
align-items: flex-start;
32133229
}
3214-
.audit-entry-meta {
3230+
.audit-entry-right {
32153231
width: 100%;
32163232
justify-content: space-between;
32173233
}

internal/admin/dashboard/static/js/dashboard.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ function dashboard() {
6464
providers: []
6565
},
6666
models: [],
67+
modelsLoading: false,
6768
categories: [],
6869
activeCategory: 'all',
6970
hasCalendarModule: calendarModuleFactory !== null,
@@ -91,6 +92,7 @@ function dashboard() {
9192
auditStatusCode: '',
9293
auditStream: '',
9394
auditFetchToken: 0,
95+
auditExpandedEntries: {},
9496

9597
// Conversation drawer state
9698
conversationOpen: false,
@@ -353,6 +355,7 @@ function dashboard() {
353355
options.signal = controller.signal;
354356
}
355357

358+
this.modelsLoading = true;
356359
try {
357360
let url = '/admin/api/v1/models';
358361
if (this.activeCategory && this.activeCategory !== 'all') {
@@ -387,7 +390,11 @@ function dashboard() {
387390
this.models = [];
388391
if (typeof this.syncDisplayModels === 'function') this.syncDisplayModels();
389392
} finally {
393+
const currentRequest = isCurrentRequest();
390394
this._clearAbortableRequest('_modelsFetchController', controller);
395+
if (currentRequest) {
396+
this.modelsLoading = false;
397+
}
391398
}
392399
},
393400

internal/admin/dashboard/static/js/modules/audit-list.js

Lines changed: 49 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
if (requestToken !== this.auditFetchToken) return;
3838
this.auditLog = payload;
3939
if (!this.auditLog.entries) this.auditLog.entries = [];
40+
this.pruneAuditExpandedEntries(this.auditLog.entries);
4041
if (typeof this.prefetchAuditExecutionPlans === 'function') {
4142
try {
4243
await this.prefetchAuditExecutionPlans(this.auditLog.entries);
@@ -51,6 +52,47 @@
5152
}
5253
},
5354

55+
auditEntryKey(entry) {
56+
return String(entry && entry.id || '').trim();
57+
},
58+
59+
isAuditEntryExpanded(entry) {
60+
const key = this.auditEntryKey(entry);
61+
if (!key) return false;
62+
return !!(this.auditExpandedEntries && this.auditExpandedEntries[key]);
63+
},
64+
65+
markAuditEntryExpanded(entry) {
66+
const key = this.auditEntryKey(entry);
67+
if (!key || this.isAuditEntryExpanded(entry)) return;
68+
69+
this.auditExpandedEntries = {
70+
...(this.auditExpandedEntries || {}),
71+
[key]: true
72+
};
73+
},
74+
75+
pruneAuditExpandedEntries(entries) {
76+
const expanded = this.auditExpandedEntries || {};
77+
const keys = new Set((Array.isArray(entries) ? entries : [])
78+
.map((entry) => this.auditEntryKey(entry))
79+
.filter(Boolean));
80+
const next = {};
81+
let changed = false;
82+
83+
Object.keys(expanded).forEach((key) => {
84+
if (keys.has(key)) {
85+
next[key] = true;
86+
return;
87+
}
88+
changed = true;
89+
});
90+
91+
if (changed) {
92+
this.auditExpandedEntries = next;
93+
}
94+
},
95+
5496
clearAuditFilters() {
5597
this.auditSearch = '';
5698
this.auditMethod = '';
@@ -80,33 +122,13 @@
80122
return (ns / 1000000000).toFixed(2) + ' s';
81123
},
82124

83-
handleAuditEntryToggle(event) {
125+
handleAuditEntryToggle(event, entry) {
84126
const detailsEl = event && event.currentTarget;
85127
if (!detailsEl) return;
86128

87-
const content = detailsEl.querySelector('.audit-entry-details');
88-
if (!content) return;
89-
90129
if (detailsEl.open) {
91-
const targetHeight = content.scrollHeight;
92-
content.style.maxHeight = targetHeight + 'px';
93-
const onTransitionEnd = () => {
94-
if (detailsEl.open) {
95-
content.style.maxHeight = 'none';
96-
}
97-
content.removeEventListener('transitionend', onTransitionEnd);
98-
};
99-
content.addEventListener('transitionend', onTransitionEnd);
100-
return;
130+
this.markAuditEntryExpanded(entry);
101131
}
102-
103-
if (!content.style.maxHeight || content.style.maxHeight === 'none') {
104-
content.style.maxHeight = content.scrollHeight + 'px';
105-
void content.offsetHeight;
106-
}
107-
requestAnimationFrame(() => {
108-
content.style.maxHeight = '0px';
109-
});
110132
},
111133

112134
statusCodeClass(statusCode) {
@@ -183,9 +205,14 @@
183205

184206
auditPaneState(pane) {
185207
const formatJSON = this.formatJSON.bind(this);
208+
const renderBody = typeof this.renderBodyWithConversationHighlights === 'function'
209+
? this.renderBodyWithConversationHighlights.bind(this)
210+
: (_entry, body) => formatJSON(body);
186211

187212
return {
188213
pane,
214+
formattedHeaders: pane && pane.showHeaders ? formatJSON(pane.headers) : '',
215+
renderedBody: pane && pane.showBody ? renderBody(pane.entry, pane.body) : '',
189216
copyState: clipboard
190217
? clipboard.createClipboardButtonState({
191218
logPrefix: 'Failed to copy audit payload:'

internal/admin/dashboard/static/js/modules/audit-list.test.js

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,51 @@ test('clearAuditFilters resets the consolidated audit controls', () => {
181181
assert.equal(fetchCalled, true);
182182
});
183183

184+
test('handleAuditEntryToggle lazily marks an opened audit row for details rendering', () => {
185+
const module = createAuditListModule();
186+
module.auditExpandedEntries = {};
187+
188+
module.handleAuditEntryToggle({ currentTarget: { open: true } }, { id: 'audit-1' });
189+
190+
assert.equal(module.isAuditEntryExpanded({ id: 'audit-1' }), true);
191+
assert.equal(JSON.stringify(module.auditExpandedEntries), JSON.stringify({ 'audit-1': true }));
192+
});
193+
194+
test('pruneAuditExpandedEntries drops expanded state for rows no longer on the page', () => {
195+
const module = createAuditListModule();
196+
module.auditExpandedEntries = {
197+
'audit-1': true,
198+
'audit-2': true
199+
};
200+
201+
module.pruneAuditExpandedEntries([{ id: 'audit-2' }, { id: 'audit-3' }]);
202+
203+
assert.equal(JSON.stringify(module.auditExpandedEntries), JSON.stringify({ 'audit-2': true }));
204+
});
205+
206+
test('auditPaneState formats pane content once for template rendering', () => {
207+
const module = createAuditListModule();
208+
const entry = { id: 'audit-1' };
209+
let renderCalls = 0;
210+
module.renderBodyWithConversationHighlights = (renderEntry, body) => {
211+
renderCalls++;
212+
assert.equal(renderEntry, entry);
213+
return 'rendered:' + body.id;
214+
};
215+
216+
const paneState = module.auditPaneState({
217+
entry,
218+
showHeaders: true,
219+
headers: { authorization: 'Bearer redacted' },
220+
showBody: true,
221+
body: { id: 'body-1' }
222+
});
223+
224+
assert.equal(paneState.formattedHeaders, '{\n "authorization": "Bearer redacted"\n}');
225+
assert.equal(paneState.renderedBody, 'rendered:body-1');
226+
assert.equal(renderCalls, 1);
227+
});
228+
184229
test('auditPaneState copies the formatted body and resets success feedback', async () => {
185230
let resetCallback = null;
186231
const writes = [];

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

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ test('mono utility only sets the font family and font-size-md carries the 13px s
7070
assert.match(fontSizeMdRule, /font-size:\s*13px/);
7171
});
7272

73-
test('dashboard layout pins Chart.js to 4.5.0', () => {
73+
test('dashboard layout pins Chart.js to 4.5.0 and avoids unused htmx', () => {
7474
const template = readFixture('../../../templates/layout.html');
7575

7676
assert.match(
@@ -85,10 +85,7 @@ test('dashboard layout pins Chart.js to 4.5.0', () => {
8585
template,
8686
/<script defer src="https:\/\/cdn\.jsdelivr\.net\/npm\/alpinejs@3\.15\.8\/dist\/cdn\.min\.js" integrity="sha384-LXWjKwDZz29o7TduNe\+r\/UxaolHh5FsSvy2W7bDHSZ8jJeGgDeuNnsDNHoxpSgDi" crossorigin="anonymous"><\/script>/
8787
);
88-
assert.match(
89-
template,
90-
/<script src="https:\/\/unpkg\.com\/htmx\.org@2\.0\.8\/dist\/htmx\.min\.js" integrity="sha384-\/TgkGk7p307TH7EXJDuUlgG3Ce1UVolAOFopFekQkkXihi5u\/6OCvVKyz1W\+idaz" crossorigin="anonymous"><\/script>/
91-
);
88+
assert.doesNotMatch(template, /htmx/i);
9289
assert.match(
9390
template,
9491
/<script src="\/admin\/static\/js\/modules\/conversation-helpers\.js"><\/script>[\s\S]*<script src="\/admin\/static\/js\/modules\/clipboard\.js"><\/script>[\s\S]*<script src="\/admin\/static\/js\/modules\/providers\.js"><\/script>[\s\S]*<script src="\/admin\/static\/js\/modules\/audit-list\.js"><\/script>[\s\S]*<script src="\/admin\/static\/js\/modules\/auth-keys\.js"><\/script>[\s\S]*<script src="\/admin\/static\/js\/modules\/guardrails\.js"><\/script>/
@@ -146,7 +143,7 @@ test('dashboard pages reuse a shared auth banner template', () => {
146143

147144
const authBannerCalls = indexTemplate.match(/{{template "auth-banner" \.}}/g) || [];
148145
assert.equal(authBannerCalls.length, 7);
149-
assert.match(indexTemplate, /<div x-show="page==='guardrails'">[\s\S]*{{template "auth-banner" \.}}/);
146+
assert.match(indexTemplate, /<template x-if="page==='guardrails'">\s*<div>[\s\S]*{{template "auth-banner" \.}}/);
150147
assert.doesNotMatch(
151148
indexTemplate,
152149
/<div class="alert alert-warning" x-show="authError">[\s\S]*Authentication required\. Enter your API key in the sidebar to view data\.[\s\S]*<\/div>/
@@ -255,11 +252,13 @@ test('audit toolbar uses a full-width search row above the select row with a rig
255252
test('audit entry metadata is rendered as a labeled pill row at the bottom of the expanded entry', () => {
256253
const indexTemplate = readFixture('../../../templates/index.html');
257254
const css = readFixture('../../css/dashboard.css');
258-
const auditEntryMatch = indexTemplate.match(/<div class="audit-entry-details">[\s\S]*?<\/div>\s*<\/details>/);
255+
const detailsStart = indexTemplate.indexOf('<div class="audit-entry-details">');
256+
const detailsEnd = indexTemplate.indexOf('</template>', detailsStart);
259257

260-
assert.ok(auditEntryMatch, 'Expected audit entry details block');
258+
assert.notEqual(detailsStart, -1, 'Expected audit entry details block');
259+
assert.notEqual(detailsEnd, -1, 'Expected lazy audit entry details wrapper');
261260

262-
const auditEntry = auditEntryMatch[0];
261+
const auditEntry = indexTemplate.slice(detailsStart, detailsEnd);
263262
const requestResponseIndex = auditEntry.indexOf('<div class="audit-request-response">');
264263
const metadataIndex = auditEntry.indexOf('<div class="audit-entry-metadata">');
265264

@@ -286,8 +285,38 @@ test('audit entry metadata is rendered as a labeled pill row at the bottom of th
286285
assert.match(metadataContextRule, /flex-wrap:\s*wrap/);
287286
});
288287

289-
test('alias rows use a shared icon-only edit action', () => {
288+
test('model category tables lazy mount only the active table body', () => {
290289
const indexTemplate = readFixture('../../../templates/index.html');
290+
const css = readFixture('../../css/dashboard.css');
291+
const modelsStart = indexTemplate.indexOf('<!-- Models Page -->');
292+
const workflowsStart = indexTemplate.indexOf('<!-- Workflows Page -->');
293+
294+
assert.notEqual(modelsStart, -1, 'Expected models page block');
295+
assert.notEqual(workflowsStart, -1, 'Expected workflows page block');
296+
297+
const modelsBlock = indexTemplate.slice(modelsStart, workflowsStart);
298+
const lazyTableMounts = modelsBlock.match(/<template x-if="\([^"]*activeCategory[^"]*">\s*<div class="table-wrapper">/g) || [];
299+
300+
assert.equal(lazyTableMounts.length, 6);
301+
assert.doesNotMatch(modelsBlock, /<div class="table-wrapper" x-show="\([^"]*activeCategory/);
302+
assert.match(modelsBlock, /activeCategory === 'embedding'[\s\S]*{{template "model-table-body" \.}}/);
303+
assert.match(modelsBlock, /activeCategory === 'utility'[\s\S]*{{template "model-table-body" \.}}/);
304+
assert.match(modelsBlock, /class="loading-state" x-show="modelsLoading && !authError" role="status" aria-live="polite"/);
305+
assert.match(modelsBlock, /x-text="displayModels\.length > 0 \? 'Refreshing models\.\.\.' : 'Loading models\.\.\.'"/);
306+
307+
const loadingRule = readCSSRule(css, '.loading-state');
308+
assert.match(loadingRule, /display:\s*flex/);
309+
assert.match(loadingRule, /min-height:\s*64px/);
310+
311+
const spinnerRule = readCSSRule(css, '.loading-spinner');
312+
assert.match(spinnerRule, /animation:\s*loading-spin 0\.8s linear infinite/);
313+
});
314+
315+
test('alias rows use a shared icon-only edit action', () => {
316+
const indexTemplate = [
317+
readFixture('../../../templates/index.html'),
318+
readFixture('../../../templates/model-table-body.html')
319+
].join('\n');
291320
const editIconTemplate = readFixture('../../../templates/edit-icon.html');
292321

293322
assert.match(
@@ -326,7 +355,7 @@ test('audit request and response sections reuse a shared audit pane template', (
326355

327356
assert.match(
328357
auditPaneTemplate,
329-
/{{define "audit-pane"}}[\s\S]*x-data="auditPaneState\({{\.\}}\)"[\s\S]*x-text="pane\.title"[\s\S]*type="button"[\s\S]*@click\.prevent="copyBody\(\)"[\s\S]*x-text="formatJSON\(pane\.headers\)"[\s\S]*renderBodyWithConversationHighlights\(pane\.entry, pane\.body\)[\s\S]*x-text="pane\.emptyMessage"[\s\S]*x-text="pane\.tooLargeMessage"[\s\S]*{{end}}/
358+
/{{define "audit-pane"}}[\s\S]*x-data="auditPaneState\({{\.\}}\)"[\s\S]*x-text="pane\.title"[\s\S]*type="button"[\s\S]*@click\.prevent="copyBody\(\)"[\s\S]*x-text="formattedHeaders"[\s\S]*x-html="renderedBody"[\s\S]*x-text="pane\.emptyMessage"[\s\S]*x-text="pane\.tooLargeMessage"[\s\S]*{{end}}/
330359
);
331360
assert.match(indexTemplate, /{{template "audit-pane" "auditRequestPane\(entry\)"}}/);
332361
assert.match(indexTemplate, /{{template "audit-pane" "auditResponsePane\(entry\)"}}/);

0 commit comments

Comments
 (0)