forked from boos/BoosAPI
persist proxy trace panel state (#331)
This commit is contained in:
@@ -97,6 +97,7 @@ function buildListResponse(overrides?: Partial<{
|
|||||||
describe('ProxyLogs server-driven page', () => {
|
describe('ProxyLogs server-driven page', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
const localStorageState = new Map<string, string>();
|
||||||
Object.defineProperty(globalThis, 'navigator', {
|
Object.defineProperty(globalThis, 'navigator', {
|
||||||
value: {
|
value: {
|
||||||
clipboard: {
|
clipboard: {
|
||||||
@@ -106,6 +107,22 @@ describe('ProxyLogs server-driven page', () => {
|
|||||||
configurable: true,
|
configurable: true,
|
||||||
writable: true,
|
writable: true,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(globalThis, 'localStorage', {
|
||||||
|
value: {
|
||||||
|
getItem: vi.fn((key: string) => (localStorageState.has(key) ? localStorageState.get(key)! : null)),
|
||||||
|
setItem: vi.fn((key: string, value: string) => {
|
||||||
|
localStorageState.set(String(key), String(value));
|
||||||
|
}),
|
||||||
|
removeItem: vi.fn((key: string) => {
|
||||||
|
localStorageState.delete(String(key));
|
||||||
|
}),
|
||||||
|
clear: vi.fn(() => {
|
||||||
|
localStorageState.clear();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
configurable: true,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
apiMock.getSites.mockResolvedValue([
|
apiMock.getSites.mockResolvedValue([
|
||||||
{ id: 9, name: 'main-site', status: 'active' },
|
{ id: 9, name: 'main-site', status: 'active' },
|
||||||
{ id: 12, name: 'backup-site', status: 'active' },
|
{ id: 12, name: 'backup-site', status: 'active' },
|
||||||
@@ -458,6 +475,67 @@ describe('ProxyLogs server-driven page', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('remembers the collapsed debug trace panel state across remounts', async () => {
|
||||||
|
let root!: WebTestRenderer;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/logs']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProxyLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const toggleButton = root.root.find((node) => (
|
||||||
|
node.type === 'button'
|
||||||
|
&& typeof node.props.onClick === 'function'
|
||||||
|
&& node.props['data-debug-trace-panel-toggle'] === true
|
||||||
|
));
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
toggleButton.props.onClick();
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
expect(globalThis.localStorage.setItem).toHaveBeenCalledWith('metapi.proxyLogs.debugTracePanelExpanded', 'false');
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root.unmount();
|
||||||
|
});
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
root = create(
|
||||||
|
<MemoryRouter initialEntries={['/logs']}>
|
||||||
|
<ToastProvider>
|
||||||
|
<ProxyLogs />
|
||||||
|
</ToastProvider>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
await flushMicrotasks();
|
||||||
|
|
||||||
|
const restoredToggleButton = root.root.find((node) => (
|
||||||
|
node.type === 'button'
|
||||||
|
&& typeof node.props.onClick === 'function'
|
||||||
|
&& node.props['data-debug-trace-panel-toggle'] === true
|
||||||
|
));
|
||||||
|
const restoredPanelBody = root.root.find((node) => (
|
||||||
|
node.type === 'div'
|
||||||
|
&& node.props['data-debug-trace-panel-body'] === true
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(globalThis.localStorage.getItem).toHaveBeenCalledWith('metapi.proxyLogs.debugTracePanelExpanded');
|
||||||
|
expect(restoredToggleButton.props['aria-expanded']).toBe(false);
|
||||||
|
expect(String(restoredPanelBody.props.className || '')).not.toContain('is-open');
|
||||||
|
} finally {
|
||||||
|
root?.unmount();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('opens debug trace detail on demand instead of preloading the first trace inline', async () => {
|
it('opens debug trace detail on demand instead of preloading the first trace inline', async () => {
|
||||||
let root!: WebTestRenderer;
|
let root!: WebTestRenderer;
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ const PAGE_SIZES = [20, 50, 100];
|
|||||||
const DEFAULT_PAGE_SIZE = 50;
|
const DEFAULT_PAGE_SIZE = 50;
|
||||||
const TRACE_TABLE_LIMIT = 20;
|
const TRACE_TABLE_LIMIT = 20;
|
||||||
const DEBUG_TRACE_PAGE_SIZE = 5;
|
const DEBUG_TRACE_PAGE_SIZE = 5;
|
||||||
|
const PROXY_LOGS_DEBUG_TRACE_PANEL_STORAGE_KEY = 'metapi.proxyLogs.debugTracePanelExpanded';
|
||||||
const PROXY_LOG_CLIENT_FAMILY_LABELS: Record<string, string> = {
|
const PROXY_LOG_CLIENT_FAMILY_LABELS: Record<string, string> = {
|
||||||
codex: 'Codex',
|
codex: 'Codex',
|
||||||
claude_code: 'Claude Code',
|
claude_code: 'Claude Code',
|
||||||
@@ -596,6 +597,24 @@ function CompactSummaryMetric({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readStoredDebugTracePanelExpanded(): boolean {
|
||||||
|
try {
|
||||||
|
const stored = globalThis.localStorage?.getItem(PROXY_LOGS_DEBUG_TRACE_PANEL_STORAGE_KEY);
|
||||||
|
if (stored == null) return true;
|
||||||
|
return stored !== 'false';
|
||||||
|
} catch {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function persistDebugTracePanelExpanded(expanded: boolean) {
|
||||||
|
try {
|
||||||
|
globalThis.localStorage?.setItem(PROXY_LOGS_DEBUG_TRACE_PANEL_STORAGE_KEY, expanded ? 'true' : 'false');
|
||||||
|
} catch {
|
||||||
|
// Ignore storage write failures and keep UI responsive.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProxyLogs() {
|
export default function ProxyLogs() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -622,7 +641,7 @@ export default function ProxyLogs() {
|
|||||||
const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false);
|
const [showDebugSettingsModal, setShowDebugSettingsModal] = useState(false);
|
||||||
const [debugPanelLoading, setDebugPanelLoading] = useState(false);
|
const [debugPanelLoading, setDebugPanelLoading] = useState(false);
|
||||||
const [debugPanelSaving, setDebugPanelSaving] = useState(false);
|
const [debugPanelSaving, setDebugPanelSaving] = useState(false);
|
||||||
const [debugTracePanelExpanded, setDebugTracePanelExpanded] = useState(true);
|
const [debugTracePanelExpanded, setDebugTracePanelExpanded] = useState(() => readStoredDebugTracePanelExpanded());
|
||||||
const [debugSettings, setDebugSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
const [debugSettings, setDebugSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
||||||
const [debugDraftSettings, setDebugDraftSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
const [debugDraftSettings, setDebugDraftSettings] = useState<ProxyDebugSettingsState>(DEFAULT_PROXY_DEBUG_SETTINGS);
|
||||||
const [debugTraces, setDebugTraces] = useState<ProxyDebugTraceListItem[]>([]);
|
const [debugTraces, setDebugTraces] = useState<ProxyDebugTraceListItem[]>([]);
|
||||||
@@ -986,6 +1005,10 @@ export default function ProxyLogs() {
|
|||||||
return () => clearInterval(timer);
|
return () => clearInterval(timer);
|
||||||
}, [debugSettings.proxyDebugTraceEnabled, loadDebugTraceList]);
|
}, [debugSettings.proxyDebugTraceEnabled, loadDebugTraceList]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
persistDebugTracePanelExpanded(debugTracePanelExpanded);
|
||||||
|
}, [debugTracePanelExpanded]);
|
||||||
|
|
||||||
const persistDebugSettings = useCallback(async (
|
const persistDebugSettings = useCallback(async (
|
||||||
nextSettings: ProxyDebugSettingsState,
|
nextSettings: ProxyDebugSettingsState,
|
||||||
options?: { successMessage?: string; closeAfterSave?: boolean },
|
options?: { successMessage?: string; closeAfterSave?: boolean },
|
||||||
|
|||||||
Reference in New Issue
Block a user