Skip to content

Commit 196ad54

Browse files
committed
fix(ui): persist call flow filter prefs across tabs
Remember host grouping and display toggles in localStorage (Homer 7 parity) so switching transaction tabs does not reset Group by alias.
1 parent 8eab482 commit 196ad54

3 files changed

Lines changed: 149 additions & 25 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { describe, expect, it, beforeEach } from 'vitest'
2+
import {
3+
FLOW_FILTER_PREFS_LS_KEY,
4+
initialFlowFilters,
5+
loadStoredFlowPrefs,
6+
saveStoredFlowPrefs,
7+
} from './flowFilterPrefs'
8+
import { DEFAULT_FILTERS } from './flowFilterPrefs'
9+
10+
describe('flowFilterPrefs', () => {
11+
beforeEach(() => {
12+
localStorage.clear()
13+
})
14+
15+
it('round-trips host grouping and toggles', () => {
16+
saveStoredFlowPrefs({
17+
...DEFAULT_FILTERS,
18+
hostGrouping: 'group-by-alias',
19+
isSimplify: true,
20+
isAbsoluteTime: true,
21+
isHighContrast: true,
22+
})
23+
expect(loadStoredFlowPrefs()).toEqual({
24+
hostGrouping: 'group-by-alias',
25+
isSimplify: true,
26+
isAbsoluteTime: true,
27+
isHighContrast: true,
28+
})
29+
})
30+
31+
it('ignores invalid stored host grouping', () => {
32+
localStorage.setItem(
33+
FLOW_FILTER_PREFS_LS_KEY,
34+
JSON.stringify({ hostGrouping: 'invalid', isSimplify: true }),
35+
)
36+
expect(loadStoredFlowPrefs()).toEqual({ isSimplify: true })
37+
})
38+
39+
it('initialFlowFilters merges stored prefs with empty exclusion sets', () => {
40+
localStorage.setItem(
41+
FLOW_FILTER_PREFS_LS_KEY,
42+
JSON.stringify({ hostGrouping: 'group-by-ip' }),
43+
)
44+
const f = initialFlowFilters()
45+
expect(f.hostGrouping).toBe('group-by-ip')
46+
expect(f.ipExcluded.size).toBe(0)
47+
expect(f.methodExcluded.size).toBe(0)
48+
})
49+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import type { HostGrouping } from './flow-data'
2+
3+
export const FLOW_FILTER_PREFS_LS_KEY = 'homer_callflow_prefs'
4+
5+
const HOST_GROUPINGS: HostGrouping[] = ['ungrouped', 'group-by-ip', 'group-by-alias']
6+
7+
export interface FlowFilters {
8+
isSimplify: boolean
9+
isAbsoluteTime: boolean
10+
isHighContrast: boolean
11+
hostGrouping: HostGrouping
12+
ipExcluded: Set<string>
13+
methodExcluded: Set<string>
14+
payloadTypeExcluded: Set<string>
15+
callIdExcluded: Set<string>
16+
}
17+
18+
export const DEFAULT_FILTERS: FlowFilters = {
19+
isSimplify: false,
20+
isAbsoluteTime: false,
21+
isHighContrast: false,
22+
hostGrouping: 'ungrouped',
23+
ipExcluded: new Set(),
24+
methodExcluded: new Set(),
25+
payloadTypeExcluded: new Set(),
26+
callIdExcluded: new Set(),
27+
}
28+
29+
export interface StoredFlowPrefs {
30+
hostGrouping?: HostGrouping
31+
isSimplify?: boolean
32+
isAbsoluteTime?: boolean
33+
isHighContrast?: boolean
34+
}
35+
36+
export function isHostGrouping(value: unknown): value is HostGrouping {
37+
return typeof value === 'string' && (HOST_GROUPINGS as string[]).includes(value)
38+
}
39+
40+
export function loadStoredFlowPrefs(): StoredFlowPrefs {
41+
if (typeof localStorage === 'undefined') return {}
42+
try {
43+
const raw = localStorage.getItem(FLOW_FILTER_PREFS_LS_KEY)
44+
if (!raw) return {}
45+
const parsed = JSON.parse(raw) as StoredFlowPrefs
46+
if (!parsed || typeof parsed !== 'object') return {}
47+
const out: StoredFlowPrefs = {}
48+
if (isHostGrouping(parsed.hostGrouping)) out.hostGrouping = parsed.hostGrouping
49+
if (typeof parsed.isSimplify === 'boolean') out.isSimplify = parsed.isSimplify
50+
if (typeof parsed.isAbsoluteTime === 'boolean') out.isAbsoluteTime = parsed.isAbsoluteTime
51+
if (typeof parsed.isHighContrast === 'boolean') out.isHighContrast = parsed.isHighContrast
52+
return out
53+
} catch {
54+
return {}
55+
}
56+
}
57+
58+
export function saveStoredFlowPrefs(filters: FlowFilters): void {
59+
if (typeof localStorage === 'undefined') return
60+
const payload: StoredFlowPrefs = {
61+
hostGrouping: filters.hostGrouping,
62+
isSimplify: filters.isSimplify,
63+
isAbsoluteTime: filters.isAbsoluteTime,
64+
isHighContrast: filters.isHighContrast,
65+
}
66+
try {
67+
localStorage.setItem(FLOW_FILTER_PREFS_LS_KEY, JSON.stringify(payload))
68+
} catch {
69+
/* ignore quota / private mode */
70+
}
71+
}
72+
73+
/** UI prefs from localStorage + fresh per-call exclusion sets. */
74+
export function initialFlowFilters(): FlowFilters {
75+
const stored = loadStoredFlowPrefs()
76+
return {
77+
...DEFAULT_FILTERS,
78+
...stored,
79+
ipExcluded: new Set(),
80+
methodExcluded: new Set(),
81+
payloadTypeExcluded: new Set(),
82+
callIdExcluded: new Set(),
83+
}
84+
}

src/ui/src/dashboard/flow/useFlowFilters.ts

Lines changed: 16 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,21 @@
1-
import { useMemo, useState } from 'react'
2-
import type { HostGrouping, RawMessage } from './flow-data'
1+
import { useEffect, useMemo, useState } from 'react'
2+
import type { RawMessage } from './flow-data'
33
import { payloadTypeOf } from './flow-data'
4+
import {
5+
DEFAULT_FILTERS,
6+
initialFlowFilters,
7+
saveStoredFlowPrefs,
8+
type FlowFilters,
9+
} from './flowFilterPrefs'
10+
11+
export type { FlowFilters } from './flowFilterPrefs'
12+
export { DEFAULT_FILTERS } from './flowFilterPrefs'
413

514
export interface FilterToken {
615
value: string
716
selected: boolean
817
}
918

10-
export interface FlowFilters {
11-
isSimplify: boolean
12-
isAbsoluteTime: boolean
13-
isHighContrast: boolean
14-
hostGrouping: HostGrouping
15-
ipExcluded: Set<string>
16-
methodExcluded: Set<string>
17-
payloadTypeExcluded: Set<string>
18-
callIdExcluded: Set<string>
19-
}
20-
21-
export const DEFAULT_FILTERS: FlowFilters = {
22-
isSimplify: false,
23-
isAbsoluteTime: false,
24-
isHighContrast: false,
25-
hostGrouping: 'ungrouped',
26-
ipExcluded: new Set(),
27-
methodExcluded: new Set(),
28-
payloadTypeExcluded: new Set(),
29-
callIdExcluded: new Set(),
30-
}
31-
3219
function collectUnique(items: RawMessage[], picker: (m: RawMessage) => string[]): string[] {
3320
const set = new Set<string>()
3421
items.forEach((m) => {
@@ -65,7 +52,11 @@ export interface UseFlowFiltersResult {
6552
}
6653

6754
export function useFlowFilters(items: RawMessage[] | null | undefined): UseFlowFiltersResult {
68-
const [filters, setFilters] = useState<FlowFilters>(DEFAULT_FILTERS)
55+
const [filters, setFilters] = useState<FlowFilters>(initialFlowFilters)
56+
57+
useEffect(() => {
58+
saveStoredFlowPrefs(filters)
59+
}, [filters.hostGrouping, filters.isSimplify, filters.isAbsoluteTime, filters.isHighContrast])
6960

7061
const { filterIP, filterMethod, filterPayloadType, filterCallId, filteredItems } = useMemo(() => {
7162
const safe = items ?? []

0 commit comments

Comments
 (0)