diff --git a/package-lock.json b/package-lock.json index 48e335a..7841b68 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://npm.w2k.jumptrading.com/@floating-ui%2fcore/-/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.3", + "resolved": "https://npm.w2k.jumptrading.com/@floating-ui%2fdom/-/dom-1.7.3.tgz", + "integrity": "sha512-uZA413QEpNuhtb3/iIKoYMSK07keHPYeXF02Zhd6e213j+d1NamLix/mCLxBUDW/Gx52sPH2m+chlUsyaBs/Ag==", "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.15", + "resolved": "https://npm.w2k.jumptrading.com/@floating-ui%2freact/-/react-0.27.15.tgz", + "integrity": "sha512-0LGxhBi3BB1DwuSNQAmuaSuertFzNAerlMdPbotjTVnvPtdOs7CkrHLaev5NIXemhzDXNC0tFzuseut7cWA5mw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.5", + "@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.5", + "resolved": "https://npm.w2k.jumptrading.com/@floating-ui%2freact-dom/-/react-dom-2.1.5.tgz", + "integrity": "sha512-HDO/1/1oH9fjj4eLgegrlH3dklZpHtUYYFiVwMUwfGvk9jWDRWqkklA2/NFScknrcNSspbV868WjXORvreDX+Q==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.3" }, "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://npm.w2k.jumptrading.com/@floating-ui%2futils/-/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.13.0", + "resolved": "https://npm.w2k.jumptrading.com/react-virtuoso/-/react-virtuoso-4.13.0.tgz", + "integrity": "sha512-XHv2Fglpx80yFPdjZkV9d1baACKghg/ucpDFEXwaix7z0AfVQj+mF6lM+YQR6UC/TwzXG2rJKydRMb3+7iV3PA==", + "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://npm.w2k.jumptrading.com/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..8987581 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -33,12 +33,7 @@ export default function App() { ); return ( - + diff --git a/src/app.css b/src/app.css index 9c83e38..778e063 100644 --- a/src/app.css +++ b/src/app.css @@ -1,4 +1,4 @@ -.app { +#app { /* radix tokens */ --color-background: #070b14; --default-font-family: "Inter Tight", sans-serif; @@ -13,8 +13,6 @@ --green-live: #3cff73; --header-row-height: 34px; - /* headder height + epoch bar height + 10 padding top/bottom */ - --header-height: 121px; font-variant-numeric: tabular-nums; } @@ -37,3 +35,14 @@ .rt-Separator { background: #65677a; } + +.sticky { + position: sticky; + z-index: 3; + background: var(--color-background); +} + +.app-width-container { + max-width: 1920px; + margin: 0 auto; +} 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..26fe191 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -15,12 +15,15 @@ import type { SlotLevel, SlotResponse, } from "./api/types"; -import { merge } from "lodash"; -import { getLeaderSlots, getStake } from "./utils"; +import { clamp, merge } from "lodash"; +import { getLeaderSlots, getSlotGroupLeader, getStake } from "./utils"; import { searchLeaderSlotsAtom } from "./features/LeaderSchedule/atoms"; import { selectedSlotAtom } from "./features/Overview/SlotPerformance/atoms"; +import { atomFamily } from "jotai/utils"; +import memoize from "micro-memoize"; export const containerElAtom = atom(); +export const slotsListElAtom = atom(); const _epochsAtom = atomWithImmer([]); export const epochAtom = atom( @@ -55,37 +58,62 @@ 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, autoScrollAtom] = + (function getSlotOverrideAtom() { + const _slotOverrideAtom = atom(); + + return [ + atom( + (get) => get(_slotOverrideAtom), + (get, set, slot: number | undefined) => { + const epoch = get(epochAtom); + if (!epoch) return; + + const clampedSlot = + slot === undefined + ? undefined + : clamp( + getSlotGroupLeader(slot), + epoch.start_slot, + epoch.end_slot, + ); + + set(_slotOverrideAtom, clampedSlot); + }, + ), + atom((get) => get(_slotOverrideAtom) === undefined), + ]; + })(); - if (startOverrideSlot !== undefined) { - startOverrideSlot = Math.min( - epoch.end_slot, - Math.max(startOverrideSlot, epoch.start_slot), - ); - } +const slotStatusAtom = atomWithImmer>({}); - set(_slotOverrideAtom, startOverrideSlot); - }, +export const getSlotStatus = memoize( + (slot?: number) => + atom((get) => + slot !== undefined + ? get(slotStatusAtom)[slot] || "incomplete" + : "incomplete", + ), + { maxSize: 1_000 }, ); -const slotStatusAtom = atomWithImmer>({}); - -export const getSlotStatus = (slot?: number) => - atom((get) => - slot !== undefined - ? get(slotStatusAtom)[slot] || "incomplete" - : "incomplete", +export enum SlotNavFilter { + AllSlots = "All Slots", + MySlots = "My Slots", +} +export const slotNavFilterAtom = (function getSlotNavFilterAtom() { + const _slotNavFilterAtom = atom(); + return atom( + (get) => get(_slotNavFilterAtom) ?? SlotNavFilter.AllSlots, + (get, set, filter: SlotNavFilter | undefined) => { + set(_slotNavFilterAtom, filter); + + // Reset scroll to selected slot or RT + const selectedSlot = get(selectedSlotAtom); + set(slotOverrideAtom, selectedSlot ?? undefined); + }, ); +})(); export const setSlotStatusAtom = atom( null, @@ -111,6 +139,8 @@ export const deleteSlotStatusBoundsAtom = atom(null, (get, set) => { const currentSlot = get(currentSlotAtom); const searchSlots = get(searchLeaderSlotsAtom); + const leaderSlots = get(leaderSlotsAtom); + const navFilter = get(slotNavFilterAtom); const slot = slotOverride ?? currentSlot; if (slot !== undefined) { @@ -120,14 +150,25 @@ export const deleteSlotStatusBoundsAtom = atom(null, (get, set) => { const cachedStatusSlots = Object.keys(draft); for (const cachedStatusSlot of cachedStatusSlots) { const numberVal = Number(cachedStatusSlot); - if (searchSlots?.length) { - const slotGroupStart = numberVal - (numberVal % slotsPerLeader); - if (searchSlots.includes(slotGroupStart)) { - continue; - } + const slotGroupStart = getSlotGroupLeader(numberVal); + + if (searchSlots?.length && searchSlots.includes(slotGroupStart)) { + continue; } - if (numberVal === selectedSlot) continue; + if ( + selectedSlot !== undefined && + slotGroupStart === getSlotGroupLeader(selectedSlot) + ) { + continue; + } + + if ( + navFilter === SlotNavFilter.MySlots && + leaderSlots?.includes(slotGroupStart) + ) { + continue; + } if ( !isNaN(numberVal) && @@ -142,13 +183,15 @@ export const deleteSlotStatusBoundsAtom = atom(null, (get, set) => { const slotResponseAtom = atomWithImmer>({}); -export const getSlotPublishAtom = (slot?: number) => +export const slotPublishAtomFamily = atomFamily((slot?: number) => atom((get) => slot !== undefined ? get(slotResponseAtom)[slot]?.publish : undefined, - ); + ), +); -export const getSlotResponseAtom = (slot?: number) => - atom((get) => (slot !== undefined ? get(slotResponseAtom)[slot] : undefined)); +export const slotResponseAtomFamily = atomFamily((slot?: number) => + atom((get) => (slot !== undefined ? get(slotResponseAtom)[slot] : undefined)), +); export const setSlotResponseAtom = atom( null, @@ -172,6 +215,8 @@ export const deleteSlotResponseBoundsAtom = atom(null, (get, set) => { const currentSlot = get(currentSlotAtom); const searchSlots = get(searchLeaderSlotsAtom); const slot = slotOverride ?? currentSlot; + const navFilter = get(slotNavFilterAtom); + const leaderSlots = get(leaderSlotsAtom); if (slot !== undefined) { set(slotResponseAtom, (draft) => { @@ -179,21 +224,32 @@ export const deleteSlotResponseBoundsAtom = atom(null, (get, set) => { const cacheSlotMax = slot + slotCacheBounds / 2; const cachedSlots = Object.keys(draft); for (const cachedSlot of cachedSlots) { - const numberVal = Number(cachedSlot); - if (searchSlots?.length) { - const slotGroupStart = numberVal - (numberVal % slotsPerLeader); - if (searchSlots.includes(slotGroupStart)) { - continue; - } + const slotNumber = Number(cachedSlot); + const slotGroupStart = getSlotGroupLeader(slotNumber); + if (searchSlots?.length && searchSlots.includes(slotGroupStart)) { + continue; } - if (numberVal === selectedSlot) continue; + if ( + selectedSlot !== undefined && + slotGroupStart === getSlotGroupLeader(selectedSlot) + ) { + continue; + } if ( - !isNaN(numberVal) && - (numberVal < cacheSlotMin || numberVal > cacheSlotMax) + navFilter === SlotNavFilter.MySlots && + leaderSlots?.includes(slotGroupStart) ) { - delete draft[numberVal]; + continue; + } + + if ( + !isNaN(slotNumber) && + (slotNumber < cacheSlotMin || slotNumber > cacheSlotMax) + ) { + delete draft[slotNumber]; + slotPublishAtomFamily.remove(slotNumber); } } }); @@ -207,6 +263,30 @@ 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); + 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 _currentSlotAtom = atom(undefined); export const currentSlotAtom = atom( (get) => get(_currentSlotAtom), @@ -240,23 +320,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 +376,21 @@ 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 peersAtomFamily = atomFamily((peer?: string) => + atom((get) => (peer !== undefined ? get(peersAtom)[peer] : undefined)), +); + +export const addPeersAtom = atom(null, (_, set, peers?: Peer[]) => { if (!peers?.length) return; set(peersAtom, (draft) => { @@ -333,7 +400,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,13 +411,14 @@ 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) => { for (const peer of peers) { if (draft[peer.identity_pubkey]) { draft[peer.identity_pubkey].removed = true; + peersAtomFamily.remove(peer.identity_pubkey); } } }); @@ -459,18 +527,73 @@ export const getSlotStateAtom = (slot?: number) => return "past"; }); -export const getIsFutureSlotAtom = (slot?: number) => +export const getIsCurrentLeaderAtom = (slot?: number) => atom((get) => { - if (slot === undefined) return true; + if (slot === undefined) return false; + const currentLeaderSlot = get(currentLeaderSlotAtom); + if (currentLeaderSlot === undefined) return false; - const currentSlot = get(currentSlotAtom); - if (currentSlot === undefined) return true; + 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 + ); + }); - if (slot >= currentSlot) return true; +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 false; + return slot < currentLeaderSlot; }); +export const getIsFutureSlotAtom = memoize( + (slot?: number) => + atom((get) => { + if (slot === undefined) return true; + + const currentSlot = get(currentSlotAtom); + if (currentSlot === undefined) return true; + + if (slot >= currentSlot) return true; + + return false; + }), + { maxSize: 1_000 }, +); + export const getIsSkippedAtom = (slot?: number) => atom((get) => { if (slot === undefined) return false; @@ -506,3 +629,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..855b028 100644 --- a/src/colors.ts +++ b/src/colors.ts @@ -16,6 +16,9 @@ export const rowSeparatorBackgroundColor = "#333333"; export const navButtonTextColor = "#F7F7F7"; +// slot navigation +export const slotNavFilterBackgroundColor = "#283551"; + // startup export const startupTextColor = "#A7A7A7"; export const startupProgressBackgroundColor = "#121213"; @@ -138,3 +141,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..35ee480 100644 --- a/src/components/PeerIcon.tsx +++ b/src/components/PeerIcon.tsx @@ -1,3 +1,4 @@ +import type { CSSProperties } from "react"; import { useState } from "react"; import privateIcon from "../assets/private.svg"; import privateYouIcon from "../assets/privateYou.svg"; @@ -12,6 +13,7 @@ interface PeerIconProps { isYou?: boolean; size: number; hideFallback?: boolean; + isRounded?: boolean; } export default function PeerIcon({ @@ -19,6 +21,7 @@ export default function PeerIcon({ size, hideFallback, isYou, + isRounded, }: PeerIconProps) { const [globalHasError, setGlobalHasError] = useAtom( getPeerIconHasErrorIcon(url), @@ -26,16 +29,20 @@ export default function PeerIcon({ const [hasError, setHasError] = useState(globalHasError); const [hasLoaded, setHasLoaded] = useState(false); + const iconStyles = { + "--height": `${size}px`, + "--width": `${size}px`, + } as CSSProperties; + + const className = clsx(styles.icon, { [styles.isRounded]: isRounded }); + if (!url || hasError) { if (hideFallback) { - return; + return
; } else if (isYou) { return ( - + ); } else { @@ -43,7 +50,8 @@ export default function PeerIcon({ private ); } @@ -57,15 +65,15 @@ export default function PeerIcon({ return ( <> setHasLoaded(true)} src={url} /> private diff --git a/src/components/SlotClient.tsx b/src/components/SlotClient.tsx new file mode 100644 index 0000000..88267d6 --- /dev/null +++ b/src/components/SlotClient.tsx @@ -0,0 +1,25 @@ +import { useSlotInfo } from "../hooks/useSlotInfo"; +import AnzaLogo from "../assets/anza_logo.svg"; +import FrankendancerLogo from "../assets/frankendancer_logo.svg"; +import { memo } from "react"; + +export default memo(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..a17812e --- /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(getSlotStatus(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/components/peerIcon.module.css b/src/components/peerIcon.module.css index aaa3d95..83f7033 100644 --- a/src/components/peerIcon.module.css +++ b/src/components/peerIcon.module.css @@ -1,3 +1,12 @@ .hide { display: none; } + +.is-rounded { + border-radius: 6px; +} + +.icon { + height: var(--height); + width: var(--width); +} diff --git a/src/consts.ts b/src/consts.ts index 9a993c0..4b1ed6e 100644 --- a/src/consts.ts +++ b/src/consts.ts @@ -1,7 +1,17 @@ export const slotsPerLeader = 4; +export const slotsListPinnedSlotOffset = 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 clusterIndicatorHeight = 5; +export const headerHeight = 35; + +export const logoWidth = 20; +export const logoRightSpacing = 8; +export const slotsListWidth = 120; +export const slotNavWidth = logoWidth + logoRightSpacing + slotsListWidth; 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..bd1daa3 100644 --- a/src/features/Header/Cluster.tsx +++ b/src/features/Header/Cluster.tsx @@ -6,26 +6,38 @@ import { commitHashAtom, scheduleStrategyAtom, } from "../../api/atoms"; -import { Text, Tooltip } from "@radix-ui/themes"; +import { Text, Tooltip, Flex } from "@radix-ui/themes"; import styles from "./cluster.module.css"; import connectedIcon from "../../assets/power.svg"; 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"; +import { clusterIndicatorHeight } from "../../consts"; -export default function Cluster() { +export function Cluster() { const cluster = useAtomValue(clusterAtom); const version = useAtomValue(versionAtom); const commitHash = useAtomValue(commitHashAtom); const socketState = useAtomValue(socketStateAtom); - const isWideScreen = useMedia("(min-width: 600px)"); if (!cluster && !version) return null; @@ -42,35 +54,77 @@ export default function Cluster() { } return ( -
- - - {clusterText} - - - {isWideScreen && ( + + + + {clusterText} + + - v{version} + v{version} - )} + + - ws status + ws status -
+
); } +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": @@ -93,8 +147,8 @@ function JitoIcon() { content={`Currently ${blockEngine.status} ${blockEngine.status === "disconnected" ? "from" : "to"} ${blockEngine.name} - ${blockEngine.url} (${blockEngine.ip})`} > @@ -135,6 +189,7 @@ function JitoIcon() { } function StrategyIcon() { + const fontSize = "14px"; const scheduleStrategy = useAtomValue(scheduleStrategyAtom); if (!scheduleStrategy) return; @@ -142,19 +197,19 @@ function StrategyIcon() { if (scheduleStrategy === ScheduleStrategyEnum.balanced) { return ( -
{icon}
+
{icon}
); } else if (scheduleStrategy === ScheduleStrategyEnum.perf) { return ( -
{icon}
+
{icon}
); } else if (scheduleStrategy === ScheduleStrategyEnum.revenue) { return ( -
{icon}
+
{icon}
); } 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..81e0637 100644 --- a/src/features/Header/IdentityKey.tsx +++ b/src/features/Header/IdentityKey.tsx @@ -11,7 +11,7 @@ import { myStakePctAtom, myStakeAmountAtom } from "../../atoms"; import type { PropsWithChildren } from "react"; import { useEffect } from "react"; import { DateTime } from "luxon"; -import { getFmtStake, getTimeTillText, slowDateTimeNow } from "../../utils"; +import { getFmtStake, getDurationText, slowDateTimeNow } from "../../utils"; import { formatNumber } from "../../numUtils"; import { useInterval, useMedia, useUpdate } from "react-use"; import clsx from "clsx"; @@ -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 && ( - +