From 1592a7ecf1a6423663b5de3c30753eec23232306 Mon Sep 17 00:00:00 2001 From: Amy Liu Date: Mon, 11 Aug 2025 10:35:01 -0500 Subject: [PATCH 01/34] feat: add vertical navigation --- package-lock.json | 65 ++- package.json | 2 + src/App.tsx | 1 + src/api/useSetAtomWsData.ts | 3 - src/app.css | 4 +- src/assets/anza_logo.svg | 5 + src/assets/frankendancer_logo.svg | 170 ++++++ src/atoms.ts | 180 ++++-- src/colors.ts | 7 + src/components/PeerIcon.tsx | 30 +- src/components/SlotClient.tsx | 24 + src/components/StatusIcon.tsx | 96 +++ src/consts.ts | 5 + src/features/EpochBar/EpochBarLive.tsx | 94 --- src/features/EpochBar/EpochBounds.tsx | 55 -- src/features/EpochBar/EpochSlider.tsx | 390 ------------- src/features/EpochBar/NavigateNext.tsx | 25 - src/features/EpochBar/NavigatePrev.tsx | 25 - src/features/EpochBar/epochBar.module.css | 11 - src/features/EpochBar/epochBarLive.module.css | 19 - src/features/EpochBar/epochBounds.module.css | 11 - src/features/EpochBar/index.tsx | 40 -- src/features/Gossip/index.tsx | 7 +- src/features/Header/Cluster.tsx | 48 +- src/features/Header/ClusterIndicator.tsx | 11 - src/features/Header/DropDownNavLinks.tsx | 53 -- src/features/Header/IdentityKey.tsx | 23 +- src/features/Header/Logo.tsx | 8 +- src/features/Header/MenuNavLinks.tsx | 21 - src/features/Header/Nav.tsx | 159 +++++ src/features/Header/NavLink.tsx | 18 - src/features/Header/NavLinks.tsx | 25 - src/features/Header/cluster.module.css | 24 +- .../Header/clusterIndicator.module.css | 7 - .../Header/dropDownNavLinks.module.css | 84 --- src/features/Header/header.module.css | 17 - src/features/Header/identityKey.module.css | 11 +- src/features/Header/index.tsx | 58 +- src/features/Header/menuNavLinks.module.css | 7 - src/features/Header/nav.module.css | 164 ++++++ src/features/Header/navlink.module.css | 15 - src/features/Header/util.ts | 30 - .../Slots/CardValidatorSummary.tsx | 31 +- .../LeaderSchedule/Slots/CurrentSlotCard.tsx | 8 +- .../LeaderSchedule/Slots/PastSlotCard.tsx | 8 +- .../LeaderSchedule/Slots/ResetLive.tsx | 14 +- .../LeaderSchedule/Slots/SlotCardGrid.tsx | 90 +-- .../LeaderSchedule/Slots/SlotCardList.tsx | 5 +- .../LeaderSchedule/Slots/UpcomingSlotCard.tsx | 10 +- .../LeaderSchedule/Slots/resetLive.module.css | 15 +- src/features/LeaderSchedule/index.tsx | 2 +- src/features/Navigation/EpochBar.tsx | 545 ++++++++++++++++++ src/features/Navigation/ResetLive.tsx | 29 + src/features/Navigation/Scrollbar.tsx | 51 ++ src/features/Navigation/SlotsList.tsx | 142 +++++ src/features/Navigation/SlotsRenderer.tsx | 355 ++++++++++++ src/features/Navigation/Status.tsx | 74 +++ .../epochBar.module.css} | 81 +-- src/features/Navigation/index.tsx | 116 ++++ src/features/Navigation/resetLive.module.css | 17 + src/features/Navigation/scrollbar.module.css | 11 + src/features/Navigation/slotsList.module.css | 3 + .../Navigation/slotsRenderer.module.css | 266 +++++++++ src/features/Navigation/status.module.css | 15 + .../ComputeUnitsCard/index.tsx | 2 +- .../Overview/SlotPerformance/ResetLive.tsx | 29 - .../Overview/SlotPerformance/SlotSelector.tsx | 316 ---------- .../BarsChartContainer.tsx | 2 +- .../TransactionBarsCard/index.tsx | 12 +- .../Overview/SlotPerformance/atoms.ts | 14 +- .../Overview/SlotPerformance/index.tsx | 4 - .../SlotPerformance/navigate.module.css | 1 - .../SlotPerformance/resetLive.module.css | 15 - .../SlotPerformance/slotSelector.module.css | 73 --- src/features/Overview/index.tsx | 38 +- src/features/Overview/overview.module.css | 22 - src/features/Overview/useSearchParams.ts | 24 - src/features/SlotDetails/index.tsx | 226 +++++++- .../SlotDetails/slotDetails.module.css | 47 ++ src/features/SlotDetails/useSearchParams.ts | 19 + src/hooks/useCurrentRoute.ts | 23 + src/hooks/useSlotInfo.ts | 21 + src/hooks/useSlotQuery.ts | 7 +- src/routes/__root.tsx | 43 +- src/routes/index.tsx | 7 - src/utils.ts | 20 +- 86 files changed, 3058 insertions(+), 1847 deletions(-) create mode 100644 src/assets/anza_logo.svg create mode 100644 src/assets/frankendancer_logo.svg create mode 100644 src/components/SlotClient.tsx create mode 100644 src/components/StatusIcon.tsx delete mode 100644 src/features/EpochBar/EpochBarLive.tsx delete mode 100644 src/features/EpochBar/EpochBounds.tsx delete mode 100644 src/features/EpochBar/EpochSlider.tsx delete mode 100644 src/features/EpochBar/NavigateNext.tsx delete mode 100644 src/features/EpochBar/NavigatePrev.tsx delete mode 100644 src/features/EpochBar/epochBar.module.css delete mode 100644 src/features/EpochBar/epochBarLive.module.css delete mode 100644 src/features/EpochBar/epochBounds.module.css delete mode 100644 src/features/EpochBar/index.tsx delete mode 100644 src/features/Header/ClusterIndicator.tsx delete mode 100644 src/features/Header/DropDownNavLinks.tsx delete mode 100644 src/features/Header/MenuNavLinks.tsx create mode 100644 src/features/Header/Nav.tsx delete mode 100644 src/features/Header/NavLink.tsx delete mode 100644 src/features/Header/NavLinks.tsx delete mode 100644 src/features/Header/clusterIndicator.module.css delete mode 100644 src/features/Header/dropDownNavLinks.module.css delete mode 100644 src/features/Header/header.module.css delete mode 100644 src/features/Header/menuNavLinks.module.css create mode 100644 src/features/Header/nav.module.css delete mode 100644 src/features/Header/navlink.module.css delete mode 100644 src/features/Header/util.ts create mode 100644 src/features/Navigation/EpochBar.tsx create mode 100644 src/features/Navigation/ResetLive.tsx create mode 100644 src/features/Navigation/Scrollbar.tsx create mode 100644 src/features/Navigation/SlotsList.tsx create mode 100644 src/features/Navigation/SlotsRenderer.tsx create mode 100644 src/features/Navigation/Status.tsx rename src/features/{EpochBar/epochSlider.module.css => Navigation/epochBar.module.css} (59%) create mode 100644 src/features/Navigation/index.tsx create mode 100644 src/features/Navigation/resetLive.module.css create mode 100644 src/features/Navigation/scrollbar.module.css create mode 100644 src/features/Navigation/slotsList.module.css create mode 100644 src/features/Navigation/slotsRenderer.module.css create mode 100644 src/features/Navigation/status.module.css delete mode 100644 src/features/Overview/SlotPerformance/ResetLive.tsx delete mode 100644 src/features/Overview/SlotPerformance/SlotSelector.tsx delete mode 100644 src/features/Overview/SlotPerformance/resetLive.module.css delete mode 100644 src/features/Overview/SlotPerformance/slotSelector.module.css delete mode 100644 src/features/Overview/overview.module.css delete mode 100644 src/features/Overview/useSearchParams.ts create mode 100644 src/features/SlotDetails/slotDetails.module.css create mode 100644 src/features/SlotDetails/useSearchParams.ts create mode 100644 src/hooks/useCurrentRoute.ts create mode 100644 src/hooks/useSlotInfo.ts diff --git a/package-lock.json b/package-lock.json index 48e335a..cbd06d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@ag-grid-community/csv-export": "^32.2.0", "@ag-grid-community/react": "^32.2.0", "@ag-grid-community/styles": "^32.2.0", + "@floating-ui/react": "^0.27.15", "@fontsource/inter-tight": "^5.2.5", "@fontsource/roboto-mono": "^5.1.0", "@nivo/core": "^0.88.0", @@ -43,6 +44,7 @@ "react-intersection-observer": "^9.13.1", "react-use": "^17.5.1", "react-virtualized-auto-sizer": "^1.0.24", + "react-virtuoso": "^4.13.0", "uplot": "^1.6.32", "use-debounce": "^10.0.3", "zod": "^3.23.8" @@ -1009,31 +1011,46 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.6.9", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", - "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", - "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.16", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.16.tgz", + "integrity": "sha512-9O8N4SeG2z++TSM8QA/KTeKFBVCNEz/AGS7gWPJf6KFRzmRWixFRnCnkPHRDwSVZW6QPDO6uT0P2SpWNKCc9/g==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.6", + "@floating-ui/utils": "^0.2.10", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -1041,9 +1058,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@fontsource/inter-tight": { @@ -8652,6 +8669,16 @@ "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-virtuoso": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.14.0.tgz", + "integrity": "sha512-fR+eiCvirSNIRvvCD7ueJPRsacGQvUbjkwgWzBZXVq+yWypoH7mRUvWJzGHIdoRaCZCT+6mMMMwIG2S1BW3uwA==", + "license": "MIT", + "peerDependencies": { + "react": ">=16 || >=17 || >= 18 || >= 19", + "react-dom": ">=16 || >=17 || >= 18 || >=19" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -9739,6 +9766,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/throttle-debounce": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", diff --git a/package.json b/package.json index 05235f4..3e77806 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "@ag-grid-community/csv-export": "^32.2.0", "@ag-grid-community/react": "^32.2.0", "@ag-grid-community/styles": "^32.2.0", + "@floating-ui/react": "^0.27.15", "@fontsource/inter-tight": "^5.2.5", "@fontsource/roboto-mono": "^5.1.0", "@nivo/core": "^0.88.0", @@ -53,6 +54,7 @@ "react-intersection-observer": "^9.13.1", "react-use": "^17.5.1", "react-virtualized-auto-sizer": "^1.0.24", + "react-virtuoso": "^4.13.0", "uplot": "^1.6.32", "use-debounce": "^10.0.3", "zod": "^3.23.8" diff --git a/src/App.tsx b/src/App.tsx index b999e39..897442f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ export default function App() { return ( { - deleteSlotStatusBounds(); deleteSlotResponseBounds(); if (epoch) { diff --git a/src/app.css b/src/app.css index 9c83e38..d7b3dee 100644 --- a/src/app.css +++ b/src/app.css @@ -13,10 +13,10 @@ --green-live: #3cff73; --header-row-height: 34px; - /* headder height + epoch bar height + 10 padding top/bottom */ - --header-height: 121px; + --header-height: 55px; font-variant-numeric: tabular-nums; + overflow: hidden; } .rt-TooltipContent { diff --git a/src/assets/anza_logo.svg b/src/assets/anza_logo.svg new file mode 100644 index 0000000..e961a6e --- /dev/null +++ b/src/assets/anza_logo.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/assets/frankendancer_logo.svg b/src/assets/frankendancer_logo.svg new file mode 100644 index 0000000..8cc7538 --- /dev/null +++ b/src/assets/frankendancer_logo.svg @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/atoms.ts b/src/atoms.ts index 861d08f..4eccd65 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -16,11 +16,12 @@ import type { SlotResponse, } from "./api/types"; import { merge } from "lodash"; -import { getLeaderSlots, getStake } from "./utils"; +import { getLeaderSlots, getSlotGroupLeader, getStake } from "./utils"; import { searchLeaderSlotsAtom } from "./features/LeaderSchedule/atoms"; import { selectedSlotAtom } from "./features/Overview/SlotPerformance/atoms"; export const containerElAtom = atom(); +export const slotsListElAtom = atom(); const _epochsAtom = atomWithImmer([]); export const epochAtom = atom( @@ -55,28 +56,68 @@ export const nextEpochAtom = atom((get) => { return nextEpoch; }); -/** The first slot of the group of 4 slots for override slot leader */ -const _slotOverrideAtom = atom(undefined); -export const slotOverrideAtom = atom( - (get) => get(_slotOverrideAtom), - (get, set, param?: number | ((prev?: number) => number | undefined)) => { - const epoch = get(epochAtom); - if (!epoch) return; - - const newValue = - typeof param === "function" ? param(get(_slotOverrideAtom)) : param; - let startOverrideSlot = newValue ? newValue - (newValue % 4) : newValue; +export const [slotOverrideAtom, setSlotScrollListFnAtom, autoScrollAtom] = + (function getSlotOverrideAtom() { + const _slotOverrideAtom = atom(); + const _scrollSlotListFn = atom<{ fn?: (slot: number | undefined) => void }>( + {}, + ); - if (startOverrideSlot !== undefined) { - startOverrideSlot = Math.min( - epoch.end_slot, - Math.max(startOverrideSlot, epoch.start_slot), - ); - } + let rafId: number | undefined = undefined; + + return [ + atom( + (get) => get(_slotOverrideAtom), + ( + get, + set, + slot: number | undefined, + skipScrollList: boolean = false, + scrollIndexOffset: number = 1, + ) => { + const epoch = get(epochAtom); + if (!epoch) return; + + let startOverrideSlot = slot ? getSlotGroupLeader(slot) : slot; + let scrollSlot = startOverrideSlot; + + if (startOverrideSlot !== undefined) { + startOverrideSlot = Math.min( + epoch.end_slot, + Math.max(startOverrideSlot, epoch.start_slot), + ); + scrollSlot = Math.min( + epoch.end_slot, + Math.max( + startOverrideSlot + (scrollIndexOffset ?? 0) * slotsPerLeader, + epoch.start_slot, + ), + ); + } - set(_slotOverrideAtom, startOverrideSlot); - }, -); + set(_slotOverrideAtom, startOverrideSlot); + if (!skipScrollList) { + const { fn } = get(_scrollSlotListFn); + if (rafId) cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(() => { + fn?.(scrollSlot); + }); + } + }, + ), + atom( + null, + ( + _, + set, + scrollFn: ((slot: number | undefined) => void) | undefined, + ) => { + set(_scrollSlotListFn, { fn: scrollFn }); + }, + ), + atom((get) => get(_slotOverrideAtom) === undefined), + ]; + })(); const slotStatusAtom = atomWithImmer>({}); @@ -181,7 +222,7 @@ export const deleteSlotResponseBoundsAtom = atom(null, (get, set) => { for (const cachedSlot of cachedSlots) { const numberVal = Number(cachedSlot); if (searchSlots?.length) { - const slotGroupStart = numberVal - (numberVal % slotsPerLeader); + const slotGroupStart = getSlotGroupLeader(numberVal); if (searchSlots.includes(slotGroupStart)) { continue; } @@ -240,23 +281,6 @@ export const nextEpochLeaderSlotsAtom = atom((get) => { return getLeaderSlots(epoch, pubkey); }); -// let _skippedSlots: number[] | undefined = undefined; -// /** In order array of your skipped leader slots */ -// export const skippedSlotsAtom = atom((get) => { -// if (_skippedSlots) return _skippedSlots; - -// const leaderSlots = get(leaderSlotsAtom); -// const currentSlot = get(currentLeaderSlotAtom); -// const skippedSlots = leaderSlots?.filter( -// (s) => Math.random() > 0.96 && s < (currentSlot ?? 0) -// ); - -// if (skippedSlots?.length ?? 0 > 1) { -// _skippedSlots = skippedSlots; -// } -// return _skippedSlots; -// }); - export const nextLeaderSlotIndexAtom = atom(undefined); /** Next slot you are leader. Once a leader slot is reached, the next starting leader group of 4 is calculated before your current group of 4 finishes */ export const nextLeaderSlotAtom = atom( @@ -313,17 +337,17 @@ export const isCurrentlyLeaderAtom = atom((get) => { return false; }); -/** The first slot of the group of 4 slots for current leader */ +/** The first slot of the group of slots for current leader */ export const currentLeaderSlotAtom = atom((get) => { const currentSlot = get(currentSlotAtom); if (currentSlot == null) return; - return currentSlot - (currentSlot % 4); + return getSlotGroupLeader(currentSlot); }); export const peersAtom = atomWithImmer>({}); -export const addPeersAtom = atom(null, (get, set, peers?: Peer[]) => { +export const addPeersAtom = atom(null, (_, set, peers?: Peer[]) => { if (!peers?.length) return; set(peersAtom, (draft) => { @@ -333,7 +357,7 @@ export const addPeersAtom = atom(null, (get, set, peers?: Peer[]) => { }); }); -export const updatePeersAtom = atom(null, (get, set, peers?: Peer[]) => { +export const updatePeersAtom = atom(null, (_, set, peers?: Peer[]) => { if (!peers?.length) return; set(peersAtom, (draft) => { @@ -344,7 +368,7 @@ export const updatePeersAtom = atom(null, (get, set, peers?: Peer[]) => { }); const removePeerDelay = 60_000 * 5; -export const removePeersAtom = atom(null, (get, set, peers?: PeerRemove[]) => { +export const removePeersAtom = atom(null, (_, set, peers?: PeerRemove[]) => { if (!peers?.length) return; set(peersAtom, (draft) => { @@ -459,6 +483,58 @@ export const getSlotStateAtom = (slot?: number) => return "past"; }); +export const getIsCurrentLeaderAtom = (slot?: number) => + atom((get) => { + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; + + return ( + currentLeaderSlot <= slot && slot < currentLeaderSlot + slotsPerLeader + ); + }); + +export const getIsPreviousLeaderAtom = (slot?: number) => + atom((get) => { + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; + + return ( + currentLeaderSlot - slotsPerLeader <= slot && slot < currentLeaderSlot + ); + }); + +export const getIsNextLeaderAtom = (slot?: number) => + atom((get) => { + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; + + return ( + currentLeaderSlot + slotsPerLeader <= slot && + slot < currentLeaderSlot + 2 * slotsPerLeader + ); + }); + +export const getIsFutureLeaderAtom = (slot?: number) => + atom((get) => { + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; + + return currentLeaderSlot + slotsPerLeader <= slot; + }); + +export const getIsPastLeaderAtom = (slot?: number) => + atom((get) => { + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; + + return slot < currentLeaderSlot; + }); + export const getIsFutureSlotAtom = (slot?: number) => atom((get) => { if (slot === undefined) return true; @@ -506,3 +582,19 @@ export const skipRateAtom = atom( }); }, ); + +export type Status = "Live" | "Past" | "Current" | "Future"; +export const statusAtom = atom((get) => { + const currentSlot = get(currentSlotAtom); + if (currentSlot === undefined) return null; + + const slotOverride = get(slotOverrideAtom); + if (slotOverride === undefined) return "Live"; + + if (getSlotGroupLeader(slotOverride) === getSlotGroupLeader(currentSlot)) + return "Current"; + + if (slotOverride > currentSlot) return "Future"; + + return "Past"; +}); diff --git a/src/colors.ts b/src/colors.ts index 73535e3..5bc73f1 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -138,3 +138,10 @@ export const circularProgressPathColor = "#0051DF"; // gossip export const gossipDelinquentPubkeyColor = "#6D6F71"; + +// slot status +export const slotStatusRed = "#871616"; +export const slotStatusGreen = "#1d863b"; +export const slotStatusBlue = "#1d6286"; +export const slotStatusTeal = "#1ce7c2"; +export const slotStatusNone = "transparent"; diff --git a/src/components/PeerIcon.tsx b/src/components/PeerIcon.tsx index ff33c75..cbd9140 100644 --- a/src/components/PeerIcon.tsx +++ b/src/components/PeerIcon.tsx @@ -1,7 +1,8 @@ +import type { CSSProperties } from "react"; import { useState } from "react"; import privateIcon from "../assets/private.svg"; import privateYouIcon from "../assets/privateYou.svg"; -import { Tooltip } from "@radix-ui/themes"; +import { Box, Tooltip } from "@radix-ui/themes"; import { useAtom } from "jotai"; import { getPeerIconHasErrorIcon } from "./peerIconAtom"; import styles from "./peerIcon.module.css"; @@ -12,6 +13,7 @@ interface PeerIconProps { isYou?: boolean; size: number; hideFallback?: boolean; + style?: CSSProperties; } export default function PeerIcon({ @@ -19,6 +21,7 @@ export default function PeerIcon({ size, hideFallback, isYou, + style, }: PeerIconProps) { const [globalHasError, setGlobalHasError] = useAtom( getPeerIconHasErrorIcon(url), @@ -26,26 +29,23 @@ export default function PeerIcon({ const [hasError, setHasError] = useState(globalHasError); const [hasLoaded, setHasLoaded] = useState(false); + const iconStyles: CSSProperties = { + ...(style ?? {}), + height: `${size}px`, + width: `${size}px`, + }; + if (!url || hasError) { if (hideFallback) { - return; + return ; } else if (isYou) { return ( - + ); } else { - return ( - private - ); + return private; } } @@ -58,14 +58,14 @@ export default function PeerIcon({ <> setHasLoaded(true)} src={url} /> private diff --git a/src/components/SlotClient.tsx b/src/components/SlotClient.tsx new file mode 100644 index 0000000..b6568d1 --- /dev/null +++ b/src/components/SlotClient.tsx @@ -0,0 +1,24 @@ +import { useSlotInfo } from "../hooks/useSlotInfo"; +import AnzaLogo from "../assets/anza_logo.svg"; +import FrankendancerLogo from "../assets/frankendancer_logo.svg"; + +export function SlotClient({ + slot, + size = "11px", +}: { + slot: number; + size?: string; +}) { + const { client } = useSlotInfo(slot); + if (!client) return; + return client === "Frankendancer" ? ( + Frankendancer Logo + ) : ( + Anza Logo + ); +} diff --git a/src/components/StatusIcon.tsx b/src/components/StatusIcon.tsx new file mode 100644 index 0000000..0c87f42 --- /dev/null +++ b/src/components/StatusIcon.tsx @@ -0,0 +1,96 @@ +import { Flex, Tooltip } from "@radix-ui/themes"; +import { useAtomValue } from "jotai"; +import { useMemo, useRef, useState } from "react"; +import { getSlotStatus, slotDurationAtom } from "../atoms"; +import { buildStyles, CircularProgressbar } from "react-circular-progressbar"; +import { useRafLoop } from "react-use"; + +import processedIcon from "../assets/checkOutline.svg"; +import optimisticalyConfirmedIcon from "../assets/checkFill.svg"; +import rootedIcon from "../assets/Rooted.svg"; +import { + circularProgressPathColor, + circularProgressTrailColor, +} from "../colors"; + +export function StatusIcon({ + slot, + isCurrent, + iconSize = "14px", +}: { + slot: number; + isCurrent: boolean; + iconSize?: string; +}) { + const status = useAtomValue(useMemo(() => getSlotStatus(slot), [slot])); + const iconStyle = useMemo( + () => ({ + width: iconSize, + height: iconSize, + }), + [iconSize], + ); + + if (isCurrent) return ; + + if (status === "incomplete") return
; + + if (status === "optimistically_confirmed") { + return ( + + optimistically_confirmed + + ); + } + + if (status === "rooted" || status === "finalized") { + return ( + + rooted + + ); + } + + return ( + + processed + + ); +} + +export const LoadingIcon = ({ + iconSize = "14px", + strokeWidth = 25, +}: { + iconSize?: string; + strokeWidth?: number; +}) => { + const startRef = useRef(performance.now()); + const slotDuration = useAtomValue(slotDurationAtom); + const [progress, setProgress] = useState(0); + + useRafLoop(() => { + if (progress >= 100) return; + + const diff = performance.now() - startRef.current; + const newProgress = Math.min(Math.floor((diff / slotDuration) * 100), 100); + setProgress(newProgress); + }); + + return ( + + + + ); +}; diff --git a/src/consts.ts b/src/consts.ts index 9a993c0..d7796b7 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,7 +1,12 @@ export const slotsPerLeader = 4; +export const slotsListFutureSlotsCount = 5; +export const scheduleUpcomingSlotsCount = 3; export const lamportsPerSol = 1_000_000_000; /** Max compute units is dynamic and pulled from the server, * this default should only be used as a fallback */ export const defaultMaxComputeUnits = 50_000_000; + +export const epochBarWidth = 35; +export const slotsListWidth = 130; diff --git a/src/features/EpochBar/EpochBarLive.tsx b/src/features/EpochBar/EpochBarLive.tsx deleted file mode 100644 index c581b69..0000000 --- a/src/features/EpochBar/EpochBarLive.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import { atom, useAtomValue, useSetAtom } from "jotai"; -import styles from "./epochBarLive.module.css"; -import { Button, Flex, Reset, Text, Tooltip } from "@radix-ui/themes"; -import liveIconGreen from "../../assets/fiber_manual_record_16dp_3CFF73_FILL1_wght400_GRAD0_opsz20.svg"; -import historyIcon from "../../assets/history.svg"; -import futureIcon from "../../assets/future.svg"; -import undoIcon from "../../assets/undo.svg"; -import redoIcon from "../../assets/redo.svg"; -import { currentSlotAtom, slotOverrideAtom } from "../../atoms"; - -const epochBarStatusAtom = atom((get) => { - const currentSlot = get(currentSlotAtom); - if (currentSlot === undefined) return; - - const slotOverride = get(slotOverrideAtom); - if (slotOverride === undefined) return "Live"; - if (slotOverride > currentSlot) return "Future"; - if (slotOverride < currentSlot) return "Past"; - return "Live"; -}); - -export default function EpochBarLive() { - return ( - - - - - ); -} - -function EpochStatusIndicator() { - const status = useAtomValue(epochBarStatusAtom); - - if (status === undefined) return null; - - if (status === "Past") - return ( - - - {status} - {status} - - - ); - - if (status === "Future") - return ( - - - {status} - {status} - - - ); - - return ( - - - live icon - Realtime - - - ); -} - -function ResetToNow() { - const status = useAtomValue(epochBarStatusAtom); - const setSlotOverride = useSetAtom(slotOverrideAtom); - - if (status === "Live") return null; - - return ( - - - - - - ); -} diff --git a/src/features/EpochBar/EpochBounds.tsx b/src/features/EpochBar/EpochBounds.tsx deleted file mode 100644 index 8b261ee..0000000 --- a/src/features/EpochBar/EpochBounds.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Flex, Box, Text } from "@radix-ui/themes"; -import { useAtomValue } from "jotai"; -import { currentSlotAtom, epochAtom, slotDurationAtom } from "../../atoms"; -import { useState } from "react"; -import { useInterval } from "react-use"; -import { getTimeTillText, slowDateTimeNow } from "../../utils"; -import styles from "./epochBounds.module.css"; -import { Duration } from "luxon"; - -const refreshRate = 30 * 1_000; - -export default function EpochBounds() { - const slot = useAtomValue(currentSlotAtom); - const epoch = useAtomValue(epochAtom); - const slotDuration = useAtomValue(slotDurationAtom); - - const [labels, setLabels] = useState< - { start: string; end: string; timeLeft: string } | undefined - >(); - - const computeLabels = () => { - if (slot === undefined || epoch === undefined) return; - - const startDiffMs = (slot - epoch.start_slot) * slotDuration; - const startDt = slowDateTimeNow.minus({ milliseconds: startDiffMs }); - - const endDiffMs = (epoch.end_slot - slot) * slotDuration; - const endDt = slowDateTimeNow.plus({ milliseconds: endDiffMs }); - - const durationLeft = Duration.fromMillis(endDiffMs).rescale(); - - setLabels({ - start: startDt.toFormat("FF"), - end: endDt.toFormat("FF"), - timeLeft: getTimeTillText(durationLeft, { showSeconds: false }), - }); - }; - - if (!labels) { - computeLabels(); - } - - useInterval(computeLabels, refreshRate); - - return ( - - {labels?.start ?? "-"} - - {labels?.timeLeft && ( - {labels.timeLeft} left - )} - {labels?.end ?? "-"} - - ); -} diff --git a/src/features/EpochBar/EpochSlider.tsx b/src/features/EpochBar/EpochSlider.tsx deleted file mode 100644 index 7bb87d1..0000000 --- a/src/features/EpochBar/EpochSlider.tsx +++ /dev/null @@ -1,390 +0,0 @@ -import * as Slider from "@radix-ui/react-slider"; -import styles from "./epochSlider.module.css"; -import type React from "react"; -import { - memo, - startTransition, - useEffect, - useMemo, - useReducer, - useRef, - useState, -} from "react"; -import { Box } from "@radix-ui/themes"; -import { atom, useAtom, useAtomValue, useSetAtom } from "jotai"; -import { skippedSlotsAtom } from "../../api/atoms"; -import warning from "../../assets/warning_16dp_FF5353_FILL1_wght400_GRAD0_opsz20.svg"; -import green_flag from "../../assets/flag.svg"; -import { - currentLeaderSlotAtom, - slotOverrideAtom, - leaderSlotsAtom, - epochAtom, - firstProcessedSlotAtom, -} from "../../atoms"; -import { useInterval, useMeasure } from "react-use"; -import type { Epoch } from "../../api/types"; -import clsx from "clsx"; - -// 1 tick about 10 leaders or 40 slots -const sliderMaxValue = 10_800; - -function slotToEpochPct({ - slot, - epochStartSlot, - epochEndSlot, -}: { - slot?: number; - epochStartSlot?: number; - epochEndSlot?: number; -}) { - if ( - !slot || - epochStartSlot === undefined || - epochEndSlot === undefined || - epochStartSlot === epochEndSlot - ) - return 0; - - slot = Math.min(Math.max(slot, epochStartSlot), epochEndSlot); - const totalEpochSlots = epochEndSlot - epochStartSlot; - const epochSlotProgress = slot - epochStartSlot; - return epochSlotProgress / totalEpochSlots; -} - -function pctToValue(pct: number) { - return Math.trunc(pct * sliderMaxValue); -} - -function valueToSlot( - value?: number, - epochStartSlot?: number, - epochEndSlot?: number, -) { - if ( - value === undefined || - epochStartSlot === undefined || - epochEndSlot === undefined - ) - return; - - const pct = value / sliderMaxValue; - const totalEpochSlots = epochEndSlot - epochStartSlot; - return Math.trunc(totalEpochSlots * pct) + epochStartSlot; -} - -function epochProgressPctReducer( - _: number, - params: { slot?: number; epochStartSlot?: number; epochEndSlot?: number }, -): number { - return slotToEpochPct(params); -} - -function getRefreshInterval(epoch: Epoch | undefined, pct: number) { - if (!epoch || !pct) return 3_000; - - const epochSlots = epoch.end_slot - epoch.start_slot; - if (epochSlots < 10_000) return 300; - if (epochSlots < 50_000) return 1_000; - if (epochSlots < 100_000) return 3_000; - if (epochSlots < 200_000) return 5_000; - if (epochSlots < 300_000) return 10_000; - if (epochSlots < 400_000) return 15_000; - return 30_000; -} - -function removeNearbyPct( - pcts: { pct: number; slot: number }[], - pctBound: number, -) { - if (!pcts.length) return pcts; - - return pcts.reduce( - (acc, cur, i) => { - if (i === 0) return acc; - - const prev = acc[acc.length - 1]; - if (Math.abs(cur.pct - prev.pct) < pctBound) { - return acc; - } - - acc.push(cur); - return acc; - }, - [pcts[0]], - ); -} - -interface EpochSliderProps { - canChange: boolean; -} - -export default memo(EpochSlider); - -function EpochSlider({ canChange }: EpochSliderProps) { - const epoch = useAtomValue(epochAtom); - const currentLeaderSlot = useAtomValue(currentLeaderSlotAtom); - const [slotOverride, setSlotOverride] = useAtom(slotOverrideAtom); - const leaderSlots = useAtomValue(leaderSlotsAtom); - const skippedSlots = useAtomValue(skippedSlotsAtom); - const firstProcessedSlot = useAtomValue(firstProcessedSlotAtom); - const [value, setValue] = useState(() => { - return [ - pctToValue( - slotToEpochPct({ - slot: currentLeaderSlot, - epochStartSlot: epoch?.start_slot, - epochEndSlot: epoch?.end_slot, - }), - ), - ]; - }); - const isChangingValueRef = useRef(false); - const [measureRef, { width }] = useMeasure(); - const leaderSlotWidth = Math.trunc(width / 300); - - const [epochProgressPct, updateEpochProgressPct] = useReducer( - epochProgressPctReducer, - { - slot: currentLeaderSlot, - epochStartSlot: epoch?.start_slot, - epochEndSlot: epoch?.end_slot, - }, - slotToEpochPct, - ); - - useInterval( - () => { - startTransition(() => { - updateEpochProgressPct({ - slot: currentLeaderSlot, - epochStartSlot: epoch?.start_slot, - epochEndSlot: epoch?.end_slot, - }); - }); - }, - getRefreshInterval(epoch, epochProgressPct), - ); - - const leaderSlotPcts = useMemo(() => { - if (!epoch || !leaderSlots?.length) return; - - const pcts = leaderSlots.map((slot) => ({ - slot, - pct: slotToEpochPct({ - slot, - epochStartSlot: epoch.start_slot, - epochEndSlot: epoch.end_slot, - }), - })); - - return removeNearbyPct(pcts, 0.003); - }, [epoch, leaderSlots]); - - const skippedSlotPcts = useMemo(() => { - if (!epoch || !skippedSlots?.length) return; - - const pcts = skippedSlots.map((slot) => ({ - slot, - pct: slotToEpochPct({ - slot, - epochStartSlot: epoch.start_slot, - epochEndSlot: epoch.end_slot, - }), - })); - - return removeNearbyPct(pcts, 0.003); - }, [epoch, skippedSlots]); - - const firstProcessedSlotPct = useMemo(() => { - if (!firstProcessedSlot || !epoch) return; - return slotToEpochPct({ - slot: firstProcessedSlot, - epochStartSlot: epoch.start_slot, - epochEndSlot: epoch.end_slot, - }); - }, [epoch, firstProcessedSlot]); - - // Sync the slider position with user scrolling the leader schedule - useEffect(() => { - if (isChangingValueRef.current) return; - - const pct = slotOverride - ? slotToEpochPct({ - slot: slotOverride, - epochStartSlot: epoch?.start_slot, - epochEndSlot: epoch?.end_slot, - }) - : epochProgressPct; - const value = pctToValue(pct); - - setValue((prev) => { - return prev[0] === value ? prev : [value]; - }); - }, [epoch?.end_slot, epoch?.start_slot, epochProgressPct, slotOverride]); - - return ( -
- { - if (canChange) { - isChangingValueRef.current = true; - setValue(newValue); - setSlotOverride( - valueToSlot(newValue[0], epoch?.start_slot, epoch?.end_slot), - ); - } - }} - onValueCommit={() => (isChangingValueRef.current = false)} - max={sliderMaxValue} - > - - - {leaderSlotPcts?.map(({ slot, pct }) => ( - - ))} - - {skippedSlotPcts?.map(({ slot, pct }) => ( - - ))} - {!!firstProcessedSlotPct && !!firstProcessedSlot && ( - - )} - - -
- ); -} - -const isFutureSlotAtom = (slot: number) => - atom((get) => { - const currentSlot = get(currentLeaderSlotAtom); - return slot > (currentSlot ?? 0); - }); - -interface LeaderSlotProps { - slot: number; - pct: number; - width: number; -} - -function LeaderSlot({ slot, pct, width }: LeaderSlotProps) { - const firstProcessedSlot = useAtomValue(firstProcessedSlotAtom); - const setSlotOverride = useSetAtom(slotOverrideAtom); - const isFutureSlot = useAtomValue( - useMemo(() => isFutureSlotAtom(slot), [slot]), - ); - const onLeaderSlotClicked = (slot: number) => (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - setSlotOverride(slot); - }; - - const slotBeforeFirstProcessedSlot = firstProcessedSlot - ? slot < firstProcessedSlot - : false; - - return ( - - ); -} - -const MLeaderSlot = memo(LeaderSlot); - -interface SkippedSlotProps { - slot: number; - pct: number; -} - -function SkippedSlot({ slot, pct }: SkippedSlotProps) { - const setSlotOverride = useSetAtom(slotOverrideAtom); - - const onLeaderSlotClicked = (slot: number) => (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - setSlotOverride(slot); - }; - - return ( - <> - - skipped slot - - ); -} - -interface FirstProcessedSlotProps { - slot: number; - pct: number; -} - -function FirstProcessedSlot({ slot, pct }: FirstProcessedSlotProps) { - const setSlotOverride = useSetAtom(slotOverrideAtom); - - const onLeaderSlotClicked = (slot: number) => (e: React.PointerEvent) => { - e.stopPropagation(); - e.preventDefault(); - setSlotOverride(slot); - }; - - return ( - <> - - first processed slot - - ); -} diff --git a/src/features/EpochBar/NavigateNext.tsx b/src/features/EpochBar/NavigateNext.tsx deleted file mode 100644 index d0eff76..0000000 --- a/src/features/EpochBar/NavigateNext.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, Text } from "@radix-ui/themes"; -import useNavigateLeaderSlot from "../../hooks/useNavigateLeaderSlot"; -import chevronRight from "../../assets/chevron_right_18dp_F7F7F7_FILL1_wght400_GRAD0_opsz20.svg"; -// import lastPage from "../../assets/last_page_18dp_F7F7F7_FILL1_wght400_GRAD0_opsz20.svg"; -import styles from "./epochBar.module.css"; - -export default function NavigateNext() { - const { navNextLeaderSlot } = useNavigateLeaderSlot(); - - return ( - <> - - {/* */} - - ); -} diff --git a/src/features/EpochBar/NavigatePrev.tsx b/src/features/EpochBar/NavigatePrev.tsx deleted file mode 100644 index 929d2fd..0000000 --- a/src/features/EpochBar/NavigatePrev.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Button, Text } from "@radix-ui/themes"; -import useNavigateLeaderSlot from "../../hooks/useNavigateLeaderSlot"; -import chevronLeft from "../../assets/chevron_left_18dp_F7F7F7_FILL1_wght400_GRAD0_opsz20.svg"; -// import firstPage from "../../assets/first_page_18dp_F7F7F7_FILL1_wght400_GRAD0_opsz20.svg"; -import styles from "./epochBar.module.css"; - -export default function NavigatePrev() { - const { navPrevLeaderSlot } = useNavigateLeaderSlot(); - - return ( - <> - {/* */} - - - ); -} diff --git a/src/features/EpochBar/epochBar.module.css b/src/features/EpochBar/epochBar.module.css deleted file mode 100644 index 517ca85..0000000 --- a/src/features/EpochBar/epochBar.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.epoch-btn { - border-radius: 5px; - background: rgba(255, 255, 255, 0.1); - padding: 0 2px; - color: var(--nav-button-text-color); - gap: 0px; - &:hover { - filter: brightness(2); - cursor: pointer; - } -} diff --git a/src/features/EpochBar/epochBarLive.module.css b/src/features/EpochBar/epochBarLive.module.css deleted file mode 100644 index b778a57..0000000 --- a/src/features/EpochBar/epochBarLive.module.css +++ /dev/null @@ -1,19 +0,0 @@ -.epoch-bar-live { - .epoch-bar-live-icon { - border-radius: 100%; - height: 7px; - width: 7px; - margin-right: 2px; - } -} - -.not-live { - color: var(--epoch-not-live-color); -} - -.reset { - color: var(--regular-text-color); - background: unset; - height: unset; - font-size: 14px; -} diff --git a/src/features/EpochBar/epochBounds.module.css b/src/features/EpochBar/epochBounds.module.css deleted file mode 100644 index d7f8d33..0000000 --- a/src/features/EpochBar/epochBounds.module.css +++ /dev/null @@ -1,11 +0,0 @@ -.ts-label { - color: var(--regular-text-color); - font-size: 12px; - line-height: normal; -} - -.duration-label { - color: var(--header-color); - font-size: 12px; - line-height: normal; -} diff --git a/src/features/EpochBar/index.tsx b/src/features/EpochBar/index.tsx deleted file mode 100644 index c77637f..0000000 --- a/src/features/EpochBar/index.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { Flex, Text } from "@radix-ui/themes"; -import EpochBarLive from "./EpochBarLive"; -import EpochSlider from "./EpochSlider"; -import NavigateNext from "./NavigateNext"; -import NavigatePrev from "./NavigatePrev"; -import EpochBounds from "./EpochBounds"; -import { useAtomValue } from "jotai"; -import { epochAtom } from "../../atoms"; -import { epochTextColor } from "../../colors"; - -interface EpochBarProps { - canMove?: boolean; -} - -export default function EpochBar({ canMove = true }: EpochBarProps) { - return ( - - - - - - - - - - - - - ); -} - -function EpochText() { - const epoch = useAtomValue(epochAtom); - - return ( - - Epoch {!!epoch && epoch.epoch} - - ); -} diff --git a/src/features/Gossip/index.tsx b/src/features/Gossip/index.tsx index 452d72b..3ca2b4c 100644 --- a/src/features/Gossip/index.tsx +++ b/src/features/Gossip/index.tsx @@ -3,12 +3,7 @@ import Grid from "./Grid"; export default function Gossip() { return ( - + ); diff --git a/src/features/Header/Cluster.tsx b/src/features/Header/Cluster.tsx index 1a4c79e..48d57cf 100644 --- a/src/features/Header/Cluster.tsx +++ b/src/features/Header/Cluster.tsx @@ -13,14 +13,27 @@ import reconnectingIcon from "../../assets/power_off_orange.svg"; import disconnectedIcon from "../../assets/power_off_red.svg"; import { socketStateAtom } from "../../api/ws/atoms"; import { SocketState } from "../../api/ws/types"; -import { getClusterColor } from "./util"; import { useMedia } from "react-use"; -import type { BlockEngineUpdate } from "../../api/types"; -import { connectedColor, connectingColor, failureColor } from "../../colors"; +import type { + BlockEngineUpdate, + Cluster as ClusterType, +} from "../../api/types"; +import { + clusterDevelopmentColor, + clusterDevnetColor, + clusterMainnetBetaColor, + clusterPythnetColor, + clusterPythtestColor, + clusterTestnetColor, + clusterUnknownColor, + connectedColor, + connectingColor, + failureColor, +} from "../../colors"; import { ScheduleStrategyEnum } from "../../api/entities"; import { scheduleStrategyIcons } from "../../strategyIcons"; -export default function Cluster() { +export function Cluster() { const cluster = useAtomValue(clusterAtom); const version = useAtomValue(versionAtom); const commitHash = useAtomValue(commitHashAtom); @@ -71,6 +84,33 @@ export default function Cluster() { ); } +export function CluserIndicator() { + const cluster = useAtomValue(clusterAtom); + const color = getClusterColor(cluster); + + return
; +} + +function getClusterColor(cluster?: ClusterType) { + switch (cluster) { + case "mainnet-beta": + return clusterMainnetBetaColor; + case "testnet": + return clusterTestnetColor; + case "development": + return clusterDevelopmentColor; + case "devnet": + return clusterDevnetColor; + case "pythnet": + return clusterPythnetColor; + case "pythtest": + return clusterPythtestColor; + case "unknown": + case undefined: + return clusterUnknownColor; + } +} + function getBlockEngineFill(blockEngineUpdate: BlockEngineUpdate) { switch (blockEngineUpdate.status) { case "connected": diff --git a/src/features/Header/ClusterIndicator.tsx b/src/features/Header/ClusterIndicator.tsx deleted file mode 100644 index b3abb23..0000000 --- a/src/features/Header/ClusterIndicator.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useAtomValue } from "jotai"; -import { clusterAtom } from "../../api/atoms"; -import styles from "./clusterIndicator.module.css"; -import { getClusterColor } from "./util"; - -export default function CluserIndicator() { - const cluster = useAtomValue(clusterAtom); - const color = getClusterColor(cluster); - - return
; -} diff --git a/src/features/Header/DropDownNavLinks.tsx b/src/features/Header/DropDownNavLinks.tsx deleted file mode 100644 index 22bece9..0000000 --- a/src/features/Header/DropDownNavLinks.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import * as DropdownMenu from "@radix-ui/react-dropdown-menu"; -import styles from "./dropDownNavLinks.module.css"; -import NavLink from "./NavLink"; -import { useLocation } from "@tanstack/react-router"; -import { Button } from "@radix-ui/themes"; -import dropDownIcon from "../../assets/dropdown_arrow.svg"; -import { useAtomValue } from "jotai"; -import { containerElAtom } from "../../atoms"; -import type { PropsWithChildren } from "react"; -import { useState } from "react"; - -export default function DropDownNavLinks({ children }: PropsWithChildren) { - const containerEl = useAtomValue(containerElAtom); - const [dropdownOpen, setDropdownOpen] = useState(false); - - const location = useLocation(); - let route = "Overview"; - if (location.pathname.toLowerCase().includes("leaderschedule")) { - route = "Leader Schedule"; - } else if (location.pathname.toLowerCase().includes("gossip")) { - route = "Gossip"; - } - - return ( - - - {children || ( - - )} - - - setDropdownOpen(false)} - > - - - - - - - - - - - - - ); -} diff --git a/src/features/Header/IdentityKey.tsx b/src/features/Header/IdentityKey.tsx index f861957..70855db 100644 --- a/src/features/Header/IdentityKey.tsx +++ b/src/features/Header/IdentityKey.tsx @@ -21,9 +21,9 @@ import PopoverDropdown from "../../components/PopoverDropdown"; export default function IdentityKey() { const { peer, identityKey } = useIdentityPeer(); - const isXXNarrowScreen = useMedia("(min-width: 550px)"); - const isXNarrowScreen = useMedia("(min-width: 750px)"); - const isNarrowScreen = useMedia("(min-width: 900px)"); + const isXXNarrowScreen = useMedia("(min-width: 400px)"); + const isXNarrowScreen = useMedia("(min-width: 600px)"); + const isNarrowScreen = useMedia("(min-width: 1100px)"); useEffect(() => { let title = "Firedancer"; @@ -41,14 +41,15 @@ export default function IdentityKey() {
+ + {isXXNarrowScreen && ( - +