Skip to content

Commit c4581bd

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 abbd193 commit c4581bd

8 files changed

Lines changed: 142 additions & 20 deletions

File tree

apps/ensadmin/src/components/datetime-utils/index.tsx

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

106106
useEffect(() => {
107-
setRelativeTime(
108-
formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo),
109-
);
107+
const updateTime = () => {
108+
setRelativeTime(
109+
formatRelativeTime(timestamp, enforcePast, includeSeconds, conciseFormatting, relativeTo),
110+
);
111+
};
112+
113+
updateTime();
114+
115+
if (includeSeconds) {
116+
const interval = setInterval(updateTime, 1000);
117+
return () => clearInterval(interval);
118+
}
110119
}, [timestamp, conciseFormatting, enforcePast, includeSeconds, relativeTo]);
111120

112121
const tooltipTriggerContent = (

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import {
2525

2626
import { ChainIcon } from "@/components/chains/ChainIcon";
2727
import { ChainName } from "@/components/chains/ChainName";
28-
import { useIndexingStatusWithSwr } from "@/components/indexing-status/use-indexing-status-with-swr";
2928
import { Badge } from "@/components/ui/badge";
3029
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
3130
import { formatChainStatus, formatOmnichainIndexingStatus } from "@/lib/indexing-status";
@@ -34,6 +33,8 @@ import { cn } from "@/lib/utils";
3433
import { BackfillStatus } from "./backfill-status";
3534
import { BlockStats } from "./block-refs";
3635
import { IndexingStatusLoading } from "./indexing-status-loading";
36+
import { ProjectionInfo } from "./projection-info";
37+
import { useIndexingStatusWithSwr } from "./use-indexing-status-with-swr";
3738

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

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

426-
<IndexingStatsShell omnichainStatus={omnichainStatusSnapshot.omnichainStatus}>
430+
<IndexingStatsShell realtimeProjection={realtimeProjection}>
427431
{indexingStats}
428432
</IndexingStatsShell>
429433
</section>
@@ -450,11 +454,5 @@ export function IndexingStats(props: IndexingStatsProps) {
450454
return <IndexingStatusLoading />;
451455
}
452456

453-
const indexingStatus = indexingStatusQuery.data;
454-
455-
return (
456-
<IndexingStatsForRealtimeStatusProjection
457-
realtimeProjection={indexingStatus.realtimeProjection}
458-
/>
459-
);
457+
return <IndexingStatsForRealtimeStatusProjection realtimeProjection={indexingStatusQuery.data} />;
460458
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"use client";
2+
3+
import { InfoIcon } from "lucide-react";
4+
5+
import type { RealtimeIndexingStatusProjection } from "@ensnode/ensnode-sdk";
6+
7+
import { RelativeTime } from "@/components/datetime-utils";
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: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,32 @@
11
"use client";
22

3+
import { secondsToMilliseconds } from "date-fns";
34
import { useCallback, useMemo } from "react";
45

56
import {
67
createIndexingStatusQueryOptions,
78
QueryParameter,
89
useENSNodeSDKConfig,
910
type useIndexingStatus,
11+
useNow,
1012
useSwrQuery,
1113
WithSDKConfigParameter,
1214
} from "@ensnode/ensnode-react";
1315
import {
16+
CrossChainIndexingStatusSnapshotOmnichain,
17+
createRealtimeIndexingStatusProjection,
1418
type IndexingStatusRequest,
1519
IndexingStatusResponseCodes,
16-
IndexingStatusResponseOk,
20+
RealtimeIndexingStatusProjection,
1721
} from "@ensnode/ensnode-sdk";
1822

19-
const DEFAULT_REFETCH_INTERVAL = 10 * 1000;
23+
const DEFAULT_REFETCH_INTERVAL = secondsToMilliseconds(10);
24+
25+
const REALTIME_PROJECTION_REFRESH_RATE = secondsToMilliseconds(1);
2026

2127
interface UseIndexingStatusParameters
2228
extends IndexingStatusRequest,
23-
QueryParameter<IndexingStatusResponseOk> {}
29+
QueryParameter<CrossChainIndexingStatusSnapshotOmnichain> {}
2430

2531
/**
2632
* A proxy hook for {@link useIndexingStatus} which applies
@@ -31,6 +37,7 @@ export function useIndexingStatusWithSwr(
3137
) {
3238
const { config, query = {} } = parameters;
3339
const _config = useENSNodeSDKConfig(config);
40+
const now = useNow(REALTIME_PROJECTION_REFRESH_RATE);
3441

3542
const queryOptions = useMemo(() => createIndexingStatusQueryOptions(_config), [_config]);
3643
const queryKey = useMemo(() => ["swr", ...queryOptions.queryKey], [queryOptions.queryKey]);
@@ -46,18 +53,35 @@ export function useIndexingStatusWithSwr(
4653
);
4754
}
4855

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

65+
// Call select function to `createRealtimeIndexingStatusProjection` each time
66+
// `now` is updated.
67+
const select = useCallback(
68+
(
69+
cachedSnapshot: CrossChainIndexingStatusSnapshotOmnichain,
70+
): RealtimeIndexingStatusProjection => {
71+
const realtimeProjection = createRealtimeIndexingStatusProjection(cachedSnapshot, now);
72+
73+
return realtimeProjection;
74+
},
75+
[now],
76+
);
77+
5578
return useSwrQuery({
5679
...queryOptions,
5780
refetchInterval: query.refetchInterval ?? DEFAULT_REFETCH_INTERVAL, // Indexing status changes frequently
5881
...query,
5982
enabled: query.enabled ?? queryOptions.enabled,
6083
queryKey,
6184
queryFn,
85+
select,
6286
});
6387
}

packages/ensnode-react/package.json

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

packages/ensnode-react/src/hooks/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./useENSNodeConfig";
22
export * from "./useENSNodeSDKConfig";
33
export * from "./useIndexingStatus";
4+
export * from "./useNow";
45
export * from "./usePrimaryName";
56
export * from "./usePrimaryNames";
67
export * from "./useRecords";
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { getUnixTime } from "date-fns";
2+
import { useEffect, useState } from "react";
3+
4+
/**
5+
* Hook that returns the current Unix timestamp, updated at a specified interval.
6+
*
7+
* @param refreshRate - How often to update the timestamp in milliseconds (default: 1000ms)
8+
* @returns Current Unix timestamp that updates every refreshRate milliseconds
9+
*
10+
* @example
11+
* ```tsx
12+
* // Updates every second
13+
* const now = useNow(1000);
14+
*
15+
* // Updates every 5 seconds
16+
* const now = useNow(5000);
17+
* ```
18+
*/
19+
export function useNow(refreshRate = 1000): number {
20+
const [now, setNow] = useState(() => getUnixTime(new Date()));
21+
22+
useEffect(() => {
23+
const interval = setInterval(() => {
24+
setNow(getUnixTime(new Date()));
25+
}, refreshRate);
26+
27+
return () => clearInterval(interval);
28+
}, [refreshRate]);
29+
30+
return now;
31+
}

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)