1
1
import { Tooltip } from "@namada/components" ;
2
+ import { accountBalanceAtom , transparentBalanceAtom } from "atoms/accounts" ;
2
3
import { indexerApiAtom } from "atoms/api" ;
3
- import { shieldedBalanceAtom , shieldedSyncProgress } from "atoms/balance/atoms " ;
4
+ import { shieldedBalanceAtom } from "atoms/balance" ;
4
5
import { fetchBlockHeightByTimestamp } from "atoms/balance/services" ;
5
6
import { chainStatusAtom } from "atoms/chain" ;
7
+ import { allProposalsAtom , votedProposalsAtom } from "atoms/proposals" ;
8
+ import {
9
+ indexerHeartbeatAtom ,
10
+ maspIndexerHeartbeatAtom ,
11
+ rpcHeartbeatAtom ,
12
+ } from "atoms/settings" ;
6
13
import {
7
14
indexerServicesSyncStatusAtom ,
8
15
syncStatusAtom ,
9
16
} from "atoms/syncStatus/atoms" ;
17
+ import { allValidatorsAtom , myValidatorsAtom } from "atoms/validators" ;
18
+ import clsx from "clsx" ;
10
19
import { useAtomValue } from "jotai" ;
11
- import { useEffect , useMemo , useState } from "react" ;
20
+ import { useEffect , useState } from "react" ;
21
+ import { IoCheckmarkCircleOutline } from "react-icons/io5" ;
12
22
import { twMerge } from "tailwind-merge" ;
13
- import { PulsingRing } from "../Common/PulsingRing" ;
14
23
15
24
const formatError = (
16
25
errors : ( string | Error ) [ ] ,
@@ -21,43 +30,62 @@ const formatError = (
21
30
}
22
31
23
32
return (
24
- < div >
25
- { label && < div > { label } :</ div > }
33
+ < div className = "mb-2" >
34
+ { label && (
35
+ < div
36
+ className = {
37
+ label === "Error" ? "text-red-500 font-medium" : "font-medium"
38
+ }
39
+ >
40
+ { label } :
41
+ </ div >
42
+ ) }
26
43
{ errors . map ( ( e ) => {
27
44
const string = e instanceof Error ? e . message : String ( e ) ;
28
- return < div key = { string } > { string } </ div > ;
45
+ return (
46
+ < div key = { string } className = "mt-1" >
47
+ { string }
48
+ </ div >
49
+ ) ;
29
50
} ) }
30
51
</ div >
31
52
) ;
32
53
} ;
33
54
55
+ const LoadingSpinner = ( ) : JSX . Element => {
56
+ return (
57
+ < i
58
+ className = { clsx (
59
+ "inline-block w-2 h-2 border-2" ,
60
+ "border-transparent border-t-yellow rounded-[50%]" ,
61
+ "animate-loadingSpinner"
62
+ ) }
63
+ />
64
+ ) ;
65
+ } ;
66
+
34
67
export const SyncIndicator = ( ) : JSX . Element => {
35
68
const syncStatus = useAtomValue ( syncStatusAtom ) ;
36
69
const indexerServicesSyncStatus = useAtomValue ( indexerServicesSyncStatusAtom ) ;
37
70
const api = useAtomValue ( indexerApiAtom ) ;
38
71
const chainStatus = useAtomValue ( chainStatusAtom ) ;
39
- const shieldedProgress = useAtomValue ( shieldedSyncProgress ) ;
40
- const { isFetching : isShieldedFetching } = useAtomValue ( shieldedBalanceAtom ) ;
72
+
73
+ // Individual atom status checks
74
+ const indexerHeartbeat = useAtomValue ( indexerHeartbeatAtom ) ;
75
+ const maspIndexerHeartbeat = useAtomValue ( maspIndexerHeartbeatAtom ) ;
76
+ const rpcHeartbeat = useAtomValue ( rpcHeartbeatAtom ) ;
77
+ const shieldedBalance = useAtomValue ( shieldedBalanceAtom ) ;
78
+ const transparentBalance = useAtomValue ( transparentBalanceAtom ) ;
79
+ const accountBalance = useAtomValue ( accountBalanceAtom ) ;
80
+ const myValidators = useAtomValue ( myValidatorsAtom ) ;
81
+ const allValidators = useAtomValue ( allValidatorsAtom ) ;
82
+ const allProposals = useAtomValue ( allProposalsAtom ) ;
83
+ const votedProposals = useAtomValue ( votedProposalsAtom ) ;
84
+
41
85
const [ blockHeightSync , setBlockHeightSync ] = useState < boolean | null > ( null ) ;
42
86
const [ indexerBlockHeight , setIndexerBlockHeight ] = useState < number | null > (
43
87
null
44
88
) ;
45
- const [ showShieldedSync , setShowShieldedSync ] = useState ( false ) ;
46
- const roundedProgress = useMemo ( ( ) => {
47
- // Only update when the progress changes by at least 1%
48
- return Math . min ( Math . floor ( shieldedProgress * 100 ) , 100 ) ;
49
- } , [ Math . floor ( shieldedProgress * 100 ) ] ) ;
50
-
51
- useEffect ( ( ) => {
52
- let timeout : ReturnType < typeof setTimeout > | undefined ;
53
- if ( isShieldedFetching && roundedProgress < 100 ) {
54
- // wait 2.5 s before we allow the ring to appear
55
- timeout = setTimeout ( ( ) => setShowShieldedSync ( true ) , 2500 ) ;
56
- } else {
57
- setShowShieldedSync ( false ) ;
58
- }
59
- return ( ) => clearTimeout ( timeout ) ;
60
- } , [ isShieldedFetching , roundedProgress ] ) ;
61
89
62
90
const { errors } = syncStatus ;
63
91
const { services } = indexerServicesSyncStatus ;
@@ -73,6 +101,18 @@ export const SyncIndicator = (): JSX.Element => {
73
101
indexerServicesSyncStatus . isSyncing ||
74
102
! blockHeightSync ;
75
103
104
+ // Check individual category status
105
+ const heartbeatStatus =
106
+ indexerHeartbeat . isSuccess &&
107
+ maspIndexerHeartbeat . isSuccess &&
108
+ rpcHeartbeat . isSuccess ;
109
+ const balancesStatus =
110
+ shieldedBalance . isSuccess &&
111
+ transparentBalance . isSuccess &&
112
+ accountBalance . isSuccess ;
113
+ const stakingStatus = myValidators . isSuccess && allValidators . isSuccess ;
114
+ const governanceStatus = allProposals . isSuccess && votedProposals . isSuccess ;
115
+
76
116
useEffect ( ( ) => {
77
117
( async ( ) => {
78
118
const indexerBlockHeight = await fetchBlockHeightByTimestamp (
@@ -86,35 +126,6 @@ export const SyncIndicator = (): JSX.Element => {
86
126
87
127
return (
88
128
< div className = "flex gap-10 px-2 py-3" >
89
- { showShieldedSync && (
90
- < div className = "relative group/tooltip" >
91
- < div className = "relative mt-1" >
92
- < PulsingRing size = "small" />
93
- < div className = "absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-2 h-2 bg-yellow-500 rounded-full" />
94
- </ div >
95
- < Tooltip
96
- position = "bottom"
97
- className = "z-10 w-max max-w-[220px] rounded-md p-4 -mb-6"
98
- >
99
- < div className = "space-y-3" >
100
- < div className = "text-md text-yellow" >
101
- Shielded sync: { roundedProgress } %
102
- </ div >
103
- < div className = "w-full bg-yellow-900 h-1" >
104
- < div
105
- className = "bg-yellow-500 h-1 transition-all duration-300"
106
- style = { { width : `${ roundedProgress } %` } }
107
- />
108
- </ div >
109
- < div className = "text-sm text-neutral-400" >
110
- Syncing your shielded assets now. Balances will update in a few
111
- seconds.
112
- </ div >
113
- </ div >
114
- </ Tooltip >
115
- </ div >
116
- ) }
117
-
118
129
< div className = "relative group/tooltip" >
119
130
< div
120
131
className = { twMerge (
@@ -124,14 +135,41 @@ export const SyncIndicator = (): JSX.Element => {
124
135
isError && ! isSyncing && "bg-red-500"
125
136
) }
126
137
/>
127
- < Tooltip
128
- position = "bottom"
129
- className = "z-10 w-max max-w-[200px] text-balance -mb-6"
130
- >
138
+ < Tooltip position = "bottom" className = "z-10 w-max text-balance -mb-6" >
131
139
{ isSyncing ?
132
- "Syncing..."
140
+ < div className = "py-2" >
141
+ < div className = "text-yellow font-medium" > Syncing...</ div >
142
+ < div >
143
+ Indexer Sync: { chainStatus ?. height ?? "-" } /{ " " }
144
+ { indexerBlockHeight ?? "-" }
145
+ </ div >
146
+ < div className = "flex items-center gap-1" >
147
+ Heartbeat:{ " " }
148
+ { heartbeatStatus ?
149
+ < IoCheckmarkCircleOutline className = "text-green-500" />
150
+ : < LoadingSpinner /> }
151
+ </ div >
152
+ < div className = "flex items-center gap-1" >
153
+ Balances:{ " " }
154
+ { balancesStatus ?
155
+ < IoCheckmarkCircleOutline className = "text-green-500" />
156
+ : < LoadingSpinner /> }
157
+ </ div >
158
+ < div className = "flex items-center gap-1" >
159
+ Staking:{ " " }
160
+ { stakingStatus ?
161
+ < IoCheckmarkCircleOutline className = "text-green-500" />
162
+ : < LoadingSpinner /> }
163
+ </ div >
164
+ < div className = "flex items-center gap-1" >
165
+ Governance:{ " " }
166
+ { governanceStatus ?
167
+ < IoCheckmarkCircleOutline className = "text-green-500" />
168
+ : < LoadingSpinner /> }
169
+ </ div >
170
+ </ div >
133
171
: isError ?
134
- < div >
172
+ < div className = "max-w-xs break-words" >
135
173
{ formatError ( errors , "Error" ) }
136
174
{ formatError ( services , "Lagging services" ) }
137
175
{ isChainStatusError && "Chain status not loaded." }
0 commit comments