Skip to content

Commit cb38729

Browse files
authored
feat: update syncing view (#2066)
1 parent 3fdf237 commit cb38729

File tree

4 files changed

+151
-72
lines changed

4 files changed

+151
-72
lines changed

apps/namadillo/src/App/Common/PulsingRing.tsx

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,9 @@ import { useScope } from "hooks/useScope";
44
import { useRef } from "react";
55
import { twMerge } from "tailwind-merge";
66

7-
type PulsingRingProps = { className?: string; size?: "small" | "large" };
7+
type PulsingRingProps = { className?: string };
88

9-
export const PulsingRing = ({
10-
className,
11-
size = "large",
12-
}: PulsingRingProps): JSX.Element => {
9+
export const PulsingRing = ({ className }: PulsingRingProps): JSX.Element => {
1310
const containerRef = useRef<HTMLDivElement>(null);
1411

1512
useScope(
@@ -49,10 +46,9 @@ export const PulsingRing = ({
4946
[]
5047
);
5148

52-
const renderRing = (className: string, key?: number): JSX.Element => {
49+
const renderRing = (className: string): JSX.Element => {
5350
return (
5451
<span
55-
key={key}
5652
data-animation="ring"
5753
className={clsx(
5854
"block absolute aspect-square border border-yellow rounded-full",
@@ -63,17 +59,14 @@ export const PulsingRing = ({
6359
);
6460
};
6561

66-
const ringSizes =
67-
size === "small" ?
68-
["h-[0.75em]", "h-[1.35em]", "h-[1.95em]"]
69-
: ["h-[1.8em]", "h-[3em]", "h-[4.2em]"];
70-
7162
return (
7263
<span
7364
ref={containerRef}
7465
className={twMerge("block relative leading-0", className)}
7566
>
76-
{ringSizes.map((sizeClass, index) => renderRing(sizeClass, index))}
67+
{renderRing("h-[1.8em]")}
68+
{renderRing("h-[3em]")}
69+
{renderRing("h-[4.2em]")}{" "}
7770
</span>
7871
);
7972
};

apps/namadillo/src/App/Layout/Sidebar.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import { ShieldedSyncProgress } from "App/Masp/ShieldedSyncProgress";
12
import { EpochInformation } from "App/Sidebars/EpochInformation";
23
import { ReactNode } from "react";
34

45
export const Sidebar = ({ children }: { children: ReactNode }): JSX.Element => {
56
return (
67
<aside className="flex flex-col gap-2 mt-1.5 lg:mt-0">
78
<EpochInformation />
9+
<ShieldedSyncProgress />
810
{children}
911
</aside>
1012
);

apps/namadillo/src/App/Layout/SyncIndicator.tsx

Lines changed: 97 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
import { Tooltip } from "@namada/components";
2+
import { accountBalanceAtom, transparentBalanceAtom } from "atoms/accounts";
23
import { indexerApiAtom } from "atoms/api";
3-
import { shieldedBalanceAtom, shieldedSyncProgress } from "atoms/balance/atoms";
4+
import { shieldedBalanceAtom } from "atoms/balance";
45
import { fetchBlockHeightByTimestamp } from "atoms/balance/services";
56
import { chainStatusAtom } from "atoms/chain";
7+
import { allProposalsAtom, votedProposalsAtom } from "atoms/proposals";
8+
import {
9+
indexerHeartbeatAtom,
10+
maspIndexerHeartbeatAtom,
11+
rpcHeartbeatAtom,
12+
} from "atoms/settings";
613
import {
714
indexerServicesSyncStatusAtom,
815
syncStatusAtom,
916
} from "atoms/syncStatus/atoms";
17+
import { allValidatorsAtom, myValidatorsAtom } from "atoms/validators";
18+
import clsx from "clsx";
1019
import { useAtomValue } from "jotai";
11-
import { useEffect, useMemo, useState } from "react";
20+
import { useEffect, useState } from "react";
21+
import { IoCheckmarkCircleOutline } from "react-icons/io5";
1222
import { twMerge } from "tailwind-merge";
13-
import { PulsingRing } from "../Common/PulsingRing";
1423

1524
const formatError = (
1625
errors: (string | Error)[],
@@ -21,43 +30,62 @@ const formatError = (
2130
}
2231

2332
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+
)}
2643
{errors.map((e) => {
2744
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+
);
2950
})}
3051
</div>
3152
);
3253
};
3354

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+
3467
export const SyncIndicator = (): JSX.Element => {
3568
const syncStatus = useAtomValue(syncStatusAtom);
3669
const indexerServicesSyncStatus = useAtomValue(indexerServicesSyncStatusAtom);
3770
const api = useAtomValue(indexerApiAtom);
3871
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+
4185
const [blockHeightSync, setBlockHeightSync] = useState<boolean | null>(null);
4286
const [indexerBlockHeight, setIndexerBlockHeight] = useState<number | null>(
4387
null
4488
);
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]);
6189

6290
const { errors } = syncStatus;
6391
const { services } = indexerServicesSyncStatus;
@@ -73,6 +101,18 @@ export const SyncIndicator = (): JSX.Element => {
73101
indexerServicesSyncStatus.isSyncing ||
74102
!blockHeightSync;
75103

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+
76116
useEffect(() => {
77117
(async () => {
78118
const indexerBlockHeight = await fetchBlockHeightByTimestamp(
@@ -86,35 +126,6 @@ export const SyncIndicator = (): JSX.Element => {
86126

87127
return (
88128
<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-
118129
<div className="relative group/tooltip">
119130
<div
120131
className={twMerge(
@@ -124,14 +135,41 @@ export const SyncIndicator = (): JSX.Element => {
124135
isError && !isSyncing && "bg-red-500"
125136
)}
126137
/>
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">
131139
{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>
133171
: isError ?
134-
<div>
172+
<div className="max-w-xs break-words">
135173
{formatError(errors, "Error")}
136174
{formatError(services, "Lagging services")}
137175
{isChainStatusError && "Chain status not loaded."}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { shieldedBalanceAtom, shieldedSyncProgress } from "atoms/balance/atoms";
2+
import { useRequiresNewShieldedSync } from "hooks/useRequiresNewShieldedSync";
3+
import { useAtomValue } from "jotai";
4+
import { useEffect, useMemo, useState } from "react";
5+
import { twMerge } from "tailwind-merge";
6+
7+
export const ShieldedSyncProgress = (): JSX.Element => {
8+
const syncProgress = useAtomValue(shieldedSyncProgress);
9+
const { isFetching } = useAtomValue(shieldedBalanceAtom);
10+
const [showShieldedSync, setShowShieldedSync] = useState(false);
11+
const requiresNewShieldedSync = useRequiresNewShieldedSync();
12+
13+
const roundedProgress = useMemo(() => {
14+
// Only update when the progress changes by at least 1%
15+
return Math.min(Math.floor(syncProgress * 100), 100);
16+
}, [Math.floor(syncProgress * 100)]);
17+
18+
useEffect(() => {
19+
let timeout = undefined;
20+
if (isFetching && roundedProgress < 100) {
21+
// wait 2.5 s before we allow the ring to appear
22+
timeout = setTimeout(() => setShowShieldedSync(true), 2500);
23+
} else {
24+
setShowShieldedSync(false);
25+
}
26+
return () => clearTimeout(timeout);
27+
}, [isFetching, roundedProgress]);
28+
29+
if (!showShieldedSync && !requiresNewShieldedSync) {
30+
return <></>;
31+
}
32+
33+
return (
34+
<div className="relative bg-black text-yellow rounded-sm overflow-hidden text-xs font-medium py-2 px-3">
35+
Shielded sync{" "}
36+
{syncProgress === 1 ? "converting..." : `progress: ${roundedProgress}%`}
37+
<div
38+
className={twMerge(
39+
"absolute bg-yellow top-0 left-0 w-full h-full mix-blend-difference origin-left",
40+
roundedProgress > 0 && "transition-all"
41+
)}
42+
style={{ transform: `scaleX(${roundedProgress}%)` }}
43+
/>
44+
</div>
45+
);
46+
};

0 commit comments

Comments
 (0)