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) => (