mirror of
https://github.com/logos-storage/metrics.git
synced 2026-02-27 08:23:12 +00:00
finishing up ui enhancements
This commit is contained in:
parent
1146db13dc
commit
acf3e90629
109
components/ui/dialog.jsx
Normal file
109
components/ui/dialog.jsx
Normal file
@ -0,0 +1,109 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { X } from "lucide-react"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Dialog = DialogPrimitive.Root
|
||||
|
||||
const DialogTrigger = DialogPrimitive.Trigger
|
||||
|
||||
const DialogPortal = DialogPrimitive.Portal
|
||||
|
||||
const DialogClose = DialogPrimitive.Close
|
||||
|
||||
const DialogOverlay = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
|
||||
|
||||
const DialogContent = React.forwardRef(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border border-neutral-800 bg-neutral-900 p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm text-white hover:text-[#7afbaf] ring-offset-neutral-900 transition-all hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-neutral-400 focus:ring-offset-2 disabled:pointer-events-none hover:bg-neutral-800/50 p-2">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
))
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName
|
||||
|
||||
const DialogHeader = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-1.5 text-center sm:text-left",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogHeader.displayName = "DialogHeader"
|
||||
|
||||
const DialogFooter = ({
|
||||
className,
|
||||
...props
|
||||
}) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
DialogFooter.displayName = "DialogFooter"
|
||||
|
||||
const DialogTitle = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight text-white",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogTitle.displayName = DialogPrimitive.Title.displayName
|
||||
|
||||
const DialogDescription = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-neutral-400", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
DialogDescription.displayName = DialogPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Dialog,
|
||||
DialogPortal,
|
||||
DialogOverlay,
|
||||
DialogClose,
|
||||
DialogTrigger,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
DialogTitle,
|
||||
DialogDescription,
|
||||
}
|
||||
45
components/ui/tabs.jsx
Normal file
45
components/ui/tabs.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Tabs = TabsPrimitive.Root
|
||||
|
||||
const TabsList = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex h-9 items-center justify-center rounded-lg p-1 text-neutral-400",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsList.displayName = TabsPrimitive.List.displayName
|
||||
|
||||
const TabsTrigger = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-white transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:text-[#7afbaf] data-[state=active]:shadow",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||
|
||||
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"mt-2 ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||
73
package-lock.json
generated
73
package-lock.json
generated
@ -15,6 +15,7 @@
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.47.8",
|
||||
"@tremor/react": "^3.18.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -27,7 +28,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
@ -1097,6 +1098,7 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.4.tgz",
|
||||
"integrity": "sha512-Ur7EV1IwQGCyaAuyDRiOLA5JIUZxELJljF+MbM/2NC0BYwfuRrbpS30BiQBJrVruscgUkieKkqXYDOoByaxIoA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
@ -1358,6 +1360,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-roving-focus": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",
|
||||
"integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-collection": "1.1.1",
|
||||
"@radix-ui/react-compose-refs": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-use-callback-ref": "1.1.0",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-select": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.4.tgz",
|
||||
@ -1467,6 +1500,36 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-tabs": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.2.tgz",
|
||||
"integrity": "sha512-9u/tQJMcC2aGq7KXpGivMm1mgq7oRJKXphDwdypPd/j21j/2znamPU8WkXgnhUaTrSFNIt8XhOyCAupg8/GbwQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@radix-ui/primitive": "1.1.1",
|
||||
"@radix-ui/react-context": "1.1.1",
|
||||
"@radix-ui/react-direction": "1.1.0",
|
||||
"@radix-ui/react-id": "1.1.0",
|
||||
"@radix-ui/react-presence": "1.1.2",
|
||||
"@radix-ui/react-primitive": "2.0.1",
|
||||
"@radix-ui/react-roving-focus": "1.1.1",
|
||||
"@radix-ui/react-use-controllable-state": "1.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-use-callback-ref": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz",
|
||||
@ -2666,6 +2729,7 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
@ -6314,9 +6378,10 @@
|
||||
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
|
||||
},
|
||||
"node_modules/tailwind-merge": {
|
||||
"version": "2.5.5",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.5.tgz",
|
||||
"integrity": "sha512-0LXunzzAZzo0tEPxV3I297ffKZPlKDrjj7NXphC8V5ak9yHC5zRmxnOe2m/Rd/7ivsOMJe3JZ2JVocoDdQTRBA==",
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||
"integrity": "sha512-P+Vu1qXfzediirmHOC3xKGAYeZtPcV9g76X+xg2FD4tYgR71ewMA35Y3sCz3zhiN/dwefRpJX0yBcgwi1fXNQA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/dcastil"
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-separator": "^1.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-tabs": "^1.1.2",
|
||||
"@supabase/supabase-js": "^2.47.8",
|
||||
"@tremor/react": "^3.18.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@ -28,7 +29,7 @@
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"recharts": "^2.15.0",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
217
pages/index.js
217
pages/index.js
@ -16,6 +16,8 @@ import {
|
||||
Search,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Globe,
|
||||
Info,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
LineChart,
|
||||
@ -26,6 +28,15 @@ import {
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
} from "recharts";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
// Initialize Supabase client
|
||||
const supabase = createClient(
|
||||
@ -204,7 +215,7 @@ export default function Dashboard() {
|
||||
const totalPeerPages = getPageCount(displayPeerIds.length);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white overflow-x-hidden">
|
||||
<div className="min-h-screen bg-gradient-to-bl from-black to-[#222222] text-white overflow-x-hidden">
|
||||
{/* Header */}
|
||||
<motion.header
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
@ -214,7 +225,7 @@ export default function Dashboard() {
|
||||
<div className="max-w-[2000px] mx-auto px-4 sm:px-6 py-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<img src="/logo.svg" alt="Codex" className="w-10 h-10" />
|
||||
<h1 className="text-lg sm:text-xl font-bold">Codex Metrics</h1>
|
||||
<h1 className="text-lg sm:text-xl font-bold">Testnet Metrics</h1>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<select
|
||||
@ -240,6 +251,75 @@ export default function Dashboard() {
|
||||
<RotateCw className={`w-5 h-5 ${loading ? 'animate-spin' : ''}`} />
|
||||
<span className="sr-only">Refresh data</span>
|
||||
</button>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className="p-2 text-neutral-400 hover:text-[#7afbaf]
|
||||
bg-neutral-900 border border-neutral-800 rounded-lg
|
||||
hover:border-neutral-700 focus:border-[#7afbaf] focus:ring-1
|
||||
focus:ring-[#7afbaf] transition-colors cursor-pointer outline-none"
|
||||
>
|
||||
<Info className="w-5 h-5" />
|
||||
<span className="sr-only">Dashboard information</span>
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[600px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="mb-6">Testnet Metrics</DialogTitle>
|
||||
<div className="w-full h-40 bg-neutral-800 rounded-lg mb-6 animate-pulse" />
|
||||
<DialogDescription className="pt-3">
|
||||
The data displayed in this dashboard is collected from Codex nodes that use the{' '}
|
||||
<a
|
||||
href="https://github.com/codex-storage/cli"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#7afbaf] hover:underline"
|
||||
>
|
||||
Codex CLI
|
||||
</a>
|
||||
{' '}for running a Codex alturistic node in the testnet. Users agree to a privacy
|
||||
disclaimer before using the Codex CLI and the data collected will be used to
|
||||
understand the testnet statistics and help troubleshooting users who face
|
||||
difficulty in getting onboarded to Codex.
|
||||
</DialogDescription>
|
||||
<div className="mt-8 space-y-4 border-neutral-800 pt-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Don't wish to provide data?</h4>
|
||||
<p className="text-sm text-neutral-400">
|
||||
You can still run a Codex node without providing any data. To do this, please follow the steps mentioned in the <a
|
||||
href="https://docs.codex.storage/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#7afbaf] hover:underline"
|
||||
>
|
||||
Codex documentation
|
||||
</a> which does not use the Codex CLI.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">Is there an incentive to run a Codex node?</h4>
|
||||
<p className="text-sm text-neutral-400">
|
||||
Codex is currently in testnet and it is not incentivized. However, in the future, Codex may be incentivized as per the roadmap. But please bear in mind that no incentives are promised for testnet node operators.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white mb-2">I have a question or suggestion</h4>
|
||||
<p className="text-sm text-neutral-400">
|
||||
The best way to get in touch with us is to join the
|
||||
<a
|
||||
href="https://discord.gg/codex-storage"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#7afbaf] hover:underline"
|
||||
>
|
||||
{" "}Codex discord
|
||||
</a> and ask your question in the #support channel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</motion.header>
|
||||
@ -326,63 +406,84 @@ export default function Dashboard() {
|
||||
className="bg-neutral-900 p-4 sm:p-6 rounded-xl h-[350px] lg:h-[450px] border border-neutral-800
|
||||
hover:border-neutral-700 transition-colors"
|
||||
>
|
||||
<h3 className="text-neutral-400 mb-4 font-medium flex items-center gap-2">
|
||||
<Activity className="w-5 h-5 opacity-60" />
|
||||
Active Nodes Over Time
|
||||
</h3>
|
||||
{componentLoading.metrics ? (
|
||||
<div className="h-[calc(100%-2rem)] flex items-center justify-center">
|
||||
<ComponentSkeleton />
|
||||
<Tabs defaultValue="nodes" className="h-full flex flex-col">
|
||||
<div className="flex items-center justify-center mb-6">
|
||||
<TabsList className="bg-neutral-800 border border-neutral-700">
|
||||
<TabsTrigger value="nodes" className="data-[state=active]:bg-neutral-900">
|
||||
<Activity className="w-4 h-4 mr-2" />
|
||||
Active Nodes
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="geo" className="data-[state=active]:bg-neutral-900">
|
||||
<Globe className="w-4 h-4 mr-2" />
|
||||
Geographic Distribution
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
) : metrics.length === 0 ? (
|
||||
<div className="h-[calc(100%-2rem)] flex items-center justify-center">
|
||||
<p className="text-neutral-400">No data available for the selected timeframe</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[calc(100%-2rem)]">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={metrics}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="#333"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#666"
|
||||
tickFormatter={(date) => format(new Date(date), "MMM d")}
|
||||
fontSize={12}
|
||||
tickMargin={10}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#666"
|
||||
fontSize={12}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid #333",
|
||||
borderRadius: "8px",
|
||||
fontFamily: "Inter",
|
||||
fontSize: "12px",
|
||||
padding: "12px",
|
||||
}}
|
||||
cursor={{ stroke: "#666" }}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="new_records_count"
|
||||
stroke="#7afbaf"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 6, fill: "#7afbaf" }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<TabsContent value="nodes" className="flex-1 mt-0">
|
||||
{componentLoading.metrics ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<ComponentSkeleton />
|
||||
</div>
|
||||
) : metrics.length === 0 ? (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<p className="text-neutral-400">No data available for the selected timeframe</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<LineChart data={metrics}>
|
||||
<CartesianGrid
|
||||
strokeDasharray="3 3"
|
||||
stroke="#333"
|
||||
vertical={false}
|
||||
/>
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
stroke="#666"
|
||||
tickFormatter={(date) => format(new Date(date), "MMM d")}
|
||||
fontSize={12}
|
||||
tickMargin={10}
|
||||
/>
|
||||
<YAxis
|
||||
stroke="#666"
|
||||
fontSize={12}
|
||||
tickMargin={10}
|
||||
axisLine={false}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{
|
||||
backgroundColor: "#1a1a1a",
|
||||
border: "1px solid #333",
|
||||
borderRadius: "8px",
|
||||
fontFamily: "var(--font-inter)",
|
||||
fontSize: "12px",
|
||||
padding: "12px",
|
||||
}}
|
||||
cursor={{ stroke: "#666" }}
|
||||
formatter={(value) => [`${value} nodes`, 'Active Nodes']}
|
||||
labelFormatter={(label) => format(new Date(label), "MMM d, yyyy")}
|
||||
/>
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="new_records_count"
|
||||
stroke="#7afbaf"
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
activeDot={{ r: 6, fill: "#7afbaf" }}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="geo" className="flex-1 mt-0">
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<p className="text-neutral-400">Geographic distribution view coming soon</p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user