fix(usage): prevent trend chart clipping on scaled displays
Make the overview vertically scrollable and preserve the chart's minimum height when summary cards and legends wrap. Fixes #269
This commit is contained in:
1 parent
abfe65e656
commit
7cde8942d3
4 files changed
+175
-1
No files matched your search
+27
-1
@@ -2350,6 +2350,32 @@ textarea:focus-visible {
|
||||
color: var(--theme-777067);
|
||||
}
|
||||
|
||||
.config-proxy-heading {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.config-proxy-heading > .config-network-label {
|
||||
min-width: 0;
|
||||
color: var(--theme-39352f);
|
||||
font-size: var(--font-size-body);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.config-proxy-system-toggle {
|
||||
display: inline-flex;
|
||||
flex: none;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
color: var(--theme-7a746c);
|
||||
font-size: var(--font-size-meta);
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.config-network-input {
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
@@ -8129,7 +8155,7 @@ textarea:focus-visible {
|
||||
.usage-overview-layout {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1.35fr) minmax(260px, 0.65fr);
|
||||
grid-template-rows: auto minmax(250px, 1fr);
|
||||
grid-template-rows: auto minmax(280px, 1fr);
|
||||
gap: 14px;
|
||||
align-items: stretch;
|
||||
overflow-x: hidden;
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
<html><body><div id="root"></div><script type="module" src="/tests/fixtures/usage-layout.tsx"></script></body></html>
|
||||
Vendored
+92
@@ -0,0 +1,92 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { mockIPC } from '@tauri-apps/api/mocks';
|
||||
import { I18nProvider } from '../../src/i18n';
|
||||
import { UsageRecordsPage } from '../../src/pages/UsageRecordsPage';
|
||||
import '../../src/styles.css';
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-TW');
|
||||
localStorage.setItem('cpa-gui.usage-records-tab.v1', 'overview');
|
||||
localStorage.setItem('cpa-gui.usage-records-range.v1', '4h');
|
||||
document.documentElement.dataset.theme = params.get('theme') || 'dark';
|
||||
|
||||
const hourKey = (date: Date) => [
|
||||
date.getFullYear(),
|
||||
String(date.getMonth() + 1).padStart(2, '0'),
|
||||
String(date.getDate()).padStart(2, '0'),
|
||||
String(date.getHours()).padStart(2, '0'),
|
||||
].join('-');
|
||||
const now = new Date();
|
||||
const trendModels = [
|
||||
'gpt-test',
|
||||
'claude-test',
|
||||
'gemini-test',
|
||||
'deepseek-test',
|
||||
'grok-test',
|
||||
'qwen-test',
|
||||
'mistral-test',
|
||||
'llama-test',
|
||||
];
|
||||
const timeline = Array.from({ length: 4 }, (_, index) => ({
|
||||
hour: hourKey(new Date(now.getTime() - (3 - index) * 60 * 60 * 1000)),
|
||||
requests: 12 + index,
|
||||
success: 11 + index,
|
||||
failure: 1,
|
||||
canceled: 0,
|
||||
tokens: 22_000 + index * 2_000,
|
||||
models: trendModels.map((key, modelIndex) => ({
|
||||
key,
|
||||
label: key,
|
||||
tokens: 1_700 + modelIndex * 300 + index * 250,
|
||||
})),
|
||||
}));
|
||||
|
||||
mockIPC(async (cmd) => {
|
||||
if (cmd === 'plugin:event|listen') return 1;
|
||||
if (cmd === 'plugin:event|unlisten' || cmd === 'set_app_locale') return null;
|
||||
if (cmd === 'get_usage_collector_status') {
|
||||
return { state: 'collecting', message: '', lastCollectedAt: now.toISOString(), totalRecords: 54 };
|
||||
}
|
||||
if (cmd === 'get_usage_analysis') {
|
||||
return { models: [], providers: [], sources: [], apiKeys: [] };
|
||||
}
|
||||
if (cmd === 'get_usage_overview') {
|
||||
return {
|
||||
totalRequests: 54,
|
||||
successCount: 50,
|
||||
failureCount: 4,
|
||||
canceledCount: 0,
|
||||
successRate: 92.6,
|
||||
inputTokens: 52_000,
|
||||
outputTokens: 24_000,
|
||||
reasoningTokens: 8_000,
|
||||
cacheReadTokens: 16_000,
|
||||
cacheCreationTokens: 0,
|
||||
totalTokens: 100_000,
|
||||
rpm: 0.23,
|
||||
tpm: 416.7,
|
||||
tps: 31.2,
|
||||
tpsSampleCount: 54,
|
||||
averageLatencyMs: 2_840,
|
||||
cacheHitRate: 0.307,
|
||||
estimatedCost: 1.2345,
|
||||
pricedRequests: 54,
|
||||
timeline,
|
||||
};
|
||||
}
|
||||
throw new Error(`Unhandled fixture command: ${cmd}`);
|
||||
});
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<I18nProvider>
|
||||
<div className="app-shell">
|
||||
<aside className="sidebar" aria-hidden="true" />
|
||||
<div className="workspace">
|
||||
<main className="content">
|
||||
<UsageRecordsPage />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</I18nProvider>,
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const base = 'http://127.0.0.1:1421';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
|
||||
try {
|
||||
const page = await browser.newPage({ viewport: { width: 1100, height: 600 } });
|
||||
await page.route('**/*', (route) => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
|
||||
await page.goto(`${base}/tests/fixtures/usage-layout.html`, { waitUntil: 'domcontentloaded' });
|
||||
await page.locator('.usage-trend-x-axis').waitFor();
|
||||
|
||||
const geometry = await page.evaluate(() => {
|
||||
const layout = document.querySelector('.usage-overview-layout');
|
||||
const panel = document.querySelector('.usage-trend-panel');
|
||||
const plot = document.querySelector('.usage-trend-plot');
|
||||
const xAxis = document.querySelector('.usage-trend-x-axis');
|
||||
const cards = Array.from(document.querySelectorAll('.usage-stat-card'));
|
||||
if (!(layout instanceof HTMLElement) || !(panel instanceof HTMLElement)
|
||||
|| !(plot instanceof HTMLElement) || !(xAxis instanceof HTMLElement)) throw new Error('Fixture did not render');
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const xAxisRect = xAxis.getBoundingClientRect();
|
||||
return {
|
||||
layoutClientHeight: layout.clientHeight,
|
||||
layoutScrollHeight: layout.scrollHeight,
|
||||
layoutClientWidth: layout.clientWidth,
|
||||
layoutScrollWidth: layout.scrollWidth,
|
||||
plotHeight: plot.getBoundingClientRect().height,
|
||||
xAxisInsidePanel: xAxisRect.bottom <= panelRect.bottom,
|
||||
cardRows: new Set(cards.map((card) => Math.round(card.getBoundingClientRect().top))).size,
|
||||
};
|
||||
});
|
||||
|
||||
assert.ok(geometry.cardRows >= 2, 'The constrained layout reproduces wrapped summary cards');
|
||||
assert.ok(geometry.layoutScrollHeight > geometry.layoutClientHeight, 'The overview becomes vertically scrollable');
|
||||
assert.equal(geometry.layoutScrollWidth, geometry.layoutClientWidth, 'The overview does not introduce horizontal scrolling');
|
||||
assert.ok(geometry.plotHeight >= 140, 'The trend plot retains its minimum drawing height');
|
||||
assert.equal(geometry.xAxisInsidePanel, true, 'The X axis remains inside the trend panel instead of being clipped');
|
||||
|
||||
await page.locator('.usage-overview-layout').evaluate((layout) => { layout.scrollTop = layout.scrollHeight; });
|
||||
assert.ok(await page.locator('.usage-trend-x-axis').evaluate((axis) => {
|
||||
const layout = axis.closest('.usage-overview-layout');
|
||||
if (!layout) return false;
|
||||
return axis.getBoundingClientRect().bottom <= layout.getBoundingClientRect().bottom + 1;
|
||||
}), 'The complete X axis is reachable by scrolling the overview');
|
||||
|
||||
console.log('PASS: wrapped usage cards preserve the full trend chart and use vertical overview scrolling.');
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
})().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Reference in new issue
Block a user