@cybermem/dashboard 0.5.12 → 0.5.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,23 +1,59 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { Card, CardContent } from "@/components/ui/card"
3
+ import { Card, CardContent } from "@/components/ui/card";
4
4
 
5
5
  interface MetricCardProps {
6
- label: string
7
- value: string
6
+ label: string;
7
+ value: string;
8
8
  // Keep these props for API compatibility but don't use them
9
- change?: string
10
- trend?: "up" | "down" | "neutral"
11
- hasData?: boolean
9
+ change?: string;
10
+ trend?: "up" | "down" | "neutral";
11
+ hasData?: boolean;
12
+ loading?: boolean;
12
13
  }
13
14
 
14
- export default function MetricCard({ label, value }: MetricCardProps) {
15
+ export default function MetricCard({
16
+ label,
17
+ value,
18
+ loading = false,
19
+ hasData = true,
20
+ }: MetricCardProps) {
21
+ const isEmpty =
22
+ !hasData || value === "N/A" || value === "0" || value === "0.0%";
23
+
24
+ // Skeleton state
25
+ if (loading) {
26
+ return (
27
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative">
28
+ <CardContent className="p-6 relative z-10">
29
+ <div className="h-4 w-24 bg-white/10 rounded animate-pulse mb-3" />
30
+ <div className="h-10 w-20 bg-white/10 rounded animate-pulse" />
31
+ </CardContent>
32
+ </Card>
33
+ );
34
+ }
35
+
36
+ // Empty state
37
+ if (isEmpty) {
38
+ return (
39
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative opacity-60">
40
+ <CardContent className="p-6 relative z-10">
41
+ <div className="text-sm font-medium text-slate-500 mb-2">{label}</div>
42
+ <div className="text-4xl font-bold text-slate-500">
43
+ {value === "N/A" ? "N/A" : "—"}
44
+ </div>
45
+ </CardContent>
46
+ </Card>
47
+ );
48
+ }
49
+
50
+ // Normal state
15
51
  return (
16
- <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative">
52
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden relative group hover:bg-white/[0.07] transition-colors">
17
53
  <CardContent className="p-6 relative z-10">
18
54
  <div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
19
55
  <div className="text-4xl font-bold text-white">{value}</div>
20
56
  </CardContent>
21
57
  </Card>
22
- )
58
+ );
23
59
  }
@@ -1,6 +1,15 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { Area, AreaChart, CartesianGrid, Legend, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
3
+ import {
4
+ Area,
5
+ AreaChart,
6
+ CartesianGrid,
7
+ Legend,
8
+ ResponsiveContainer,
9
+ Tooltip,
10
+ XAxis,
11
+ YAxis,
12
+ } from "recharts";
4
13
 
5
14
  // Fallback color generator
6
15
  function stringToColor(str: string): string {
@@ -8,127 +17,145 @@ function stringToColor(str: string): string {
8
17
  for (let i = 0; i < str.length; i++) {
9
18
  hash = str.charCodeAt(i) + ((hash << 5) - hash);
10
19
  }
11
- let color = '#';
20
+ let color = "#";
12
21
  for (let i = 0; i < 3; i++) {
13
- const value = (hash >> (i * 8)) & 0xFF;
14
- color += ('00' + value.toString(16)).substr(-2);
22
+ const value = (hash >> (i * 8)) & 0xff;
23
+ color += ("00" + value.toString(16)).substr(-2);
15
24
  }
16
25
  return color;
17
26
  }
18
27
 
19
28
  interface MetricsChartProps {
20
- data: any[]
21
- isMultiSeries: boolean
22
- clientNames: string[]
23
- clientConfigs: any[]
24
- hovered: string | null
25
- setHovered: (id: string | null) => void
29
+ data: any[];
30
+ isMultiSeries: boolean;
31
+ clientNames: string[];
32
+ clientConfigs: any[];
33
+ hovered: string | null;
34
+ setHovered: (id: string | null) => void;
26
35
  }
27
36
 
28
37
  export default function MetricsChart({
29
- data,
30
- isMultiSeries,
31
- clientNames,
32
- clientConfigs,
33
- hovered,
34
- setHovered
38
+ data,
39
+ isMultiSeries,
40
+ clientNames,
41
+ clientConfigs,
42
+ hovered,
43
+ setHovered,
35
44
  }: MetricsChartProps) {
36
- return (
37
- <ResponsiveContainer width="100%" height="100%">
38
- <AreaChart data={data} margin={{ top: 10, right: 20, left: 0, bottom: 0 }}>
39
- <CartesianGrid strokeDasharray="0" stroke="#2D3135" opacity={0.3} vertical={false} horizontal={true} />
40
- <XAxis
41
- dataKey="time"
42
- stroke="#6B7280"
43
- fontSize={11}
44
- tickLine={false}
45
- axisLine={false}
46
- minTickGap={40}
47
- tick={{ fill: '#6B7280' }}
48
- />
49
- <YAxis
50
- stroke="#6B7280"
51
- fontSize={11}
52
- tickLine={false}
53
- axisLine={false}
54
- tickFormatter={(value) => `${value}`}
55
- tick={{ fill: '#6B7280' }}
56
- width={40}
57
- />
58
- <Tooltip
59
- contentStyle={{
60
- backgroundColor: "rgba(11, 17, 22, 0.8)",
61
- border: "1px solid rgba(255, 255, 255, 0.1)",
62
- borderRadius: "12px",
63
- color: "#fff",
64
- backdropFilter: "blur(12px)",
65
- boxShadow: "0 4px 20px rgba(0, 0, 0, 0.5)"
66
- }}
67
- itemStyle={{ color: "#fff", fontSize: "12px", padding: "2px 0" }}
68
- labelStyle={{ color: "#9ca3af", marginBottom: "8px", fontSize: "12px", fontWeight: 500 }}
69
- cursor={{ stroke: 'rgba(255,255,255,0.2)', strokeWidth: 1 }}
70
- />
71
- {isMultiSeries && (
72
- <Legend
73
- verticalAlign="bottom"
74
- height={36}
75
- iconType="circle"
76
- onMouseEnter={(e: any) => {
77
- if (e.dataKey) setHovered(e.dataKey.toString())
78
- }}
79
- onMouseLeave={() => setHovered(null)}
80
- formatter={(value: any, entry: any) => {
81
- // value is usually the name set on the Area, which we set below.
82
- // But if that fails, we fallback to finding config.
83
- return <span className="text-white">{value}</span>
84
- }}
85
- />
86
- )}
87
- {isMultiSeries ? (
88
- clientNames.map((client, i) => {
89
- // Find matching config using regex
90
- const keyLower = client.toLowerCase()
91
- const config = clientConfigs.find((c: any) => new RegExp(c.match, 'i').test(keyLower))
45
+ return (
46
+ <ResponsiveContainer width="100%" height="100%">
47
+ <AreaChart
48
+ data={data}
49
+ margin={{ top: 10, right: 20, left: 0, bottom: 0 }}
50
+ >
51
+ <CartesianGrid
52
+ strokeDasharray="0"
53
+ stroke="#2D3135"
54
+ opacity={0.3}
55
+ vertical={false}
56
+ horizontal={true}
57
+ />
58
+ <XAxis
59
+ dataKey="time"
60
+ stroke="#6B7280"
61
+ fontSize={11}
62
+ tickLine={false}
63
+ axisLine={false}
64
+ minTickGap={40}
65
+ tick={{ fill: "#6B7280" }}
66
+ />
67
+ <YAxis
68
+ stroke="#6B7280"
69
+ fontSize={11}
70
+ tickLine={false}
71
+ axisLine={false}
72
+ tickFormatter={(value) => `${value}`}
73
+ tick={{ fill: "#6B7280" }}
74
+ width={40}
75
+ />
76
+ <Tooltip
77
+ contentStyle={{
78
+ backgroundColor: "rgba(11, 17, 22, 0.8)",
79
+ border: "1px solid rgba(255, 255, 255, 0.1)",
80
+ borderRadius: "12px",
81
+ color: "#fff",
82
+ backdropFilter: "blur(12px)",
83
+ boxShadow: "0 4px 20px rgba(0, 0, 0, 0.5)",
84
+ }}
85
+ itemStyle={{ color: "#fff", fontSize: "12px", padding: "2px 0" }}
86
+ labelStyle={{
87
+ color: "#9ca3af",
88
+ marginBottom: "8px",
89
+ fontSize: "12px",
90
+ fontWeight: 500,
91
+ }}
92
+ cursor={{ stroke: "rgba(255,255,255,0.2)", strokeWidth: 1 }}
93
+ />
94
+ {isMultiSeries && (
95
+ <Legend
96
+ verticalAlign="bottom"
97
+ height={36}
98
+ iconType="circle"
99
+ onMouseEnter={(e: any) => {
100
+ if (e.dataKey) setHovered(e.dataKey.toString());
101
+ }}
102
+ onMouseLeave={() => setHovered(null)}
103
+ formatter={(value: any, entry: any) => {
104
+ // value is usually the name set on the Area, which we set below.
105
+ // But if that fails, we fallback to finding config.
106
+ return <span className="text-white">{value}</span>;
107
+ }}
108
+ />
109
+ )}
110
+ {isMultiSeries ? (
111
+ clientNames.map((client, i) => {
112
+ // Find matching config using regex
113
+ const keyLower = client.toLowerCase();
114
+ const config = clientConfigs.find((c: any) =>
115
+ new RegExp(c.match, "i").test(keyLower),
116
+ );
92
117
 
93
- // Use config if found, otherwise fallback
94
- const name = config?.name || client
95
- const color = config?.color || stringToColor(client)
118
+ // Use config if found, otherwise fallback
119
+ const name = config?.name || client;
120
+ const color = config?.color || stringToColor(client);
96
121
 
97
- const isHovered = hovered === client
98
- const isAnyHovered = hovered !== null
122
+ const isHovered = hovered === client;
123
+ const isAnyHovered = hovered !== null;
99
124
 
100
- return (
101
- <Area
102
- key={client}
103
- type="monotone"
104
- dataKey={client}
105
- name={name}
106
- stroke={color}
107
- strokeWidth={isHovered ? 2.5 : 1.5}
108
- fillOpacity={isHovered ? 0.5 : (isAnyHovered ? 0.1 : 0.2)}
109
- fill={color}
110
- stackId="1"
111
- activeDot={{ r: 4, strokeWidth: 0 }}
112
- dot={false}
113
- onMouseEnter={() => setHovered(client)}
114
- onMouseLeave={() => setHovered(null)}
115
- />
116
- )
117
- })
118
- ) : (
119
- <Area
125
+ return (
126
+ <Area
127
+ key={client}
120
128
  type="monotone"
121
- dataKey="value"
122
- stroke="#10b981"
123
- strokeWidth={1.5}
124
- fillOpacity={0.2}
125
- fill="#10b981"
126
- activeDot={{ r: 3, strokeWidth: 0 }}
129
+ dataKey={client}
130
+ stackId="1"
131
+ isAnimationActive={false}
132
+ name={name}
133
+ stroke={color}
134
+ strokeWidth={isHovered ? 2.5 : 1.5}
135
+ fillOpacity={isHovered ? 0.6 : isAnyHovered ? 0.1 : 0.4}
136
+ fill={color}
137
+ activeDot={{ r: 4, strokeWidth: 0 }}
127
138
  dot={false}
128
- // No hover effect needed for single series as there's nothing to distinguish from
129
- />
130
- )}
131
- </AreaChart>
132
- </ResponsiveContainer>
133
- )
139
+ onMouseEnter={() => setHovered(client)}
140
+ onMouseLeave={() => setHovered(null)}
141
+ />
142
+ );
143
+ })
144
+ ) : (
145
+ <Area
146
+ type="monotone"
147
+ dataKey="value"
148
+ stroke="#10b981"
149
+ strokeWidth={1.5}
150
+ fillOpacity={0.2}
151
+ fill="#10b981"
152
+ isAnimationActive={false}
153
+ activeDot={{ r: 3, strokeWidth: 0 }}
154
+ dot={false}
155
+ // No hover effect needed for single series as there's nothing to distinguish from
156
+ />
157
+ )}
158
+ </AreaChart>
159
+ </ResponsiveContainer>
160
+ );
134
161
  }
@@ -1,51 +1,102 @@
1
- "use client"
1
+ "use client";
2
2
 
3
- import { Card, CardContent } from "@/components/ui/card"
4
- import MetricCard from "./metric-card"
3
+ import { Card, CardContent } from "@/components/ui/card";
4
+ import MetricCard from "./metric-card";
5
5
 
6
6
  // Types
7
7
  interface TrendState {
8
- change: string
9
- trend: "up" | "down" | "neutral"
10
- hasData: boolean
8
+ change: string;
9
+ trend: "up" | "down" | "neutral";
10
+ hasData: boolean;
11
11
  }
12
12
 
13
13
  interface MetricsGridProps {
14
14
  stats: {
15
- memoryRecords: number
16
- totalClients: number
17
- successRate: number
18
- totalRequests: number
19
- topWriter: { name: string; count: number }
20
- topReader: { name: string; count: number }
21
- lastWriter: { name: string; timestamp: number }
22
- lastReader: { name: string; timestamp: number }
23
- }
15
+ memoryRecords: number;
16
+ totalClients: number;
17
+ successRate: number;
18
+ totalRequests: number;
19
+ topWriter: { name: string; count: number };
20
+ topReader: { name: string; count: number };
21
+ lastWriter: { name: string; timestamp: number };
22
+ lastReader: { name: string; timestamp: number };
23
+ };
24
24
  trends: {
25
- memory: TrendState
26
- clients: TrendState
27
- success: TrendState
28
- requests: TrendState
29
- }
25
+ memory: TrendState;
26
+ clients: TrendState;
27
+ success: TrendState;
28
+ requests: TrendState;
29
+ };
30
+ loading?: boolean;
30
31
  }
31
32
 
33
+ // Client card skeleton component
34
+ function ClientCardSkeleton({ label }: { label: string }) {
35
+ return (
36
+ <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
37
+ <CardContent className="pt-6 pb-6 relative">
38
+ <div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
39
+ <div className="h-10 w-32 bg-white/10 rounded animate-pulse mb-2" />
40
+ <div className="h-5 w-20 bg-white/10 rounded animate-pulse" />
41
+ </CardContent>
42
+ </Card>
43
+ );
44
+ }
45
+
46
+ // Client card with empty state
47
+ function ClientCard({
48
+ label,
49
+ name,
50
+ subtitle,
51
+ isEmpty,
52
+ }: {
53
+ label: string;
54
+ name: string;
55
+ subtitle: string;
56
+ isEmpty: boolean;
57
+ }) {
58
+ return (
59
+ <Card
60
+ className={`bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden ${isEmpty ? "opacity-60" : ""}`}
61
+ >
62
+ <CardContent className="pt-6 pb-6 relative">
63
+ <div className="text-sm font-medium text-slate-400 mb-2">{label}</div>
64
+ <div
65
+ className={`text-4xl font-bold mb-1 truncate ${isEmpty ? "text-slate-500" : "text-white"}`}
66
+ >
67
+ {name}
68
+ </div>
69
+ <div
70
+ className={`text-xl whitespace-nowrap ${isEmpty ? "text-slate-600" : "text-white/80"}`}
71
+ >
72
+ {subtitle}
73
+ </div>
74
+ </CardContent>
75
+ </Card>
76
+ );
77
+ }
32
78
 
33
- export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
79
+ export default function MetricsGrid({
80
+ stats,
81
+ trends,
82
+ loading = false,
83
+ }: MetricsGridProps) {
34
84
  // Note: Client names are now normalized by backend API, no frontend transformation needed
35
85
 
36
86
  const formatTimestamp = (timestamp: number) => {
37
- if (timestamp <= 0) return "No activity"
38
- const date = new Date(timestamp)
39
- const now = new Date()
40
- const isToday = date.getDate() === now.getDate() &&
41
- date.getMonth() === now.getMonth() &&
42
- date.getFullYear() === now.getFullYear()
87
+ if (timestamp <= 0) return "No activity";
88
+ const date = new Date(timestamp);
89
+ const now = new Date();
90
+ const isToday =
91
+ date.getDate() === now.getDate() &&
92
+ date.getMonth() === now.getMonth() &&
93
+ date.getFullYear() === now.getFullYear();
43
94
 
44
95
  if (isToday) {
45
- return date.toLocaleTimeString()
96
+ return date.toLocaleTimeString();
46
97
  }
47
- return date.toLocaleString()
48
- }
98
+ return date.toLocaleString();
99
+ };
49
100
 
50
101
  return (
51
102
  <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4">
@@ -56,6 +107,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
56
107
  change={trends.memory.change}
57
108
  trend={trends.memory.trend}
58
109
  hasData={trends.memory.hasData}
110
+ loading={loading}
59
111
  />
60
112
 
61
113
  {/* 2. Total Clients */}
@@ -65,6 +117,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
65
117
  change={trends.clients.change}
66
118
  trend={trends.clients.trend}
67
119
  hasData={trends.clients.hasData}
120
+ loading={loading}
68
121
  />
69
122
 
70
123
  {/* 3. Success Rate */}
@@ -74,6 +127,7 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
74
127
  change={trends.success.change}
75
128
  trend={trends.success.trend} // Trend UP is good (green) for success rate
76
129
  hasData={trends.success.hasData}
130
+ loading={loading}
77
131
  />
78
132
 
79
133
  {/* 4. Total Requests */}
@@ -83,57 +137,68 @@ export default function MetricsGrid({ stats, trends }: MetricsGridProps) {
83
137
  change={trends.requests.change}
84
138
  trend={trends.requests.trend}
85
139
  hasData={trends.requests.hasData}
140
+ loading={loading}
86
141
  />
87
142
 
88
143
  {/* 5. Top Writer */}
89
- <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
90
- <CardContent className="pt-6 pb-6 relative">
91
- <div className="text-sm font-medium text-slate-400 mb-2">Top Writer</div>
92
- <div className="text-4xl font-bold text-white mb-1 truncate">{stats.topWriter.name}</div>
93
- <div className="text-xl text-white/80 whitespace-nowrap">
94
- {stats.topWriter.count > 0 ? `${stats.topWriter.count.toLocaleString()} writes` : ""}
95
- </div>
96
- </CardContent>
97
- </Card>
144
+ {loading ? (
145
+ <ClientCardSkeleton label="Top Writer" />
146
+ ) : (
147
+ <ClientCard
148
+ label="Top Writer"
149
+ name={stats.topWriter.count > 0 ? stats.topWriter.name : "N/A"}
150
+ subtitle={
151
+ stats.topWriter.count > 0
152
+ ? `${stats.topWriter.count.toLocaleString()} writes`
153
+ : ""
154
+ }
155
+ isEmpty={stats.topWriter.count <= 0}
156
+ />
157
+ )}
98
158
 
99
159
  {/* 6. Top Reader */}
100
- <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
101
- <CardContent className="pt-6 pb-6 relative">
102
- <div className="text-sm font-medium text-slate-400 mb-2">Top Reader</div>
103
- <div className="text-4xl font-bold text-white mb-1 truncate">
104
- {stats.topReader.count > 0 ? stats.topReader.name : "N/A"}
105
- </div>
106
- <div className="text-xl text-white/80 whitespace-nowrap">
107
- {stats.topReader.count > 0 ? `${stats.topReader.count.toLocaleString()} reads` : ""}
108
- </div>
109
- </CardContent>
110
- </Card>
160
+ {loading ? (
161
+ <ClientCardSkeleton label="Top Reader" />
162
+ ) : (
163
+ <ClientCard
164
+ label="Top Reader"
165
+ name={stats.topReader.count > 0 ? stats.topReader.name : "N/A"}
166
+ subtitle={
167
+ stats.topReader.count > 0
168
+ ? `${stats.topReader.count.toLocaleString()} reads`
169
+ : ""
170
+ }
171
+ isEmpty={stats.topReader.count <= 0}
172
+ />
173
+ )}
111
174
 
112
175
  {/* 7. Last Writer */}
113
- <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
114
- <CardContent className="pt-6 pb-6 relative">
115
- <div className="text-sm font-medium text-slate-400 mb-2">Last Writer</div>
116
- <div className="text-4xl font-bold text-white mb-1 truncate">
117
- {stats.lastWriter.name !== "N/A" ? stats.lastWriter.name : "N/A"}
118
- </div>
119
- <div className="text-xl text-white/80 whitespace-nowrap">
120
- {stats.lastWriter.timestamp > 0 ? formatTimestamp(stats.lastWriter.timestamp) : "No activity"}
121
- </div>
122
- </CardContent>
123
- </Card>
176
+ {loading ? (
177
+ <ClientCardSkeleton label="Last Writer" />
178
+ ) : (
179
+ <ClientCard
180
+ label="Last Writer"
181
+ name={stats.lastWriter.name !== "N/A" ? stats.lastWriter.name : "N/A"}
182
+ subtitle={formatTimestamp(stats.lastWriter.timestamp)}
183
+ isEmpty={
184
+ stats.lastWriter.name === "N/A" || stats.lastWriter.timestamp <= 0
185
+ }
186
+ />
187
+ )}
124
188
 
125
189
  {/* 8. Last Reader */}
126
- <Card className="bg-white/5 border-white/10 backdrop-blur-md text-white shadow-lg overflow-hidden">
127
- <CardContent className="pt-6 pb-6 relative">
128
- <div className="text-sm font-medium text-slate-400 mb-2">Last Reader</div>
129
- <div className="text-4xl font-bold text-white mb-1 truncate">
130
- {stats.lastReader.name !== "N/A" ? stats.lastReader.name : "N/A"}
131
- </div>
132
- <div className="text-xl text-white/80 whitespace-nowrap">
133
- {stats.lastReader.timestamp > 0 ? formatTimestamp(stats.lastReader.timestamp) : "No activity"}
134
- </div>
135
- </CardContent>
136
- </Card>
190
+ {loading ? (
191
+ <ClientCardSkeleton label="Last Reader" />
192
+ ) : (
193
+ <ClientCard
194
+ label="Last Reader"
195
+ name={stats.lastReader.name !== "N/A" ? stats.lastReader.name : "N/A"}
196
+ subtitle={formatTimestamp(stats.lastReader.timestamp)}
197
+ isEmpty={
198
+ stats.lastReader.name === "N/A" || stats.lastReader.timestamp <= 0
199
+ }
200
+ />
201
+ )}
137
202
  </div>
138
- )
203
+ );
139
204
  }