From e838bcd86c83528d57d7913b781c04f6d9056d0d Mon Sep 17 00:00:00 2001 From: devinxl Date: Tue, 19 Mar 2024 14:30:46 +0800 Subject: [PATCH] fix(dcellar-web-ui): disable selection of SPs with invalid status in SPSelector --- apps/dcellar-web-ui/package.json | 3 +- .../components/SPSelector/ErrorBadge.tsx | 69 +++++++ .../components/SPSelector/OptionItem.tsx | 82 ++++++++ .../{SPSelector.tsx => SPSelector/index.tsx} | 177 ++---------------- .../bucket/components/SPSelector/style.ts | 57 ++++++ common/config/rush/pnpm-lock.yaml | 12 ++ 6 files changed, 234 insertions(+), 166 deletions(-) create mode 100644 apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/ErrorBadge.tsx create mode 100644 apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx rename apps/dcellar-web-ui/src/modules/bucket/components/{SPSelector.tsx => SPSelector/index.tsx} (51%) create mode 100644 apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts diff --git a/apps/dcellar-web-ui/package.json b/apps/dcellar-web-ui/package.json index 92cf7a20..7c84177c 100644 --- a/apps/dcellar-web-ui/package.json +++ b/apps/dcellar-web-ui/package.json @@ -56,7 +56,8 @@ "react-dnd-html5-backend": "16.0.1", "viem": "~1.19.11", "@wagmi/core": "~1.4.10", - "set-interval-async": "~3.0.3" + "set-interval-async": "~3.0.3", + "radash": "~12.1.0" }, "devDependencies": { "@commitlint/cli": "^17.4.3", diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/ErrorBadge.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/ErrorBadge.tsx new file mode 100644 index 00000000..6857d11e --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/ErrorBadge.tsx @@ -0,0 +1,69 @@ +import { GREENFIELD_CHAIN_EXPLORER_URL } from '@/base/env'; +import { IconFont } from '@/components/IconFont'; +import { DCTooltip } from '@/components/common/DCTooltip'; +import { Status as StorageProviderStatus } from '@bnb-chain/greenfield-cosmos-types/greenfield/sp/types'; +import { ExternalLinkIcon } from '@node-real/icons'; +import { Badge } from '@node-real/uikit'; +import { A } from './style'; +import { capitalize } from 'radash'; +import { memo } from 'react'; + +type ErrorBadgeProps = { + access: boolean; + address: string; + status: number; +}; + +export const ErrorBadge = memo(function ErrorBadge({ access, address, status }: ErrorBadgeProps) { + const renderUnavailableBadge = () => ( + + + SP Error + + + ); + + const renderStatusBadge = () => { + const statusText = (StorageProviderStatus[status] || 'Unknown') + .replace('STATUS_', '') + .split('_') + .map((s) => capitalize(s)) + .join(' '); + + return ( + + {statusText} + + ); + }; + + const renderAccessIcon = () => ( + e.stopPropagation()} + > + + + ); + + if (!access) { + if (status === 0) { + return renderUnavailableBadge(); + } else { + return renderStatusBadge(); + } + } else { + return renderAccessIcon(); + } +}); diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx new file mode 100644 index 00000000..8e57740e --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/OptionItem.tsx @@ -0,0 +1,82 @@ +import { useAppSelector } from '@/store'; +import { Flex, Text } from '@node-real/uikit'; +import { TD } from './style'; +import { DCTooltip } from '@/components/common/DCTooltip'; +import { formatBytes } from '@/utils/formatter'; +import { ErrorBadge } from './ErrorBadge'; +import { memo } from 'react'; + +interface OptionItemProps { + address: string; + name: string; + endpoint: string; + access: boolean; + status: number; +} + +export const OptionItem = memo(function OptionItem({ + address, + name, + endpoint, + access, + status, +}: OptionItemProps) { + const spMetaRecords = useAppSelector((root) => root.sp.spMetaRecords); + const spLatencyRecords = useAppSelector((root) => root.sp.spLatencyRecords); + const meta = spMetaRecords[endpoint]; + const spLatency = spLatencyRecords[endpoint.toLowerCase()] || 0; + const textColor = access ? 'readable.secondary' : 'readable.disable'; + const tooltipColor = access ? 'tertiary' : 'disable'; + + return ( + + + + + {name} + + + + + + + {endpoint} + + + + + {meta ? formatBytes(meta.FreeReadQuota) : '--'} + + + {access && spLatency ? `${spLatency}ms` : '--'} + + + ); +}); diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector.tsx b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx similarity index 51% rename from apps/dcellar-web-ui/src/modules/bucket/components/SPSelector.tsx rename to apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx index 7a0a046c..f6316fb2 100644 --- a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector.tsx +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/index.tsx @@ -1,28 +1,22 @@ -import { GREENFIELD_CHAIN_EXPLORER_URL } from '@/base/env'; -import { IconFont } from '@/components/IconFont'; import { MenuOption } from '@/components/common/DCMenuList'; import { DCSelect } from '@/components/common/DCSelect'; -import { DCTooltip } from '@/components/common/DCTooltip'; import { useAppDispatch, useAppSelector } from '@/store'; import { SpEntity, setSpLatency, setupSpLatency } from '@/store/slices/sp'; -import { transientOptions } from '@/utils/css'; -import { formatBytes } from '@/utils/formatter'; import { trimLongStr } from '@/utils/string'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import { ExternalLinkIcon } from '@node-real/icons'; -import { Box, Flex, Text } from '@node-real/uikit'; + import { useMount } from 'ahooks'; import { find, sortBy } from 'lodash-es'; import { useEffect, useMemo, useRef, useState } from 'react'; import { memo } from 'react'; +import { TH } from './style'; +import { OptionItem } from './OptionItem'; interface SPSelectorProps { onChange: (value: SpEntity) => void; } -export const SPSelector = memo(function SPSelector(props) { +export const SPSelector = memo(function SPSelector({ onChange }) { const dispatch = useAppDispatch(); const loginAccount = useAppSelector((root) => root.persist.loginAccount); const unAvailableSps = useAppSelector((root) => root.persist.unAvailableSps); @@ -30,12 +24,9 @@ export const SPSelector = memo(function SPSelector(props) { const specifiedSp = useAppSelector((root) => root.sp.specifiedSp); const allSpList = useAppSelector((root) => root.sp.allSpList); const spMetaRecords = useAppSelector((root) => root.sp.spMetaRecords); - const [sp, setSP] = useState({} as SpEntity); const [total, setTotal] = useState(0); - const len = allSpList.length; - const { onChange } = props; const saveOnChangeRef = useRef(onChange); saveOnChangeRef.current = onChange; @@ -46,6 +37,7 @@ export const SPSelector = memo(function SPSelector(props) { address={sp.operatorAddress} name={sp.moniker} endpoint={sp.endpoint} + status={sp.status} access={!disabled} /> ); @@ -66,17 +58,22 @@ export const SPSelector = memo(function SPSelector(props) { return tmpValue.includes(tmpKeyword) || tmpName.includes(tmpKeyword); }; + // Sort SPs with unavailable HTTP services or unavailable statuses last, and the rest by ascending latency. const options: MenuOption[] = useMemo( () => sortBy(allSpList, [ (i) => (unAvailableSps.includes(i.operatorAddress) ? 1 : 0), + (i) => (i.status !== 0 ? 1 : 0), (sp) => { const meta = spMetaRecords[sp.endpoint]; return meta ? meta.Latency : Infinity; }, ]).map((item) => { - const { operatorAddress, moniker } = item; - const access = !unAvailableSps.includes(operatorAddress); + const { operatorAddress, moniker, status } = item; + const spServiceAvailable = !unAvailableSps.includes(operatorAddress); + const spStatusAvailable = status === 0; + const access = spServiceAvailable && spStatusAvailable; + return { label: moniker, value: operatorAddress, @@ -161,153 +158,3 @@ export const SPSelector = memo(function SPSelector(props) { const renderItem = (moniker: string, address: string) => { return [moniker, trimLongStr(address, 10, 6, 4)].filter(Boolean).join(' | '); }; - -function OptionItem(props: any) { - const spMetaRecords = useAppSelector((root) => root.sp.spMetaRecords); - const spLatencyRecords = useAppSelector((root) => root.sp.spLatencyRecords); - - const { address, name, endpoint, access } = props; - const meta = spMetaRecords[endpoint]; - - const link = !access ? ( - - - - SP Error - - - - ) : ( - e.stopPropagation()} - > - - - ); - - const spLatency = spLatencyRecords[endpoint.toLowerCase()] || 0; - - return ( - - - - - {name} - - {link} - - - - - {endpoint} - - - - - {meta ? formatBytes(meta.FreeReadQuota) : '--'} - - - {spLatency && access ? spLatency + 'ms' : '--'} - - - ); -} - -const A = styled.a` - :hover { - color: #00ba34; - } - - margin-left: 4px; -`; - -const TH = styled(Box)` - padding: 8px; - - &:first-of-type { - padding-left: 12px; - padding-right: 12px; - } - - svg { - color: #aeb4bc; - - :hover { - color: #76808f; - } - } -`; - -const TD = styled(Box, transientOptions)<{ $dot?: number }>` - height: 31px; - position: relative; - font-size: 14px; - font-weight: 400; - - ${(props) => - props.$dot && - css` - :before { - position: relative; - top: -1px; - margin-right: 4px; - display: inline-flex; - content: ''; - width: 8px; - height: 8px; - border-radius: 100%; - - background-color: ${props.$dot < 100 - ? '#00BA34' - : props.$dot < 200 - ? '#EEBE11' - : '#EE3911'}; - } - `} -`; diff --git a/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts new file mode 100644 index 00000000..313ad87e --- /dev/null +++ b/apps/dcellar-web-ui/src/modules/bucket/components/SPSelector/style.ts @@ -0,0 +1,57 @@ +import { transientOptions } from '@/utils/css'; +import styled from '@emotion/styled'; +import { Box } from '@node-real/uikit'; +import { css } from '@emotion/react'; + +export const A = styled.a` + :hover { + color: #00ba34; + } + + margin-left: 4px; +`; + +export const TH = styled(Box)` + padding: 8px; + + &:first-of-type { + padding-left: 12px; + padding-right: 12px; + } + + svg { + color: #aeb4bc; + + :hover { + color: #76808f; + } + } +`; + +export const TD = styled(Box, transientOptions)<{ $dot?: number }>` + height: 31px; + position: relative; + font-size: 14px; + font-weight: 400; + + ${(props) => + props.$dot && + css` + :before { + position: relative; + top: -1px; + margin-right: 4px; + display: inline-flex; + content: ''; + width: 8px; + height: 8px; + border-radius: 100%; + + background-color: ${props.$dot < 100 + ? '#00BA34' + : props.$dot < 200 + ? '#EEBE11' + : '#EE3911'}; + } + `} +`; diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 53581af4..b7d1bcab 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -55,6 +55,7 @@ importers: next-transpile-modules: ~10.0.1 prettier: ^2.8.4 query-string: ^8.1.0 + radash: ~12.1.0 react: ~18.2.0 react-dnd: 16.0.1 react-dnd-html5-backend: 16.0.1 @@ -101,6 +102,7 @@ importers: next-redux-wrapper: 8.1.0_sqwlmqhdtfirfp7b3jp6tjdu3a next-transpile-modules: 10.0.1 query-string: 8.2.0 + radash: 12.1.0 react: 18.2.0 react-dnd: 16.0.1_7acmz257hs6yohiyquldbuobdm react-dnd-html5-backend: 16.0.1 @@ -2822,6 +2824,7 @@ packages: dependencies: is-glob: 4.0.3 micromatch: 4.0.5 + napi-wasm: 1.1.0 dev: false bundledDependencies: - napi-wasm @@ -7918,6 +7921,10 @@ packages: hasBin: true dev: false + /napi-wasm/1.1.0: + resolution: {integrity: sha512-lHwIAJbmLSjF9VDRm9GoVOy9AGp3aIvkjv+Kvz9h16QR3uSVYH78PNQUnT2U4X53mhlnV2M7wrhibQ3GHicDmg==} + dev: false + /natural-compare/1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} dev: true @@ -8469,6 +8476,11 @@ packages: engines: {node: '>=8'} dev: true + /radash/12.1.0: + resolution: {integrity: sha512-b0Zcf09AhqKS83btmUeYBS8tFK7XL2e3RvLmZcm0sTdF1/UUlHSsjXdCcWNxe7yfmAlPve5ym0DmKGtTzP6kVQ==} + engines: {node: '>=14.18.0'} + dev: false + /radix3/1.1.0: resolution: {integrity: sha512-pNsHDxbGORSvuSScqNJ+3Km6QAVqk8CfsCBIEoDgpqLrkD2f3QM4I7d1ozJJ172OmIcoUcerZaNWqtLkRXTV3A==} dev: false