Skip to content

Commit e68bb60

Browse files
Adding overview/leader schedule pages state to url search params for deep linking
1 parent 7d18591 commit e68bb60

19 files changed

+946
-685
lines changed

package-lock.json

+721-546
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+6-5
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"@radix-ui/react-icons": "^1.3.0",
2222
"@radix-ui/themes": "^3.1.3",
2323
"@react-spring/web": "^9.7.4",
24-
"@tanstack/react-router": "^1.58.9",
24+
"@tanstack/react-router": "^1.114.25",
25+
"@tanstack/zod-adapter": "^1.114.23",
2526
"@types/byte-size": "^8.1.2",
2627
"@types/hammerjs": "^2.0.46",
2728
"@types/lodash": "^4.17.9",
@@ -50,8 +51,8 @@
5051
},
5152
"devDependencies": {
5253
"@eslint/js": "^9.11.1",
53-
"@tanstack/router-devtools": "^1.58.9",
54-
"@tanstack/router-vite-plugin": "^1.58.4",
54+
"@tanstack/router-devtools": "^1.114.25",
55+
"@tanstack/router-vite-plugin": "^1.114.25",
5556
"@types/node": "^22.7.0",
5657
"@types/react": "^18.3.9",
5758
"@types/react-dom": "^18.3.0",
@@ -63,8 +64,8 @@
6364
"eslint-plugin-react-refresh": "^0.4.12",
6465
"optionator": "^0.9.4",
6566
"rollup-plugin-license": "^3.5.3",
66-
"typescript": "^5.6.2",
67-
"typescript-eslint": "^8.7.0",
67+
"typescript": "^5.8.2",
68+
"typescript-eslint": "^8.26.1",
6869
"typescript-plugin-css-modules": "^5.1.0",
6970
"vite": "^5.4.8",
7071
"vite-plugin-checker": "^0.8.0"

src/App.tsx

+2-19
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,11 @@ import { createRouter, RouterProvider } from "@tanstack/react-router";
44
import "./app.css";
55
import { routeTree } from "./routeTree.gen";
66
import { ConnectionProvider } from "./api/ws/ConnectionContext";
7-
import { getDefaultStore, useSetAtom } from "jotai";
8-
import { containerElAtom, slotOverrideAtom } from "./atoms";
9-
import { selectedSlotStrAtom } from "./features/Overview/SlotPerformance/atoms";
10-
import {
11-
searchLeaderSlotsAtom,
12-
SearchType,
13-
searchTypeAtom,
14-
} from "./features/LeaderSchedule/atoms";
15-
16-
const store = getDefaultStore();
7+
import { useSetAtom } from "jotai";
8+
import { containerElAtom } from "./atoms";
179

1810
const router = createRouter({ routeTree });
1911

20-
router.subscribe("onBeforeLoad", (_) => {
21-
store.set(slotOverrideAtom, undefined);
22-
23-
store.set(selectedSlotStrAtom, undefined);
24-
25-
store.set(searchTypeAtom, SearchType.Text);
26-
store.set(searchLeaderSlotsAtom, undefined);
27-
});
28-
2912
// Register the router instance for type safety
3013
declare module "@tanstack/react-router" {
3114
interface Register {

src/atoms.ts

-14
Original file line numberDiff line numberDiff line change
@@ -40,19 +40,6 @@ export const epochAtom = atom(
4040
(get, set, epoch: Epoch) => {
4141
set(_epochsAtom, (draft) => {
4242
draft.push(epoch);
43-
// if (draft.length > 2) {
44-
// const removed = draft.splice(0, draft.length - 2);
45-
// for (const removedEpoch of removed) {
46-
// for (
47-
// let i = removedEpoch.start_slot;
48-
// i <= removedEpoch.end_slot;
49-
// i++
50-
// ) {
51-
// set(deleteSlotStatusAtom, i);
52-
// set(deleteSlotResponseAtom, i);
53-
// }
54-
// }
55-
// }
5643
});
5744
}
5845
);
@@ -207,7 +194,6 @@ export const deleteSlotResponseBoundsAtom = atom(null, (get, set) => {
207194
(numberVal < cacheSlotMin || numberVal > cacheSlotMax)
208195
) {
209196
delete draft[numberVal];
210-
// set(deleteHasQueryedAtom, numberVal);
211197
}
212198
}
213199
});

src/features/Header/util.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function getClusterColor(cluster?: Cluster) {
1515
case "pythtest":
1616
return "#E71C88";
1717
case "unknown":
18-
default:
18+
case undefined:
1919
return "#898989";
2020
}
2121
}

src/features/LeaderSchedule/Search.tsx

+68-48
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,9 @@ import {
88
Reset,
99
Tooltip,
1010
} from "@radix-ui/themes";
11-
import { atom, useAtom, useAtomValue, useSetAtom } from "jotai";
12-
import {
13-
setSearchLeaderSlotsAtom,
14-
SearchType,
15-
searchTypeAtom,
16-
searchLeaderSlotsAtom,
17-
} from "./atoms";
18-
import { useMount, useUnmount } from "react-use";
11+
import { atom, useAtomValue, useSetAtom } from "jotai";
12+
import { setSearchLeaderSlotsAtom, searchLeaderSlotsAtom } from "./atoms";
13+
import { useMount } from "react-use";
1914
import {
2015
currentLeaderSlotAtom,
2116
leaderSlotsAtom,
@@ -26,32 +21,45 @@ import { useDebouncedCallback } from "use-debounce";
2621
import NextSlotStatus from "../Overview/SlotPerformance/NextSlotStatus";
2722
import styles from "./search.module.css";
2823
import { skippedSlotsAtom } from "../../api/atoms";
29-
import { useState } from "react";
24+
import { useCallback, useEffect, useState } from "react";
3025
import * as Toggle from "@radix-ui/react-toggle";
26+
import { SearchTypeEnum } from "../../routes/leaderSchedule";
27+
import {
28+
useSearchTextSearchParam,
29+
useSearchTypeSearchParam,
30+
} from "./useSearchParams";
3131

3232
const isVisibleAtom = atom((get) => !!get(currentLeaderSlotAtom));
3333

3434
export default function Search() {
3535
const isVisible = useAtomValue(isVisibleAtom);
3636
const setSearch = useSetAtom(setSearchLeaderSlotsAtom);
37-
const [localSearch, setLocalSearch] = useState("");
3837

39-
const setSearchType = useSetAtom(searchTypeAtom);
4038
const setSlotOverride = useSetAtom(slotOverrideAtom);
4139

40+
const { searchType } = useSearchTypeSearchParam();
41+
const { searchText, setSearchText } = useSearchTextSearchParam();
42+
const [localValue, setLocalValue] = useState(searchText);
43+
4244
const debouncedSetSearch = useDebouncedCallback((value: string) => {
4345
setSearch(value);
44-
}, 1000);
46+
setSearchText(value);
47+
}, 1_000);
48+
49+
if (!debouncedSetSearch.isPending() && localValue !== searchText) {
50+
setLocalValue(searchText);
51+
}
4552

4653
const reset = () => {
47-
setLocalSearch("");
54+
setSearchText("");
4855
setSlotOverride(undefined);
4956
setSearch("");
5057
};
5158

52-
useMount(reset);
53-
useUnmount(() => {
54-
setSearchType(SearchType.Text);
59+
useMount(() => {
60+
if (searchType === SearchTypeEnum.text) {
61+
setSearch(searchText);
62+
}
5563
});
5664

5765
if (!isVisible) return;
@@ -64,11 +72,10 @@ export default function Search() {
6472
variant="soft"
6573
color="gray"
6674
onChange={(e) => {
67-
setSearchType(SearchType.Text);
68-
setLocalSearch(e.currentTarget.value);
75+
setLocalValue(e.currentTarget.value);
6976
debouncedSetSearch(e.currentTarget.value);
7077
}}
71-
value={localSearch}
78+
value={localValue}
7279
>
7380
<TextField.Slot>
7481
<MagnifyingGlassIcon
@@ -77,7 +84,7 @@ export default function Search() {
7784
style={{ color: "#AFB2C2" }}
7885
/>
7986
</TextField.Slot>
80-
{localSearch && (
87+
{localValue && (
8188
<TextField.Slot>
8289
<IconButton size="1" variant="ghost">
8390
<Cross1Icon
@@ -107,16 +114,6 @@ function SkipRate() {
107114
let value = "-";
108115

109116
if (skipRate !== undefined) {
110-
// if (skipRate.slots_processed)
111-
// if (skipRate.slots_processed === 0 || skipRate.slots_skipped === 0) {
112-
// value = "0";
113-
// } else {
114-
// const skipRatePct = skipRate.slots_skipped / skipRate.slots_processed;
115-
// value = (skipRatePct * 100).toLocaleString(undefined, {
116-
// minimumFractionDigits: 0,
117-
// maximumFractionDigits: 2,
118-
// });
119-
// }
120117
value = (skipRate.skip_rate * 100).toLocaleString(undefined, {
121118
minimumFractionDigits: 0,
122119
maximumFractionDigits: 2,
@@ -140,29 +137,41 @@ interface MySlotsProps {
140137
}
141138

142139
function MySlots({ resetSearchText }: MySlotsProps) {
143-
const slots = useAtomValue(leaderSlotsAtom);
140+
const leaderSlots = useAtomValue(leaderSlotsAtom);
144141
const setSearch = useSetAtom(searchLeaderSlotsAtom);
145142
const setSlotOverride = useSetAtom(slotOverrideAtom);
146143

147-
const [searchType, setSearchType] = useAtom(searchTypeAtom);
144+
const { searchType, setSearchType } = useSearchTypeSearchParam();
145+
146+
const slotCount = (leaderSlots?.length ?? 0) * 4;
148147

149-
const slotCount = (slots?.length ?? 0) * 4;
148+
const setMySlots = useCallback(() => {
149+
setSearch(leaderSlots);
150+
}, [setSearch, leaderSlots]);
151+
152+
useEffect(() => {
153+
if (searchType === SearchTypeEnum.mySlots) {
154+
setSearch(leaderSlots);
155+
}
156+
// On mount / data load
157+
// eslint-disable-next-line react-hooks/exhaustive-deps
158+
}, [leaderSlots, setSearch]);
150159

151160
const handleClick = () => {
152161
resetSearchText();
153162

154163
setSlotOverride(undefined);
155164

156-
if (searchType === SearchType.MySlots) {
157-
setSearchType(SearchType.Text);
165+
if (searchType === SearchTypeEnum.mySlots) {
166+
setSearchType(SearchTypeEnum.text);
158167
} else {
159-
setSearchType(SearchType.MySlots);
160-
setSearch(slots);
168+
setSearchType(SearchTypeEnum.mySlots);
169+
setMySlots();
161170
}
162171
};
163172

164-
const isSelected = searchType === SearchType.MySlots;
165-
const isDisabled = !slots?.length;
173+
const isSelected = searchType === SearchTypeEnum.mySlots;
174+
const isDisabled = !leaderSlots?.length;
166175

167176
return (
168177
<Tooltip content="Number of slots this validator is leader in the current epoch. Toggle to filter">
@@ -193,26 +202,37 @@ function SkippedSlots({ resetSearchText }: SkippedSlotsProps) {
193202
const setSearch = useSetAtom(searchLeaderSlotsAtom);
194203
const setSlotOverride = useSetAtom(slotOverrideAtom);
195204

196-
const [searchType, setSearchType] = useAtom(searchTypeAtom);
205+
const { searchType, setSearchType } = useSearchTypeSearchParam();
197206

198207
const skippedCount = skippedSlots?.length ?? 0;
199208

209+
const setSkippedSlots = useCallback(() => {
210+
const slotStarts = skippedSlots?.map((slot) => slot - (slot % 4));
211+
setSearch([...new Set(slotStarts)]);
212+
}, [setSearch, skippedSlots]);
213+
214+
useEffect(() => {
215+
if (searchType === SearchTypeEnum.skippedSlots) {
216+
setSkippedSlots();
217+
}
218+
// On mount / data load
219+
// eslint-disable-next-line react-hooks/exhaustive-deps
220+
}, [setSkippedSlots]);
221+
200222
const handleClick = () => {
201223
resetSearchText();
202224

203225
setSlotOverride(undefined);
204226

205-
if (searchType === SearchType.SkippedSlots) {
206-
setSearchType(SearchType.Text);
227+
if (searchType === SearchTypeEnum.skippedSlots) {
228+
setSearchType(SearchTypeEnum.text);
207229
} else if (skippedSlots?.length) {
208-
setSearchType(SearchType.SkippedSlots);
209-
210-
const slotStarts = skippedSlots?.map((slot) => slot - (slot % 4));
211-
setSearch([...new Set(slotStarts)]);
230+
setSearchType(SearchTypeEnum.skippedSlots);
231+
setSkippedSlots();
212232
}
213233
};
214234

215-
const isSelected = searchType === SearchType.SkippedSlots;
235+
const isSelected = searchType === SearchTypeEnum.skippedSlots;
216236
const isDisabled = !skippedSlots?.length;
217237

218238
return (
@@ -224,7 +244,7 @@ function SkippedSlots({ resetSearchText }: SkippedSlotsProps) {
224244
onClick={handleClick}
225245
aria-label="Toggle skipped slots"
226246
pressed={isSelected}
227-
disabled={isDisabled}
247+
disabled={!isSelected && isDisabled}
228248
>
229249
<Text className={styles.label}>My Skipped Slots</Text>
230250
<Text>{skippedCount}</Text>

src/features/LeaderSchedule/atoms.ts

-9
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,6 @@ import {
88
import { getLeaderSlots } from "../../utils";
99
import { slotsPerLeader } from "../../consts";
1010

11-
export const enum SearchType {
12-
MySlots,
13-
SkippedSlots,
14-
Text,
15-
}
16-
17-
export const searchTypeAtom = atom<SearchType>(SearchType.Text);
1811

1912
export const searchLeaderSlotsAtom = atom<number[] | undefined>(undefined);
2013

@@ -32,8 +25,6 @@ export const setSearchLeaderSlotsAtom = atom(
3225
return;
3326
}
3427

35-
set(searchTypeAtom, SearchType.Text);
36-
3728
const searchSlot = parseInt(searchText, 10);
3829
if (
3930
!isNaN(searchSlot) &&

src/features/LeaderSchedule/index.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { Flex } from "@radix-ui/themes";
22
import Slots from "./Slots";
3-
import { useSetAtom } from "jotai";
3+
import { useAtomValue, useSetAtom } from "jotai";
4+
import { epochAtom, slotOverrideAtom } from "../../atoms";
45
import { useMount } from "react-use";
5-
import { slotOverrideAtom } from "../../atoms";
66

77
export function LeaderSchedule() {
8+
const epoch = useAtomValue(epochAtom);
89
const setSlotOverride = useSetAtom(slotOverrideAtom);
910

1011
useMount(() => setSlotOverride(undefined));
@@ -15,6 +16,8 @@ export function LeaderSchedule() {
1516
}
1617
};
1718

19+
if (!epoch) return;
20+
1821
return (
1922
<Flex
2023
direction="column"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { useNavigate } from "@tanstack/react-router";
2+
import { Route, SearchType, SearchTypeEnum } from "../../routes/leaderSchedule";
3+
import { useCallback } from "react";
4+
5+
export function useSearchTypeSearchParam() {
6+
const { searchType } = Route.useSearch();
7+
const navigate = useNavigate({ from: Route.fullPath });
8+
9+
const setSearchType = useCallback(
10+
(searchType: SearchType) => {
11+
void navigate({ search: { searchType }, replace: true });
12+
},
13+
[navigate]
14+
);
15+
16+
return { searchType, setSearchType };
17+
}
18+
19+
export function useSearchTextSearchParam() {
20+
const { searchText } = Route.useSearch();
21+
const navigate = useNavigate({ from: Route.fullPath });
22+
23+
const setSearchText = useCallback(
24+
(searchText: string) => {
25+
void navigate({
26+
search: { searchText, searchType: SearchTypeEnum.text },
27+
replace: true,
28+
});
29+
},
30+
[navigate]
31+
);
32+
33+
return { searchText, setSearchText };
34+
}

0 commit comments

Comments
 (0)