From ddb1d4a52370b98031e2c94a05f2f46dea91a97e Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Tue, 7 Oct 2025 15:17:03 -0500 Subject: [PATCH] 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 | 36 +- src/colors.ts | 14 +- src/features/Navigation/EpochSlider.tsx | 8 +- src/features/SlotDetails/SlotSearch.tsx | 319 ++++++++++++++++++ src/features/SlotDetails/index.tsx | 243 +------------ .../SlotDetails/slotDetails.module.css | 35 -- .../SlotDetails/slotSearch.module.css | 53 +++ src/hooks/useSlotRankings.ts | 15 + 12 files changed, 477 insertions(+), 289 deletions(-) create mode 100644 src/features/SlotDetails/SlotSearch.tsx create mode 100644 src/features/SlotDetails/slotSearch.module.css 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..ed215872 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -288,28 +288,30 @@ export const firstProcessedSlotAtom = atom((get) => { return startupProgress.ledger_max_slot + 1; }); -export const earliestProcessedSlotLeaderAtom = atom((get) => { - const firstProcessedSlot = get(firstProcessedSlotAtom); +export const firstProcessedLeaderIndexAtom = atom((get) => { const leaderSlots = get(leaderSlotsAtom); + const firstProcessedSlot = get(firstProcessedSlotAtom); - if (firstProcessedSlot === undefined || !leaderSlots?.length) return; - return leaderSlots.find((s) => s >= firstProcessedSlot); + if (!leaderSlots || firstProcessedSlot === undefined) return; + + const leaderIndex = leaderSlots.findIndex((s) => s >= firstProcessedSlot); + return leaderIndex !== -1 ? leaderIndex : undefined; }); -export const mostRecentSlotLeaderAtom = atom((get) => { - const earliestProcessedSlotLeader = get(earliestProcessedSlotLeaderAtom); +export const firstProcessedLeaderAtom = atom((get) => { const leaderSlots = get(leaderSlotsAtom); - const currentLeaderSlot = get(currentLeaderSlotAtom); - - if ( - earliestProcessedSlotLeader === undefined || - currentLeaderSlot === undefined || - !leaderSlots?.length - ) - return; - return leaderSlots.findLast( - (s) => earliestProcessedSlotLeader <= s && s <= currentLeaderSlot, - ); + const firstProcessedLeaderIndex = get(firstProcessedLeaderIndexAtom); + return firstProcessedLeaderIndex + ? leaderSlots?.[firstProcessedLeaderIndex] + : undefined; +}); + +export const lastProcessedLeaderAtom = atom((get) => { + const leaderSlots = get(leaderSlotsAtom); + const nextLeaderSlotIndex = get(nextLeaderSlotIndexAtom); + return nextLeaderSlotIndex + ? leaderSlots?.[nextLeaderSlotIndex - 1] + : undefined; }); const _currentSlotAtom = atom(undefined); diff --git a/src/colors.ts b/src/colors.ts index f9133d09..4d534a73 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -164,11 +164,15 @@ 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 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/features/Navigation/EpochSlider.tsx b/src/features/Navigation/EpochSlider.tsx index bad84f3f..b8f42769 100644 --- a/src/features/Navigation/EpochSlider.tsx +++ b/src/features/Navigation/EpochSlider.tsx @@ -8,7 +8,7 @@ import { epochAtom, firstProcessedSlotAtom, leaderSlotsAtom, - mostRecentSlotLeaderAtom, + lastProcessedLeaderAtom, SlotNavFilter, slotNavFilterAtom, slotOverrideAtom, @@ -228,7 +228,7 @@ function SliderEpochProgress({ setSliderValue: React.Dispatch>; }) { const epoch = useAtomValue(epochAtom); - const mostRecentSlotLeader = useAtomValue(mostRecentSlotLeaderAtom); + const lastProcessedLeader = useAtomValue(lastProcessedLeaderAtom); const currentLeaderSlot = useAtomValue(currentLeaderSlotAtom); const slotOverride = useAtomValue(slotOverrideAtom); const navFilter = useAtomValue(slotNavFilterAtom); @@ -270,7 +270,7 @@ function SliderEpochProgress({ }) : navFilter === SlotNavFilter.MySlots ? slotToEpochPct({ - slot: mostRecentSlotLeader, + slot: lastProcessedLeader, epochStartSlot: epoch?.start_slot, epochEndSlot: epoch?.end_slot, }) @@ -288,7 +288,7 @@ function SliderEpochProgress({ setSliderValue, slotOverride, navFilter, - mostRecentSlotLeader, + lastProcessedLeader, ]); return ( diff --git a/src/features/SlotDetails/SlotSearch.tsx b/src/features/SlotDetails/SlotSearch.tsx new file mode 100644 index 00000000..e145a04b --- /dev/null +++ b/src/features/SlotDetails/SlotSearch.tsx @@ -0,0 +1,319 @@ +import { useCallback, useMemo, useState, type CSSProperties } from "react"; +import { useSlotSearchParam } from "./useSearchParams"; +import { + epochAtom, + firstProcessedLeaderIndexAtom, + leaderSlotsAtom, + nextLeaderSlotIndexAtom, +} from "../../atoms"; +import { useAtomValue } from "jotai"; +import { + baseSelectedSlotAtom, + SelectedSlotValidityState, +} from "../Overview/SlotPerformance/atoms"; +import { Label } from "radix-ui"; +import { Flex, IconButton, TextField, Text, Grid } from "@radix-ui/themes"; + +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"; + +const numQuickSearchCardsPerRow = 3; +const quickSearchCardWidth = 226; +const slotSearchGap = 40; +const slotSearchPadding = 20; +const slotSearchMaxWidth = + 2 * slotSearchPadding + + numQuickSearchCardsPerRow * quickSearchCardWidth + + (numQuickSearchCardsPerRow - 1) * slotSearchGap; + +export function SlotSearch() { + const { selectedSlot, setSelectedSlot } = useSlotSearchParam(); + const [searchSlot, setSearchSlot] = useState( + selectedSlot === undefined ? "" : String(selectedSlot), + ); + const epoch = useAtomValue(epochAtom); + const { isValid } = useAtomValue(baseSelectedSlotAtom); + + const submitSearch = useCallback(() => { + if (searchSlot === "") setSelectedSlot(undefined); + else setSelectedSlot(Number(searchSlot)); + }, [searchSlot, setSelectedSlot]); + + return ( + + +
{ + e.preventDefault(); + submitSearch(); + }} + > + + Search Slot ID + + setSearchSlot(e.target.value)} + size="3" + color={isValid ? "teal" : "red"} + autoFocus + > + + + + + + + {!isValid && } + +
+ +
+ ); +} + +function getSolStringWithFourDecimals(lamportAmount: bigint) { + return `${getSolString(lamportAmount, 4)} SOL`; +} + +function QuickSearch() { + useSlotRankings(true); + const slotRankings = useAtomValue(slotRankingsAtom); + const leaderSlots = useAtomValue(leaderSlotsAtom); + const firstProcessedLeaderIndex = useAtomValue(firstProcessedLeaderIndexAtom); + const nextLeaderIndex = useAtomValue(nextLeaderSlotIndexAtom); + const earliestQuickSlots = useMemo( + () => + firstProcessedLeaderIndex !== undefined + ? leaderSlots?.slice(firstProcessedLeaderIndex) + : undefined, + [firstProcessedLeaderIndex, leaderSlots], + ); + const mostRecentQuickSlots = useMemo( + () => + nextLeaderIndex !== undefined + ? leaderSlots?.slice(0, nextLeaderIndex)?.toReversed() + : undefined, + [leaderSlots, nextLeaderIndex], + ); + + return ( + <> + } + label="Earliest Slots" + color={slotDetailsEarliestSlotColor} + slots={earliestQuickSlots} + /> + } + label="Most Recent Slots" + color={slotDetailsRecentSlotColor} + slots={mostRecentQuickSlots} + /> + } + label="Last Skipped Slots" + color={slotDetailsSkippedSlotColor} + slots={slotRankings?.slots_largest_skipped} + /> + } + label="Highest Fees" + color={slotDetailsFeesSlotColor} + slots={slotRankings?.slots_largest_fees} + metricOptions={{ + metrics: slotRankings?.vals_largest_fees, + metricsFmt: getSolStringWithFourDecimals, + }} + /> + } + label="Highest Tips" + color={slotDetailsTipsSlotColor} + slots={slotRankings?.slots_largest_tips} + metricOptions={{ + metrics: slotRankings?.vals_largest_tips, + metricsFmt: getSolStringWithFourDecimals, + }} + /> + } + label="Highest Rewards" + color={slotDetailsRewardsSlotColor} + slots={slotRankings?.slots_largest_rewards} + metricOptions={{ + metrics: slotRankings?.vals_largest_rewards, + metricsFmt: getSolStringWithFourDecimals, + }} + /> + + ); +} + +interface MetricOptions { + metrics?: T[]; + metricsFmt?: (m: T) => string | undefined; +} +interface QuickSearchCardProps { + icon: React.ReactNode; + label: string; + color: string; + slots?: number[]; + metricOptions?: MetricOptions; +} + +function QuickSearchCard({ + icon, + label, + color, + slots, + metricOptions, +}: QuickSearchCardProps) { + return ( + + + {icon} + {label} + + + + ); +} + +const numQuickSearchSlots = 3; + +function QuickSearchSlots({ + slots, + metricOptions, +}: { + slots?: number[]; + metricOptions?: MetricOptions; +}) { + const { setSelectedSlot } = useSlotSearchParam(); + + return ( + + {Array.from({ length: numQuickSearchSlots }).map((_, i) => { + const slot = slots?.[i]; + return ( + + {slot === undefined ? ( + -- + ) : ( + setSelectedSlot(slot)} + > + {slot} + + )} + + + + + ); + })} + + ); +} + +function QuickSearchMetric({ + slot, + metric, + metricsFmt, +}: { + slot?: number; + metric?: T; + metricsFmt?: MetricOptions["metricsFmt"]; +}) { + if (slot === undefined) return "--"; + if (!metricsFmt) return ; + if (metric === undefined) return "--"; + return metricsFmt(metric) ?? "--"; +} + +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 ca7cb7c9..3332b0cb 100644 --- a/src/features/SlotDetails/index.tsx +++ b/src/features/SlotDetails/index.tsx @@ -1,4 +1,4 @@ -import { Flex, TextField, Text, IconButton } from "@radix-ui/themes"; +import { Flex, Text } from "@radix-ui/themes"; import SlotPerformance from "../Overview/SlotPerformance"; import ComputeUnitsCard from "../Overview/SlotPerformance/ComputeUnitsCard"; import TransactionBarsCard from "../Overview/SlotPerformance/TransactionBarsCard"; @@ -7,19 +7,16 @@ import { useAtomValue, useSetAtom } from "jotai"; import { selectedSlotAtom, baseSelectedSlotAtom, - SelectedSlotValidityState, } from "../Overview/SlotPerformance/atoms"; -import type { FC, SVGProps } from "react"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo } from "react"; import { useMedia, useUnmount } from "react-use"; -import { MagnifyingGlassIcon } from "@radix-ui/react-icons"; import { useSlotInfo } from "../../hooks/useSlotInfo"; import styles from "./slotDetails.module.css"; import PeerIcon from "../../components/PeerIcon"; import { - earliestProcessedSlotLeaderAtom, + firstProcessedLeaderAtom, epochAtom, - mostRecentSlotLeaderAtom, + lastProcessedLeaderAtom, } from "../../atoms"; import { clusterIndicatorHeight, @@ -31,31 +28,18 @@ import { useSlotQueryPublish } from "../../hooks/useSlotQuery"; import { getLeaderSlots, getSlotGroupLeader } from "../../utils"; import { SkippedIcon, StatusIcon } from "../../components/StatusIcon"; import clsx from "clsx"; -import { - slotDetailsEarliestSlotColor, - slotDetailsLastSkippedSlotColor, - slotDetailsMostRecentSlotColor, - slotDetailsQuickSearchTextPrimaryColor, - slotDetailsQuickSearchTextSecondaryColor, -} from "../../colors"; - -import skippedIcon from "../../assets/Skipped.svg?react"; -import history from "../../assets/history.svg?react"; -import checkFill from "../../assets/checkFill.svg?react"; - -import { skippedSlotsAtom } from "../../api/atoms"; -import { useTimeAgo } from "../../hooks/useTimeAgo"; import SlotClient from "../../components/SlotClient"; import { Link } from "@tanstack/react-router"; +import { SlotSearch } from "./SlotSearch"; export default function SlotDetails() { const selectedSlot = useAtomValue(selectedSlotAtom); return ( - + <> {selectedSlot === undefined ? : } - + ); } @@ -76,210 +60,13 @@ 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 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(); - }} - > - setSearchSlot(e.target.value)} - size="3" - color={isValid ? "teal" : "red"} - > - - - - - - -
- {!isValid && } - -
- ); -} - -function QuickSearch() { - return ( - - - - - - - - ); -} - -function EarliestProcessedSlotSearch() { - const earliestProcessedSlotLeader = useAtomValue( - earliestProcessedSlotLeaderAtom, - ); - return ( - - ); -} - -function MostRecentSlotSearch() { - const mostRecentSlotLeader = useAtomValue(mostRecentSlotLeaderAtom); - - return ( - - ); -} - -function LastSkippedSlotSearch() { - const skippedSlots = useAtomValue(skippedSlotsAtom); - const slot = useMemo( - () => (skippedSlots ? skippedSlots[skippedSlots?.length - 1] : undefined), - [skippedSlots], - ); - - return ( - - ); -} - -function QuickSearchCard({ - Icon, - label, - color, - slot, - disabled = false, -}: { - Icon: FC>; - label: string; - color: string; - slot?: number; - disabled?: boolean; -}) { - const { setSelectedSlot } = useSlotSearchParam(); - - return ( - setSelectedSlot(slot)} - aria-disabled={disabled} - > - - - {label} - - - - {slot ?? "--"} - - {slot && } - - - ); -} - -function TimeAgo({ slot }: { slot: number }) { - const { timeAgoText } = useTimeAgo(slot, { - showOnlyTwoSignificantUnits: true, - }); - - return ( - - {timeAgoText} - - ); -} - const navigationTop = clusterIndicatorHeight + headerHeight; function SlotNavigation({ slot }: { slot: number }) { const { pubkey } = useSlotInfo(slot); const epoch = useAtomValue(epochAtom); - const earliestProcessedSlotLeader = useAtomValue( - earliestProcessedSlotLeaderAtom, - ); - const mostRecentSlotLeader = useAtomValue(mostRecentSlotLeaderAtom); + const firstProcessedLeader = useAtomValue(firstProcessedLeaderAtom); + const lastProcessedLeader = useAtomValue(lastProcessedLeaderAtom); const { previousSlotGroupLeader, @@ -302,15 +89,15 @@ function SlotNavigation({ slot }: { slot: number }) { : undefined; const isPreviousDisabled = previousSlotGroupLastSlot === undefined || - earliestProcessedSlotLeader === undefined || - previousSlotGroupLastSlot < earliestProcessedSlotLeader; + firstProcessedLeader === undefined || + previousSlotGroupLastSlot < firstProcessedLeader; const nextSlotGroupLeader = leaderSlotsForValidator[slotIndexForValidator + 1]; const isNextDisabled = nextSlotGroupLeader === undefined || - mostRecentSlotLeader === undefined || - mostRecentSlotLeader + slotsPerLeader <= nextSlotGroupLeader; + lastProcessedLeader === undefined || + lastProcessedLeader + slotsPerLeader <= nextSlotGroupLeader; return { previousSlotGroupLeader, @@ -319,7 +106,7 @@ function SlotNavigation({ slot }: { slot: number }) { nextSlotGroupLeader, isNextDisabled, }; - }, [earliestProcessedSlotLeader, epoch, mostRecentSlotLeader, pubkey, slot]); + }, [firstProcessedLeader, epoch, lastProcessedLeader, pubkey, slot]); return ( + diff --git a/src/features/SlotDetails/slotDetails.module.css b/src/features/SlotDetails/slotDetails.module.css index 03b1850d..084d46b6 100644 --- a/src/features/SlotDetails/slotDetails.module.css +++ b/src/features/SlotDetails/slotDetails.module.css @@ -1,38 +1,3 @@ -.search { - text-align: center; -} - -.error-text { - color: var(--failure-color); -} - -.clickable { - cursor: pointer; -} - -.quick-search-row { - justify-content: center; - align-items: center; - flex-wrap: wrap; - gap: 8px; -} - -.quick-search { - width: 164px; - padding: 15px; - flex-direction: column; - gap: 10px; - border-radius: 8px; - border: 1px solid var(--container-border-color); - background: var(--container-background-color); - - &:not(.clickable) { - cursor: not-allowed; - pointer-events: none; - opacity: 0.6; - } -} - .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..a371243d --- /dev/null +++ b/src/features/SlotDetails/slotSearch.module.css @@ -0,0 +1,53 @@ +.search-grid { + align-content: center; +} + +.search-label { + font-size: 16px; + font-weight: 500; + color: var(--slot-details-search-label-color); +} + +.search-field { + width: 100%; +} + +.error-text { + color: var(--failure-color); +} + +.quick-search-card { + 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: var(--slot-details-clickable-slot-color); + } + + &:not(.clickable) { + cursor: not-allowed; + pointer-events: none; + } +} + +.quick-search-metric { + font-size: 12px; +} 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); +}