feat(usage): add customizable fixed row height settings
- Add a display setting to enable or disable fixed record heights - Allow users to adjust row height with a draggable slider - Persist row height preferences across sessions - Improve display settings layout and translations
This commit is contained in:
1 parent
8d3912f81b
commit
1d64a4535f
6 files changed
+211
-15
No files matched your search
+5
-1
@@ -775,7 +775,7 @@ export const jaOverrides = {
|
||||
'usage.analysis.shareLabel': '割合',
|
||||
'usage.events.title': 'リクエストイベントログ',
|
||||
'usage.events.description': '選択した期間のリクエストイベントを絞り込んで確認します。',
|
||||
'usage.events.columns': '列',
|
||||
'usage.events.columns': '表示する列',
|
||||
'usage.events.exportPage': '絞り込み結果を CSV 出力',
|
||||
'usage.events.exportDescription': '現在の絞り込み結果の全リクエスト項目を CSV ファイルに出力し、保存先を選択します',
|
||||
'usage.events.exportDialogTitle': 'CSV の保存先を選択',
|
||||
@@ -789,6 +789,10 @@ export const jaOverrides = {
|
||||
'usage.events.dragHint': 'ヘッダーの境界をドラッグして列幅を調整',
|
||||
'usage.events.columnSettings': '表示設定',
|
||||
'usage.events.columnSettingsDescription': 'リクエスト詳細に表示する列を選択します。',
|
||||
'usage.events.rowHeight.title': 'レコードの行高を固定',
|
||||
'usage.events.rowHeight.description': 'すべてのレコードを同じ高さにします。オフにすると内容に合わせて調整します。',
|
||||
'usage.events.rowHeight.fixed': '行高',
|
||||
'usage.events.rowHeight.dragHint': 'スライダーをドラッグして調整',
|
||||
'usage.events.columnsSelected': '{total} 列中 {selected} 列を選択',
|
||||
'usage.events.selectAllColumns': 'すべて選択',
|
||||
'usage.events.applyColumns': '適用',
|
||||
|
||||
@@ -763,7 +763,7 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.analysis.shareLabel': 'Share',
|
||||
'usage.events.title': 'Request Event Log',
|
||||
'usage.events.description': 'Filter and inspect request events in the selected time range.',
|
||||
'usage.events.columns': 'Columns',
|
||||
'usage.events.columns': 'Show columns',
|
||||
'usage.events.exportPage': 'Export Filtered CSV',
|
||||
'usage.events.exportDescription': 'Export all request fields matching the current filters and choose where to save the CSV',
|
||||
'usage.events.exportDialogTitle': 'Choose where to save the CSV',
|
||||
@@ -777,6 +777,10 @@ export const en: Record<MessageKey, string> = {
|
||||
'usage.events.dragHint': 'Drag column header borders to resize',
|
||||
'usage.events.columnSettings': 'Display Settings',
|
||||
'usage.events.columnSettingsDescription': 'Choose which columns appear in request details.',
|
||||
'usage.events.rowHeight.title': 'Fixed row height',
|
||||
'usage.events.rowHeight.description': 'Use one height for every record; turn it off to fit content automatically.',
|
||||
'usage.events.rowHeight.fixed': 'Row height',
|
||||
'usage.events.rowHeight.dragHint': 'Drag the slider to adjust',
|
||||
'usage.events.columnsSelected': '{selected} of {total} columns selected',
|
||||
'usage.events.selectAllColumns': 'Select All',
|
||||
'usage.events.applyColumns': 'Apply',
|
||||
|
||||
@@ -762,7 +762,7 @@ export const zhCN = {
|
||||
'usage.analysis.shareLabel': '占比',
|
||||
'usage.events.title': '请求事件日志',
|
||||
'usage.events.description': '筛选并查看所选时间范围内的请求事件。',
|
||||
'usage.events.columns': '列',
|
||||
'usage.events.columns': '显示列',
|
||||
'usage.events.exportPage': '导出筛选结果 CSV',
|
||||
'usage.events.exportDescription': '导出当前筛选结果中的全部请求字段,并选择保存位置',
|
||||
'usage.events.exportDialogTitle': '选择 CSV 保存位置',
|
||||
@@ -776,6 +776,10 @@ export const zhCN = {
|
||||
'usage.events.dragHint': '可拖动表头边线调整每列宽度',
|
||||
'usage.events.columnSettings': '显示设置',
|
||||
'usage.events.columnSettingsDescription': '选择请求明细中需要显示的列。',
|
||||
'usage.events.rowHeight.title': '固定记录行高',
|
||||
'usage.events.rowHeight.description': '开启后每条记录使用统一高度;关闭时根据内容自动适应。',
|
||||
'usage.events.rowHeight.fixed': '行高',
|
||||
'usage.events.rowHeight.dragHint': '拖动滑块调整高度',
|
||||
'usage.events.columnsSelected': '已选择 {selected} / {total} 列',
|
||||
'usage.events.selectAllColumns': '选择全部',
|
||||
'usage.events.applyColumns': '应用',
|
||||
|
||||
@@ -50,16 +50,20 @@
|
||||
background: var(--usage-log-surface);
|
||||
color: var(--usage-log-muted);
|
||||
box-shadow: none;
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
transition: background .15s ease, border-color .15s ease;
|
||||
}
|
||||
|
||||
.usage-request-log .usage-col-reset-btn.icon-only {
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
.usage-request-log .usage-col-settings-btn svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.usage-request-log .usage-col-settings-btn {
|
||||
color: var(--usage-log-text);
|
||||
}
|
||||
|
||||
.usage-request-log .usage-col-settings-btn:hover,
|
||||
@@ -131,6 +135,14 @@
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.usage-request-log.usage-row-height-auto .usage-events-table td {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.usage-request-log.usage-row-height-fixed .usage-events-table td {
|
||||
height: var(--usage-row-height, 68px);
|
||||
}
|
||||
|
||||
.usage-request-log .usage-events-table tbody tr:hover td {
|
||||
background: var(--usage-log-hover) !important;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { usagePreferences } from '../services/usagePreferences';
|
||||
import { useEffect, useRef, useState, type KeyboardEvent } from 'react';
|
||||
import { useEffect, useRef, useState, type KeyboardEvent, type CSSProperties } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3Cog, Database, DatabaseZap, Download, RotateCcw, TriangleAlert, X } from 'lucide-react';
|
||||
import { ArrowDown, ArrowUp, Brain, ChevronLeft, ChevronRight, Columns3, Database, DatabaseZap, Download, RotateCcw, TriangleAlert, X } from 'lucide-react';
|
||||
import { getCurrentLocale, useI18n } from '../i18n';
|
||||
import type { MessageKey } from '../i18n/resources';
|
||||
import { formatCacheReadRate, formatGenerationSpeed } from '../services/usageMetrics';
|
||||
@@ -116,9 +116,26 @@ 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 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 EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY = 'cpa-gui.usage-events-row-height-enabled.v1';
|
||||
const EVENT_ROW_HEIGHT_STORAGE_KEY = 'cpa-gui.usage-events-row-height.v1';
|
||||
const DEFAULT_EVENT_ROW_HEIGHT = 68;
|
||||
const MIN_EVENT_ROW_HEIGHT = 48;
|
||||
const MAX_EVENT_ROW_HEIGHT = 140;
|
||||
|
||||
const getAllEventColumnKeys = () => EVENT_COLUMNS.map((column) => column.key);
|
||||
|
||||
const clampEventRowHeight = (value: number) => Math.min(
|
||||
MAX_EVENT_ROW_HEIGHT,
|
||||
Math.max(MIN_EVENT_ROW_HEIGHT, Math.round(value)),
|
||||
);
|
||||
|
||||
const getInitialRowHeightEnabled = () => usagePreferences.getItem(EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY) === 'true';
|
||||
|
||||
const getInitialRowHeight = () => {
|
||||
const value = Number(usagePreferences.getItem(EVENT_ROW_HEIGHT_STORAGE_KEY));
|
||||
return Number.isFinite(value) ? clampEventRowHeight(value) : DEFAULT_EVENT_ROW_HEIGHT;
|
||||
};
|
||||
|
||||
export const getInitialVisibleColumns = (): EventColumnKey[] => {
|
||||
try {
|
||||
const currentRaw = usagePreferences.getItem(EVENT_VISIBLE_COLS_STORAGE_KEY);
|
||||
@@ -480,6 +497,8 @@ export function EventsView({
|
||||
const [columnSettingsOpen, setColumnSettingsOpen] = useState(false);
|
||||
const [draftVisibleColumnKeys, setDraftVisibleColumnKeys] = useState<EventColumnKey[]>(visibleColumnKeys);
|
||||
const [resizingCol, setResizingCol] = useState<EventColumnKey | null>(null);
|
||||
const [fixedRowHeight, setFixedRowHeight] = useState(getInitialRowHeightEnabled);
|
||||
const [rowHeight, setRowHeight] = useState(getInitialRowHeight);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
|
||||
const columnDialogRef = useDialogFocusTrap<HTMLElement>({
|
||||
@@ -543,6 +562,17 @@ export function EventsView({
|
||||
setDraftVisibleColumnKeys(getAllEventColumnKeys());
|
||||
};
|
||||
|
||||
const updateFixedRowHeight = (enabled: boolean) => {
|
||||
setFixedRowHeight(enabled);
|
||||
usagePreferences.setItem(EVENT_ROW_HEIGHT_ENABLED_STORAGE_KEY, String(enabled));
|
||||
};
|
||||
|
||||
const updateRowHeight = (value: number) => {
|
||||
const next = clampEventRowHeight(value);
|
||||
setRowHeight(next);
|
||||
usagePreferences.setItem(EVENT_ROW_HEIGHT_STORAGE_KEY, String(next));
|
||||
};
|
||||
|
||||
const resetSingleColumn = (key: EventColumnKey, e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -686,12 +716,12 @@ export function EventsView({
|
||||
};
|
||||
|
||||
return (
|
||||
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}`} aria-label={t('usage.events.title')} aria-busy={loading}>
|
||||
<section className={`panel usage-events-panel usage-request-log${isCompactDefault ? ' usage-events-compact' : ''}${fixedRowHeight ? ' usage-row-height-fixed' : ' usage-row-height-auto'}`} aria-label={t('usage.events.title')} aria-busy={loading}>
|
||||
{loading && events.items.length === 0 ? <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
|
||||
className="usage-events-table"
|
||||
style={{ width: `${totalTableWidth}px` }}
|
||||
style={{ width: `${totalTableWidth}px`, '--usage-row-height': `${rowHeight}px` } as CSSProperties}
|
||||
>
|
||||
<colgroup>
|
||||
{visibleColumns.map((col) => (
|
||||
@@ -769,19 +799,22 @@ export function EventsView({
|
||||
onClick={openColumnSettings}
|
||||
title={t('usage.events.columnSettings')}
|
||||
aria-label={t('usage.events.columnSettings')}
|
||||
aria-haspopup="dialog"
|
||||
aria-expanded={columnSettingsOpen}
|
||||
>
|
||||
<Columns3Cog size={14} aria-hidden="true" />
|
||||
<Columns3 size={16} aria-hidden="true" />
|
||||
<span>{t('usage.events.columns')}</span>
|
||||
</button>
|
||||
{isCustomized ? (
|
||||
<button
|
||||
type="button"
|
||||
className="usage-col-reset-btn icon-only"
|
||||
className="usage-col-reset-btn"
|
||||
onClick={resetAllWidths}
|
||||
title={t('usage.events.resetColumns')}
|
||||
aria-label={t('usage.events.resetColumns')}
|
||||
>
|
||||
<RotateCcw size={13} aria-hidden="true" />
|
||||
<RotateCcw size={14} aria-hidden="true" />
|
||||
<span>{t('usage.events.resetColumns')}</span>
|
||||
</button>
|
||||
) : null}
|
||||
<button type="button" className="usage-events-export-btn" disabled={loading || exporting || events.total === 0} onClick={() => void exportFilteredEvents()} title={t('usage.events.exportDescription')}>
|
||||
@@ -820,7 +853,7 @@ export function EventsView({
|
||||
>
|
||||
<div className="usage-column-dialog-heading">
|
||||
<div>
|
||||
<Columns3Cog size={19} aria-hidden="true" />
|
||||
<Columns3 size={19} aria-hidden="true" />
|
||||
<h2 id="usage-column-dialog-title">{t('usage.events.columnSettings')}</h2>
|
||||
</div>
|
||||
<button
|
||||
@@ -836,6 +869,40 @@ export function EventsView({
|
||||
<p className="usage-column-dialog-description">
|
||||
{t('usage.events.columnSettingsDescription')}
|
||||
</p>
|
||||
<section className="usage-row-height-settings" aria-labelledby="usage-row-height-title">
|
||||
<div className="usage-row-height-heading">
|
||||
<div>
|
||||
<strong id="usage-row-height-title">{t('usage.events.rowHeight.title')}</strong>
|
||||
<span>{t('usage.events.rowHeight.description')}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className={`usage-settings-switch${fixedRowHeight ? ' active' : ''}`}
|
||||
role="switch"
|
||||
aria-checked={fixedRowHeight}
|
||||
onClick={() => updateFixedRowHeight(!fixedRowHeight)}
|
||||
>
|
||||
<span aria-hidden="true" />
|
||||
<span>{fixedRowHeight ? t('common.enabled') : t('common.disabled')}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className="usage-row-height-control">
|
||||
<label htmlFor="usage-row-height-range">{t('usage.events.rowHeight.fixed')}</label>
|
||||
<input
|
||||
id="usage-row-height-range"
|
||||
type="range"
|
||||
min={MIN_EVENT_ROW_HEIGHT}
|
||||
max={MAX_EVENT_ROW_HEIGHT}
|
||||
step="1"
|
||||
value={rowHeight}
|
||||
disabled={!fixedRowHeight}
|
||||
onChange={(event) => updateRowHeight(Number(event.currentTarget.value))}
|
||||
aria-label={t('usage.events.rowHeight.fixed')}
|
||||
/>
|
||||
<output htmlFor="usage-row-height-range">{rowHeight}px</output>
|
||||
<span className="usage-row-height-hint">{t('usage.events.rowHeight.dragHint')}</span>
|
||||
</div>
|
||||
</section>
|
||||
<div className="usage-column-options">
|
||||
{EVENT_COLUMNS.map((column) => {
|
||||
const checked = draftVisibleColumnKeys.includes(column.key);
|
||||
|
||||
+105
@@ -9622,6 +9622,97 @@ textarea:focus-visible {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.usage-row-height-settings {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin: 0 0 16px;
|
||||
padding: 14px 16px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 10px;
|
||||
background: var(--bg-tertiary);
|
||||
}
|
||||
|
||||
.usage-row-height-heading,
|
||||
.usage-row-height-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.usage-row-height-heading {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.usage-row-height-heading > div {
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.usage-row-height-heading strong,
|
||||
.usage-row-height-control label {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-size-body);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.usage-row-height-heading span,
|
||||
.usage-row-height-hint {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-caption);
|
||||
}
|
||||
|
||||
.usage-settings-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-size-caption);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.usage-settings-switch > span:first-child {
|
||||
position: relative;
|
||||
width: 34px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--border-primary);
|
||||
transition: background .15s ease;
|
||||
}
|
||||
|
||||
.usage-settings-switch > span:first-child::after {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
left: 3px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
content: '';
|
||||
transition: transform .15s ease;
|
||||
}
|
||||
|
||||
.usage-settings-switch.active > span:first-child { background: var(--ui-accent); }
|
||||
.usage-settings-switch.active > span:first-child::after { transform: translateX(14px); }
|
||||
|
||||
.usage-row-height-control {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.usage-row-height-control input[type='range'] {
|
||||
flex: 1 1 180px;
|
||||
accent-color: var(--ui-accent);
|
||||
}
|
||||
|
||||
.usage-row-height-control output {
|
||||
min-width: 42px;
|
||||
color: var(--text-primary);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: var(--font-size-caption);
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.usage-column-options {
|
||||
display: grid;
|
||||
min-height: 0;
|
||||
@@ -10017,6 +10108,20 @@ body.table-col-resizing * {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.usage-row-height-heading {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-row-height-control {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.usage-row-height-control input[type='range'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.usage-column-dialog-footer {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in new issue
Block a user