refactor(ui): simplify and polish usage records view
This commit is contained in:
1 parent
b3c6aa6fad
commit
f4bba22817
10 files changed
+481
-108
No files matched your search
@@ -563,6 +563,10 @@ export const jaOverrides = {
|
||||
'usage.tab.pricing': '料金統計',
|
||||
'usage.tab.dataManagement': 'データ管理',
|
||||
'usage.refresh': '更新',
|
||||
'usage.filter.title': '絞り込み',
|
||||
'usage.filter.more': 'その他の絞り込み',
|
||||
'usage.filter.hideMore': '絞り込みを閉じる',
|
||||
'usage.filter.active': '{count} 件を適用中',
|
||||
'usage.filter.timeRange': '期間',
|
||||
'usage.range.4h': '過去 4 時間',
|
||||
'usage.range.24h': '過去 24 時間',
|
||||
|
||||
@@ -550,6 +550,10 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.tab.pricing': 'Price Statistics',
|
||||
'usage.tab.dataManagement': 'Data Management',
|
||||
'usage.refresh': 'Refresh',
|
||||
'usage.filter.title': 'Filters',
|
||||
'usage.filter.more': 'More filters',
|
||||
'usage.filter.hideMore': 'Hide filters',
|
||||
'usage.filter.active': '{count} active',
|
||||
'usage.filter.timeRange': 'Time range',
|
||||
'usage.range.4h': 'Last 4 Hours',
|
||||
'usage.range.24h': 'Last 24 Hours',
|
||||
|
||||
@@ -549,6 +549,10 @@ export const zhCN = {
|
||||
'usage.tab.pricing': '价格统计',
|
||||
'usage.tab.dataManagement': '数据管理',
|
||||
'usage.refresh': '刷新数据',
|
||||
'usage.filter.title': '筛选条件',
|
||||
'usage.filter.more': '更多筛选',
|
||||
'usage.filter.hideMore': '收起筛选',
|
||||
'usage.filter.active': '已启用 {count} 项',
|
||||
'usage.filter.timeRange': '时间范围',
|
||||
'usage.range.4h': '最近 4 小时',
|
||||
'usage.range.24h': '最近 24 小时',
|
||||
|
||||
@@ -84,30 +84,33 @@ type EventColumnDef = {
|
||||
};
|
||||
|
||||
const EVENT_COLUMNS: readonly EventColumnDef[] = [
|
||||
{ key: 'time', labelKey: 'usage.column.time', defaultWidth: 84, minWidth: 76, align: 'left' },
|
||||
{ key: 'provider', labelKey: 'usage.column.provider', defaultWidth: 108, minWidth: 88, align: 'left' },
|
||||
{ key: 'time', labelKey: 'usage.column.time', defaultWidth: 82, minWidth: 76, align: 'left' },
|
||||
{ key: 'model', labelKey: 'usage.column.model', defaultWidth: 150, minWidth: 112, align: 'left' },
|
||||
{ key: 'provider', labelKey: 'usage.column.provider', defaultWidth: 92, minWidth: 84, align: 'left' },
|
||||
{ key: 'result', labelKey: 'usage.column.result', defaultWidth: 78, minWidth: 68, align: 'left' },
|
||||
{ key: 'total', labelKey: 'usage.column.tokens', defaultWidth: 112, minWidth: 100, align: 'left' },
|
||||
{ key: 'cache', labelKey: 'usage.column.cache', defaultWidth: 94, minWidth: 84, align: 'left' },
|
||||
{ key: 'latency', labelKey: 'usage.column.latency', defaultWidth: 104, minWidth: 92, align: 'left' },
|
||||
{ key: 'speed', labelKey: 'usage.column.speed', defaultWidth: 82, minWidth: 72, align: 'left' },
|
||||
{ key: 'cost', labelKey: 'usage.column.cost', defaultWidth: 96, minWidth: 86, align: 'left' },
|
||||
{ key: 'key', labelKey: 'usage.column.key', defaultWidth: 112, minWidth: 96, align: 'left' },
|
||||
{ key: 'source', labelKey: 'usage.column.source', defaultWidth: 128, minWidth: 104, align: 'left' },
|
||||
{ key: 'model', labelKey: 'usage.column.model', defaultWidth: 160, minWidth: 104, align: 'left' },
|
||||
{ key: 'effort', labelKey: 'usage.column.effort', defaultWidth: 76, minWidth: 64, align: 'left' },
|
||||
{ key: 'result', labelKey: 'usage.column.result', defaultWidth: 80, minWidth: 68, align: 'left' },
|
||||
{ key: 'request', labelKey: 'usage.column.request', defaultWidth: 128, minWidth: 88, align: 'left' },
|
||||
{ key: 'latency', labelKey: 'usage.column.latency', defaultWidth: 112, minWidth: 96, align: 'left' },
|
||||
{ key: 'speed', labelKey: 'usage.column.speed', defaultWidth: 88, minWidth: 72, align: 'left' },
|
||||
{ key: 'total', labelKey: 'usage.column.tokens', defaultWidth: 120, minWidth: 112, align: 'left' },
|
||||
{ key: 'cache', labelKey: 'usage.column.cache', defaultWidth: 104, minWidth: 88, align: 'left' },
|
||||
{ key: 'cost', labelKey: 'usage.column.cost', defaultWidth: 112, minWidth: 96, align: 'left' },
|
||||
] as const;
|
||||
|
||||
const DEFAULT_EVENT_VISIBLE_COLUMNS: readonly EventColumnKey[] = [
|
||||
'time', 'model', 'provider', 'result', 'total', 'cache', 'latency', 'speed', 'cost',
|
||||
];
|
||||
const PREVIOUS_DEFAULT_EVENT_VISIBLE_COLUMNS: readonly string[] = [
|
||||
'time', 'provider', 'key', 'source', 'model', 'effort', 'result', 'request', 'latency', 'speed', 'total', 'cache', 'cost',
|
||||
];
|
||||
const LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS: readonly string[] = [
|
||||
'time', 'model', 'input', 'output', 'cache', 'cacheRate', 'total', 'speed', 'ttft', 'latency', 'result', 'provider', 'source',
|
||||
];
|
||||
|
||||
const EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v3';
|
||||
const LEGACY_EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v2';
|
||||
const EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v4';
|
||||
const LEGACY_EVENT_COL_WIDTHS_STORAGE_KEY = 'cpa-gui.usage-events-col-widths.v3';
|
||||
const PREVIOUS_COMPACT_WIDTHS: Partial<Record<EventColumnKey, number>> = {
|
||||
key: 120, source: 180, model: 132, request: 112,
|
||||
};
|
||||
@@ -116,8 +119,12 @@ const PREVIOUS_EVENT_COLUMN_WIDTHS: Record<EventColumnKey, number> = {
|
||||
request: 145, latency: 125, speed: 110, total: 145, cache: 135, provider: 135,
|
||||
cost: 112,
|
||||
};
|
||||
const EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v5';
|
||||
const LEGACY_EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v4';
|
||||
const PREVIOUS_GENEROUS_DEFAULT_WIDTHS: Record<EventColumnKey, number> = {
|
||||
time: 84, model: 160, provider: 108, result: 80, total: 120, cache: 104,
|
||||
latency: 112, speed: 88, cost: 112, key: 112, source: 128, effort: 76, request: 128,
|
||||
};
|
||||
const EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v6';
|
||||
const LEGACY_EVENT_VISIBLE_COLS_STORAGE_KEY = 'cpa-gui.usage-events-visible-cols.v5';
|
||||
|
||||
const getAllEventColumnKeys = () => EVENT_COLUMNS.map((column) => column.key);
|
||||
|
||||
@@ -141,10 +148,12 @@ const getInitialVisibleColumns = (): EventColumnKey[] => {
|
||||
return true;
|
||||
});
|
||||
if (savedKeys.length > 0) {
|
||||
const wasLegacyDefault = currentRaw === null
|
||||
&& parsed.length === LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.length
|
||||
&& LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => parsed.includes(key));
|
||||
if (wasLegacyDefault) return [...DEFAULT_EVENT_VISIBLE_COLUMNS];
|
||||
const wasPreviousDefault = currentRaw === null
|
||||
&& (parsed.length === PREVIOUS_DEFAULT_EVENT_VISIBLE_COLUMNS.length
|
||||
&& PREVIOUS_DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => parsed.includes(key))
|
||||
|| parsed.length === LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.length
|
||||
&& LEGACY_DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => parsed.includes(key)));
|
||||
if (wasPreviousDefault) return [...DEFAULT_EVENT_VISIBLE_COLUMNS];
|
||||
if (currentRaw === null) {
|
||||
if (!savedKeys.includes('effort')) savedKeys.push('effort');
|
||||
if (previousRaw === null && !savedKeys.includes('cost')) savedKeys.push('cost');
|
||||
@@ -177,9 +186,14 @@ const getInitialColumnWidths = (): Record<EventColumnKey, number> => {
|
||||
parsed[col.key] >= col.minWidth
|
||||
) {
|
||||
// Adopt tighter defaults while retaining columns the user resized.
|
||||
const oldDefault = previousRaw === null ? PREVIOUS_EVENT_COLUMN_WIDTHS[col.key]
|
||||
: (PREVIOUS_COMPACT_WIDTHS[col.key] ?? col.defaultWidth);
|
||||
const wasDefault = currentRaw === null && parsed[col.key] === oldDefault;
|
||||
const previousDefault = PREVIOUS_EVENT_COLUMN_WIDTHS[col.key];
|
||||
const compactDefault = PREVIOUS_COMPACT_WIDTHS[col.key];
|
||||
const generousDefault = PREVIOUS_GENEROUS_DEFAULT_WIDTHS[col.key];
|
||||
const wasDefault = currentRaw === null && (
|
||||
parsed[col.key] === previousDefault
|
||||
|| parsed[col.key] === generousDefault
|
||||
|| (compactDefault !== undefined && parsed[col.key] === compactDefault)
|
||||
);
|
||||
if (!wasDefault) initial[col.key] = Math.min(800, Math.round(parsed[col.key]));
|
||||
}
|
||||
}
|
||||
@@ -273,7 +287,7 @@ function UsageResultCell({ record }: { record: UsageRecord }) {
|
||||
const { t } = useI18n();
|
||||
const state = record.canceled ? 'canceled' : record.failed ? 'failed' : 'success';
|
||||
const detail = [
|
||||
record.failure_status > 0 ? `HTTP ${record.failure_status}` : '',
|
||||
record.failure_status > 0 && (state !== 'success' || record.failure_status >= 400) ? `HTTP ${record.failure_status}` : '',
|
||||
record.failure_body.trim(),
|
||||
]
|
||||
.filter(Boolean)
|
||||
@@ -293,10 +307,14 @@ function UsageEventCell({
|
||||
record,
|
||||
columnKey,
|
||||
noRemarkLabel,
|
||||
showDate,
|
||||
compact,
|
||||
}: {
|
||||
record: UsageRecord;
|
||||
columnKey: EventColumnKey;
|
||||
noRemarkLabel: string;
|
||||
showDate?: boolean;
|
||||
compact?: boolean;
|
||||
}) {
|
||||
const { t, formatDate } = useI18n();
|
||||
|
||||
@@ -305,7 +323,7 @@ function UsageEventCell({
|
||||
return (
|
||||
<td className="usage-td-time usage-stacked-cell align-left" title={formatDate(record.timestamp)}>
|
||||
<strong>{formatTime(record.timestamp)}</strong>
|
||||
<small>{formatEventDate(record.timestamp)}</small>
|
||||
{showDate !== false ? <small>{formatEventDate(record.timestamp)}</small> : null}
|
||||
</td>
|
||||
);
|
||||
case 'model': {
|
||||
@@ -380,12 +398,17 @@ function UsageEventCell({
|
||||
>
|
||||
<strong>{formatCacheReadRate({ inputTokens: record.tokens.input_tokens, cacheReadTokens: record.tokens.cache_read_tokens })}</strong>
|
||||
<div className="usage-event-metrics"><span className="tone-cache-read" title={`${t('usage.token.cacheRead')}: ${record.tokens.cache_read_tokens.toLocaleString()}`} aria-label={`${t('usage.token.cacheRead')}: ${record.tokens.cache_read_tokens}`}><Database size={11} aria-hidden="true" />{compactNumber(record.tokens.cache_read_tokens)}</span></div>
|
||||
<div className="usage-event-metrics"><span className="tone-cache-write" title={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens.toLocaleString()}`} aria-label={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens}`}><DatabaseZap size={11} aria-hidden="true" />{compactNumber(record.tokens.cache_creation_tokens)}</span></div>
|
||||
<div className="usage-event-metrics usage-cache-write"><span className="tone-cache-write" title={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens.toLocaleString()}`} aria-label={`${t('usage.token.cacheCreation')}: ${record.tokens.cache_creation_tokens}`}><DatabaseZap size={11} aria-hidden="true" />{compactNumber(record.tokens.cache_creation_tokens)}</span></div>
|
||||
</td>
|
||||
);
|
||||
case 'total':
|
||||
return (
|
||||
<td className="usage-td-token usage-td-total align-left" title={`${record.tokens.total_tokens.toLocaleString()} tokens`}>
|
||||
<td
|
||||
className="usage-td-token usage-td-total align-left"
|
||||
title={compact
|
||||
? `${record.tokens.total_tokens.toLocaleString()} tokens · ${t('usage.column.input')}: ${record.tokens.input_tokens.toLocaleString()} · ${t('usage.column.output')}: ${record.tokens.output_tokens.toLocaleString()} · ${t('usage.column.reasoning')}: ${record.tokens.reasoning_tokens.toLocaleString()}`
|
||||
: `${record.tokens.total_tokens.toLocaleString()} tokens`}
|
||||
>
|
||||
<strong>{compactNumber(record.tokens.total_tokens)}</strong>
|
||||
<div className="usage-event-metrics"><span className="tone-input" title={`${t('usage.column.input')}: ${record.tokens.input_tokens.toLocaleString()}`} aria-label={`${t('usage.column.input')}: ${record.tokens.input_tokens}`}><ArrowUp size={11} aria-hidden="true" />{compactNumber(record.tokens.input_tokens)}</span></div>
|
||||
<div className="usage-event-metrics">
|
||||
@@ -443,6 +466,8 @@ export function EventsView({
|
||||
|
||||
const visibleColumnKeySet = new Set(visibleColumnKeys);
|
||||
const visibleColumns = EVENT_COLUMNS.filter((column) => visibleColumnKeySet.has(column.key));
|
||||
const isCompactDefault = visibleColumns.length === DEFAULT_EVENT_VISIBLE_COLUMNS.length
|
||||
&& DEFAULT_EVENT_VISIBLE_COLUMNS.every((key) => visibleColumnKeySet.has(key));
|
||||
const isCustomized = EVENT_COLUMNS.some((col) => widths[col.key] !== col.defaultWidth);
|
||||
const noRemarkLabel = t('usage.key.noRemark');
|
||||
|
||||
@@ -606,7 +631,7 @@ export function EventsView({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="panel usage-events-panel usage-request-log" aria-label={t('usage.events.title')} aria-busy={loading}>
|
||||
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}`} aria-label={t('usage.events.title')} aria-busy={loading}>
|
||||
{loading ? <div className="usage-empty" role="status"><Database size={20} aria-hidden="true" /><span>{t('usage.loading')}</span></div> : events.items.length ? (
|
||||
<div ref={tableWrapRef} className="usage-table-wrap" tabIndex={0} role="region" aria-label={t('usage.events.title')}>
|
||||
<table
|
||||
@@ -651,18 +676,26 @@ export function EventsView({
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.items.map((record) => (
|
||||
<tr key={record.row_id}>
|
||||
{visibleColumns.map((column) => (
|
||||
<UsageEventCell
|
||||
key={column.key}
|
||||
record={record}
|
||||
columnKey={column.key}
|
||||
noRemarkLabel={noRemarkLabel}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
{events.items.map((record, index) => {
|
||||
const previousRecord = events.items[index - 1];
|
||||
const showDate = index === 0
|
||||
|| !previousRecord
|
||||
|| formatEventDate(previousRecord.timestamp) !== formatEventDate(record.timestamp);
|
||||
return (
|
||||
<tr key={record.row_id}>
|
||||
{visibleColumns.map((column) => (
|
||||
<UsageEventCell
|
||||
key={column.key}
|
||||
record={record}
|
||||
columnKey={column.key}
|
||||
noRemarkLabel={noRemarkLabel}
|
||||
showDate={showDate}
|
||||
compact={isCompactDefault}
|
||||
/>
|
||||
))}
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Activity,
|
||||
Database,
|
||||
Gauge,
|
||||
ListFilter,
|
||||
Pencil,
|
||||
RefreshCw,
|
||||
Trash2,
|
||||
@@ -247,6 +248,7 @@ export function UsageRecordsPage() {
|
||||
const [source, setSource] = useState('');
|
||||
const [apiKeyHash, setApiKeyHash] = useState('');
|
||||
const [result, setResult] = useState('all');
|
||||
const [advancedFiltersOpen, setAdvancedFiltersOpen] = useState(false);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [status, setStatus] = useState<CollectorStatus | null>(null);
|
||||
@@ -445,6 +447,7 @@ export function UsageRecordsPage() {
|
||||
};
|
||||
|
||||
const collectorTone = status?.state === 'error' ? 'error' : status?.state === 'collecting' ? 'success' : '';
|
||||
const activeAdvancedFilterCount = [model, provider, source, apiKeyHash].filter(Boolean).length;
|
||||
const hasCurrentSnapshot = loadedScopeKey === scopeKey;
|
||||
const showInitialLoading =
|
||||
activeTab !== 'data-management' &&
|
||||
@@ -467,12 +470,18 @@ export function UsageRecordsPage() {
|
||||
};
|
||||
|
||||
const filterPanel = (
|
||||
<section className="panel usage-filter-panel">
|
||||
<div className="usage-filter-row">
|
||||
<div className="usage-filter-group">
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('usage.filter.timeRange')}</span>
|
||||
<select
|
||||
<section className="panel usage-filter-panel">
|
||||
<div className="usage-filter-header">
|
||||
<span className="usage-filter-title">{t('usage.filter.title')}</span>
|
||||
{activeAdvancedFilterCount > 0 ? (
|
||||
<span className="usage-filter-active">{t('usage.filter.active', { count: activeAdvancedFilterCount })}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="usage-filter-row">
|
||||
<div className="usage-filter-group usage-filter-primary">
|
||||
<label className="usage-filter-item">
|
||||
<span className="usage-filter-label">{t('usage.filter.timeRange')}</span>
|
||||
<select
|
||||
value={range}
|
||||
onChange={(event) => {
|
||||
setRange(event.currentTarget.value as UsageRange);
|
||||
@@ -491,71 +500,7 @@ export function UsageRecordsPage() {
|
||||
</label>
|
||||
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('usage.filter.model')}</span>
|
||||
<select
|
||||
value={model}
|
||||
onChange={(event) => changeFilter(setModel, event.currentTarget.value)}
|
||||
aria-label={t('usage.filter.model')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allModels')}</option>
|
||||
{filterOptions(optionsAnalysis.models).map((item) => (
|
||||
<option value={item.key} key={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('usage.column.provider')}</span>
|
||||
<select
|
||||
value={provider}
|
||||
onChange={(event) => changeFilter(setProvider, event.currentTarget.value)}
|
||||
aria-label={t('usage.column.provider')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allProviders')}</option>
|
||||
{filterOptions(optionsAnalysis.providers).map((item) => (
|
||||
<option value={item.key} key={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('usage.filter.source')}</span>
|
||||
<select
|
||||
value={source}
|
||||
onChange={(event) => changeFilter(setSource, event.currentTarget.value)}
|
||||
aria-label={t('usage.filter.source')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allSources')}</option>
|
||||
{filterOptions(optionsAnalysis.sources).map((item) => (
|
||||
<option value={item.key} key={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('apiAccess.field.key')}</span>
|
||||
<select
|
||||
value={apiKeyHash}
|
||||
onChange={(event) => changeFilter(setApiKeyHash, event.currentTarget.value)}
|
||||
aria-label={t('apiAccess.field.key')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allKeys')}</option>
|
||||
{filterOptions(optionsAnalysis.apiKeys).map((item) => (
|
||||
<option value={item.key} key={item.key}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="usage-filter-item">
|
||||
<span className="sr-only">{t('usage.filter.result')}</span>
|
||||
<span className="usage-filter-label">{t('usage.filter.result')}</span>
|
||||
<select
|
||||
value={result}
|
||||
onChange={(event) => changeFilter(setResult, event.currentTarget.value)}
|
||||
@@ -568,8 +513,75 @@ export function UsageRecordsPage() {
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`usage-filter-toggle${advancedFiltersOpen ? ' is-open' : ''}`}
|
||||
aria-expanded={advancedFiltersOpen}
|
||||
onClick={() => setAdvancedFiltersOpen((open) => !open)}
|
||||
>
|
||||
<ListFilter size={15} aria-hidden="true" />
|
||||
<span>{advancedFiltersOpen ? t('usage.filter.hideMore') : t('usage.filter.more')}</span>
|
||||
{activeAdvancedFilterCount > 0 ? <b>{activeAdvancedFilterCount}</b> : null}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{advancedFiltersOpen ? (
|
||||
<div className="usage-filter-advanced">
|
||||
<label className="usage-filter-item">
|
||||
<span className="usage-filter-label">{t('usage.filter.model')}</span>
|
||||
<select
|
||||
value={model}
|
||||
onChange={(event) => changeFilter(setModel, event.currentTarget.value)}
|
||||
aria-label={t('usage.filter.model')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allModels')}</option>
|
||||
{filterOptions(optionsAnalysis.models).map((item) => (
|
||||
<option value={item.key} key={item.key}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="usage-filter-item">
|
||||
<span className="usage-filter-label">{t('usage.column.provider')}</span>
|
||||
<select
|
||||
value={provider}
|
||||
onChange={(event) => changeFilter(setProvider, event.currentTarget.value)}
|
||||
aria-label={t('usage.column.provider')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allProviders')}</option>
|
||||
{filterOptions(optionsAnalysis.providers).map((item) => (
|
||||
<option value={item.key} key={item.key}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="usage-filter-item">
|
||||
<span className="usage-filter-label">{t('usage.filter.source')}</span>
|
||||
<select
|
||||
value={source}
|
||||
onChange={(event) => changeFilter(setSource, event.currentTarget.value)}
|
||||
aria-label={t('usage.filter.source')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allSources')}</option>
|
||||
{filterOptions(optionsAnalysis.sources).map((item) => (
|
||||
<option value={item.key} key={item.key}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<label className="usage-filter-item">
|
||||
<span className="usage-filter-label">{t('apiAccess.field.key')}</span>
|
||||
<select
|
||||
value={apiKeyHash}
|
||||
onChange={(event) => changeFilter(setApiKeyHash, event.currentTarget.value)}
|
||||
aria-label={t('apiAccess.field.key')}
|
||||
>
|
||||
<option value="">{t('usage.filter.allKeys')}</option>
|
||||
{filterOptions(optionsAnalysis.apiKeys).map((item) => (
|
||||
<option value={item.key} key={item.key}>{item.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{range === 'custom' ? (
|
||||
<div className="usage-custom-range">
|
||||
<input
|
||||
|
||||
@@ -97,3 +97,173 @@
|
||||
.usage-records-page .usage-tab-panel .usage-price-row-actions button:hover {
|
||||
background: var(--usage-page-hover);
|
||||
}
|
||||
|
||||
/* Usage records: keep the common path quiet, and move diagnostic filters out of the way. */
|
||||
.usage-records-page .usage-filter-panel {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 22px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-title {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 650;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-active {
|
||||
padding: 2px 7px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--ui-accent) 10%, transparent);
|
||||
color: var(--ui-accent-strong);
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-group.usage-filter-primary {
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
flex-wrap: nowrap;
|
||||
grid-template-columns: none;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-primary .usage-filter-item {
|
||||
flex: 0 1 230px;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-toggle {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
height: 36px;
|
||||
padding: 0 11px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
font: inherit;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: border-color .15s ease, background .15s ease, color .15s ease;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-toggle:hover,
|
||||
.usage-records-page .usage-filter-toggle.is-open {
|
||||
border-color: var(--ui-accent);
|
||||
background: color-mix(in srgb, var(--ui-accent) 7%, var(--bg-primary));
|
||||
color: var(--ui-accent-strong);
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-toggle b {
|
||||
display: inline-grid;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
place-items: center;
|
||||
padding: 0 5px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--ui-accent) 14%, transparent);
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-advanced {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr));
|
||||
gap: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-advanced .usage-filter-item {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-custom-range {
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
/* The default event view is intentionally compact; secondary diagnostics remain available via tooltips and Columns. */
|
||||
.usage-records-page .usage-request-log .usage-events-table th,
|
||||
.usage-records-page .usage-request-log .usage-events-table th .usage-th-content {
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table td {
|
||||
height: 60px;
|
||||
padding: 7px 9px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table td strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table td small {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-event-metrics {
|
||||
gap: 2px 7px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log.usage-events-compact .usage-cache-write,
|
||||
.usage-records-page .usage-request-log.usage-events-compact .tone-reasoning {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-request-log .usage-events-table tbody tr:nth-child(even) td {
|
||||
background: color-mix(in srgb, var(--usage-log-surface) 97%, var(--ui-accent));
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.usage-records-page .usage-filter-row {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-group.usage-filter-primary {
|
||||
width: 100%;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-primary .usage-filter-item {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-toggle {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-advanced {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.usage-records-page .usage-filter-group.usage-filter-primary,
|
||||
.usage-records-page .usage-filter-advanced {
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-records-page .usage-filter-advanced {
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user