Skip to content

Commit b3eb901

Browse files
committed
feat(ensadmin): include projection info on status page
Also, make `<RelativeTime includeSeconds />` component to re-render every second to present relative time to the current time.
1 parent 4b148de commit b3eb901

8 files changed

Lines changed: 134 additions & 32 deletions

File tree

apps/ensadmin/src/app/mock/indexing-stats/page.tsx

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
"use client";
22

33
import { useQuery } from "@tanstack/react-query";
4+
import { getUnixTime } from "date-fns";
45
import { useEffect, useState } from "react";
56

67
import {
7-
type IndexingStatusResponse,
8+
CrossChainIndexingStatusSnapshot,
9+
createRealtimeIndexingStatusProjection,
810
IndexingStatusResponseOk,
911
OmnichainIndexingStatusIds,
1012
} from "@ensnode/ensnode-sdk";
@@ -13,10 +15,7 @@ import { IndexingStats } from "@/components/indexing-status/indexing-stats";
1315
import { Button } from "@/components/ui/button";
1416
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
1517

16-
import {
17-
indexingStatusResponseError,
18-
indexingStatusResponseOkOmnichain,
19-
} from "../indexing-status-api.mock";
18+
import { indexingStatusResponseOkOmnichain } from "../indexing-status-api.mock";
2019

2120
type LoadingVariant = "Loading" | "Loading Error";
2221
type ResponseOkVariant = keyof typeof indexingStatusResponseOkOmnichain;
@@ -37,7 +36,7 @@ let loadingTimeoutId: number;
3736

3837
async function fetchMockedIndexingStatus(
3938
selectedVariant: Variant,
40-
): Promise<IndexingStatusResponseOk> {
39+
): Promise<CrossChainIndexingStatusSnapshot> {
4140
// always try clearing loading timeout when performing a mocked fetch
4241
// this way we get a fresh and very long request to observe the loading state
4342
if (loadingTimeoutId) {
@@ -48,14 +47,19 @@ async function fetchMockedIndexingStatus(
4847
case OmnichainIndexingStatusIds.Unstarted:
4948
case OmnichainIndexingStatusIds.Backfill:
5049
case OmnichainIndexingStatusIds.Following:
51-
case OmnichainIndexingStatusIds.Completed:
52-
return indexingStatusResponseOkOmnichain[selectedVariant] as IndexingStatusResponseOk;
50+
case OmnichainIndexingStatusIds.Completed: {
51+
const response = indexingStatusResponseOkOmnichain[
52+
selectedVariant
53+
] as IndexingStatusResponseOk;
54+
55+
return response.realtimeProjection.snapshot;
56+
}
5357
case "Error ResponseCode":
5458
throw new Error(
5559
"Received Indexing Status response with responseCode other than 'ok' which will not be cached.",
5660
);
5761
case "Loading":
58-
return new Promise<IndexingStatusResponseOk>((_resolve, reject) => {
62+
return new Promise<CrossChainIndexingStatusSnapshot>((_resolve, reject) => {
5963
loadingTimeoutId = +setTimeout(reject, 5 * 60 * 1_000);
6064
});
6165
case "Loading Error":
@@ -67,10 +71,12 @@ export default function MockIndexingStatusPage() {
6771
const [selectedVariant, setSelectedVariant] = useState<Variant>(
6872
OmnichainIndexingStatusIds.Unstarted,
6973
);
74+
const now = getUnixTime(new Date());
7075

7176
const mockedIndexingStatus = useQuery({
7277
queryKey: ["mock", "useIndexingStatus", selectedVariant],
7378
queryFn: () => fetchMockedIndexingStatus(selectedVariant),
79+
select: (cachedSnapshot) => createRealtimeIndexingStatusProjection(cachedSnapshot, now),
7480
retry: false, // allows loading error to be observed immediately
7581
});
7682

apps/ensadmin/src/components/indexing-status/indexing-stats.tsx

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import {
2424
sortChainStatusesByStartBlockAsc,
2525
} from "@ensnode/ensnode-sdk";
2626

27-
import { useIndexingStatusWithSwr } from "@/components/indexing-status/use-indexing-status-with-swr";
2827
import { Badge } from "@/components/ui/badge";
2928
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3029
import { formatChainStatus, formatOmnichainIndexingStatus } from "@/lib/indexing-status";
@@ -33,6 +32,8 @@ import { cn } from "@/lib/utils";
3332
import { BackfillStatus } from "./backfill-status";
3433
import { BlockStats } from "./block-refs";
3534
import { IndexingStatusLoading } from "./indexing-status-loading";
35+
import { ProjectionInfo } from "./projection-info";
36+
import { useIndexingStatusWithSwr } from "./use-indexing-status-with-swr";
3637

3738
interface IndexingStatsForOmnichainStatusSnapshotProps<
3839
OmnichainIndexingStatusSnapshotType extends
@@ -322,15 +323,18 @@ export function IndexingStatsForSnapshotFollowing({
322323
* UI component for presenting indexing stats UI for specific overall status.
323324
*/
324325
export function IndexingStatsShell({
325-
omnichainStatus,
326+
realtimeProjection,
326327
children,
327-
}: PropsWithChildren<{ omnichainStatus?: OmnichainIndexingStatusId }>) {
328+
}: PropsWithChildren<{ realtimeProjection?: RealtimeIndexingStatusProjection }>) {
329+
const omnichainStatus = realtimeProjection?.snapshot.omnichainSnapshot.omnichainStatus;
328330
return (
329331
<Card className="w-full flex flex-col gap-2">
330332
<CardHeader>
331333
<CardTitle className="flex gap-2 items-center">
332334
<span>Indexing Status</span>
333335

336+
{realtimeProjection && <ProjectionInfo realtimeProjection={realtimeProjection} />}
337+
334338
{omnichainStatus && (
335339
<Badge
336340
className={cn("uppercase text-xs leading-none")}
@@ -422,7 +426,7 @@ export function IndexingStatsForRealtimeStatusProjection({
422426
<section className="flex flex-col gap-6">
423427
{maybeIndexingTimeline}
424428

425-
<IndexingStatsShell omnichainStatus={omnichainStatusSnapshot.omnichainStatus}>
429+
<IndexingStatsShell realtimeProjection={realtimeProjection}>
426430
{indexingStats}
427431
</IndexingStatsShell>
428432
</section>
@@ -449,11 +453,9 @@ export function IndexingStats(props: IndexingStatsProps) {
449453
return <IndexingStatusLoading />;
450454
}
451455

452-
const indexingStatus = indexingStatusQuery.data;
453-
454456
return (
455457
<IndexingStatsForRealtimeStatusProjection
456-
realtimeProjection={indexingStatus.realtimeProjection}
458+
realtimeProjection={indexingStatusQuery.data.realtimeProjection}
457459
/>
458460
);
459461
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { RelativeTime } from "@namehash/namehash-ui";
4+
import { InfoIcon } from "lucide-react";
5+
6+
import type { RealtimeIndexingStatusProjection } from "@ensnode/ensnode-sdk";
7+
8+
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
9+
10+
interface ProjectionInfoProps {
11+
realtimeProjection: RealtimeIndexingStatusProjection;
12+
}
13+
14+
/**
15+
* Displays metadata about the current indexing status projection in a tooltip.
16+
* Shows when the projection was generated, when the snapshot was taken, and worst-case distance.
17+
*/
18+
export function ProjectionInfo({ realtimeProjection }: ProjectionInfoProps) {
19+
const { projectedAt, snapshot, worstCaseDistance } = realtimeProjection;
20+
const { snapshotTime } = snapshot;
21+
22+
return (
23+
<Tooltip delayDuration={300}>
24+
<TooltipTrigger asChild>
25+
<button
26+
type="button"
27+
className="inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors hover:bg-accent hover:text-accent-foreground h-8 w-8"
28+
aria-label="Indexing Status Metadata"
29+
>
30+
<InfoIcon className="h-4 w-4" />
31+
</button>
32+
</TooltipTrigger>
33+
<TooltipContent
34+
side="right"
35+
className="bg-gray-50 text-sm text-black shadow-md outline-none w-80 p-4"
36+
>
37+
<div className="flex flex-col gap-3">
38+
<div className="flex flex-col gap-1">
39+
<div className="font-semibold text-xs text-gray-500 uppercase">
40+
Worst-Case Distance*
41+
</div>
42+
<div className="text-sm">
43+
{worstCaseDistance !== null ? `${worstCaseDistance} seconds` : "N/A"}
44+
</div>
45+
</div>
46+
47+
<div className="text-xs text-gray-600 leading-relaxed">
48+
* as of real-time projection generated just now from indexing status snapshot captured{" "}
49+
<RelativeTime timestamp={snapshotTime} includeSeconds conciseFormatting />.
50+
</div>
51+
</div>
52+
</TooltipContent>
53+
</Tooltip>
54+
);
55+
}

apps/ensadmin/src/components/indexing-status/use-indexing-status-with-swr.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"use client";
22

3+
import { useNow } from "@namehash/namehash-ui";
4+
import { secondsToMilliseconds } from "date-fns";
35
import { useCallback, useMemo } from "react";
46

57
import {
@@ -11,16 +13,21 @@ import {
1113
WithSDKConfigParameter,
1214
} from "@ensnode/ensnode-react";
1315
import {
16+
CrossChainIndexingStatusSnapshotOmnichain,
17+
createRealtimeIndexingStatusProjection,
18+
Duration,
1419
type IndexingStatusRequest,
1520
IndexingStatusResponseCodes,
1621
IndexingStatusResponseOk,
1722
} from "@ensnode/ensnode-sdk";
1823

19-
const DEFAULT_REFETCH_INTERVAL = 10 * 1000;
24+
const DEFAULT_REFETCH_INTERVAL = secondsToMilliseconds(10);
25+
26+
const REALTIME_PROJECTION_REFRESH_RATE: Duration = 1;
2027

2128
interface UseIndexingStatusParameters
2229
extends IndexingStatusRequest,
23-
QueryParameter<IndexingStatusResponseOk> {}
30+
QueryParameter<CrossChainIndexingStatusSnapshotOmnichain> {}
2431

2532
/**
2633
* A proxy hook for {@link useIndexingStatus} which applies
@@ -31,6 +38,7 @@ export function useIndexingStatusWithSwr(
3138
) {
3239
const { config, query = {} } = parameters;
3340
const _config = useENSNodeSDKConfig(config);
41+
const now = useNow({ timeToRefresh: REALTIME_PROJECTION_REFRESH_RATE });
3442

3543
const queryOptions = useMemo(() => createIndexingStatusQueryOptions(_config), [_config]);
3644
const queryKey = useMemo(() => ["swr", ...queryOptions.queryKey], [queryOptions.queryKey]);
@@ -46,18 +54,39 @@ export function useIndexingStatusWithSwr(
4654
);
4755
}
4856

49-
// successful response to be cached
50-
return response;
57+
// The indexing status snapshot has been fetched and successfully validated for caching.
58+
// Therefore, return it so that query cache for `queryOptions.queryKey` will:
59+
// - Replace the currently cached value (if any) with this new value.
60+
// - Return this non-null value.
61+
return response.realtimeProjection.snapshot;
5162
}),
5263
[queryOptions.queryFn],
5364
);
5465

66+
// Call select function to `createRealtimeIndexingStatusProjection` each time
67+
// `now` is updated.
68+
const select = useCallback(
69+
(cachedSnapshot: CrossChainIndexingStatusSnapshotOmnichain): IndexingStatusResponseOk => {
70+
const realtimeProjection = createRealtimeIndexingStatusProjection(cachedSnapshot, now);
71+
72+
// Maintain the original response shape of `IndexingStatusResponse`
73+
// for the consumers. Creating a new projection from the cached snapshot
74+
// each time `now` is updated should be implementation detail.
75+
return {
76+
responseCode: IndexingStatusResponseCodes.Ok,
77+
realtimeProjection,
78+
} satisfies IndexingStatusResponseOk;
79+
},
80+
[now],
81+
);
82+
5583
return useSwrQuery({
5684
...queryOptions,
5785
refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently
5886
...query,
5987
enabled: query.enabled ?? queryOptions.enabled,
6088
queryKey,
6189
queryFn,
90+
select,
6291
});
6392
}

apps/ensadmin/src/components/registrar-actions/use-stateful-fetch-registrar-actions.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { useENSNodeConfig, useRegistrarActions } from "@ensnode/ensnode-react";
22
import {
3-
IndexingStatusResponseCodes,
43
RegistrarActionsOrders,
54
RegistrarActionsResponseCodes,
65
registrarActionsPrerequisites,
@@ -44,13 +43,10 @@ export function useStatefulRegistrarActions({
4443

4544
let isRegistrarActionsApiSupported = false;
4645

47-
if (
48-
ensNodeConfigQuery.isSuccess &&
49-
indexingStatusQuery.isSuccess &&
50-
indexingStatusQuery.data.responseCode === IndexingStatusResponseCodes.Ok
51-
) {
46+
if (ensNodeConfigQuery.isSuccess && indexingStatusQuery.isSuccess) {
5247
const { ensIndexerPublicConfig } = ensNodeConfigQuery.data;
53-
const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot;
48+
const { realtimeProjection } = indexingStatusQuery.data;
49+
const { omnichainSnapshot } = realtimeProjection.snapshot;
5450

5551
isRegistrarActionsApiSupported =
5652
hasEnsIndexerConfigSupport(ensIndexerPublicConfig) &&
@@ -100,7 +96,8 @@ export function useStatefulRegistrarActions({
10096
} satisfies StatefulFetchRegistrarActionsUnsupported;
10197
}
10298

103-
const { omnichainSnapshot } = indexingStatusQuery.data.realtimeProjection.snapshot;
99+
const { realtimeProjection } = indexingStatusQuery.data;
100+
const { omnichainSnapshot } = realtimeProjection.snapshot;
104101

105102
// fetching is temporarily not possible due to indexing status being not advanced enough
106103
if (!hasIndexingStatusSupport(omnichainSnapshot.omnichainStatus)) {

packages/ensnode-react/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"vitest": "catalog:"
5757
},
5858
"dependencies": {
59-
"@ensnode/ensnode-sdk": "workspace:*"
59+
"@ensnode/ensnode-sdk": "workspace:*",
60+
"date-fns": "catalog:"
6061
}
6162
}

packages/namehash-ui/src/components/datetime/RelativeTime.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,18 @@ export function RelativeTime({
8181
const [relativeTime, setRelativeTime] = useState<string>("");
8282

8383
useEffect(() => {
84-
setRelativeTime(
85-
formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo),
86-
);
84+
const updateTime = () => {
85+
setRelativeTime(
86+
formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo),
87+
);
88+
};
89+
90+
updateTime();
91+
92+
if (includeSeconds) {
93+
const interval = setInterval(updateTime, 1000);
94+
return () => clearInterval(interval);
95+
}
8796
}, [timestamp, conciseFormatting, enforcePast, includeSeconds, relativeTo]);
8897

8998
const tooltipTriggerContent = (

pnpm-lock.yaml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)