Skip to content

Commit 4efd8e5

Browse files
Network tab filtering, basic search functionality (#19)
* implement basic search functionality * fix types * remove log * Style fix * Update app/components/AppHeader.tsx Co-authored-by: Sean Barker <43788519+SeanBarker182@users.noreply.github.com> Signed-off-by: Steven Conner <steven.c.conner@gmail.com> * integrate memoization --------- Signed-off-by: Steven Conner <steven.c.conner@gmail.com> Co-authored-by: Sean Barker <theseanbarker@gmail.com> Co-authored-by: Sean Barker <43788519+SeanBarker182@users.noreply.github.com>
1 parent 0aecd60 commit 4efd8e5

File tree

6 files changed

+106
-23
lines changed

6 files changed

+106
-23
lines changed

app/components/AppHeader.tsx

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export function AppHeader() {
2020
const [isConnected] = useGlobal("isConnected", false)
2121
const [error] = useGlobal("error", null)
2222
const [clientIds] = useGlobal("clientIds", [])
23+
const [search, setSearch] = useGlobal("search", "")
2324
const arch = (global as any)?.nativeFabricUIManager ? "Fabric" : "Paper"
2425

2526
return (
@@ -31,9 +32,14 @@ export function AppHeader() {
3132
</View>
3233
<HeaderTitle title={`Reactotron ${reactotronAppId}`} />
3334
<View style={$statusRow()}>
34-
<View>
35-
<TextInput value={"SEARCHING"} placeholder="Search" style={$searchInput} />
36-
<Text>Search</Text>
35+
<View style={$searchContainer()}>
36+
<TextInput
37+
value={search}
38+
placeholder="Search"
39+
style={$searchInput()}
40+
placeholderTextColor={theme === "dark" ? "white" : "black"}
41+
onChangeText={setSearch}
42+
/>
3743
</View>
3844
<View style={$statusItem()}>
3945
<View style={[$dot(), error ? $dotRed() : isConnected ? $dotGreen() : $dotGray()]} />
@@ -103,4 +109,16 @@ const $statusText = themed<TextStyle>(({ colors }) => ({
103109
fontWeight: "600",
104110
}))
105111

106-
const $searchInput: ViewStyle = { width: 100 }
112+
const $searchInput = themed<TextStyle>(({ colors, typography, spacing }) => ({
113+
width: 140,
114+
fontSize: typography.body,
115+
backgroundColor: colors.background,
116+
borderWidth: 1,
117+
borderRadius: 4,
118+
padding: spacing.xxs,
119+
zIndex: 1,
120+
}))
121+
122+
const $searchContainer = themed<ViewStyle>(({ spacing }) => ({
123+
marginRight: spacing.md,
124+
}))

app/components/Sidebar/SidebarMenu.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const MENU_ITEMS = [
1010
{ id: "plugins", label: "Plugins", icon: "plug" },
1111
] as const
1212

13-
type MenuItemId = (typeof MENU_ITEMS)[number]["id"]
13+
export type MenuItemId = (typeof MENU_ITEMS)[number]["id"]
1414

1515
interface SidebarMenuProps {
1616
progress: Animated.Value
@@ -34,7 +34,7 @@ export const SidebarMenu = ({ progress, mounted, collapsedWidth }: SidebarMenuPr
3434
* - centers the icon when collapsed without animating padding
3535
* - becomes the left column when expanded (outer padding forms the gutter)
3636
*/
37-
const iconColumnWidth = collapsedWidth - theme.spacing.sm * 2
37+
const iconColumnWidth = collapsedWidth - theme.spacing.sm * 2.8
3838

3939
return (
4040
<View style={$menu}>
@@ -47,15 +47,14 @@ export const SidebarMenu = ({ progress, mounted, collapsedWidth }: SidebarMenuPr
4747
$menuItem(),
4848
active && $menuItemActive(),
4949
pressed && $menuItemPressed,
50-
$menuItemLabel(),
5150
]}
5251
onPress={() => setActiveItem(item.id)}
5352
accessibilityRole="button"
5453
accessibilityState={{ selected: active }}
5554
accessibilityLabel={item.label}
5655
>
5756
{/* Fixed-width icon column (centers icon when collapsed) */}
58-
<View style={[{ width: iconColumnWidth }, $iconColumn]}>
57+
<View style={[{ width: iconColumnWidth }, $iconColumn()]}>
5958
<Icon
6059
icon={item.icon}
6160
size={18}
@@ -111,16 +110,13 @@ const $menuItemText = themed<TextStyle>((theme) => ({
111110
fontSize: theme.typography.caption,
112111
color: theme.colors.mainText,
113112
fontWeight: "700",
113+
marginLeft: theme.spacing.xs,
114114
}))
115115

116116
const $menuItemTextActive = themed<TextStyle>((theme) => ({
117117
color: theme.colors.mainTextInverted,
118118
}))
119119

120-
const $iconColumn: ViewStyle = {
121-
alignItems: "center",
122-
}
123-
124-
const $menuItemLabel = themed<TextStyle>(({ spacing }) => ({
120+
const $iconColumn = themed<ViewStyle>(({ spacing }) => ({
125121
marginLeft: spacing.xs,
126122
}))

app/screens/TimelineScreen.tsx

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import { Separator } from "../components/Separator"
1212
import { themed, useThemeName } from "../theme/theme"
1313
import { $flex, $row } from "../theme/basics"
1414
import { useTimeline } from "../utils/useTimeline"
15+
import { MenuItemId } from "app/components/Sidebar/SidebarMenu"
16+
import { useEffect } from "react"
17+
import { FilterType } from "app/components/TimelineToolbar"
1518

1619
/**
1720
* Renders the correct component for each timeline item.
@@ -46,13 +49,31 @@ const TimelineItemRenderer = ({
4649
return null
4750
}
4851

52+
function getTimelineTypes(activeItem: MenuItemId): FilterType[] {
53+
switch (activeItem) {
54+
case "logs":
55+
return ["log", "display"]
56+
case "network":
57+
return ["api.request", "api.response"]
58+
default:
59+
return ["log", "display", "api.request", "api.response"]
60+
}
61+
}
62+
4963
export function TimelineScreen() {
50-
// TODO: Use a global state for the filters, set by the user in the TimelineToolbar
51-
const timelineItems = useTimeline({ types: ["log", "display", "api.request", "api.response"] })
64+
const [activeItem] = useGlobal<MenuItemId>("sidebar-active-item", "logs", {
65+
persist: true,
66+
})
67+
const timelineItems = useTimeline({
68+
types: getTimelineTypes(activeItem),
69+
})
5270
const [timelineWidth, setTimelineWidth] = useGlobal<number>("timelineWidth", 300, {
5371
persist: true,
5472
})
5573
const { selectedItem, setSelectedItemId } = useSelectedTimelineItems()
74+
useEffect(() => {
75+
setSelectedItemId(null)
76+
}, [activeItem])
5677

5778
const handleSelectItem = (item: TimelineItem) => {
5879
// Toggle selection: if clicking the same item, deselect it

app/utils/normalize.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function normalize(v: unknown): string {
2+
return String(v ?? "")
3+
.toLowerCase()
4+
.trim()
5+
.normalize("NFD")
6+
.replace(/[\u0300-\u036f]/g, "")
7+
}

app/utils/safeTime.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export function safeTime(d: unknown): number {
2+
const t = new Date(String(d ?? "")).getTime()
3+
return Number.isFinite(t) ? t : 0
4+
}

app/utils/useTimeline.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,57 @@
11
import { TimelineItem } from "../types"
22
import { TimelineFilters } from "../components/TimelineToolbar"
33
import { useGlobal } from "../state/useGlobal"
4+
import { useMemo } from "react"
5+
import { normalize } from "./normalize"
6+
import { safeTime } from "./safeTime"
47

58
export function useTimeline(filters: TimelineFilters): TimelineItem[] {
69
const [items] = useGlobal<TimelineItem[]>("timelineItems", [], { persist: true })
10+
const [search] = useGlobal("search", "")
711

8-
const filteredItems = items.filter((item) => {
9-
// if there are any filters selected, only show items that match the filters
10-
return filters.types.includes(item.type)
11-
})
12+
return useMemo(() => {
13+
// 1) Types filter: if none selected, show everything
14+
const byType =
15+
(filters.types?.length ?? 0) === 0
16+
? items.slice() // clone so we can sort safely later
17+
: items.filter((item) => filters.types.includes(item.type))
1218

13-
filteredItems.sort((a, b) => {
14-
return new Date(b.date).getTime() - new Date(a.date).getTime()
15-
})
19+
// 2) Search (normalize once)
20+
const q = normalize(search)
21+
const visited = new WeakSet<object>()
22+
const matches = (val: unknown): boolean => {
23+
if (val === null || val === undefined) return false
1624

17-
return filteredItems
25+
if (typeof val === "string" || typeof val === "number" || typeof val === "boolean") {
26+
return normalize(val).includes(q)
27+
}
28+
if (val instanceof Date) {
29+
return normalize(val.toISOString()).includes(q)
30+
}
31+
if (Array.isArray(val)) {
32+
for (const v of val) if (matches(v)) return true
33+
return false
34+
}
35+
if (typeof val === "object") {
36+
const obj = val as Record<string, unknown>
37+
if (visited.has(obj)) return false
38+
visited.add(obj)
39+
for (const k in obj) {
40+
if (matches(obj[k])) return true
41+
}
42+
return false
43+
}
44+
45+
return false
46+
}
47+
48+
const bySearch = q ? byType.filter((item) => matches(item)) : byType
49+
50+
// 3) Sort newest first, safely handling bad dates
51+
bySearch.sort((a, b) => safeTime(b.date) - safeTime(a.date))
52+
53+
return bySearch
54+
}, [items, JSON.stringify(filters.types ?? []), search])
1855

1956
// TODO: User controlled sorting and level filtering
2057

0 commit comments

Comments
 (0)