Merge ticket/024-popout-tab-independent-rolling-range-and-controls

Resolves conflicts with tickets 006 (header-click collapse) and 009
(all-dashboards print view) in CollapsibleSection.tsx, Dashboards.tsx,
and Metrics.tsx: dropped a spurious onToggle addition from the
worktree's stale base that duplicated already-merged header-collapse
behavior, keeping only the activePreset plumbing; wired activePreset
through Dashboards' non-empty-state header too, which the source
branch (based on a pre-print-view commit) never had to touch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 19:44:28 +01:00
co-authored by Claude Sonnet 5
5 changed files with 136 additions and 39 deletions
+19 -8
View File
@@ -1,5 +1,5 @@
import { useState } from 'react'
import type { TimeRange } from './LogTimeline'
import type { TimeRange, TimeRangePresetId } from './LogTimeline'
/// Shared look for a primary section's title bar: shaded so it reads as a
/// distinct band rather than blending into the section body below it.
@@ -98,23 +98,34 @@ export function handleSectionHeaderKeyDown(onToggle: () => void) {
/// The four primary sections poppable into their own tab; matches the `?section=` query param a standalone page reads.
export type PopOutSectionId = 'log' | 'params' | 'metrics' | 'dashboards'
function popOutUrl(section: PopOutSectionId, range?: TimeRange | null): string {
/// A rolling preset (anything but a manually dragged/fixed range) is carried over by id so the popped-out tab can resolve and keep re-resolving its own independent window of the same size, instead of freezing whatever absolute bounds happened to be active at click time. A fixed/dragged range has no such id, so it's carried over as absolute bounds, same as before.
function popOutUrl(section: PopOutSectionId, options?: { range?: TimeRange | null; presetId?: TimeRangePresetId | null }): string {
const query = new URLSearchParams({ section })
if (range) {
query.set('startMs', String(range.startMs))
query.set('endMs', String(range.endMs))
if (options?.presetId != null) {
query.set('preset', options.presetId)
} else if (options?.range) {
query.set('startMs', String(options.range.startMs))
query.set('endMs', String(options.range.endMs))
}
return `?${query.toString()}`
}
/// `range` is only needed for sections without their own LogTimeline — the popped-out page can't pick a range itself, so the current one is carried over via query params.
export function PopOutButton({ section, range }: { section: PopOutSectionId; range?: TimeRange | null }) {
/// `range`/`activePreset` are only needed for sections without their own LogTimeline — the popped-out page can't pick a range itself until it mounts, so the current one is carried over via query params.
export function PopOutButton({
section,
range,
activePreset,
}: {
section: PopOutSectionId
range?: TimeRange | null
activePreset?: TimeRangePresetId | null
}) {
return (
<button
type="button"
onClick={(e) => {
e.stopPropagation()
window.open(popOutUrl(section, range), '_blank')
window.open(popOutUrl(section, { range, presetId: activePreset }), '_blank')
}}
title="Open in new tab"
aria-label="Open in new tab"