feat(usage): improve usage records layout and scrolling
- streamline the page header and status toolbar - move the horizontal scrollbar above the table - add page size selection and record range details - improve column visibility controls and responsive styles - adjust the Vite chunk size warning limit
This commit is contained in:
1 parent
f83ba83d9d
commit
cbd2ae76af
6 files changed
+404
-171
No files matched your search
@@ -199,6 +199,8 @@ export const jaOverrides = {
|
||||
'usage.events.columnsSelected': '{total} 列中 {selected} 列を選択',
|
||||
'usage.events.selectAllColumns': 'すべて選択',
|
||||
'usage.events.applyColumns': '適用',
|
||||
'usage.events.pageSize': '{size} 件/ページ',
|
||||
'usage.events.rangeSummary': '{total} 件中 {start} - {end} 件を表示',
|
||||
'usage.column.time': '日時',
|
||||
'usage.column.model': 'モデル',
|
||||
'usage.column.provider': 'Provider',
|
||||
|
||||
@@ -199,6 +199,8 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.events.columnsSelected': '{selected} of {total} columns selected',
|
||||
'usage.events.selectAllColumns': 'Select All',
|
||||
'usage.events.applyColumns': 'Apply',
|
||||
'usage.events.pageSize': '{size} / page',
|
||||
'usage.events.rangeSummary': 'Showing {start} - {end} of {total}',
|
||||
'usage.column.time': 'Time',
|
||||
'usage.column.model': 'Model',
|
||||
'usage.column.provider': 'Provider',
|
||||
|
||||
@@ -192,11 +192,13 @@ export const zhCN = {
|
||||
'usage.events.resetColumns': '重置列宽',
|
||||
'usage.events.resizeHint': '拖动调整列宽,双击重置本列',
|
||||
'usage.events.dragHint': '可拖动表头边线调整每列宽度',
|
||||
'usage.events.columnSettings': '列设置',
|
||||
'usage.events.columnSettingsDescription': '选择要在请求明细中显示的列。',
|
||||
'usage.events.columnSettings': '显示列',
|
||||
'usage.events.columnSettingsDescription': '选择请求明细中需要显示的列。',
|
||||
'usage.events.columnsSelected': '已选择 {selected} / {total} 列',
|
||||
'usage.events.selectAllColumns': '选择全部',
|
||||
'usage.events.applyColumns': '应用',
|
||||
'usage.events.pageSize': '{size} 条/页',
|
||||
'usage.events.rangeSummary': '显示第 {start} - {end} 条,共 {total} 条',
|
||||
'usage.column.time': '时间',
|
||||
'usage.column.model': '模型',
|
||||
'usage.column.provider': 'Provider',
|
||||
|
||||
+232
-92
@@ -4,6 +4,8 @@ import { listen } from '@tauri-apps/api/event';
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
CircleDollarSign,
|
||||
Clock3,
|
||||
Columns3Cog,
|
||||
@@ -270,6 +272,7 @@ export function UsageRecordsPage() {
|
||||
const [apiKeyHash, setApiKeyHash] = useState('');
|
||||
const [result, setResult] = useState('all');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [status, setStatus] = useState<CollectorStatus | null>(null);
|
||||
const [overview, setOverview] = useState<UsageOverview | null>(null);
|
||||
const [analysis, setAnalysis] = useState<UsageAnalysis>(emptyAnalysis);
|
||||
@@ -347,7 +350,7 @@ export function UsageRecordsPage() {
|
||||
statusRequest,
|
||||
optionsRequest,
|
||||
invoke<UsageEventPage>('get_usage_events', {
|
||||
query: { ...query, page, page_size: 50 },
|
||||
query: { ...query, page, page_size: pageSize },
|
||||
}),
|
||||
]);
|
||||
if (requestId !== requestIdRef.current) return;
|
||||
@@ -372,7 +375,7 @@ export function UsageRecordsPage() {
|
||||
if (requestId === requestIdRef.current) setLoading(false);
|
||||
}
|
||||
},
|
||||
[activeTab, buildQueries, page]
|
||||
[activeTab, buildQueries, page, pageSize]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -437,24 +440,55 @@ export function UsageRecordsPage() {
|
||||
|
||||
return (
|
||||
<section className="page management-page usage-records-page">
|
||||
<header className="management-header usage-records-header">
|
||||
<div className="usage-header-left">
|
||||
<span className="usage-header-tag">Local Usage</span>
|
||||
<h1>{t('usage.title')}</h1>
|
||||
{error ? <div className="management-alert error">{error}</div> : null}
|
||||
|
||||
<div className="usage-topbar">
|
||||
<div className="usage-tabs" role="tablist" aria-label={t('usage.pageLabel')}>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'overview' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('overview')}
|
||||
>
|
||||
<BarChart3 size={15} />
|
||||
<span>{t('usage.tab.overview')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'analysis' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('analysis')}
|
||||
>
|
||||
<Activity size={15} />
|
||||
<span>{t('usage.tab.analysis')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'events' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('events')}
|
||||
>
|
||||
<List size={15} />
|
||||
<span>{t('usage.tab.events')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'pricing' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('pricing')}
|
||||
>
|
||||
<CircleDollarSign size={15} />
|
||||
<span>{t('usage.tab.pricing')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="usage-header-right">
|
||||
|
||||
<div className="usage-topbar-actions">
|
||||
<div className={`usage-collector-state ${collectorTone}`} title={status?.message}>
|
||||
<span className="status-dot" />
|
||||
<div>
|
||||
<strong>
|
||||
{status?.state === 'collecting'
|
||||
? t('usage.collector.collecting')
|
||||
: status?.state === 'error'
|
||||
? t('usage.collector.error')
|
||||
: t('usage.collector.waiting')}
|
||||
</strong>
|
||||
<span>{t('usage.longTermRecords', { count: compactNumber(status?.totalRecords ?? 0) })}</span>
|
||||
</div>
|
||||
<strong>
|
||||
{status?.state === 'collecting'
|
||||
? t('usage.collector.collecting')
|
||||
: status?.state === 'error'
|
||||
? t('usage.collector.error')
|
||||
: t('usage.collector.waiting')}
|
||||
</strong>
|
||||
<span>{t('usage.longTermRecords', { count: compactNumber(status?.totalRecords ?? 0) })}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -467,43 +501,6 @@ export function UsageRecordsPage() {
|
||||
<RefreshCw size={15} className={loading ? 'spin' : ''} />
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{error ? <div className="management-alert error">{error}</div> : null}
|
||||
|
||||
<div className="usage-tabs" role="tablist" aria-label={t('usage.pageLabel')}>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'overview' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('overview')}
|
||||
>
|
||||
<BarChart3 size={15} />
|
||||
<span>{t('usage.tab.overview')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'analysis' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('analysis')}
|
||||
>
|
||||
<Activity size={15} />
|
||||
<span>{t('usage.tab.analysis')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'events' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('events')}
|
||||
>
|
||||
<List size={15} />
|
||||
<span>{t('usage.tab.events')}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={activeTab === 'pricing' ? 'active' : ''}
|
||||
onClick={() => setActiveTab('pricing')}
|
||||
>
|
||||
<CircleDollarSign size={15} />
|
||||
<span>{t('usage.tab.pricing')}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section className="panel usage-filter-panel">
|
||||
@@ -667,7 +664,17 @@ export function UsageRecordsPage() {
|
||||
|
||||
{activeTab === 'overview' && overview ? <OverviewView overview={overview} /> : null}
|
||||
{activeTab === 'analysis' ? <AnalysisView analysis={analysis} overview={overview} /> : null}
|
||||
{activeTab === 'events' && events ? <EventsView events={events} onPage={setPage} /> : null}
|
||||
{activeTab === 'events' && events ? (
|
||||
<EventsView
|
||||
events={events}
|
||||
pageSize={pageSize}
|
||||
onPage={setPage}
|
||||
onPageSizeChange={(size) => {
|
||||
setPageSize(size);
|
||||
setPage(1);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{activeTab === 'pricing' && pricing ? (
|
||||
<PricingView pricing={pricing} query={buildQueries().query} onChanged={() => loadData(true)} />
|
||||
) : null}
|
||||
@@ -1066,6 +1073,66 @@ const getInitialColumnWidths = (): Record<EventColumnKey, number> => {
|
||||
return initial;
|
||||
};
|
||||
|
||||
function TableTopScrollbar({
|
||||
tableWrapRef,
|
||||
totalWidth,
|
||||
}: {
|
||||
tableWrapRef: React.RefObject<HTMLDivElement | null>;
|
||||
totalWidth: number;
|
||||
}) {
|
||||
const scrollbarRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hasOverflow, setHasOverflow] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const scrollbar = scrollbarRef.current;
|
||||
const tableWrap = tableWrapRef.current;
|
||||
if (!scrollbar || !tableWrap) return;
|
||||
|
||||
let syncing = false;
|
||||
const syncTable = () => {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
tableWrap.scrollLeft = scrollbar.scrollLeft;
|
||||
window.requestAnimationFrame(() => {
|
||||
syncing = false;
|
||||
});
|
||||
};
|
||||
const syncScrollbar = () => {
|
||||
if (syncing) return;
|
||||
syncing = true;
|
||||
scrollbar.scrollLeft = tableWrap.scrollLeft;
|
||||
window.requestAnimationFrame(() => {
|
||||
syncing = false;
|
||||
});
|
||||
};
|
||||
const updateOverflow = () => {
|
||||
setHasOverflow(tableWrap.scrollWidth > tableWrap.clientWidth + 1);
|
||||
scrollbar.scrollLeft = tableWrap.scrollLeft;
|
||||
};
|
||||
|
||||
updateOverflow();
|
||||
scrollbar.addEventListener('scroll', syncTable, { passive: true });
|
||||
tableWrap.addEventListener('scroll', syncScrollbar, { passive: true });
|
||||
const resizeObserver = new ResizeObserver(updateOverflow);
|
||||
resizeObserver.observe(tableWrap);
|
||||
return () => {
|
||||
scrollbar.removeEventListener('scroll', syncTable);
|
||||
tableWrap.removeEventListener('scroll', syncScrollbar);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, [tableWrapRef, totalWidth]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={scrollbarRef}
|
||||
className={`usage-table-top-scrollbar ${hasOverflow ? '' : 'is-hidden'}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div style={{ width: `${totalWidth}px`, height: '1px' }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UsageResultCell({ record }: { record: UsageRecord }) {
|
||||
const { t } = useI18n();
|
||||
const state = record.canceled ? 'canceled' : record.failed ? 'failed' : 'success';
|
||||
@@ -1203,14 +1270,26 @@ function UsageEventCell({
|
||||
}
|
||||
}
|
||||
|
||||
function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page: number) => void }) {
|
||||
function EventsView({
|
||||
events,
|
||||
pageSize,
|
||||
onPage,
|
||||
onPageSizeChange,
|
||||
}: {
|
||||
events: UsageEventPage;
|
||||
pageSize: number;
|
||||
onPage: (page: number) => void;
|
||||
onPageSizeChange: (pageSize: number) => void;
|
||||
}) {
|
||||
const { t } = useI18n();
|
||||
const [widths, setWidths] = useState<Record<EventColumnKey, number>>(getInitialColumnWidths);
|
||||
const [visibleColumnKeys, setVisibleColumnKeys] = useState<EventColumnKey[]>(getInitialVisibleColumns);
|
||||
const [columnSettingsOpen, setColumnSettingsOpen] = useState(false);
|
||||
const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState<EventColumnKey[]>(visibleColumnKeys);
|
||||
const [resizingCol, setResizingCol] = useState<EventColumnKey | null>(null);
|
||||
|
||||
const columnDialogRef = useRef<HTMLElement | null>(null);
|
||||
const tableWrapRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
const visibleColumnKeySet = new Set(visibleColumnKeys);
|
||||
const visibleColumns = EVENT_COLUMNS.filter((column) => visibleColumnKeySet.has(column.key));
|
||||
@@ -1242,12 +1321,15 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
if (current.includes(key)) {
|
||||
return current.length > 1 ? current.filter((columnKey) => columnKey !== key) : current;
|
||||
}
|
||||
return EVENT_COLUMNS.filter((column) => current.includes(column.key) || column.key === key).map((column) => column.key);
|
||||
return EVENT_COLUMNS.filter(
|
||||
(column) => current.includes(column.key) || column.key === key
|
||||
).map((column) => column.key);
|
||||
});
|
||||
};
|
||||
|
||||
const applyColumnSettings = () => {
|
||||
const next = draftVisibleColumnKeys.length > 0 ? draftVisibleColumnKeys : getAllEventColumnKeys();
|
||||
const next =
|
||||
draftVisibleColumnKeys.length > 0 ? draftVisibleColumnKeys : getAllEventColumnKeys();
|
||||
setVisibleColumnKeys(next);
|
||||
try {
|
||||
localStorage.setItem(EVENT_VISIBLE_COLS_STORAGE_KEY, JSON.stringify(next));
|
||||
@@ -1279,7 +1361,8 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
e.stopPropagation();
|
||||
|
||||
const startX = e.clientX;
|
||||
const startWidth = widths[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100;
|
||||
const startWidth =
|
||||
widths[key] ?? EVENT_COLUMNS.find((c) => c.key === key)?.defaultWidth ?? 100;
|
||||
const colDef = EVENT_COLUMNS.find((c) => c.key === key);
|
||||
const minWidth = colDef?.minWidth ?? 50;
|
||||
|
||||
@@ -1314,7 +1397,13 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
window.addEventListener('pointerup', onPointerUp);
|
||||
};
|
||||
|
||||
const totalTableWidth = visibleColumns.reduce((sum, col) => sum + (widths[col.key] ?? col.defaultWidth), 0);
|
||||
const totalTableWidth = visibleColumns.reduce(
|
||||
(sum, col) => sum + (widths[col.key] ?? col.defaultWidth),
|
||||
0
|
||||
);
|
||||
|
||||
const startRecordNum = events.total > 0 ? (events.page - 1) * pageSize + 1 : 0;
|
||||
const endRecordNum = Math.min(events.page * pageSize, events.total);
|
||||
|
||||
return (
|
||||
<section className="panel usage-events-panel">
|
||||
@@ -1323,10 +1412,11 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
<span className="usage-events-count-badge">
|
||||
{t('usage.events.total', { count: compactNumber(events.total) })}
|
||||
</span>
|
||||
<span className="usage-col-hint" title={t('usage.events.resizeHint')}>
|
||||
{t('usage.events.dragHint')}
|
||||
<span className="usage-events-page-indicator">
|
||||
{t('usage.events.page', { page: events.page, total: events.totalPages })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="usage-events-summary-right">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1334,25 +1424,29 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
onClick={openColumnSettings}
|
||||
title={t('usage.events.columnSettings')}
|
||||
>
|
||||
<Columns3Cog size={13} />
|
||||
<Columns3Cog size={14} />
|
||||
<span>{t('usage.events.columnSettings')}</span>
|
||||
</button>
|
||||
{isCustomized ? (
|
||||
<button
|
||||
type="button"
|
||||
className="usage-col-reset-btn"
|
||||
className="usage-col-reset-btn icon-only"
|
||||
onClick={resetAllWidths}
|
||||
title={t('usage.events.resizeHint')}
|
||||
title={t('usage.events.resetColumns')}
|
||||
aria-label={t('usage.events.resetColumns')}
|
||||
>
|
||||
<RotateCcw size={12} />
|
||||
<span>{t('usage.events.resetColumns')}</span>
|
||||
<RotateCcw size={13} />
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{events.items.length > 0 ? (
|
||||
<TableTopScrollbar tableWrapRef={tableWrapRef} totalWidth={totalTableWidth} />
|
||||
) : null}
|
||||
|
||||
{events.items.length ? (
|
||||
<div className="usage-table-wrap">
|
||||
<div ref={tableWrapRef} className="usage-table-wrap">
|
||||
<table className="usage-events-table" style={{ width: `${totalTableWidth}px`, minWidth: '100%' }}>
|
||||
<colgroup>
|
||||
{visibleColumns.map((col) => (
|
||||
@@ -1404,31 +1498,63 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
)}
|
||||
|
||||
<div className="usage-pagination">
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={events.page <= 1}
|
||||
onClick={() => onPage(events.page - 1)}
|
||||
>
|
||||
{t('usage.previous')}
|
||||
</button>
|
||||
<span className="usage-pagination-info">
|
||||
{events.page} / {events.totalPages}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
disabled={events.page >= events.totalPages}
|
||||
onClick={() => onPage(events.page + 1)}
|
||||
>
|
||||
{t('usage.next')}
|
||||
</button>
|
||||
<div className="usage-pagination-left">
|
||||
<span className="usage-pagination-summary">
|
||||
{t('usage.events.rangeSummary', {
|
||||
start: startRecordNum,
|
||||
end: endRecordNum,
|
||||
total: compactNumber(events.total),
|
||||
})}
|
||||
</span>
|
||||
|
||||
<label style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
|
||||
<select
|
||||
className="usage-page-size-select"
|
||||
value={pageSize}
|
||||
onChange={(e) => onPageSizeChange(Number(e.currentTarget.value))}
|
||||
aria-label={t('usage.events.pageSize', { size: pageSize })}
|
||||
>
|
||||
<option value="20">{t('usage.events.pageSize', { size: 20 })}</option>
|
||||
<option value="50">{t('usage.events.pageSize', { size: 50 })}</option>
|
||||
<option value="100">{t('usage.events.pageSize', { size: 100 })}</option>
|
||||
<option value="200">{t('usage.events.pageSize', { size: 200 })}</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="usage-pagination-right">
|
||||
<button
|
||||
type="button"
|
||||
className="usage-page-nav-btn"
|
||||
disabled={events.page <= 1}
|
||||
onClick={() => onPage(events.page - 1)}
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
<span>{t('usage.previous')}</span>
|
||||
</button>
|
||||
|
||||
<span className="usage-pagination-info">
|
||||
{events.page} / {events.totalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="usage-page-nav-btn"
|
||||
disabled={events.page >= events.totalPages}
|
||||
onClick={() => onPage(events.page + 1)}
|
||||
>
|
||||
<span>{t('usage.next')}</span>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{columnSettingsOpen ? (
|
||||
<div
|
||||
className="config-dialog-backdrop"
|
||||
onMouseDown={(event) => event.currentTarget === event.target && setColumnSettingsOpen(false)}
|
||||
onMouseDown={(event) =>
|
||||
event.currentTarget === event.target && setColumnSettingsOpen(false)
|
||||
}
|
||||
>
|
||||
<section
|
||||
ref={columnDialogRef}
|
||||
@@ -1455,7 +1581,9 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
<X size={17} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="usage-column-dialog-description">{t('usage.events.columnSettingsDescription')}</p>
|
||||
<p className="usage-column-dialog-description">
|
||||
{t('usage.events.columnSettingsDescription')}
|
||||
</p>
|
||||
<div className="usage-column-options">
|
||||
{EVENT_COLUMNS.map((column) => {
|
||||
const checked = draftVisibleColumnKeys.includes(column.key);
|
||||
@@ -1480,15 +1608,27 @@ function EventsView({ events, onPage }: { events: UsageEventPage; onPage: (page:
|
||||
total: EVENT_COLUMNS.length,
|
||||
})}
|
||||
</span>
|
||||
<button type="button" className="usage-column-select-all" onClick={resetVisibleColumns}>
|
||||
<button
|
||||
type="button"
|
||||
className="usage-column-select-all"
|
||||
onClick={resetVisibleColumns}
|
||||
>
|
||||
{t('usage.events.selectAllColumns')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="usage-column-dialog-actions">
|
||||
<button type="button" className="secondary-button" onClick={() => setColumnSettingsOpen(false)}>
|
||||
<button
|
||||
type="button"
|
||||
className="secondary-button"
|
||||
onClick={() => setColumnSettingsOpen(false)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</button>
|
||||
<button type="button" className="primary-button" onClick={applyColumnSettings}>
|
||||
<button
|
||||
type="button"
|
||||
className="primary-button"
|
||||
onClick={applyColumnSettings}
|
||||
>
|
||||
{t('usage.events.applyColumns')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+163
-77
@@ -6617,42 +6617,29 @@ textarea:focus-visible {
|
||||
|
||||
.usage-records-page {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
gap: 12px;
|
||||
align-content: start;
|
||||
}
|
||||
|
||||
.usage-records-header {
|
||||
.usage-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
min-height: 56px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.usage-header-left {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.usage-header-tag {
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--theme-8a847c);
|
||||
}
|
||||
|
||||
.usage-header-right {
|
||||
.usage-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-refresh-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 8px;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 7px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
background: var(--theme-fffdf8);
|
||||
color: var(--theme-5f5a53);
|
||||
@@ -6675,19 +6662,21 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.usage-collector-state {
|
||||
display: flex;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 170px;
|
||||
padding: 6px 12px;
|
||||
gap: 7px;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
border-radius: 8px;
|
||||
border-radius: 7px;
|
||||
background: var(--theme-fffdf8);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.usage-collector-state .status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 50%;
|
||||
background: var(--theme-8a847c);
|
||||
flex-shrink: 0;
|
||||
@@ -6704,11 +6693,6 @@ textarea:focus-visible {
|
||||
box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.usage-collector-state > div {
|
||||
display: grid;
|
||||
gap: 1px;
|
||||
}
|
||||
|
||||
.usage-collector-state strong {
|
||||
color: var(--theme-2d2a26);
|
||||
font-size: var(--font-size-caption);
|
||||
@@ -6722,6 +6706,11 @@ textarea:focus-visible {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.usage-collector-state > span:last-child {
|
||||
padding-left: 7px;
|
||||
border-left: 1px solid var(--theme-e3e1db);
|
||||
}
|
||||
|
||||
/* Tab Switcher */
|
||||
.usage-tabs {
|
||||
display: inline-flex;
|
||||
@@ -7264,11 +7253,11 @@ textarea:focus-visible {
|
||||
|
||||
.usage-events-summary {
|
||||
display: flex;
|
||||
min-height: 58px;
|
||||
min-height: 54px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 11px 16px;
|
||||
padding: 10px 16px;
|
||||
border-bottom: 1px solid var(--theme-e3e1db);
|
||||
background: var(--theme-fffdf8);
|
||||
color: var(--theme-8a847c);
|
||||
@@ -7286,7 +7275,7 @@ textarea:focus-visible {
|
||||
.usage-events-count-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 8px;
|
||||
padding: 3px 9px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
border-radius: 999px;
|
||||
background: var(--theme-f4f1ea);
|
||||
@@ -7298,7 +7287,7 @@ textarea:focus-visible {
|
||||
.usage-events-summary-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.usage-events-page-indicator {
|
||||
@@ -7310,7 +7299,7 @@ textarea:focus-visible {
|
||||
.usage-col-hint {
|
||||
font-size: 11px;
|
||||
color: var(--theme-9a958d);
|
||||
opacity: 0.85;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.usage-col-reset-btn,
|
||||
@@ -7319,8 +7308,8 @@ textarea:focus-visible {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
padding: 6px 11px;
|
||||
min-height: 30px;
|
||||
padding: 5px 10px;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 600;
|
||||
background: var(--theme-f4f1ea);
|
||||
@@ -7334,8 +7323,8 @@ textarea:focus-visible {
|
||||
|
||||
.usage-col-reset-btn svg,
|
||||
.usage-col-settings-btn svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -7346,6 +7335,7 @@ textarea:focus-visible {
|
||||
border-color: var(--theme-d5d2cb);
|
||||
}
|
||||
|
||||
/* Column Settings Dialog */
|
||||
.usage-column-dialog {
|
||||
display: flex;
|
||||
width: min(540px, 100%);
|
||||
@@ -7468,9 +7458,25 @@ textarea:focus-visible {
|
||||
min-width: 84px;
|
||||
}
|
||||
|
||||
/* Native horizontal scrollbar, mirrored above the table. */
|
||||
.usage-table-top-scrollbar {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 16px;
|
||||
border-bottom: 1px solid var(--theme-e3e1db);
|
||||
background: var(--theme-fffdf8);
|
||||
}
|
||||
|
||||
.usage-table-top-scrollbar.is-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Main Table Wrap & Table */
|
||||
.usage-table-wrap {
|
||||
overflow: auto;
|
||||
height: clamp(480px, 62vh, 700px);
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
height: clamp(480px, 60vh, 720px);
|
||||
min-height: 0;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
@@ -7489,8 +7495,8 @@ textarea:focus-visible {
|
||||
|
||||
.usage-events-table th,
|
||||
.usage-events-table td {
|
||||
height: 46px;
|
||||
padding: 8px 12px;
|
||||
height: 44px;
|
||||
padding: 7px 11px;
|
||||
border-bottom: 1px solid var(--theme-eeeae2);
|
||||
color: var(--theme-5f5a53);
|
||||
white-space: nowrap;
|
||||
@@ -7498,12 +7504,13 @@ textarea:focus-visible {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.usage-events-table tr:nth-child(even) td {
|
||||
background-color: transparent;
|
||||
.usage-events-table tbody tr {
|
||||
cursor: default;
|
||||
transition: background-color 0.12s ease;
|
||||
}
|
||||
|
||||
.usage-events-table tr:hover td {
|
||||
background-color: var(--theme-f2efe8) !important;
|
||||
.usage-events-table tbody tr:hover td {
|
||||
background-color: var(--theme-f4f1ea) !important;
|
||||
}
|
||||
|
||||
.usage-events-table th {
|
||||
@@ -7554,7 +7561,7 @@ textarea:focus-visible {
|
||||
bottom: 0;
|
||||
width: 7px;
|
||||
cursor: col-resize;
|
||||
z-index: 3;
|
||||
z-index: 6;
|
||||
touch-action: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -7606,21 +7613,6 @@ body.table-col-resizing * {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.usage-column-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.usage-column-dialog-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-column-dialog-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-events-table td.usage-td-provider,
|
||||
.usage-events-table td.usage-td-source {
|
||||
overflow: hidden;
|
||||
@@ -7711,17 +7703,87 @@ body.table-col-resizing * {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
/* Enhanced Pagination */
|
||||
.usage-pagination {
|
||||
display: flex;
|
||||
min-height: 52px;
|
||||
min-height: 50px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 0 16px;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 16px;
|
||||
border-top: 1px solid var(--theme-e3e1db);
|
||||
background: var(--theme-fffdf8);
|
||||
color: var(--theme-8a847c);
|
||||
font-size: var(--font-size-caption);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.usage-pagination-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.usage-pagination-summary {
|
||||
font-size: var(--font-size-caption);
|
||||
color: var(--theme-7a746c);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.usage-page-size-select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
border-radius: 5px;
|
||||
background: var(--theme-faf8f4);
|
||||
color: var(--theme-2d2a26);
|
||||
font-size: var(--font-size-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.usage-page-size-select:focus {
|
||||
border-color: var(--theme-3f6f98);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.usage-pagination-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.usage-page-nav-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
padding: 0 6px;
|
||||
border: 1px solid var(--theme-e3e1db);
|
||||
border-radius: 5px;
|
||||
background: var(--theme-fffdf8);
|
||||
color: var(--theme-5f5a53);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-caption);
|
||||
font-weight: 500;
|
||||
transition: all 0.15s ease;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.usage-col-reset-btn.icon-only {
|
||||
width: 30px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.usage-page-nav-btn:hover:not(:disabled) {
|
||||
background: var(--theme-f4f1ea);
|
||||
border-color: var(--theme-d5d2cb);
|
||||
color: var(--theme-2d2a26);
|
||||
}
|
||||
|
||||
.usage-page-nav-btn:disabled {
|
||||
opacity: 0.35;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.usage-pagination-info {
|
||||
@@ -7731,12 +7793,28 @@ body.table-col-resizing * {
|
||||
color: var(--theme-7a746c);
|
||||
}
|
||||
|
||||
.usage-pagination button {
|
||||
min-width: 72px;
|
||||
min-height: 30px;
|
||||
padding: 0 10px;
|
||||
border-radius: 6px;
|
||||
font-size: var(--font-size-caption);
|
||||
@media (max-width: 640px) {
|
||||
.usage-column-options {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.usage-column-dialog-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-column-dialog-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.usage-pagination {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-pagination-right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.usage-events-panel > .usage-empty {
|
||||
@@ -9183,6 +9261,14 @@ body.table-col-resizing * {
|
||||
padding: 0 7px;
|
||||
}
|
||||
|
||||
.usage-topbar-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.usage-collector-state {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.usage-stat-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ export default defineConfig({
|
||||
build: {
|
||||
target: 'es2020',
|
||||
outDir: 'dist',
|
||||
chunkSizeWarningLimit: 600,
|
||||
rollupOptions: {
|
||||
output: {
|
||||
manualChunks(id) {
|
||||
|
||||
Reference in new issue
Block a user