@littlebearapps/platform-admin-sdk 2.0.0 → 2.1.0
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.
- package/README.md +2 -2
- package/dist/templates.d.ts +1 -1
- package/dist/templates.js +86 -2
- package/package.json +1 -1
- package/templates/full/dashboard/src/components/reports/DigestStats.tsx +151 -0
- package/templates/full/dashboard/src/components/reports/HealthTrendsReport.tsx +192 -0
- package/templates/full/dashboard/src/components/usage/AIModelBreakdown.tsx +364 -0
- package/templates/full/dashboard/src/components/usage/unified/Recommendations.tsx +149 -0
- package/templates/full/dashboard/src/lib/cloudflare/alerting.ts +486 -0
- package/templates/full/dashboard/src/lib/cloudflare/graphql.ts +4785 -0
- package/templates/full/dashboard/src/lib/cloudflare/project-registry.ts +451 -0
- package/templates/full/dashboard/src/lib/notifications/api.ts +197 -0
- package/templates/full/dashboard/src/lib/notifications/types.ts.hbs +97 -0
- package/templates/full/dashboard/src/lib/patterns/api.ts +120 -0
- package/templates/full/dashboard/src/lib/patterns/types.ts +127 -0
- package/templates/full/dashboard/src/lib/reports/types.ts +231 -0
- package/templates/full/dashboard/src/lib/search/api.ts +258 -0
- package/templates/full/dashboard/src/lib/search/types.ts.hbs +115 -0
- package/templates/full/dashboard/src/lib/settings/api.ts.hbs +201 -0
- package/templates/full/dashboard/src/lib/settings/types.ts.hbs +104 -0
- package/templates/full/dashboard/src/lib/usage/allowance-config.ts.hbs +547 -0
- package/templates/full/dashboard/src/lib/usage/providers.ts +331 -0
- package/templates/shared/dashboard/src/components/reports/ReportInfoButton.tsx +98 -0
- package/templates/shared/dashboard/src/components/usage/react/DashboardShell.tsx +263 -0
- package/templates/shared/dashboard/src/components/usage/react/StatusBadge.tsx +77 -0
- package/templates/shared/dashboard/src/components/usage/react/UsageChart.tsx +391 -0
- package/templates/shared/dashboard/src/components/usage/react/index.ts.hbs +30 -0
- package/templates/shared/dashboard/src/components/usage/react/types.ts +137 -0
- package/templates/shared/dashboard/src/components/usage/transformers.ts +478 -0
- package/templates/shared/dashboard/src/components/usage/unified/AlertBanner.tsx +172 -0
- package/templates/shared/dashboard/src/components/usage/unified/HeroCardsRow.tsx +757 -0
- package/templates/shared/dashboard/src/components/usage/unified/LiveHeader.tsx +169 -0
- package/templates/shared/dashboard/src/components/usage/unified/ProjectsTable.tsx +448 -0
- package/templates/shared/dashboard/src/components/usage/unified/ResourceBreakdown.tsx +236 -0
- package/templates/shared/dashboard/src/components/usage/unified/Sparkline.tsx +127 -0
- package/templates/shared/dashboard/src/components/usage/unified/UnifiedShell.tsx +893 -0
- package/templates/shared/dashboard/src/components/usage/unified/index.ts.hbs +50 -0
- package/templates/shared/dashboard/src/components/usage/unified/types.ts +416 -0
- package/templates/shared/dashboard/src/lib/cloudflare/analytics.ts +310 -0
- package/templates/shared/dashboard/src/lib/cloudflare/d1.ts +55 -0
- package/templates/shared/dashboard/src/lib/cloudflare/index.ts.hbs +120 -0
- package/templates/shared/dashboard/src/lib/infrastructure/types.ts +116 -0
- package/templates/shared/dashboard/src/lib/usage/fetchWithDedup.ts +101 -0
- package/templates/shared/dashboard/src/lib/usage/index.ts.hbs +12 -0
- package/templates/shared/tests/e2e/usage-api.test.ts +909 -0
- package/templates/shared/tests/helpers/mock-storage.ts +166 -0
- package/templates/shared/tests/integration/kv-cache.test.ts +252 -0
- package/templates/shared/tests/integration/platform-usage.test.ts +956 -0
- package/templates/shared/tests/unit/billing.test.ts +331 -0
- package/templates/shared/tests/unit/cloudflare/graphql.test.ts +217 -0
- package/templates/shared/tests/unit/components/usage-transformers.test.ts +473 -0
- package/templates/shared/tests/unit/control.test.ts +226 -0
- package/templates/shared/tests/unit/cost-calculator.test.ts +141 -0
- package/templates/shared/tests/unit/economics.test.ts +365 -0
- package/templates/shared/tests/unit/telemetry-sampling.test.ts +401 -0
- package/templates/standard/dashboard/src/components/reports/CircuitBreakerReport.tsx +474 -0
- package/templates/standard/dashboard/src/components/reports/CostTrendsReport.tsx +229 -0
- package/templates/standard/dashboard/src/components/reports/ErrorTrendsReport.tsx +244 -0
- package/templates/standard/dashboard/src/components/reports/ProjectHealthTable.tsx +251 -0
- package/templates/standard/dashboard/src/components/reports/WarningDigestsTable.tsx +298 -0
- package/templates/standard/dashboard/src/components/usage/react/UsageTable.tsx +385 -0
- package/templates/standard/dashboard/src/components/usage/unified/CircuitBreakerEvents.tsx +305 -0
- package/templates/standard/dashboard/src/components/usage/unified/FeatureBudgets.tsx +472 -0
- package/templates/standard/dashboard/src/lib/errors/api.ts +84 -0
- package/templates/standard/dashboard/src/lib/errors/types.ts +75 -0
- package/templates/standard/dashboard/src/lib/infrastructure/api.ts +141 -0
- package/templates/standard/dashboard/src/lib/infrastructure/gatus.ts.hbs +112 -0
- package/templates/standard/dashboard/src/lib/services/proxy/index.ts +20 -0
- package/templates/standard/dashboard/src/lib/services/proxy/proxy.ts +244 -0
- package/templates/standard/dashboard/src/lib/services/proxy/types.ts +81 -0
- package/templates/standard/tests/integration/platform-sentinel.test.ts +497 -0
- package/templates/standard/tests/unit/cloudflare/alerting.test.ts +480 -0
- package/templates/standard/tests/unit/error-collector/dedup.test.ts +350 -0
- package/templates/standard/tests/unit/error-collector/github.test.ts +187 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResourceBreakdown Component
|
|
3
|
+
*
|
|
4
|
+
* Displays resource metrics for an expanded project row.
|
|
5
|
+
* Shows D1, KV, R2, Vectorize, Workers, etc. with sparklines.
|
|
6
|
+
* Highlights top 3 cost drivers with flame icons.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { clsx } from 'clsx';
|
|
10
|
+
import { Flame } from 'lucide-react';
|
|
11
|
+
import type { ProjectBreakdown, ResourceMetric, ResourceType } from './types';
|
|
12
|
+
import { RESOURCE_COLORS, RESOURCE_LABELS } from './types';
|
|
13
|
+
import { Sparkline } from './Sparkline';
|
|
14
|
+
|
|
15
|
+
interface ResourceBreakdownProps {
|
|
16
|
+
projectId: string;
|
|
17
|
+
breakdown: ProjectBreakdown | null;
|
|
18
|
+
isLoading: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function formatCost(cost: number): string {
|
|
22
|
+
if (cost >= 1000) return `$${(cost / 1000).toFixed(1)}K`;
|
|
23
|
+
if (cost >= 1) return `$${cost.toFixed(2)}`;
|
|
24
|
+
if (cost >= 0.01) return `$${cost.toFixed(3)}`;
|
|
25
|
+
return `$${cost.toFixed(4)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Loading skeleton for resource breakdown
|
|
30
|
+
*/
|
|
31
|
+
function LoadingSkeleton() {
|
|
32
|
+
return (
|
|
33
|
+
<div className="space-y-2 animate-pulse">
|
|
34
|
+
{[1, 2, 3].map((i) => (
|
|
35
|
+
<div
|
|
36
|
+
key={i}
|
|
37
|
+
className="flex items-center gap-4 p-2 bg-gray-100/50 dark:bg-slate-800/50 rounded"
|
|
38
|
+
>
|
|
39
|
+
<div className="w-3 h-3 rounded bg-slate-700" />
|
|
40
|
+
<div className="w-20 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
|
|
41
|
+
<div className="flex-1" />
|
|
42
|
+
<div className="w-16 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
|
|
43
|
+
<div className="w-24 h-4 bg-gray-200 dark:bg-slate-700 rounded" />
|
|
44
|
+
<div className="w-40 h-3 bg-gray-200 dark:bg-slate-700 rounded" />
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Empty state when no resources found
|
|
53
|
+
*/
|
|
54
|
+
function EmptyState() {
|
|
55
|
+
return (
|
|
56
|
+
<div className="py-4 text-center">
|
|
57
|
+
<p className="text-gray-500 dark:text-slate-500 text-xs font-mono">
|
|
58
|
+
No resource data available
|
|
59
|
+
</p>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resource type icon/indicator
|
|
66
|
+
*/
|
|
67
|
+
function ResourceIcon({ type }: { type: ResourceType }) {
|
|
68
|
+
const colors = RESOURCE_COLORS[type] || { stroke: '#64748b', bg: 'rgba(100, 116, 139, 0.3)' };
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
className="w-3 h-3 rounded-sm"
|
|
73
|
+
style={{ backgroundColor: colors.stroke }}
|
|
74
|
+
title={RESOURCE_LABELS[type] || type}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Progress bar for limit percentage
|
|
81
|
+
*/
|
|
82
|
+
function LimitBar({ percentage }: { percentage: number }) {
|
|
83
|
+
const pct = Math.min(percentage, 100);
|
|
84
|
+
const barColor =
|
|
85
|
+
percentage >= 90
|
|
86
|
+
? 'bg-rose-500'
|
|
87
|
+
: percentage >= 75
|
|
88
|
+
? 'bg-orange-500'
|
|
89
|
+
: percentage >= 50
|
|
90
|
+
? 'bg-amber-500'
|
|
91
|
+
: 'bg-emerald-500';
|
|
92
|
+
|
|
93
|
+
return (
|
|
94
|
+
<div className="w-24 flex items-center gap-2">
|
|
95
|
+
<div className="flex-1 h-1 bg-gray-200 dark:bg-slate-700 rounded-full overflow-hidden">
|
|
96
|
+
<div
|
|
97
|
+
className={clsx('h-full rounded-full transition-all', barColor)}
|
|
98
|
+
style={{ width: `${pct}%` }}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<span className="text-xs font-mono text-gray-500 dark:text-slate-500 w-10 text-right">
|
|
102
|
+
{percentage > 999 ? '>999' : percentage.toFixed(0)}%
|
|
103
|
+
</span>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Single resource row
|
|
110
|
+
*/
|
|
111
|
+
function ResourceRow({
|
|
112
|
+
resource,
|
|
113
|
+
isCostDriver,
|
|
114
|
+
}: {
|
|
115
|
+
resource: ResourceMetric;
|
|
116
|
+
isCostDriver: boolean;
|
|
117
|
+
}) {
|
|
118
|
+
const colors = RESOURCE_COLORS[resource.type] || {
|
|
119
|
+
stroke: '#64748b',
|
|
120
|
+
bg: 'rgba(100, 116, 139, 0.3)',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div
|
|
125
|
+
className={clsx(
|
|
126
|
+
'flex items-center gap-4 px-3 py-2 rounded transition-colors',
|
|
127
|
+
isCostDriver
|
|
128
|
+
? 'bg-amber-50/80 dark:bg-amber-900/20 hover:bg-amber-100/80 dark:hover:bg-amber-900/30'
|
|
129
|
+
: 'hover:bg-gray-100/50 dark:hover:bg-slate-800/50'
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{/* Type indicator */}
|
|
133
|
+
<ResourceIcon type={resource.type} />
|
|
134
|
+
|
|
135
|
+
{/* Label with flame icon for cost drivers */}
|
|
136
|
+
<span
|
|
137
|
+
className={clsx(
|
|
138
|
+
'w-28 text-xs font-medium truncate flex items-center gap-1',
|
|
139
|
+
isCostDriver
|
|
140
|
+
? 'text-amber-700 dark:text-amber-400 font-semibold'
|
|
141
|
+
: 'text-gray-600 dark:text-slate-400'
|
|
142
|
+
)}
|
|
143
|
+
title={isCostDriver ? `${resource.label} - Top cost driver this period` : resource.label}
|
|
144
|
+
>
|
|
145
|
+
{isCostDriver && <Flame className="w-3 h-3 text-amber-500" />}
|
|
146
|
+
{RESOURCE_LABELS[resource.type] || resource.label}
|
|
147
|
+
</span>
|
|
148
|
+
|
|
149
|
+
{/* Cost */}
|
|
150
|
+
<span
|
|
151
|
+
className={clsx(
|
|
152
|
+
'w-16 text-xs font-mono text-right',
|
|
153
|
+
isCostDriver
|
|
154
|
+
? 'text-amber-800 dark:text-amber-300 font-semibold'
|
|
155
|
+
: 'text-gray-700 dark:text-slate-300'
|
|
156
|
+
)}
|
|
157
|
+
>
|
|
158
|
+
{formatCost(resource.cost)}
|
|
159
|
+
</span>
|
|
160
|
+
|
|
161
|
+
{/* Usage */}
|
|
162
|
+
<span className="w-28 text-xs font-mono text-gray-500 dark:text-slate-500">
|
|
163
|
+
{resource.usageFormatted}
|
|
164
|
+
</span>
|
|
165
|
+
|
|
166
|
+
{/* Limit bar (if applicable) */}
|
|
167
|
+
{resource.limitPct !== undefined ? (
|
|
168
|
+
<LimitBar percentage={resource.limitPct} />
|
|
169
|
+
) : (
|
|
170
|
+
<div className="w-24" /> // Spacer
|
|
171
|
+
)}
|
|
172
|
+
|
|
173
|
+
{/* Sparkline */}
|
|
174
|
+
{resource.trend && resource.trend.length > 1 ? (
|
|
175
|
+
<Sparkline data={resource.trend} width={60} height={16} color={colors.stroke} />
|
|
176
|
+
) : (
|
|
177
|
+
<div className="w-[60px]" /> // Spacer
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function ResourceBreakdown({ projectId, breakdown, isLoading }: ResourceBreakdownProps) {
|
|
184
|
+
if (isLoading) {
|
|
185
|
+
return <LoadingSkeleton />;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (!breakdown || breakdown.resources.length === 0) {
|
|
189
|
+
return <EmptyState />;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Sort resources by cost (descending)
|
|
193
|
+
const sortedResources = [...breakdown.resources].sort((a, b) => b.cost - a.cost);
|
|
194
|
+
|
|
195
|
+
// Identify top 3 cost drivers (only if they have non-zero cost)
|
|
196
|
+
const costDriverTypes = new Set(
|
|
197
|
+
sortedResources
|
|
198
|
+
.filter((r) => r.cost > 0)
|
|
199
|
+
.slice(0, 3)
|
|
200
|
+
.map((r) => r.type)
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<div className="space-y-1">
|
|
205
|
+
{/* Header */}
|
|
206
|
+
<div className="flex items-center gap-4 px-3 py-1 text-xs font-mono text-gray-400 dark:text-slate-600 uppercase tracking-wider">
|
|
207
|
+
<span className="w-3" /> {/* Icon spacer */}
|
|
208
|
+
<span className="w-28">Resource</span>
|
|
209
|
+
<span className="w-16 text-right">Cost</span>
|
|
210
|
+
<span className="w-28">Usage</span>
|
|
211
|
+
<span className="w-24">Limit</span>
|
|
212
|
+
<span className="w-[60px]">Trend</span>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
{/* Resource rows */}
|
|
216
|
+
{sortedResources.map((resource) => (
|
|
217
|
+
<ResourceRow
|
|
218
|
+
key={`${projectId}-${resource.type}`}
|
|
219
|
+
resource={resource}
|
|
220
|
+
isCostDriver={costDriverTypes.has(resource.type)}
|
|
221
|
+
/>
|
|
222
|
+
))}
|
|
223
|
+
|
|
224
|
+
{/* Total row */}
|
|
225
|
+
<div className="flex items-center gap-4 px-3 py-2 mt-2 border-t border-gray-200 dark:border-slate-800">
|
|
226
|
+
<span className="w-3" />
|
|
227
|
+
<span className="w-28 text-xs font-semibold text-gray-600 dark:text-slate-400">Total</span>
|
|
228
|
+
<span className="w-16 text-xs font-mono font-semibold text-gray-800 dark:text-slate-200 text-right">
|
|
229
|
+
{formatCost(breakdown.totalCost)}
|
|
230
|
+
</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export default ResourceBreakdown;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sparkline Component
|
|
3
|
+
*
|
|
4
|
+
* Inline SVG sparkline for activity trends.
|
|
5
|
+
* Minimal, industrial aesthetic.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useMemo } from 'react';
|
|
9
|
+
|
|
10
|
+
interface SparklineProps {
|
|
11
|
+
data: number[];
|
|
12
|
+
width?: number;
|
|
13
|
+
height?: number;
|
|
14
|
+
color?: string;
|
|
15
|
+
strokeWidth?: number;
|
|
16
|
+
showFill?: boolean;
|
|
17
|
+
className?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function Sparkline({
|
|
21
|
+
data,
|
|
22
|
+
width = 60,
|
|
23
|
+
height = 20,
|
|
24
|
+
color = '#3b82f6',
|
|
25
|
+
strokeWidth = 1.5,
|
|
26
|
+
showFill = true,
|
|
27
|
+
className = '',
|
|
28
|
+
}: SparklineProps) {
|
|
29
|
+
const pathData = useMemo(() => {
|
|
30
|
+
if (data.length < 2) return { line: '', fill: '' };
|
|
31
|
+
|
|
32
|
+
const min = Math.min(...data);
|
|
33
|
+
const max = Math.max(...data);
|
|
34
|
+
const range = max - min || 1;
|
|
35
|
+
|
|
36
|
+
// Padding to prevent clipping
|
|
37
|
+
const padding = 2;
|
|
38
|
+
const chartWidth = width - padding * 2;
|
|
39
|
+
const chartHeight = height - padding * 2;
|
|
40
|
+
|
|
41
|
+
const points = data.map((value, index) => {
|
|
42
|
+
const x = padding + (index / (data.length - 1)) * chartWidth;
|
|
43
|
+
const y = padding + chartHeight - ((value - min) / range) * chartHeight;
|
|
44
|
+
return { x, y };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Build line path
|
|
48
|
+
const linePath = points
|
|
49
|
+
.map((p, i) => (i === 0 ? `M ${p.x} ${p.y}` : `L ${p.x} ${p.y}`))
|
|
50
|
+
.join(' ');
|
|
51
|
+
|
|
52
|
+
// Build fill path (close at bottom)
|
|
53
|
+
const fillPath = `${linePath} L ${points[points.length - 1].x} ${height - padding} L ${padding} ${height - padding} Z`;
|
|
54
|
+
|
|
55
|
+
return { line: linePath, fill: fillPath };
|
|
56
|
+
}, [data, width, height]);
|
|
57
|
+
|
|
58
|
+
if (data.length < 2) {
|
|
59
|
+
return (
|
|
60
|
+
<svg
|
|
61
|
+
width={width}
|
|
62
|
+
height={height}
|
|
63
|
+
className={className}
|
|
64
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
65
|
+
aria-hidden="true"
|
|
66
|
+
>
|
|
67
|
+
<line
|
|
68
|
+
x1={2}
|
|
69
|
+
y1={height / 2}
|
|
70
|
+
x2={width - 2}
|
|
71
|
+
y2={height / 2}
|
|
72
|
+
stroke={color}
|
|
73
|
+
strokeWidth={strokeWidth}
|
|
74
|
+
strokeOpacity={0.3}
|
|
75
|
+
strokeDasharray="2 2"
|
|
76
|
+
/>
|
|
77
|
+
</svg>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<svg
|
|
83
|
+
width={width}
|
|
84
|
+
height={height}
|
|
85
|
+
className={className}
|
|
86
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
87
|
+
aria-hidden="true"
|
|
88
|
+
>
|
|
89
|
+
{/* Fill gradient */}
|
|
90
|
+
{showFill && (
|
|
91
|
+
<>
|
|
92
|
+
<defs>
|
|
93
|
+
<linearGradient
|
|
94
|
+
id={`sparkline-fill-${color.replace('#', '')}`}
|
|
95
|
+
x1="0"
|
|
96
|
+
y1="0"
|
|
97
|
+
x2="0"
|
|
98
|
+
y2="1"
|
|
99
|
+
>
|
|
100
|
+
<stop offset="0%" stopColor={color} stopOpacity={0.3} />
|
|
101
|
+
<stop offset="100%" stopColor={color} stopOpacity={0} />
|
|
102
|
+
</linearGradient>
|
|
103
|
+
</defs>
|
|
104
|
+
<path d={pathData.fill} fill={`url(#sparkline-fill-${color.replace('#', '')})`} />
|
|
105
|
+
</>
|
|
106
|
+
)}
|
|
107
|
+
{/* Line */}
|
|
108
|
+
<path
|
|
109
|
+
d={pathData.line}
|
|
110
|
+
fill="none"
|
|
111
|
+
stroke={color}
|
|
112
|
+
strokeWidth={strokeWidth}
|
|
113
|
+
strokeLinecap="round"
|
|
114
|
+
strokeLinejoin="round"
|
|
115
|
+
/>
|
|
116
|
+
{/* End dot */}
|
|
117
|
+
<circle
|
|
118
|
+
cx={width - 2}
|
|
119
|
+
cy={pathData.line ? parseFloat(pathData.line.split(' ').slice(-1)[0]) : height / 2}
|
|
120
|
+
r={2}
|
|
121
|
+
fill={color}
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export default Sparkline;
|