From 99fe437dc226a4be48c345ef72543765ef7403a2 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Tue, 7 Oct 2025 15:17:03 -0500 Subject: [PATCH 1/7] feat: add quick select list options --- src/api/atoms.ts | 3 + src/api/entities.ts | 31 ++ src/api/types.ts | 3 + src/api/useSetAtomWsData.ts | 6 + src/atoms.ts | 34 +- src/colors.ts | 12 +- src/consts.ts | 9 + src/features/SlotDetails/index.tsx | 325 ++++++++++++------ .../SlotDetails/slotDetails.module.css | 60 +++- src/hooks/useSlotRankings.ts | 15 + 10 files changed, 361 insertions(+), 137 deletions(-) create mode 100644 src/hooks/useSlotRankings.ts diff --git a/src/api/atoms.ts b/src/api/atoms.ts index 0c9d4a8b..92782ab6 100644 --- a/src/api/atoms.ts +++ b/src/api/atoms.ts @@ -23,6 +23,7 @@ import type { BlockEngineUpdate, VoteBalance, ScheduleStrategy, + SlotRankings, } from "./types"; import { rafAtom } from "../atomUtils"; @@ -85,3 +86,5 @@ export const voteDistanceAtom = atom(undefined); export const skippedSlotsAtom = atom(undefined); export const blockEngineAtom = atom(undefined); + +export const slotRankingsAtom = atom(undefined); diff --git a/src/api/entities.ts b/src/api/entities.ts index d7fbfc0e..7bd1c4c0 100644 --- a/src/api/entities.ts +++ b/src/api/entities.ts @@ -474,6 +474,33 @@ export const slotResponseSchema = z.object({ export const slotSkippedHistorySchema = z.number().array(); +export const slotRankingsSchema = z.object({ + slots_largest_tips: z.number().array(), + vals_largest_tips: z.coerce.bigint().array(), + slots_smallest_tips: z.number().array(), + vals_smallest_tips: z.coerce.bigint().array(), + slots_largest_fees: z.number().array(), + vals_largest_fees: z.coerce.bigint().array(), + slots_smallest_fees: z.number().array(), + vals_smallest_fees: z.coerce.bigint().array(), + slots_largest_rewards: z.number().array(), + vals_largest_rewards: z.coerce.bigint().array(), + slots_smallest_rewards: z.number().array(), + vals_smallest_rewards: z.coerce.bigint().array(), + slots_largest_duration: z.number().array(), + vals_largest_duration: z.coerce.bigint().array(), + slots_smallest_duration: z.number().array(), + vals_smallest_duration: z.coerce.bigint().array(), + slots_largest_compute_units: z.number().array(), + vals_largest_compute_units: z.coerce.bigint().array(), + slots_smallest_compute_units: z.number().array(), + vals_smallest_compute_units: z.coerce.bigint().array(), + slots_largest_skipped: z.number().array(), + vals_largest_skipped: z.coerce.bigint().array(), + slots_smallest_skipped: z.number().array(), + vals_smallest_skipped: z.coerce.bigint().array(), +}); + export const slotSchema = z.discriminatedUnion("key", [ slotTopicSchema.extend({ key: z.literal("skipped_history"), @@ -487,6 +514,10 @@ export const slotSchema = z.discriminatedUnion("key", [ key: z.literal("query"), value: slotResponseSchema.nullable(), }), + slotTopicSchema.extend({ + key: z.literal("query_rankings"), + value: slotRankingsSchema, + }), ]); export const blockEngineStatusSchema = z.enum([ diff --git a/src/api/types.ts b/src/api/types.ts index 2117aada..8b4c372e 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -40,6 +40,7 @@ import type { slotTransactionsSchema, voteBalanceSchema, scheduleStrategySchema, + slotRankingsSchema, } from "./entities"; export type Client = z.infer; @@ -131,3 +132,5 @@ export type SkippedSlots = z.infer; export type BlockEngineUpdate = z.infer; export type BlockEngineStatus = z.infer; + +export type SlotRankings = z.infer; diff --git a/src/api/useSetAtomWsData.ts b/src/api/useSetAtomWsData.ts index 15540e4a..6679fef9 100644 --- a/src/api/useSetAtomWsData.ts +++ b/src/api/useSetAtomWsData.ts @@ -20,6 +20,7 @@ import { voteStateAtom, voteBalanceAtom, scheduleStrategyAtom, + slotRankingsAtom, } from "./atoms"; import { blockEngineSchema, @@ -130,6 +131,7 @@ export function useSetAtomWsData() { const setSkippedSlots = useSetAtom(skippedSlotsAtom); const setSlotResponse = useSetAtom(setSlotResponseAtom); + const setSlotRankings = useSetAtom(slotRankingsAtom); const [epoch, setEpoch] = useAtom(epochAtom); @@ -280,6 +282,10 @@ export function useSetAtomWsData() { } break; } + case "query_rankings": { + setSlotRankings(value); + break; + } } } else if (topic === "block_engine") { const { key, value } = blockEngineSchema.parse(msg); diff --git a/src/atoms.ts b/src/atoms.ts index 4f66d6e3..944fc3fa 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -288,30 +288,34 @@ export const firstProcessedSlotAtom = atom((get) => { return startupProgress.ledger_max_slot + 1; }); -export const earliestProcessedSlotLeaderAtom = atom((get) => { - const firstProcessedSlot = get(firstProcessedSlotAtom); - const leaderSlots = get(leaderSlotsAtom); - - if (firstProcessedSlot === undefined || !leaderSlots?.length) return; - return leaderSlots.find((s) => s >= firstProcessedSlot); -}); - -export const mostRecentSlotLeaderAtom = atom((get) => { - const earliestProcessedSlotLeader = get(earliestProcessedSlotLeaderAtom); +export const processedLeaderSlotsAtom = atom((get) => { const leaderSlots = get(leaderSlotsAtom); + const firstProcessedSlot = get(firstProcessedSlotAtom); const currentLeaderSlot = get(currentLeaderSlotAtom); if ( - earliestProcessedSlotLeader === undefined || - currentLeaderSlot === undefined || - !leaderSlots?.length + !leaderSlots?.length || + firstProcessedSlot === undefined || + currentLeaderSlot === undefined ) return; - return leaderSlots.findLast( - (s) => earliestProcessedSlotLeader <= s && s <= currentLeaderSlot, + return leaderSlots.filter( + (slot) => firstProcessedSlot <= slot && slot <= currentLeaderSlot, ); }); +export const earliestProcessedSlotLeaderAtom = atom((get) => { + const processedLeaderSlots = get(processedLeaderSlotsAtom); + return processedLeaderSlots?.[0]; +}); + +export const mostRecentSlotLeaderAtom = atom((get) => { + const processedLeaderSlots = get(processedLeaderSlotsAtom); + return processedLeaderSlots + ? processedLeaderSlots[processedLeaderSlots.length - 1] + : undefined; +}); + const _currentSlotAtom = atom(undefined); export const currentSlotAtom = atom( (get) => get(_currentSlotAtom), diff --git a/src/colors.ts b/src/colors.ts index f9133d09..bc4e2f90 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -164,11 +164,13 @@ export const circularProgressPathColor = slotStatusBlue; // slot details export const slotDetailsMySlotsColor = "#0080e6"; -export const slotDetailsQuickSearchTextPrimaryColor = "#cecece"; -export const slotDetailsQuickSearchTextSecondaryColor = "#646464"; -export const slotDetailsEarliestSlotColor = "#00A2C7"; -export const slotDetailsMostRecentSlotColor = "#1D863B"; -export const slotDetailsLastSkippedSlotColor = "#EB6262"; +export const slotDetailsQuickSearchTextColor = "#8D8D8D"; +export const slotDetailsEarliestSlotColor = "#1CE7C2"; +export const slotDetailsRecentSlotColor = "#3DB9CF"; +export const slotDetailsSkippedSlotColor = "#EB6262"; +export const slotDetailsFeesSlotColor = "#7CE2FE"; +export const slotDetailsTipsSlotColor = "#5BB98B"; +export const slotDetailsRewardsSlotColor = "#8DA4EF"; export const slotDetailsBackgroundColor = "#15181e"; export const slotDetailsColor = "#9aabc3"; export const slotDetailsSkippedBackgroundColor = "#250f0f"; diff --git a/src/consts.ts b/src/consts.ts index 336319c9..bff9d881 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -77,3 +77,12 @@ export const slotNavWidth = export const narrowNavMedia = "(max-width: 768px)"; export const maxZIndex = 110; + +export const numQuickSearchSlots = 3; +export const quickSearchCardWidth = 226; +export const quickSearchSpacing = 40; +export const slotSearchPadding = 20; +export const slotSearchWidth = + numQuickSearchSlots * quickSearchCardWidth + + (numQuickSearchSlots - 1) * quickSearchSpacing + + slotSearchPadding * 2; diff --git a/src/features/SlotDetails/index.tsx b/src/features/SlotDetails/index.tsx index ca7cb7c9..e3e9744d 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -1,4 +1,5 @@ import { Flex, TextField, Text, IconButton } from "@radix-ui/themes"; +import { Label } from "radix-ui"; import SlotPerformance from "../Overview/SlotPerformance"; import ComputeUnitsCard from "../Overview/SlotPerformance/ComputeUnitsCard"; import TransactionBarsCard from "../Overview/SlotPerformance/TransactionBarsCard"; @@ -9,10 +10,23 @@ import { baseSelectedSlotAtom, SelectedSlotValidityState, } from "../Overview/SlotPerformance/atoms"; -import type { FC, SVGProps } from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import type React from "react"; +import { + useCallback, + useEffect, + useMemo, + useState, + type CSSProperties, +} from "react"; import { useMedia, useUnmount } from "react-use"; -import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; +import { + CounterClockwiseClockIcon, + TimerIcon, + DoubleArrowUpIcon, + PlusCircledIcon, + TextAlignTopIcon, + MagnifyingGlassIcon, +} from "@radix-ui/react-icons"; import { useSlotInfo } from "../../hooks/useSlotInfo"; import styles from "./slotDetails.module.css"; import PeerIcon from "../../components/PeerIcon"; @@ -20,39 +34,45 @@ import { earliestProcessedSlotLeaderAtom, epochAtom, mostRecentSlotLeaderAtom, + processedLeaderSlotsAtom, } from "../../atoms"; import { clusterIndicatorHeight, headerHeight, maxZIndex, + numQuickSearchSlots, + quickSearchCardWidth, + quickSearchSpacing, + slotSearchPadding, + slotSearchWidth, slotsPerLeader, } from "../../consts"; import { useSlotQueryPublish } from "../../hooks/useSlotQuery"; -import { getLeaderSlots, getSlotGroupLeader } from "../../utils"; +import { getLeaderSlots, getSlotGroupLeader, getSolString } from "../../utils"; import { SkippedIcon, StatusIcon } from "../../components/StatusIcon"; import clsx from "clsx"; import { slotDetailsEarliestSlotColor, - slotDetailsLastSkippedSlotColor, - slotDetailsMostRecentSlotColor, - slotDetailsQuickSearchTextPrimaryColor, - slotDetailsQuickSearchTextSecondaryColor, + slotDetailsSkippedSlotColor, + slotDetailsRecentSlotColor, + slotDetailsTipsSlotColor, + slotDetailsFeesSlotColor, + slotDetailsRewardsSlotColor, } from "../../colors"; -import skippedIcon from "../../assets/Skipped.svg?react"; -import history from "../../assets/history.svg?react"; -import checkFill from "../../assets/checkFill.svg?react"; +import Skipped from "../../assets/Skipped.svg?react"; -import { skippedSlotsAtom } from "../../api/atoms"; +import { slotRankingsAtom } from "../../api/atoms"; import { useTimeAgo } from "../../hooks/useTimeAgo"; import SlotClient from "../../components/SlotClient"; import { Link } from "@tanstack/react-router"; +import useSlotRankings from "../../hooks/useSlotRankings"; export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); return ( - + {selectedSlot === undefined ? : } @@ -122,25 +142,28 @@ function SlotSearch() { return (
{ e.preventDefault(); submitSearch(); }} > + + Search Slot ID + + {!isValid && } - {!isValid && }
); } function QuickSearch() { - return ( - - - - - - - - ); -} + useSlotRankings(true); + const slotRankings = useAtomValue(slotRankingsAtom); + const processedLeaderSlots = useAtomValue(processedLeaderSlotsAtom); + const reversedProcessedLeaderSlots = useMemo(() => { + return processedLeaderSlots + ? [...processedLeaderSlots].reverse() + : undefined; + }, [processedLeaderSlots]); -function EarliestProcessedSlotSearch() { - const earliestProcessedSlotLeader = useAtomValue( - earliestProcessedSlotLeaderAtom, - ); return ( - + + } + label="Earliest Slots" + color={slotDetailsEarliestSlotColor} + slots={processedLeaderSlots} + /> + } + label="Most Recent Slots" + color={slotDetailsRecentSlotColor} + slots={reversedProcessedLeaderSlots} + /> + } + label="Last Skipped Slots" + color={slotDetailsSkippedSlotColor} + slots={slotRankings?.slots_largest_skipped} + /> + } + label="Highest Fees" + color={slotDetailsFeesSlotColor} + slots={slotRankings?.slots_largest_fees} + metrics={slotRankings?.vals_largest_fees} + metricsFmt={getSolString} + unit="SOL" + /> + } + label="Highest Tips" + color={slotDetailsTipsSlotColor} + slots={slotRankings?.slots_largest_tips} + metrics={slotRankings?.vals_largest_tips} + metricsFmt={getSolString} + unit="SOL" + /> + } + label="Highest Rewards" + color={slotDetailsRewardsSlotColor} + slots={slotRankings?.slots_largest_rewards} + metrics={slotRankings?.vals_largest_rewards} + metricsFmt={getSolString} + unit="SOL" + /> + ); } -function MostRecentSlotSearch() { - const mostRecentSlotLeader = useAtomValue(mostRecentSlotLeaderAtom); +function QuickSearchCard({ + icon, + label, + color, + slots, + metrics, + metricsFmt, + unit, +}: { + icon: React.ReactNode; + label: string; + color: string; + slots?: number[]; + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; + unit?: string; +}) { + const isDisabled = !slots?.length; return ( - + + + {icon} + {label} + + + ); } -function LastSkippedSlotSearch() { - const skippedSlots = useAtomValue(skippedSlotsAtom); - const slot = useMemo( - () => (skippedSlots ? skippedSlots[skippedSlots?.length - 1] : undefined), - [skippedSlots], +function QuickSearchSlots({ + slots, + metrics, + metricsFmt, + unit, +}: { + slots?: number[]; + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; + unit?: string; +}) { + const { setSelectedSlot } = useSlotSearchParam(); + const hasDefaultMetric = !metrics; + const fixedLengthSlots = useMemo( + () => + !slots + ? Array(numQuickSearchSlots).fill(undefined) + : slots.length < numQuickSearchSlots + ? [ + ...slots, + ...Array( + numQuickSearchSlots - slots.length, + ).fill(undefined), + ] + : slots.slice(0, numQuickSearchSlots), + [slots], ); return ( - + + {fixedLengthSlots.map((slot, i) => { + const formattedMetric = + metrics && metrics[i] !== undefined + ? metricsFmt + ? metricsFmt(metrics[i]) + : metrics[i].toLocaleString() + : undefined; + + return ( + + {slot === undefined ? ( + -- + ) : ( + setSelectedSlot(slot)} + > + {slot} + + )} + + + ); + })} + ); } -function QuickSearchCard({ - Icon, - label, - color, +function QuickSearchMetric({ slot, - disabled = false, + hasDefaultMetric, + formattedMetric, + unit, }: { - Icon: FC>; - label: string; - color: string; slot?: number; - disabled?: boolean; + hasDefaultMetric: boolean; + formattedMetric?: string; + unit?: string; }) { - const { setSelectedSlot } = useSlotSearchParam(); - - return ( - setSelectedSlot(slot)} - aria-disabled={disabled} - > - - - {label} - - - - {slot ?? "--"} - - {slot && } - - - ); + let content; + + if (slot === undefined) { + content = "--"; + } else if (hasDefaultMetric) { + content = ; + } else if (formattedMetric === undefined) { + content = "--"; + } else { + content = unit ? `${formattedMetric} ${unit}` : formattedMetric; + } + + return {content}; } function TimeAgo({ slot }: { slot: number }) { @@ -264,11 +389,7 @@ function TimeAgo({ slot }: { slot: number }) { showOnlyTwoSignificantUnits: true, }); - return ( - - {timeAgoText} - - ); + return timeAgoText; } const navigationTop = clusterIndicatorHeight + headerHeight; @@ -452,7 +573,7 @@ function SlotContent() { const slotGroupLeader = getSlotGroupLeader(slot); return ( - + diff --git a/src/features/SlotDetails/slotDetails.module.css b/src/features/SlotDetails/slotDetails.module.css index 03b1850d..23110a6b 100644 --- a/src/features/SlotDetails/slotDetails.module.css +++ b/src/features/SlotDetails/slotDetails.module.css @@ -1,38 +1,68 @@ .search { - text-align: center; + form { + display: flex; + flex-direction: column; + width: 100%; + align-items: flex-start; + gap: 8px; + } } -.error-text { - color: var(--failure-color); +.search-label { + font-size: 16px; + font-weight: 510; } -.clickable { - cursor: pointer; +.search-field { + width: 100%; } -.quick-search-row { - justify-content: center; - align-items: center; - flex-wrap: wrap; - gap: 8px; +.error-text { + color: var(--failure-color); } -.quick-search { - width: 164px; - padding: 15px; +.quick-search-card { flex-direction: column; - gap: 10px; border-radius: 8px; border: 1px solid var(--container-border-color); background: var(--container-background-color); + color: var(--slot-details-quick-search-text-color); + + &.disabled { + opacity: 0.6; + } +} + +.quick-search-header { + color: var(--quick-search-color); + font-size: 18px; + + svg { + width: 32px; + height: 32px; + + fill: var(--quick-search-color); + } +} + +.quick-search-slot { + font-size: 12px; + + &.clickable { + cursor: pointer; + color: #5eb1ef; + } &:not(.clickable) { cursor: not-allowed; pointer-events: none; - opacity: 0.6; } } +.quick-search-metric { + font-size: 12px; +} + .slot-name { font-size: 18px; font-weight: 600; diff --git a/src/hooks/useSlotRankings.ts b/src/hooks/useSlotRankings.ts new file mode 100644 index 00000000..0b6347dd --- /dev/null +++ b/src/hooks/useSlotRankings.ts @@ -0,0 +1,15 @@ +import { useWebSocketSend } from "../api/ws/utils"; +import { useInterval } from "react-use"; + +export default function useSlotRankings(mine: boolean = false) { + const wsSend = useWebSocketSend(); + + useInterval(() => { + wsSend({ + topic: "slot", + key: "query_rankings", + id: 32, + params: { mine }, + }); + }, 5_000); +} From e6cf31268b8274aeef89c6d089f1fff6ded22eec Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Wed, 8 Oct 2025 07:30:37 -0500 Subject: [PATCH 2/7] fix: sync search width with card row width --- src/consts.ts | 4 -- src/features/SlotDetails/index.tsx | 21 +++++---- .../SlotDetails/slotDetails.module.css | 18 +++----- src/hooks/useQuickSearchWidth.ts | 43 +++++++++++++++++++ 4 files changed, 61 insertions(+), 25 deletions(-) create mode 100644 src/hooks/useQuickSearchWidth.ts diff --git a/src/consts.ts b/src/consts.ts index bff9d881..abb8daf3 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -82,7 +82,3 @@ export const numQuickSearchSlots = 3; export const quickSearchCardWidth = 226; export const quickSearchSpacing = 40; export const slotSearchPadding = 20; -export const slotSearchWidth = - numQuickSearchSlots * quickSearchCardWidth + - (numQuickSearchSlots - 1) * quickSearchSpacing + - slotSearchPadding * 2; diff --git a/src/features/SlotDetails/index.tsx b/src/features/SlotDetails/index.tsx index e3e9744d..10910cbb 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -44,7 +44,6 @@ import { quickSearchCardWidth, quickSearchSpacing, slotSearchPadding, - slotSearchWidth, slotsPerLeader, } from "../../consts"; import { useSlotQueryPublish } from "../../hooks/useSlotQuery"; @@ -67,6 +66,7 @@ import { useTimeAgo } from "../../hooks/useTimeAgo"; import SlotClient from "../../components/SlotClient"; import { Link } from "@tanstack/react-router"; import useSlotRankings from "../../hooks/useSlotRankings"; +import { useQuickSearchWidth } from "../../hooks/useQuickSearchWidth"; export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); @@ -129,6 +129,7 @@ function SlotSearch() { const [searchSlot, setSearchSlot] = useState(""); const epoch = useAtomValue(epochAtom); const { isValid } = useAtomValue(baseSelectedSlotAtom); + const quickSearchWidth = useQuickSearchWidth(); const submitSearch = useCallback(() => { if (searchSlot === "") setSelectedSlot(undefined); @@ -142,9 +143,8 @@ function SlotSearch() { return ( Search Slot ID @@ -184,6 +185,10 @@ function SlotSearch() { ); } +function getSolStringWithFourDecimals(lamportAmount: bigint) { + return getSolString(lamportAmount, 4); +} + function QuickSearch() { useSlotRankings(true); const slotRankings = useAtomValue(slotRankingsAtom); @@ -225,7 +230,7 @@ function QuickSearch() { color={slotDetailsFeesSlotColor} slots={slotRankings?.slots_largest_fees} metrics={slotRankings?.vals_largest_fees} - metricsFmt={getSolString} + metricsFmt={getSolStringWithFourDecimals} unit="SOL" /> @@ -267,11 +272,9 @@ function QuickSearchCard({ metricsFmt?: (m: T) => string | undefined; unit?: string; }) { - const isDisabled = !slots?.length; - return ( { + function handleResize() { + setWidth(window.innerWidth); + } + window.addEventListener("resize", handleResize); + return () => window.removeEventListener("resize", handleResize); + }, []); + + const outletWidth = showOnlyEpochBar + ? width - slotNavWithoutListWidth + : showNav + ? width - slotNavWidth + : width; + + const numCardsPerRow = Math.min( + numQuickSearchSlots, + Math.floor( + (outletWidth - 2 * slotSearchPadding + quickSearchSpacing) / + (quickSearchCardWidth + quickSearchSpacing), + ), + ); + const quickSearchWidth = + numCardsPerRow * quickSearchCardWidth + + (numCardsPerRow - 1) * quickSearchSpacing + + 2 * slotSearchPadding; + + return quickSearchWidth; +} From 0809ff8fb30c1a37eca73585824e72ef734397de Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Wed, 8 Oct 2025 07:41:53 -0500 Subject: [PATCH 3/7] chore: move slot search --- src/features/SlotDetails/SlotSearch.tsx | 345 ++++++++++++++++++ src/features/SlotDetails/index.tsx | 343 +---------------- .../SlotDetails/slotDetails.module.css | 59 --- .../SlotDetails/slotSearch.module.css | 58 +++ 4 files changed, 407 insertions(+), 398 deletions(-) create mode 100644 src/features/SlotDetails/SlotSearch.tsx create mode 100644 src/features/SlotDetails/slotSearch.module.css diff --git a/src/features/SlotDetails/SlotSearch.tsx b/src/features/SlotDetails/SlotSearch.tsx new file mode 100644 index 00000000..9825f95a --- /dev/null +++ b/src/features/SlotDetails/SlotSearch.tsx @@ -0,0 +1,345 @@ +import { + useCallback, + useEffect, + useMemo, + useState, + type CSSProperties, +} from "react"; +import { useSlotSearchParam } from "./useSearchParams"; +import { epochAtom, processedLeaderSlotsAtom } from "../../atoms"; +import { useAtomValue } from "jotai"; +import { + baseSelectedSlotAtom, + SelectedSlotValidityState, +} from "../Overview/SlotPerformance/atoms"; +import { useQuickSearchWidth } from "../../hooks/useQuickSearchWidth"; +import { Label } from "radix-ui"; +import { Flex, IconButton, TextField, Text } from "@radix-ui/themes"; +import { + numQuickSearchSlots, + quickSearchCardWidth, + quickSearchSpacing, + slotSearchPadding, +} from "../../consts"; +import styles from "./slotSearch.module.css"; +import { + CounterClockwiseClockIcon, + DoubleArrowUpIcon, + MagnifyingGlassIcon, + PlusCircledIcon, + TextAlignTopIcon, + TimerIcon, +} from "@radix-ui/react-icons"; +import Skipped from "../../assets/Skipped.svg?react"; +import { getSolString } from "../../utils"; +import useSlotRankings from "../../hooks/useSlotRankings"; +import { slotRankingsAtom } from "../../api/atoms"; +import { + slotDetailsEarliestSlotColor, + slotDetailsFeesSlotColor, + slotDetailsRecentSlotColor, + slotDetailsRewardsSlotColor, + slotDetailsSkippedSlotColor, + slotDetailsTipsSlotColor, +} from "../../colors"; +import clsx from "clsx"; +import { useTimeAgo } from "../../hooks/useTimeAgo"; + +export function SlotSearch() { + const { selectedSlot, setSelectedSlot } = useSlotSearchParam(); + const [searchSlot, setSearchSlot] = useState(""); + const epoch = useAtomValue(epochAtom); + const { isValid } = useAtomValue(baseSelectedSlotAtom); + const quickSearchWidth = useQuickSearchWidth(); + + const submitSearch = useCallback(() => { + if (searchSlot === "") setSelectedSlot(undefined); + else setSelectedSlot(Number(searchSlot)); + }, [searchSlot, setSelectedSlot]); + + useEffect(() => { + if (selectedSlot === undefined) setSearchSlot(""); + else setSearchSlot(String(selectedSlot)); + }, [selectedSlot]); + + return ( + +
{ + e.preventDefault(); + submitSearch(); + }} + className={styles.searchForm} + > + + Search Slot ID + + setSearchSlot(e.target.value)} + size="3" + color={isValid ? "teal" : "red"} + > + + + + + + + {!isValid && } + + +
+ ); +} + +function getSolStringWithFourDecimals(lamportAmount: bigint) { + return getSolString(lamportAmount, 4); +} + +function QuickSearch() { + useSlotRankings(true); + const slotRankings = useAtomValue(slotRankingsAtom); + const processedLeaderSlots = useAtomValue(processedLeaderSlotsAtom); + const reversedProcessedLeaderSlots = useMemo(() => { + return processedLeaderSlots + ? [...processedLeaderSlots].reverse() + : undefined; + }, [processedLeaderSlots]); + + return ( + + } + label="Earliest Slots" + color={slotDetailsEarliestSlotColor} + slots={processedLeaderSlots} + /> + } + label="Most Recent Slots" + color={slotDetailsRecentSlotColor} + slots={reversedProcessedLeaderSlots} + /> + } + label="Last Skipped Slots" + color={slotDetailsSkippedSlotColor} + slots={slotRankings?.slots_largest_skipped} + /> + } + label="Highest Fees" + color={slotDetailsFeesSlotColor} + slots={slotRankings?.slots_largest_fees} + metrics={slotRankings?.vals_largest_fees} + metricsFmt={getSolStringWithFourDecimals} + unit="SOL" + /> + } + label="Highest Tips" + color={slotDetailsTipsSlotColor} + slots={slotRankings?.slots_largest_tips} + metrics={slotRankings?.vals_largest_tips} + metricsFmt={getSolStringWithFourDecimals} + unit="SOL" + /> + } + label="Highest Rewards" + color={slotDetailsRewardsSlotColor} + slots={slotRankings?.slots_largest_rewards} + metrics={slotRankings?.vals_largest_rewards} + metricsFmt={getSolStringWithFourDecimals} + unit="SOL" + /> + + ); +} + +function QuickSearchCard({ + icon, + label, + color, + slots, + metrics, + metricsFmt, + unit, +}: { + icon: React.ReactNode; + label: string; + color: string; + slots?: number[]; + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; + unit?: string; +}) { + return ( + + + {icon} + {label} + + + + ); +} + +function QuickSearchSlots({ + slots, + metrics, + metricsFmt, + unit, +}: { + slots?: number[]; + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; + unit?: string; +}) { + const { setSelectedSlot } = useSlotSearchParam(); + const hasDefaultMetric = !metrics; + const fixedLengthSlots = useMemo( + () => + !slots + ? Array(numQuickSearchSlots).fill(undefined) + : slots.length < numQuickSearchSlots + ? [ + ...slots, + ...Array( + numQuickSearchSlots - slots.length, + ).fill(undefined), + ] + : slots.slice(0, numQuickSearchSlots), + [slots], + ); + + return ( + + {fixedLengthSlots.map((slot, i) => { + const formattedMetric = + metrics && metrics[i] !== undefined + ? metricsFmt + ? metricsFmt(metrics[i]) + : metrics[i].toLocaleString() + : undefined; + + return ( + + {slot === undefined ? ( + -- + ) : ( + setSelectedSlot(slot)} + > + {slot} + + )} + + + ); + })} + + ); +} + +function QuickSearchMetric({ + slot, + hasDefaultMetric, + formattedMetric, + unit, +}: { + slot?: number; + hasDefaultMetric: boolean; + formattedMetric?: string; + unit?: string; +}) { + let content; + + if (slot === undefined) { + content = "--"; + } else if (hasDefaultMetric) { + content = ; + } else if (formattedMetric === undefined) { + content = "--"; + } else { + content = unit ? `${formattedMetric} ${unit}` : formattedMetric; + } + + return {content}; +} + +function TimeAgo({ slot }: { slot: number }) { + const { timeAgoText } = useTimeAgo(slot, { + showOnlyTwoSignificantUnits: true, + }); + + return timeAgoText; +} + +function Errors() { + const { slot, state } = useAtomValue(baseSelectedSlotAtom); + + const epoch = useAtomValue(epochAtom); + const message = useMemo(() => { + switch (state) { + case SelectedSlotValidityState.NotReady: + return `Slot ${slot} validity cannot be determined because epoch and leader slot data is not available yet.`; + case SelectedSlotValidityState.OutsideEpoch: + return `Slot ${slot} is outside this epoch. Please try again with a different ID between ${epoch?.start_slot} - ${epoch?.end_slot}.`; + case SelectedSlotValidityState.NotYou: + return `Slot ${slot} belongs to another validator. Please try again with a slot number processed by you.`; + case SelectedSlotValidityState.BeforeFirstProcessed: + return `Slot ${slot} is in this epoch but its details are unavailable because it was processed before the restart.`; + case SelectedSlotValidityState.Future: + return `Slot ${slot} is valid but in the future. To view details, check again after it has been processed.`; + case SelectedSlotValidityState.Valid: + return ""; + } + }, [epoch?.end_slot, epoch?.start_slot, slot, state]); + + return ( + + {message} + + ); +} diff --git a/src/features/SlotDetails/index.tsx b/src/features/SlotDetails/index.tsx index 10910cbb..ea566257 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -1,5 +1,4 @@ -import { Flex, TextField, Text, IconButton } from "@radix-ui/themes"; -import { Label } from "radix-ui"; +import { Flex, Text } from "@radix-ui/themes"; import SlotPerformance from "../Overview/SlotPerformance"; import ComputeUnitsCard from "../Overview/SlotPerformance/ComputeUnitsCard"; import TransactionBarsCard from "../Overview/SlotPerformance/TransactionBarsCard"; @@ -8,25 +7,9 @@ import { useAtomValue, useSetAtom } from "jotai"; import { selectedSlotAtom, baseSelectedSlotAtom, - SelectedSlotValidityState, } from "../Overview/SlotPerformance/atoms"; -import type React from "react"; -import { - useCallback, - useEffect, - useMemo, - useState, - type CSSProperties, -} from "react"; +import { useEffect, useMemo } from "react"; import { useMedia, useUnmount } from "react-use"; -import { - CounterClockwiseClockIcon, - TimerIcon, - DoubleArrowUpIcon, - PlusCircledIcon, - TextAlignTopIcon, - MagnifyingGlassIcon, -} from "@radix-ui/react-icons"; import { useSlotInfo } from "../../hooks/useSlotInfo"; import styles from "./slotDetails.module.css"; import PeerIcon from "../../components/PeerIcon"; @@ -34,39 +17,20 @@ import { earliestProcessedSlotLeaderAtom, epochAtom, mostRecentSlotLeaderAtom, - processedLeaderSlotsAtom, } from "../../atoms"; import { clusterIndicatorHeight, headerHeight, maxZIndex, - numQuickSearchSlots, - quickSearchCardWidth, - quickSearchSpacing, - slotSearchPadding, slotsPerLeader, } from "../../consts"; import { useSlotQueryPublish } from "../../hooks/useSlotQuery"; -import { getLeaderSlots, getSlotGroupLeader, getSolString } from "../../utils"; +import { getLeaderSlots, getSlotGroupLeader } from "../../utils"; import { SkippedIcon, StatusIcon } from "../../components/StatusIcon"; import clsx from "clsx"; -import { - slotDetailsEarliestSlotColor, - slotDetailsSkippedSlotColor, - slotDetailsRecentSlotColor, - slotDetailsTipsSlotColor, - slotDetailsFeesSlotColor, - slotDetailsRewardsSlotColor, -} from "../../colors"; - -import Skipped from "../../assets/Skipped.svg?react"; - -import { slotRankingsAtom } from "../../api/atoms"; -import { useTimeAgo } from "../../hooks/useTimeAgo"; import SlotClient from "../../components/SlotClient"; import { Link } from "@tanstack/react-router"; -import useSlotRankings from "../../hooks/useSlotRankings"; -import { useQuickSearchWidth } from "../../hooks/useQuickSearchWidth"; +import { SlotSearch } from "./SlotSearch"; export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); @@ -96,305 +60,6 @@ function Setup() { return null; } -function Errors() { - const { slot, state } = useAtomValue(baseSelectedSlotAtom); - - const epoch = useAtomValue(epochAtom); - const message = useMemo(() => { - switch (state) { - case SelectedSlotValidityState.NotReady: - return `Slot ${slot} validity cannot be determined because epoch and leader slot data is not available yet.`; - case SelectedSlotValidityState.OutsideEpoch: - return `Slot ${slot} is outside this epoch. Please try again with a different ID between ${epoch?.start_slot} - ${epoch?.end_slot}.`; - case SelectedSlotValidityState.NotYou: - return `Slot ${slot} belongs to another validator. Please try again with a slot number processed by you.`; - case SelectedSlotValidityState.BeforeFirstProcessed: - return `Slot ${slot} is in this epoch but its details are unavailable because it was processed before the restart.`; - case SelectedSlotValidityState.Future: - return `Slot ${slot} is valid but in the future. To view details, check again after it has been processed.`; - case SelectedSlotValidityState.Valid: - return ""; - } - }, [epoch?.end_slot, epoch?.start_slot, slot, state]); - - return ( - - {message} - - ); -} - -function SlotSearch() { - const { selectedSlot, setSelectedSlot } = useSlotSearchParam(); - const [searchSlot, setSearchSlot] = useState(""); - const epoch = useAtomValue(epochAtom); - const { isValid } = useAtomValue(baseSelectedSlotAtom); - const quickSearchWidth = useQuickSearchWidth(); - - const submitSearch = useCallback(() => { - if (searchSlot === "") setSelectedSlot(undefined); - else setSelectedSlot(Number(searchSlot)); - }, [searchSlot, setSelectedSlot]); - - useEffect(() => { - if (selectedSlot === undefined) setSearchSlot(""); - else setSearchSlot(String(selectedSlot)); - }, [selectedSlot]); - - return ( - -
{ - e.preventDefault(); - submitSearch(); - }} - className={styles.searchForm} - > - - Search Slot ID - - setSearchSlot(e.target.value)} - size="3" - color={isValid ? "teal" : "red"} - > - - - - - - - {!isValid && } - - -
- ); -} - -function getSolStringWithFourDecimals(lamportAmount: bigint) { - return getSolString(lamportAmount, 4); -} - -function QuickSearch() { - useSlotRankings(true); - const slotRankings = useAtomValue(slotRankingsAtom); - const processedLeaderSlots = useAtomValue(processedLeaderSlotsAtom); - const reversedProcessedLeaderSlots = useMemo(() => { - return processedLeaderSlots - ? [...processedLeaderSlots].reverse() - : undefined; - }, [processedLeaderSlots]); - - return ( - - } - label="Earliest Slots" - color={slotDetailsEarliestSlotColor} - slots={processedLeaderSlots} - /> - } - label="Most Recent Slots" - color={slotDetailsRecentSlotColor} - slots={reversedProcessedLeaderSlots} - /> - } - label="Last Skipped Slots" - color={slotDetailsSkippedSlotColor} - slots={slotRankings?.slots_largest_skipped} - /> - } - label="Highest Fees" - color={slotDetailsFeesSlotColor} - slots={slotRankings?.slots_largest_fees} - metrics={slotRankings?.vals_largest_fees} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" - /> - } - label="Highest Tips" - color={slotDetailsTipsSlotColor} - slots={slotRankings?.slots_largest_tips} - metrics={slotRankings?.vals_largest_tips} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" - /> - } - label="Highest Rewards" - color={slotDetailsRewardsSlotColor} - slots={slotRankings?.slots_largest_rewards} - metrics={slotRankings?.vals_largest_rewards} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" - /> - - ); -} - -function QuickSearchCard({ - icon, - label, - color, - slots, - metrics, - metricsFmt, - unit, -}: { - icon: React.ReactNode; - label: string; - color: string; - slots?: number[]; - metrics?: T[]; - metricsFmt?: (m: T) => string | undefined; - unit?: string; -}) { - return ( - - - {icon} - {label} - - - - ); -} - -function QuickSearchSlots({ - slots, - metrics, - metricsFmt, - unit, -}: { - slots?: number[]; - metrics?: T[]; - metricsFmt?: (m: T) => string | undefined; - unit?: string; -}) { - const { setSelectedSlot } = useSlotSearchParam(); - const hasDefaultMetric = !metrics; - const fixedLengthSlots = useMemo( - () => - !slots - ? Array(numQuickSearchSlots).fill(undefined) - : slots.length < numQuickSearchSlots - ? [ - ...slots, - ...Array( - numQuickSearchSlots - slots.length, - ).fill(undefined), - ] - : slots.slice(0, numQuickSearchSlots), - [slots], - ); - - return ( - - {fixedLengthSlots.map((slot, i) => { - const formattedMetric = - metrics && metrics[i] !== undefined - ? metricsFmt - ? metricsFmt(metrics[i]) - : metrics[i].toLocaleString() - : undefined; - - return ( - - {slot === undefined ? ( - -- - ) : ( - setSelectedSlot(slot)} - > - {slot} - - )} - - - ); - })} - - ); -} - -function QuickSearchMetric({ - slot, - hasDefaultMetric, - formattedMetric, - unit, -}: { - slot?: number; - hasDefaultMetric: boolean; - formattedMetric?: string; - unit?: string; -}) { - let content; - - if (slot === undefined) { - content = "--"; - } else if (hasDefaultMetric) { - content = ; - } else if (formattedMetric === undefined) { - content = "--"; - } else { - content = unit ? `${formattedMetric} ${unit}` : formattedMetric; - } - - return {content}; -} - -function TimeAgo({ slot }: { slot: number }) { - const { timeAgoText } = useTimeAgo(slot, { - showOnlyTwoSignificantUnits: true, - }); - - return timeAgoText; -} - const navigationTop = clusterIndicatorHeight + headerHeight; function SlotNavigation({ slot }: { slot: number }) { diff --git a/src/features/SlotDetails/slotDetails.module.css b/src/features/SlotDetails/slotDetails.module.css index 05bc27a4..084d46b6 100644 --- a/src/features/SlotDetails/slotDetails.module.css +++ b/src/features/SlotDetails/slotDetails.module.css @@ -1,62 +1,3 @@ -.searchForm { - display: flex; - flex-direction: column; - width: 100%; - align-items: flex-start; - gap: 8px; -} - -.search-label { - font-size: 16px; - font-weight: 510; -} - -.search-field { - width: 100%; -} - -.error-text { - color: var(--failure-color); -} - -.quick-search-card { - flex-direction: column; - border-radius: 8px; - border: 1px solid var(--container-border-color); - background: var(--container-background-color); - color: var(--slot-details-quick-search-text-color); -} - -.quick-search-header { - color: var(--quick-search-color); - font-size: 18px; - - svg { - width: 32px; - height: 32px; - - fill: var(--quick-search-color); - } -} - -.quick-search-slot { - font-size: 12px; - - &.clickable { - cursor: pointer; - color: #5eb1ef; - } - - &:not(.clickable) { - cursor: not-allowed; - pointer-events: none; - } -} - -.quick-search-metric { - font-size: 12px; -} - .slot-name { font-size: 18px; font-weight: 600; diff --git a/src/features/SlotDetails/slotSearch.module.css b/src/features/SlotDetails/slotSearch.module.css new file mode 100644 index 00000000..6940f3b0 --- /dev/null +++ b/src/features/SlotDetails/slotSearch.module.css @@ -0,0 +1,58 @@ +.searchForm { + display: flex; + flex-direction: column; + width: 100%; + align-items: flex-start; + gap: 8px; +} + +.search-label { + font-size: 16px; + font-weight: 510; +} + +.search-field { + width: 100%; +} + +.error-text { + color: var(--failure-color); +} + +.quick-search-card { + flex-direction: column; + border-radius: 8px; + border: 1px solid var(--container-border-color); + background: var(--container-background-color); + color: var(--slot-details-quick-search-text-color); +} + +.quick-search-header { + color: var(--quick-search-color); + font-size: 18px; + + svg { + width: 32px; + height: 32px; + + fill: var(--quick-search-color); + } +} + +.quick-search-slot { + font-size: 12px; + + &.clickable { + cursor: pointer; + color: #5eb1ef; + } + + &:not(.clickable) { + cursor: not-allowed; + pointer-events: none; + } +} + +.quick-search-metric { + font-size: 12px; +} From 41d6ceeff8c449cf16672fb1faed572cdf8ff6a8 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Wed, 8 Oct 2025 07:51:40 -0500 Subject: [PATCH 4/7] fix: color tokens --- src/colors.ts | 15 ++++++++------- src/features/SlotDetails/SlotSearch.tsx | 8 ++++---- src/features/SlotDetails/slotSearch.module.css | 2 +- src/hooks/useQuickSearchWidth.ts | 7 ++++--- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/colors.ts b/src/colors.ts index bc4e2f90..0b0d677b 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -164,13 +164,14 @@ export const circularProgressPathColor = slotStatusBlue; // slot details export const slotDetailsMySlotsColor = "#0080e6"; -export const slotDetailsQuickSearchTextColor = "#8D8D8D"; -export const slotDetailsEarliestSlotColor = "#1CE7C2"; -export const slotDetailsRecentSlotColor = "#3DB9CF"; -export const slotDetailsSkippedSlotColor = "#EB6262"; -export const slotDetailsFeesSlotColor = "#7CE2FE"; -export const slotDetailsTipsSlotColor = "#5BB98B"; -export const slotDetailsRewardsSlotColor = "#8DA4EF"; +export const slotDetailsQuickSearchTextColor = "#7B7B7B"; +export const slotDetailsEarliestSlotColor = "#12A594"; +export const slotDetailsRecentSlotColor = "#00A2C7"; +export const slotDetailsSkippedSlotColor = "#B54548"; +export const slotDetailsFeesSlotColor = "#197CAE"; +export const slotDetailsTipsSlotColor = "#30A46C"; +export const slotDetailsRewardsSlotColor = "#5472E4"; +export const slotDetailsClickableSlotColor = "#0090FF"; export const slotDetailsBackgroundColor = "#15181e"; export const slotDetailsColor = "#9aabc3"; export const slotDetailsSkippedBackgroundColor = "#250f0f"; diff --git a/src/features/SlotDetails/SlotSearch.tsx b/src/features/SlotDetails/SlotSearch.tsx index 9825f95a..fbbb76f2 100644 --- a/src/features/SlotDetails/SlotSearch.tsx +++ b/src/features/SlotDetails/SlotSearch.tsx @@ -235,13 +235,13 @@ function QuickSearchSlots({ const fixedLengthSlots = useMemo( () => !slots - ? Array(numQuickSearchSlots).fill(undefined) + ? Array(numQuickSearchSlots).fill(undefined) : slots.length < numQuickSearchSlots ? [ ...slots, - ...Array( - numQuickSearchSlots - slots.length, - ).fill(undefined), + ...Array(numQuickSearchSlots - slots.length).fill( + undefined, + ), ] : slots.slice(0, numQuickSearchSlots), [slots], diff --git a/src/features/SlotDetails/slotSearch.module.css b/src/features/SlotDetails/slotSearch.module.css index 6940f3b0..6416e616 100644 --- a/src/features/SlotDetails/slotSearch.module.css +++ b/src/features/SlotDetails/slotSearch.module.css @@ -44,7 +44,7 @@ &.clickable { cursor: pointer; - color: #5eb1ef; + color: var(--slot-details-clickable-slot-color); } &:not(.clickable) { diff --git a/src/hooks/useQuickSearchWidth.ts b/src/hooks/useQuickSearchWidth.ts index 44d6f91b..adcc62db 100644 --- a/src/hooks/useQuickSearchWidth.ts +++ b/src/hooks/useQuickSearchWidth.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { useSlotsNavigation } from "./useSlotsNavigation"; import { numQuickSearchSlots, @@ -8,18 +8,19 @@ import { slotNavWithoutListWidth, slotSearchPadding, } from "../consts"; +import { useMount } from "react-use"; export function useQuickSearchWidth() { const [width, setWidth] = useState(window.innerWidth); const { showOnlyEpochBar, showNav } = useSlotsNavigation(); - useEffect(() => { + useMount(() => { function handleResize() { setWidth(window.innerWidth); } window.addEventListener("resize", handleResize); return () => window.removeEventListener("resize", handleResize); - }, []); + }); const outletWidth = showOnlyEpochBar ? width - slotNavWithoutListWidth From 8c3306e60739c18e76199ca41051ec15b8c61342 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Wed, 8 Oct 2025 10:35:31 -0500 Subject: [PATCH 5/7] fix: update width state less --- src/hooks/useQuickSearchWidth.ts | 62 +++++++++++++++++++------------- 1 file changed, 37 insertions(+), 25 deletions(-) diff --git a/src/hooks/useQuickSearchWidth.ts b/src/hooks/useQuickSearchWidth.ts index adcc62db..5aad8f28 100644 --- a/src/hooks/useQuickSearchWidth.ts +++ b/src/hooks/useQuickSearchWidth.ts @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useCallback, useEffect, useState } from "react"; import { useSlotsNavigation } from "./useSlotsNavigation"; import { numQuickSearchSlots, @@ -8,37 +8,49 @@ import { slotNavWithoutListWidth, slotSearchPadding, } from "../consts"; -import { useMount } from "react-use"; +import { throttle } from "lodash"; export function useQuickSearchWidth() { - const [width, setWidth] = useState(window.innerWidth); const { showOnlyEpochBar, showNav } = useSlotsNavigation(); + const getQuickSearchWidth = useCallback( + (width: number) => { + const outletWidth = showOnlyEpochBar + ? width - slotNavWithoutListWidth + : showNav + ? width - slotNavWidth + : width; - useMount(() => { - function handleResize() { - setWidth(window.innerWidth); - } - window.addEventListener("resize", handleResize); - return () => window.removeEventListener("resize", handleResize); - }); + const numCardsPerRow = Math.min( + numQuickSearchSlots, + Math.floor( + (outletWidth - 2 * slotSearchPadding + quickSearchSpacing) / + (quickSearchCardWidth + quickSearchSpacing), + ), + ); - const outletWidth = showOnlyEpochBar - ? width - slotNavWithoutListWidth - : showNav - ? width - slotNavWidth - : width; + return ( + numCardsPerRow * quickSearchCardWidth + + (numCardsPerRow - 1) * quickSearchSpacing + + 2 * slotSearchPadding + ); + }, + [showNav, showOnlyEpochBar], + ); - const numCardsPerRow = Math.min( - numQuickSearchSlots, - Math.floor( - (outletWidth - 2 * slotSearchPadding + quickSearchSpacing) / - (quickSearchCardWidth + quickSearchSpacing), - ), + const [quickSearchWidth, setQuickSearchWidth] = useState( + getQuickSearchWidth(window.innerWidth), ); - const quickSearchWidth = - numCardsPerRow * quickSearchCardWidth + - (numCardsPerRow - 1) * quickSearchSpacing + - 2 * slotSearchPadding; + + useEffect(() => { + const handleResize = throttle( + () => setQuickSearchWidth(getQuickSearchWidth(window.innerWidth)), + 50, + { leading: false, trailing: true }, + ); + window.addEventListener("resize", handleResize); + handleResize(); + return () => window.removeEventListener("resize", handleResize); + }, [getQuickSearchWidth]); return quickSearchWidth; } From 13466fc4d4941316a4e7f703686c4c845093e0a8 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Wed, 8 Oct 2025 12:36:17 -0500 Subject: [PATCH 6/7] fix: replace flex with grid --- src/colors.ts | 17 +++--- src/consts.ts | 6 +- src/features/SlotDetails/SlotSearch.tsx | 31 +++++----- src/features/SlotDetails/index.tsx | 2 +- .../SlotDetails/slotSearch.module.css | 5 +- src/hooks/useQuickSearchWidth.ts | 56 ------------------- 6 files changed, 30 insertions(+), 87 deletions(-) delete mode 100644 src/hooks/useQuickSearchWidth.ts diff --git a/src/colors.ts b/src/colors.ts index 0b0d677b..4d534a73 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -164,14 +164,15 @@ export const circularProgressPathColor = slotStatusBlue; // slot details export const slotDetailsMySlotsColor = "#0080e6"; -export const slotDetailsQuickSearchTextColor = "#7B7B7B"; -export const slotDetailsEarliestSlotColor = "#12A594"; -export const slotDetailsRecentSlotColor = "#00A2C7"; -export const slotDetailsSkippedSlotColor = "#B54548"; -export const slotDetailsFeesSlotColor = "#197CAE"; -export const slotDetailsTipsSlotColor = "#30A46C"; -export const slotDetailsRewardsSlotColor = "#5472E4"; -export const slotDetailsClickableSlotColor = "#0090FF"; +export const slotDetailsSearchLabelColor = "#FFF"; +export const slotDetailsQuickSearchTextColor = "var(--gray-10)"; +export const slotDetailsEarliestSlotColor = "var(--teal-9)"; +export const slotDetailsRecentSlotColor = "var(--cyan-9)"; +export const slotDetailsSkippedSlotColor = "var(--red-8)"; +export const slotDetailsFeesSlotColor = "var(--sky-8)"; +export const slotDetailsTipsSlotColor = "var(--green-9)"; +export const slotDetailsRewardsSlotColor = "var(--indigo-10)"; +export const slotDetailsClickableSlotColor = "var(--blue-9)"; export const slotDetailsBackgroundColor = "#15181e"; export const slotDetailsColor = "#9aabc3"; export const slotDetailsSkippedBackgroundColor = "#250f0f"; diff --git a/src/consts.ts b/src/consts.ts index abb8daf3..b61a4499 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -80,5 +80,9 @@ export const maxZIndex = 110; export const numQuickSearchSlots = 3; export const quickSearchCardWidth = 226; -export const quickSearchSpacing = 40; +export const slotSearchGap = 40; export const slotSearchPadding = 20; +export const slotSearchMaxWidth = + 2 * slotSearchPadding + + numQuickSearchSlots * quickSearchCardWidth + + (numQuickSearchSlots - 1) * slotSearchGap; diff --git a/src/features/SlotDetails/SlotSearch.tsx b/src/features/SlotDetails/SlotSearch.tsx index fbbb76f2..6ad534ba 100644 --- a/src/features/SlotDetails/SlotSearch.tsx +++ b/src/features/SlotDetails/SlotSearch.tsx @@ -12,13 +12,13 @@ import { baseSelectedSlotAtom, SelectedSlotValidityState, } from "../Overview/SlotPerformance/atoms"; -import { useQuickSearchWidth } from "../../hooks/useQuickSearchWidth"; import { Label } from "radix-ui"; -import { Flex, IconButton, TextField, Text } from "@radix-ui/themes"; +import { Flex, IconButton, TextField, Text, Grid } from "@radix-ui/themes"; import { numQuickSearchSlots, quickSearchCardWidth, - quickSearchSpacing, + slotSearchGap, + slotSearchMaxWidth, slotSearchPadding, } from "../../consts"; import styles from "./slotSearch.module.css"; @@ -50,7 +50,6 @@ export function SlotSearch() { const [searchSlot, setSearchSlot] = useState(""); const epoch = useAtomValue(epochAtom); const { isValid } = useAtomValue(baseSelectedSlotAtom); - const quickSearchWidth = useQuickSearchWidth(); const submitSearch = useCallback(() => { if (searchSlot === "") setSelectedSlot(undefined); @@ -63,14 +62,14 @@ export function SlotSearch() { }, [selectedSlot]); return ( -
{ @@ -92,6 +91,7 @@ export function SlotSearch() { onChange={(e) => setSearchSlot(e.target.value)} size="3" color={isValid ? "teal" : "red"} + autoFocus > @@ -102,7 +102,7 @@ export function SlotSearch() { {!isValid && } -
+ ); } @@ -121,12 +121,7 @@ function QuickSearch() { }, [processedLeaderSlots]); return ( - + <> } label="Earliest Slots" @@ -172,7 +167,7 @@ function QuickSearch() { metricsFmt={getSolStringWithFourDecimals} unit="SOL" /> - + ); } diff --git a/src/features/SlotDetails/index.tsx b/src/features/SlotDetails/index.tsx index ea566257..631cb724 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -36,7 +36,7 @@ export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); return ( - + {selectedSlot === undefined ? : } diff --git a/src/features/SlotDetails/slotSearch.module.css b/src/features/SlotDetails/slotSearch.module.css index 6416e616..4a6d5013 100644 --- a/src/features/SlotDetails/slotSearch.module.css +++ b/src/features/SlotDetails/slotSearch.module.css @@ -1,14 +1,14 @@ .searchForm { display: flex; flex-direction: column; - width: 100%; - align-items: flex-start; gap: 8px; + grid-column: 1 / -1; } .search-label { font-size: 16px; font-weight: 510; + color: var(--slot-details-search-label-color); } .search-field { @@ -34,7 +34,6 @@ svg { width: 32px; height: 32px; - fill: var(--quick-search-color); } } diff --git a/src/hooks/useQuickSearchWidth.ts b/src/hooks/useQuickSearchWidth.ts deleted file mode 100644 index 5aad8f28..00000000 --- a/src/hooks/useQuickSearchWidth.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { useCallback, useEffect, useState } from "react"; -import { useSlotsNavigation } from "./useSlotsNavigation"; -import { - numQuickSearchSlots, - quickSearchCardWidth, - quickSearchSpacing, - slotNavWidth, - slotNavWithoutListWidth, - slotSearchPadding, -} from "../consts"; -import { throttle } from "lodash"; - -export function useQuickSearchWidth() { - const { showOnlyEpochBar, showNav } = useSlotsNavigation(); - const getQuickSearchWidth = useCallback( - (width: number) => { - const outletWidth = showOnlyEpochBar - ? width - slotNavWithoutListWidth - : showNav - ? width - slotNavWidth - : width; - - const numCardsPerRow = Math.min( - numQuickSearchSlots, - Math.floor( - (outletWidth - 2 * slotSearchPadding + quickSearchSpacing) / - (quickSearchCardWidth + quickSearchSpacing), - ), - ); - - return ( - numCardsPerRow * quickSearchCardWidth + - (numCardsPerRow - 1) * quickSearchSpacing + - 2 * slotSearchPadding - ); - }, - [showNav, showOnlyEpochBar], - ); - - const [quickSearchWidth, setQuickSearchWidth] = useState( - getQuickSearchWidth(window.innerWidth), - ); - - useEffect(() => { - const handleResize = throttle( - () => setQuickSearchWidth(getQuickSearchWidth(window.innerWidth)), - 50, - { leading: false, trailing: true }, - ); - window.addEventListener("resize", handleResize); - handleResize(); - return () => window.removeEventListener("resize", handleResize); - }, [getQuickSearchWidth]); - - return quickSearchWidth; -} From 07486396a8bd2d0e222d31b310c45f85b4735c53 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Thu, 9 Oct 2025 16:15:55 -0500 Subject: [PATCH 7/7] fix: address feedback --- src/features/SlotDetails/SlotSearch.tsx | 220 ++++++++---------- src/features/SlotDetails/index.tsx | 2 +- .../SlotDetails/slotSearch.module.css | 8 - 3 files changed, 95 insertions(+), 135 deletions(-) diff --git a/src/features/SlotDetails/SlotSearch.tsx b/src/features/SlotDetails/SlotSearch.tsx index 6ad534ba..6a8f2350 100644 --- a/src/features/SlotDetails/SlotSearch.tsx +++ b/src/features/SlotDetails/SlotSearch.tsx @@ -1,10 +1,4 @@ -import { - useCallback, - useEffect, - useMemo, - useState, - type CSSProperties, -} from "react"; +import { useCallback, useMemo, useState, type CSSProperties } from "react"; import { useSlotSearchParam } from "./useSearchParams"; import { epochAtom, processedLeaderSlotsAtom } from "../../atoms"; import { useAtomValue } from "jotai"; @@ -47,7 +41,9 @@ import { useTimeAgo } from "../../hooks/useTimeAgo"; export function SlotSearch() { const { selectedSlot, setSelectedSlot } = useSlotSearchParam(); - const [searchSlot, setSearchSlot] = useState(""); + const [searchSlot, setSearchSlot] = useState( + selectedSlot === undefined ? "" : String(selectedSlot), + ); const epoch = useAtomValue(epochAtom); const { isValid } = useAtomValue(baseSelectedSlotAtom); @@ -56,11 +52,6 @@ export function SlotSearch() { else setSelectedSlot(Number(searchSlot)); }, [searchSlot, setSelectedSlot]); - useEffect(() => { - if (selectedSlot === undefined) setSearchSlot(""); - else setSearchSlot(String(selectedSlot)); - }, [selectedSlot]); - return ( -
{ - e.preventDefault(); - submitSearch(); - }} - className={styles.searchForm} - > - - Search Slot ID - - setSearchSlot(e.target.value)} - size="3" - color={isValid ? "teal" : "red"} - autoFocus + + { + e.preventDefault(); + submitSearch(); + }} > - - - - - - - {!isValid && } - + + Search Slot ID + + setSearchSlot(e.target.value)} + size="3" + color={isValid ? "teal" : "red"} + autoFocus + > + + + + + + + {!isValid && } + +
); @@ -114,11 +106,10 @@ function QuickSearch() { useSlotRankings(true); const slotRankings = useAtomValue(slotRankingsAtom); const processedLeaderSlots = useAtomValue(processedLeaderSlotsAtom); - const reversedProcessedLeaderSlots = useMemo(() => { - return processedLeaderSlots - ? [...processedLeaderSlots].reverse() - : undefined; - }, [processedLeaderSlots]); + const reversedProcessedLeaderSlots = useMemo( + () => processedLeaderSlots?.toReversed(), + [processedLeaderSlots], + ); return ( <> @@ -145,53 +136,61 @@ function QuickSearch() { label="Highest Fees" color={slotDetailsFeesSlotColor} slots={slotRankings?.slots_largest_fees} - metrics={slotRankings?.vals_largest_fees} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" + metricOptions={{ + metrics: slotRankings?.vals_largest_fees, + metricsFmt: getSolStringWithFourDecimals, + unit: "SOL", + }} /> } label="Highest Tips" color={slotDetailsTipsSlotColor} slots={slotRankings?.slots_largest_tips} - metrics={slotRankings?.vals_largest_tips} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" + metricOptions={{ + metrics: slotRankings?.vals_largest_tips, + metricsFmt: getSolStringWithFourDecimals, + unit: "SOL", + }} /> } label="Highest Rewards" color={slotDetailsRewardsSlotColor} slots={slotRankings?.slots_largest_rewards} - metrics={slotRankings?.vals_largest_rewards} - metricsFmt={getSolStringWithFourDecimals} - unit="SOL" + metricOptions={{ + metrics: slotRankings?.vals_largest_rewards, + metricsFmt: getSolStringWithFourDecimals, + unit: "SOL", + }} /> ); } +interface MetricOptions { + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; + unit?: string; +} + function QuickSearchCard({ icon, label, color, slots, - metrics, - metricsFmt, - unit, + metricOptions, }: { icon: React.ReactNode; label: string; color: string; slots?: number[]; - metrics?: T[]; - metricsFmt?: (m: T) => string | undefined; - unit?: string; + metricOptions?: MetricOptions; }) { return ( @@ -204,57 +203,27 @@ function QuickSearchCard({ {icon} {label} - +
); } function QuickSearchSlots({ slots, - metrics, - metricsFmt, - unit, + metricOptions, }: { slots?: number[]; - metrics?: T[]; - metricsFmt?: (m: T) => string | undefined; - unit?: string; + metricOptions?: MetricOptions; }) { const { setSelectedSlot } = useSlotSearchParam(); - const hasDefaultMetric = !metrics; - const fixedLengthSlots = useMemo( - () => - !slots - ? Array(numQuickSearchSlots).fill(undefined) - : slots.length < numQuickSearchSlots - ? [ - ...slots, - ...Array(numQuickSearchSlots - slots.length).fill( - undefined, - ), - ] - : slots.slice(0, numQuickSearchSlots), - [slots], - ); return ( - {fixedLengthSlots.map((slot, i) => { - const formattedMetric = - metrics && metrics[i] !== undefined - ? metricsFmt - ? metricsFmt(metrics[i]) - : metrics[i].toLocaleString() - : undefined; - + {Array.from({ length: numQuickSearchSlots }).map((_, i) => { + const slot = slots?.[i]; return ( - - {slot === undefined ? ( + + {slots === undefined ? ( -- ) : ( ({ {slot} )} - + + + ); })} @@ -277,30 +247,28 @@ function QuickSearchSlots({ ); } -function QuickSearchMetric({ - slot, - hasDefaultMetric, - formattedMetric, - unit, +function QuickSearchMetric({ + index, + slots, + metricOptions, }: { - slot?: number; - hasDefaultMetric: boolean; - formattedMetric?: string; - unit?: string; + index: number; + slots?: number[]; + metricOptions?: MetricOptions; }) { - let content; + if (slots?.[index] === undefined) return "--"; + if (!metricOptions) return ; - if (slot === undefined) { - content = "--"; - } else if (hasDefaultMetric) { - content = ; - } else if (formattedMetric === undefined) { - content = "--"; - } else { - content = unit ? `${formattedMetric} ${unit}` : formattedMetric; - } + const { metrics, metricsFmt, unit } = metricOptions; + const formattedMetric = + metrics && metrics[index] !== undefined + ? metricsFmt + ? metricsFmt(metrics[index]) + : metrics[index].toLocaleString() + : undefined; - return {content}; + if (formattedMetric === undefined) return "--"; + return unit ? `${formattedMetric} ${unit}` : formattedMetric; } function TimeAgo({ slot }: { slot: number }) { diff --git a/src/features/SlotDetails/index.tsx b/src/features/SlotDetails/index.tsx index 631cb724..d2faeef3 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -36,7 +36,7 @@ export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); return ( - + {selectedSlot === undefined ? : } diff --git a/src/features/SlotDetails/slotSearch.module.css b/src/features/SlotDetails/slotSearch.module.css index 4a6d5013..ee1ce93e 100644 --- a/src/features/SlotDetails/slotSearch.module.css +++ b/src/features/SlotDetails/slotSearch.module.css @@ -1,10 +1,3 @@ -.searchForm { - display: flex; - flex-direction: column; - gap: 8px; - grid-column: 1 / -1; -} - .search-label { font-size: 16px; font-weight: 510; @@ -20,7 +13,6 @@ } .quick-search-card { - flex-direction: column; border-radius: 8px; border: 1px solid var(--container-border-color); background: var(--container-background-color);