Skip to content

Commit 1e0e64a

Browse files
authored
Merge pull request #366 from ethpandaops/feat/add-ref-node-banner
feat: add EIP-7870 hardware specs banner component
2 parents 5008b8d + f6a8ce6 commit 1e0e64a

File tree

14 files changed

+630
-78
lines changed

14 files changed

+630
-78
lines changed
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import { type JSX } from 'react';
2+
import { clsx } from 'clsx';
3+
import { ServerIcon, CpuChipIcon, CircleStackIcon, ArrowTopRightOnSquareIcon } from '@heroicons/react/24/outline';
4+
5+
import { Dialog } from '@/components/Overlays/Dialog';
6+
import { getClusterSpec, CLUSTER_COLORS } from '@/constants/eip7870';
7+
import type { ClusterSpecsModalProps } from './ClusterSpecsModal.types';
8+
9+
/**
10+
* Modal displaying detailed hardware specifications for an EIP-7870 reference node cluster.
11+
*/
12+
export function ClusterSpecsModal({ open, onClose, clusterName }: ClusterSpecsModalProps): JSX.Element {
13+
const cluster = clusterName ? getClusterSpec(clusterName) : null;
14+
const clusterColor = clusterName ? CLUSTER_COLORS[clusterName] : 'text-muted';
15+
16+
if (!cluster) {
17+
return (
18+
<Dialog open={open} onClose={onClose} title="Unknown Cluster" size="sm">
19+
<p className="text-sm text-muted">No hardware specifications found for this cluster.</p>
20+
</Dialog>
21+
);
22+
}
23+
24+
return (
25+
<Dialog
26+
open={open}
27+
onClose={onClose}
28+
title={
29+
<span className="flex items-center gap-2">
30+
<ServerIcon className={clsx('size-5', clusterColor)} />
31+
<span>{cluster.name} cluster</span>
32+
<span className="rounded-xs bg-accent/10 px-1.5 py-0.5 text-xs font-medium text-accent">EIP-7870</span>
33+
</span>
34+
}
35+
size="md"
36+
>
37+
<div className="space-y-4">
38+
{/* CPU Section */}
39+
<div className="rounded-xs border border-border bg-surface/50 p-4">
40+
<div className="mb-3 flex items-center gap-2">
41+
<CpuChipIcon className="size-4 text-muted" />
42+
<span className="text-sm font-medium text-foreground">CPU</span>
43+
</div>
44+
<div className="space-y-2 text-sm">
45+
<div className="flex justify-between">
46+
<span className="text-muted">Model</span>
47+
<span className="font-medium text-foreground">{cluster.cpu.model}</span>
48+
</div>
49+
<div className="flex justify-between">
50+
<span className="text-muted">Cores / Threads</span>
51+
<span className="font-medium text-foreground">
52+
{cluster.cpu.cores}c / {cluster.cpu.threads}t
53+
</span>
54+
</div>
55+
<div className="flex justify-between">
56+
<span className="text-muted">Max Frequency</span>
57+
<span className="font-medium text-foreground">{cluster.cpu.maxFrequency}</span>
58+
</div>
59+
<div className="flex justify-between">
60+
<span className="text-muted">Passmark (Single / Multi)</span>
61+
<span className="font-medium text-foreground">
62+
{cluster.cpu.passmarkSingle} / {cluster.cpu.passmarkMulti}
63+
</span>
64+
</div>
65+
</div>
66+
</div>
67+
68+
{/* Memory Section */}
69+
<div className="rounded-xs border border-border bg-surface/50 p-4">
70+
<div className="mb-3 flex items-center gap-2">
71+
<svg className="size-4 text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={1.5}>
72+
<path
73+
strokeLinecap="round"
74+
strokeLinejoin="round"
75+
d="M8 9h8M8 13h6M3 17V7a2 2 0 012-2h14a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"
76+
/>
77+
</svg>
78+
<span className="text-sm font-medium text-foreground">Memory</span>
79+
</div>
80+
<div className="space-y-2 text-sm">
81+
<div className="flex justify-between">
82+
<span className="text-muted">Total</span>
83+
<span className="font-medium text-foreground">{cluster.memory.total}</span>
84+
</div>
85+
<div className="flex justify-between">
86+
<span className="text-muted">Type</span>
87+
<span className="font-medium text-foreground">{cluster.memory.type}</span>
88+
</div>
89+
<div className="flex justify-between">
90+
<span className="text-muted">Speed</span>
91+
<span className="font-medium text-foreground">{cluster.memory.speed}</span>
92+
</div>
93+
</div>
94+
</div>
95+
96+
{/* Storage Section */}
97+
<div className="rounded-xs border border-border bg-surface/50 p-4">
98+
<div className="mb-3 flex items-center gap-2">
99+
<CircleStackIcon className="size-4 text-muted" />
100+
<span className="text-sm font-medium text-foreground">Storage</span>
101+
</div>
102+
<div className="space-y-2 text-sm">
103+
<div className="flex justify-between">
104+
<span className="text-muted">Model</span>
105+
<span className="font-medium text-foreground">{cluster.storage.model}</span>
106+
</div>
107+
<div className="flex justify-between">
108+
<span className="text-muted">Capacity</span>
109+
<span className="font-medium text-foreground">{cluster.storage.capacity}</span>
110+
</div>
111+
<div className="flex justify-between">
112+
<span className="text-muted">Interface</span>
113+
<span className="font-medium text-foreground">{cluster.storage.interface}</span>
114+
</div>
115+
</div>
116+
</div>
117+
118+
{/* Footer info */}
119+
<div className="flex items-center justify-between border-t border-border pt-4">
120+
<p className="text-xs text-muted">Reference nodes controlled by ethPandaOps</p>
121+
<a
122+
href="https://eips.ethereum.org/EIPS/eip-7870"
123+
target="_blank"
124+
rel="noopener noreferrer"
125+
className="flex items-center gap-1 text-xs text-accent hover:underline"
126+
>
127+
View EIP-7870
128+
<ArrowTopRightOnSquareIcon className="size-3" />
129+
</a>
130+
</div>
131+
</div>
132+
</Dialog>
133+
);
134+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
export type ClusterSpecsModalProps = {
2+
/** Whether the modal is open */
3+
open: boolean;
4+
/** Callback when the modal is closed */
5+
onClose: () => void;
6+
/** The cluster name to display specs for (utility or sigma) */
7+
clusterName: string | null;
8+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { ClusterSpecsModal } from './ClusterSpecsModal';
2+
export type { ClusterSpecsModalProps } from './ClusterSpecsModal.types';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Meta, StoryObj } from '@storybook/react-vite';
2+
3+
import { EIP7870SpecsBanner } from './EIP7870SpecsBanner';
4+
5+
const meta = {
6+
title: 'Components/Ethereum/EIP7870SpecsBanner',
7+
component: EIP7870SpecsBanner,
8+
parameters: {
9+
layout: 'centered',
10+
},
11+
tags: ['autodocs'],
12+
decorators: [
13+
Story => (
14+
<div className="min-w-[700px] rounded-sm bg-background p-6">
15+
<Story />
16+
</div>
17+
),
18+
],
19+
} satisfies Meta<typeof EIP7870SpecsBanner>;
20+
21+
export default meta;
22+
type Story = StoryObj<typeof meta>;
23+
24+
export const Default: Story = {
25+
args: {},
26+
};
27+
28+
export const Collapsed: Story = {
29+
args: {},
30+
parameters: {
31+
docs: {
32+
description: {
33+
story: 'Shows the collapsed view with summary hardware specs',
34+
},
35+
},
36+
},
37+
};
38+
39+
export const WithCustomClass: Story = {
40+
args: {
41+
className: 'border-primary/30',
42+
},
43+
parameters: {
44+
docs: {
45+
description: {
46+
story: 'Banner with custom border styling',
47+
},
48+
},
49+
},
50+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { useState, type JSX } from 'react';
2+
import { clsx } from 'clsx';
3+
import { ChevronDownIcon, ChevronUpIcon, ServerIcon } from '@heroicons/react/24/outline';
4+
5+
import { CLUSTER_SPECS, CLUSTER_COLORS } from '@/constants/eip7870';
6+
import type { EIP7870SpecsBannerProps } from './EIP7870SpecsBanner.types';
7+
8+
/**
9+
* Displays EIP-7870 hardware specifications in a compact, expandable banner.
10+
* Shows cluster specs with option to expand for full details.
11+
*/
12+
export function EIP7870SpecsBanner({ className }: EIP7870SpecsBannerProps): JSX.Element {
13+
const [isExpanded, setIsExpanded] = useState(true);
14+
15+
return (
16+
<div className={clsx('rounded-xs border border-border bg-surface/50', className)}>
17+
{/* Collapsed view - always visible */}
18+
<button
19+
type="button"
20+
onClick={() => setIsExpanded(!isExpanded)}
21+
className="flex w-full items-center justify-between gap-3 px-4 py-2.5 text-left transition-colors hover:bg-surface"
22+
>
23+
<div className="flex items-center gap-2.5">
24+
<ServerIcon className="size-4 shrink-0 text-muted" />
25+
<span className="text-xs font-medium text-foreground">Reference Node Hardware Specs</span>
26+
</div>
27+
<div className="flex items-center gap-2">
28+
<span className="text-xs text-accent">EIP-7870</span>
29+
{isExpanded ? (
30+
<ChevronUpIcon className="size-4 text-muted" />
31+
) : (
32+
<ChevronDownIcon className="size-4 text-muted" />
33+
)}
34+
</div>
35+
</button>
36+
37+
{/* Expanded view - cluster specs table */}
38+
{isExpanded && (
39+
<div className="border-t border-border px-4 py-3">
40+
<div className="overflow-x-auto">
41+
<table className="w-full text-left text-xs">
42+
<thead>
43+
<tr className="border-b border-border">
44+
<th className="pr-4 pb-2 font-medium text-foreground">Cluster</th>
45+
<th className="pr-4 pb-2 font-medium text-foreground">CPU</th>
46+
<th className="pr-4 pb-2 font-medium text-foreground">Cores / Threads</th>
47+
<th className="pr-4 pb-2 font-medium text-foreground">Passmark</th>
48+
<th className="pr-4 pb-2 font-medium text-foreground">Memory</th>
49+
<th className="pb-2 font-medium text-foreground">Storage</th>
50+
</tr>
51+
</thead>
52+
<tbody className="text-muted">
53+
{CLUSTER_SPECS.map(cluster => (
54+
<tr key={cluster.name} className="border-b border-border/50 last:border-0">
55+
<td className="py-2 pr-4">
56+
<div className="flex items-center gap-2">
57+
<ServerIcon className={clsx('size-4 shrink-0', CLUSTER_COLORS[cluster.name])} />
58+
<span className="font-medium text-foreground">{cluster.name}</span>
59+
</div>
60+
</td>
61+
<td className="py-2 pr-4">
62+
<div>{cluster.cpu.model}</div>
63+
<div className="text-muted/70">up to {cluster.cpu.maxFrequency}</div>
64+
</td>
65+
<td className="py-2 pr-4">
66+
{cluster.cpu.cores}c / {cluster.cpu.threads}t
67+
</td>
68+
<td className="py-2 pr-4">
69+
<div>{cluster.cpu.passmarkSingle} Single</div>
70+
<div className="text-muted/70">{cluster.cpu.passmarkMulti} Multi</div>
71+
</td>
72+
<td className="py-2 pr-4">
73+
<div>{cluster.memory.total}</div>
74+
<div className="text-muted/70">
75+
{cluster.memory.type} @ {cluster.memory.speed}
76+
</div>
77+
</td>
78+
<td className="py-2">
79+
<div>
80+
{cluster.storage.model} {cluster.storage.capacity}
81+
</div>
82+
<div className="text-muted/70">{cluster.storage.interface}</div>
83+
</td>
84+
</tr>
85+
))}
86+
</tbody>
87+
</table>
88+
</div>
89+
<div className="mt-3 flex items-center justify-between border-t border-border/50 pt-3">
90+
<p className="text-xs text-muted">
91+
Reference nodes are controlled by ethPandaOps and follow EIP-7870 hardware specifications.
92+
</p>
93+
<a
94+
href="https://eips.ethereum.org/EIPS/eip-7870"
95+
target="_blank"
96+
rel="noopener noreferrer"
97+
className="shrink-0 text-xs text-accent hover:underline"
98+
>
99+
View EIP-7870
100+
</a>
101+
</div>
102+
</div>
103+
)}
104+
</div>
105+
);
106+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type EIP7870SpecsBannerProps = {
2+
/**
3+
* Additional CSS classes
4+
*/
5+
className?: string;
6+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { EIP7870SpecsBanner } from './EIP7870SpecsBanner';
2+
export type { EIP7870SpecsBannerProps } from './EIP7870SpecsBanner.types';

0 commit comments

Comments
 (0)