Add sort_by ordering for the log viewer, with a missing-param fallback/drop toggle (ticket 045)

/api/logs now accepts sort_by (an already-declared param name) and
sort_missing_fallback: lines carrying that param are ordered by its value,
while lines missing it are either appended in ottyl_seq order (fallback,
the default) or dropped from the response. Default ottyl_ts-tracking
order is unchanged when sort_by is omitted.

The log viewer (both the inline panel and its popout) exposes this as a
"Sort by" control next to the column picker, persisted to localStorage
under one shared key (ottyl.logPreview.sort) — the same global-viewer-
setting convention already used for visible param columns and the log
viewer's height, since there's no per-dashboard scope for this to divide
by.

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-06 01:58:30 +01:00
co-authored by Claude Sonnet 5
parent 70d98562d1
commit 3d2652e95b
7 changed files with 381 additions and 20 deletions
+33 -7
View File
@@ -9,7 +9,7 @@ import {
type TimeRange,
type TimeRangePresetId,
} from './components/LogTimeline'
import { LogPreview } from './components/LogPreview'
import { LogPreview, loadLogSortSettings, saveLogSortSettings, type LogSortSettings } from './components/LogPreview'
import { Params } from './components/Params'
import { Metrics } from './components/Metrics'
import { Dashboards } from './components/Dashboards'
@@ -260,6 +260,12 @@ function StandaloneLog() {
const [activePreset, setActivePreset] = useState<TimeRangePresetId | null>('all')
const [filter, setFilter] = useState('')
const [filterError, setFilterError] = useState<string | null>(null)
const [sortSettings, setSortSettings] = useState<LogSortSettings>(loadLogSortSettings)
function handleSortSettingsChange(next: LogSortSettings) {
setSortSettings(next)
saveLogSortSettings(next)
}
const rangeRef = useRef(range)
useEffect(() => {
@@ -310,7 +316,14 @@ function StandaloneLog() {
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
filter: filter || undefined,
}),
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
fetchLogs({
startMs: effectiveRange?.startMs,
endMs: effectiveRange?.endMs,
limit: 1000,
filter: filter || undefined,
sortBy: sortSettings.sortBy ?? undefined,
sortMissingFallback: sortSettings.sortBy ? sortSettings.sortMissingFallback : undefined,
}),
])
if (!cancelled) {
setBuckets(nextBuckets)
@@ -331,7 +344,7 @@ function StandaloneLog() {
clearInterval(id)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, activePreset])
}, [filter, activePreset, sortSettings])
return (
<StandalonePage>
@@ -345,7 +358,7 @@ function StandaloneLog() {
activePreset={activePreset}
onSelectPreset={selectPreset}
/>
<LogPreview logs={logs} params={params} />
<LogPreview logs={logs} params={params} sortSettings={sortSettings} onSortSettingsChange={handleSortSettingsChange} />
</div>
</div>
</StandalonePage>
@@ -373,6 +386,12 @@ function FullApp() {
const [filterError, setFilterError] = useState<string | null>(null)
const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer')
const [logViewerHeight, setLogViewerHeight] = useState(() => loadLogViewerHeight())
const [sortSettings, setSortSettings] = useState<LogSortSettings>(loadLogSortSettings)
function handleSortSettingsChange(next: LogSortSettings) {
setSortSettings(next)
saveLogSortSettings(next)
}
function handleLogViewerResize(deltaY: number) {
setLogViewerHeight((current) => clampLogViewerHeight(current + deltaY))
@@ -442,7 +461,14 @@ function FullApp() {
bucketMs: effectiveRange ? computeBucketMs(effectiveRange.endMs - effectiveRange.startMs) : undefined,
filter: filter || undefined,
}),
fetchLogs({ startMs: effectiveRange?.startMs, endMs: effectiveRange?.endMs, limit: 1000, filter: filter || undefined }),
fetchLogs({
startMs: effectiveRange?.startMs,
endMs: effectiveRange?.endMs,
limit: 1000,
filter: filter || undefined,
sortBy: sortSettings.sortBy ?? undefined,
sortMissingFallback: sortSettings.sortBy ? sortSettings.sortMissingFallback : undefined,
}),
])
if (!cancelled) {
setStats(nextStats)
@@ -465,7 +491,7 @@ function FullApp() {
}
// Intentionally excludes `range` to avoid restarting this interval on every rolling-preset tick.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filter, activePreset])
}, [filter, activePreset, sortSettings])
return (
<DocsNavProvider>
@@ -498,7 +524,7 @@ function FullApp() {
activePreset={activePreset}
onSelectPreset={selectPreset}
/>
<LogPreview logs={logs} params={params} />
<LogPreview logs={logs} params={params} sortSettings={sortSettings} onSortSettingsChange={handleSortSettingsChange} />
</div>
{!logViewerCollapsed && <ResizeHandle onResize={handleLogViewerResize} onResizeEnd={handleLogViewerResizeEnd} />}
<Params params={params} onDeclared={loadParams} />