From 78a6672cedb988ac8626523d0bb643ded18e9b3a Mon Sep 17 00:00:00 2001 From: Hugh Kaznowski Date: Sun, 6 Sep 2026 09:34:46 +0100 Subject: [PATCH] Move log sort selector from Log Preview to Timeline header The "Sort by" / "fall back to ottyl_seq" control now lives in LogTimeline's header next to the time-range presets, since the setting actually drives the shared range/log fetch rather than being a Log-Preview-only concern. LogSortSettings and its load/save helpers moved into LogTimeline.tsx alongside SortPicker; the localStorage key was renamed to ottyl.logTimeline.sort, with loadLogSortSettings falling back to the old ottyl.logPreview.sort key so an existing choice isn't lost on first load after the move. LogPreview now just renders logs in whatever order the caller already sorted them. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o --- tickets/053-move-sort-selector-to-timeline.md | 69 ------------ webapp/src/App.tsx | 15 ++- webapp/src/components/LogPreview.tsx | 88 +-------------- webapp/src/components/LogTimeline.tsx | 105 +++++++++++++++++- 4 files changed, 117 insertions(+), 160 deletions(-) delete mode 100644 tickets/053-move-sort-selector-to-timeline.md diff --git a/tickets/053-move-sort-selector-to-timeline.md b/tickets/053-move-sort-selector-to-timeline.md deleted file mode 100644 index 19012a9..0000000 --- a/tickets/053-move-sort-selector-to-timeline.md +++ /dev/null @@ -1,69 +0,0 @@ -# 053 — Move the log sort selector from Log Preview to the Timeline - -## Summary - -The "Sort by" control lives in the wrong component. It's currently -rendered inside `LogPreview`'s own section header -(`webapp/src/components/LogPreview.tsx:327-336`) as a `SortPicker` -(`LogPreview.tsx:68-107`, a ` onChange({ ...settings, sortBy: e.target.value || null })} - className="rounded border border-border-soft bg-surface-0 px-1 py-0.5 text-text" - > - - {paramNames.map((name) => ( - - ))} - - - {settings.sortBy && ( - - )} - - ) -} - function ColumnPicker({ paramNames, visible, @@ -293,14 +212,10 @@ function useScrollAnchor(containerRef: React.RefObject, d export function LogPreview({ logs, params, - sortSettings, - onSortSettingsChange, }: { logs: LogEntry[] + /** Ticket 045/053: already sorted by the caller's `fetchLogs` call per its own sort setting (owned and rendered up in `LogTimeline`'s header now) — this component just renders whatever order it's given. */ params: ParamDeclaration[] - /** Ticket 045: current ordering choice, owned by the caller (it also feeds `fetchLogs`) rather than internally like `visibleParamColumns` — see `App.tsx`'s poll loop. */ - sortSettings: LogSortSettings - onSortSettingsChange: (next: LogSortSettings) => void }) { const paramNames = params.map((d) => d.name) const [visibleParamColumns, setVisibleParamColumns] = useState(loadVisibleParamColumns) @@ -330,7 +245,6 @@ export function LogPreview({
-
diff --git a/webapp/src/components/LogTimeline.tsx b/webapp/src/components/LogTimeline.tsx index 8d95414..2b6e4fa 100644 --- a/webapp/src/components/LogTimeline.tsx +++ b/webapp/src/components/LogTimeline.tsx @@ -41,18 +41,116 @@ function formatTimestamp(ms: number): string { return new Date(ms).toLocaleTimeString() } +// Ticket 053: moved here from LogPreview.tsx, along with the `SortPicker` control below — the +// setting now renders in the Timeline's header, but it still drives `fetchLogs` up in App.tsx the +// same way it always did. Renamed from the old `ottyl.logPreview.sort` key since the control no +// longer lives in "log preview"; `loadLogSortSettings` still falls back to the old key so an +// existing choice survives this move instead of silently resetting on first load. +const LOG_SORT_STORAGE_KEY = 'ottyl.logTimeline.sort' +const LEGACY_LOG_SORT_STORAGE_KEY = 'ottyl.logPreview.sort' + +export interface LogSortSettings { + /** An already-declared param name to order log lines by, or `null` for the default (`ottyl_ts`-tracking) order. */ + sortBy: string | null + /** Only meaningful when `sortBy` is set. `true` (default) orders lines missing that param by `ottyl_seq`, appended after the sorted lines; `false` drops them from view. */ + sortMissingFallback: boolean +} + +const DEFAULT_LOG_SORT_SETTINGS: LogSortSettings = { sortBy: null, sortMissingFallback: true } + +function parseLogSortSettings(raw: string | null): LogSortSettings | null { + if (!raw) return null + try { + const parsed = JSON.parse(raw) + if (!parsed || typeof parsed !== 'object') return null + const sortBy = typeof parsed.sortBy === 'string' ? parsed.sortBy : null + const sortMissingFallback = typeof parsed.sortMissingFallback === 'boolean' ? parsed.sortMissingFallback : true + return { sortBy, sortMissingFallback } + } catch { + return null + } +} + +export function loadLogSortSettings(): LogSortSettings { + try { + return ( + parseLogSortSettings(localStorage.getItem(LOG_SORT_STORAGE_KEY)) ?? + parseLogSortSettings(localStorage.getItem(LEGACY_LOG_SORT_STORAGE_KEY)) ?? + DEFAULT_LOG_SORT_SETTINGS + ) + } catch { + return DEFAULT_LOG_SORT_SETTINGS + } +} + +export function saveLogSortSettings(settings: LogSortSettings) { + try { + localStorage.setItem(LOG_SORT_STORAGE_KEY, JSON.stringify(settings)) + } catch { + // best-effort — private browsing or full storage just means the selection won't persist across reloads + } +} + +function SortPicker({ + paramNames, + settings, + onChange, +}: { + paramNames: string[] + settings: LogSortSettings + onChange: (next: LogSortSettings) => void +}) { + return ( +
+ + {settings.sortBy && ( + + )} +
+ ) +} + export function LogTimeline({ buckets, range, onSelectRange, activePreset, onSelectPreset, + paramNames, + sortSettings, + onSortSettingsChange, }: { buckets: TimelineBucket[] range: TimeRange | null onSelectRange: (range: TimeRange | null) => void activePreset: TimeRangePresetId | null onSelectPreset: (id: TimeRangePresetId) => void + /** Ticket 045/053: current ordering choice, owned by the caller (it also feeds `fetchLogs`). Omitted by callers with no log list to sort (e.g. the standalone metrics/dashboards popout). */ + paramNames?: string[] + sortSettings?: LogSortSettings + onSortSettingsChange?: (next: LogSortSettings) => void }) { const containerRef = useRef(null) const [dragStartX, setDragStartX] = useState(null) @@ -122,7 +220,11 @@ export function LogTimeline({ Timeline -
+
+ {sortSettings && onSortSettingsChange && ( + + )} +
{TIME_RANGE_PRESETS.map((preset) => (