@wakastellar/ui 2.1.2 → 2.3.2
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/dist/blocks/apm-overview/index.d.ts +58 -0
- package/dist/blocks/cicd-builder/index.d.ts +47 -0
- package/dist/blocks/cloud-cost-dashboard/index.d.ts +49 -0
- package/dist/blocks/container-orchestrator/index.d.ts +63 -0
- package/dist/blocks/database-admin/index.d.ts +84 -0
- package/dist/blocks/gitops-sync-status/index.d.ts +45 -0
- package/dist/blocks/incident-manager/index.d.ts +44 -0
- package/dist/blocks/index.d.ts +10 -0
- package/dist/blocks/infrastructure-map/index.d.ts +32 -0
- package/dist/blocks/on-call-schedule/index.d.ts +43 -0
- package/dist/blocks/release-notes/index.d.ts +49 -0
- package/dist/components/index.d.ts +34 -0
- package/dist/components/waka-ad-banner/index.d.ts +36 -0
- package/dist/components/waka-ad-fallback/index.d.ts +33 -0
- package/dist/components/waka-ad-inline/index.d.ts +15 -0
- package/dist/components/waka-ad-interstitial/index.d.ts +26 -0
- package/dist/components/waka-ad-placeholder/index.d.ts +17 -0
- package/dist/components/waka-ad-provider/index.d.ts +103 -0
- package/dist/components/waka-ad-sidebar/index.d.ts +18 -0
- package/dist/components/waka-ad-sticky-footer/index.d.ts +17 -0
- package/dist/components/waka-alert-panel/index.d.ts +45 -0
- package/dist/components/waka-artifact-list/index.d.ts +32 -0
- package/dist/components/waka-build-matrix/index.d.ts +36 -0
- package/dist/components/waka-config-comparator/index.d.ts +37 -0
- package/dist/components/waka-container-list/index.d.ts +51 -0
- package/dist/components/waka-content-recommendation/index.d.ts +23 -0
- package/dist/components/waka-database-card/index.d.ts +46 -0
- package/dist/components/waka-dependency-tree/index.d.ts +38 -0
- package/dist/components/waka-env-var-editor/index.d.ts +30 -0
- package/dist/components/waka-feature-flag-row/index.d.ts +45 -0
- package/dist/components/waka-kubernetes-overview/index.d.ts +98 -0
- package/dist/components/waka-log-viewer/index.d.ts +38 -0
- package/dist/components/waka-migration-list/index.d.ts +36 -0
- package/dist/components/waka-outstream-video/index.d.ts +24 -0
- package/dist/components/waka-pod-card/index.d.ts +73 -0
- package/dist/components/waka-query-explain/index.d.ts +48 -0
- package/dist/components/waka-secret-card/index.d.ts +43 -0
- package/dist/components/waka-security-scan-result/index.d.ts +45 -0
- package/dist/components/waka-service-graph/index.d.ts +44 -0
- package/dist/components/waka-sponsored-badge/index.d.ts +20 -0
- package/dist/components/waka-sponsored-card/index.d.ts +25 -0
- package/dist/components/waka-sponsored-feed/index.d.ts +31 -0
- package/dist/components/waka-test-report/index.d.ts +60 -0
- package/dist/components/waka-trace-viewer/index.d.ts +36 -0
- package/dist/components/waka-video-ad/index.d.ts +32 -0
- package/dist/components/waka-video-overlay/index.d.ts +26 -0
- package/dist/index.cjs.js +251 -200
- package/dist/index.d.ts +1 -0
- package/dist/index.es.js +47315 -35823
- package/dist/utils/security.d.ts +96 -0
- package/package.json +4 -4
- package/src/blocks/apm-overview/index.tsx +672 -0
- package/src/blocks/cicd-builder/index.tsx +738 -0
- package/src/blocks/cloud-cost-dashboard/index.tsx +597 -0
- package/src/blocks/container-orchestrator/index.tsx +729 -0
- package/src/blocks/database-admin/index.tsx +679 -0
- package/src/blocks/gitops-sync-status/index.tsx +557 -0
- package/src/blocks/incident-manager/index.tsx +586 -0
- package/src/blocks/index.ts +119 -0
- package/src/blocks/infrastructure-map/index.tsx +638 -0
- package/src/blocks/on-call-schedule/index.tsx +615 -0
- package/src/blocks/release-notes/index.tsx +643 -0
- package/src/blocks/sidebar/index.tsx +6 -6
- package/src/components/DataTable/templates/index.tsx +3 -2
- package/src/components/index.ts +283 -0
- package/src/components/waka-3d-pie-chart/index.tsx +11 -11
- package/src/components/waka-achievement-unlock/index.tsx +16 -16
- package/src/components/waka-ad-banner/index.tsx +275 -0
- package/src/components/waka-ad-fallback/index.tsx +181 -0
- package/src/components/waka-ad-inline/index.tsx +103 -0
- package/src/components/waka-ad-interstitial/index.tsx +278 -0
- package/src/components/waka-ad-placeholder/index.tsx +84 -0
- package/src/components/waka-ad-provider/index.tsx +329 -0
- package/src/components/waka-ad-sidebar/index.tsx +113 -0
- package/src/components/waka-ad-sticky-footer/index.tsx +125 -0
- package/src/components/waka-alert-panel/index.tsx +493 -0
- package/src/components/waka-artifact-list/index.tsx +416 -0
- package/src/components/waka-badge-showcase/index.tsx +12 -11
- package/src/components/waka-build-matrix/index.tsx +396 -0
- package/src/components/waka-command-bar/index.tsx +2 -1
- package/src/components/waka-config-comparator/index.tsx +416 -0
- package/src/components/waka-container-list/index.tsx +475 -0
- package/src/components/waka-content-recommendation/index.tsx +294 -0
- package/src/components/waka-cost-breakdown/index.tsx +10 -10
- package/src/components/waka-database-card/index.tsx +473 -0
- package/src/components/waka-dependency-tree/index.tsx +542 -0
- package/src/components/waka-env-var-editor/index.tsx +417 -0
- package/src/components/waka-feature-flag-row/index.tsx +386 -0
- package/src/components/waka-funnel-chart/index.tsx +8 -8
- package/src/components/waka-health-pulse/index.tsx +6 -6
- package/src/components/waka-kubernetes-overview/index.tsx +536 -0
- package/src/components/waka-leaderboard/index.tsx +9 -9
- package/src/components/waka-log-viewer/index.tsx +386 -0
- package/src/components/waka-loot-box/index.tsx +20 -20
- package/src/components/waka-migration-list/index.tsx +487 -0
- package/src/components/waka-outstream-video/index.tsx +240 -0
- package/src/components/waka-player-card/index.tsx +5 -5
- package/src/components/waka-pod-card/index.tsx +528 -0
- package/src/components/waka-query-explain/index.tsx +657 -0
- package/src/components/waka-quota-bar/index.tsx +4 -4
- package/src/components/waka-radar-score/index.tsx +10 -10
- package/src/components/waka-scratch-card/index.tsx +5 -4
- package/src/components/waka-secret-card/index.tsx +371 -0
- package/src/components/waka-security-scan-result/index.tsx +473 -0
- package/src/components/waka-server-rack/index.tsx +28 -27
- package/src/components/waka-service-graph/index.tsx +445 -0
- package/src/components/waka-sponsored-badge/index.tsx +97 -0
- package/src/components/waka-sponsored-card/index.tsx +275 -0
- package/src/components/waka-sponsored-feed/index.tsx +127 -0
- package/src/components/waka-spotlight/index.tsx +2 -1
- package/src/components/waka-success-explosion/index.tsx +4 -4
- package/src/components/waka-test-report/index.tsx +469 -0
- package/src/components/waka-trace-viewer/index.tsx +490 -0
- package/src/components/waka-video-ad/index.tsx +406 -0
- package/src/components/waka-video-overlay/index.tsx +257 -0
- package/src/components/waka-xp-bar/index.tsx +13 -13
- package/src/styles/base.css +16 -0
- package/src/styles/tailwind.preset.js +12 -0
- package/src/styles/themes/forest.css +16 -0
- package/src/styles/themes/monochrome.css +16 -0
- package/src/styles/themes/perpetuity.css +16 -0
- package/src/styles/themes/sunset.css +16 -0
- package/src/styles/themes/twilight.css +16 -0
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { cn } from "../../utils/cn"
|
|
5
|
+
import { Badge } from "../../components/badge"
|
|
6
|
+
import { Button } from "../../components/button"
|
|
7
|
+
import { Card, CardContent, CardHeader, CardTitle } from "../../components/card"
|
|
8
|
+
import { Progress } from "../../components/progress"
|
|
9
|
+
import { ScrollArea } from "../../components/scroll-area"
|
|
10
|
+
import {
|
|
11
|
+
Select,
|
|
12
|
+
SelectContent,
|
|
13
|
+
SelectItem,
|
|
14
|
+
SelectTrigger,
|
|
15
|
+
SelectValue,
|
|
16
|
+
} from "../../components/select"
|
|
17
|
+
import {
|
|
18
|
+
DollarSign,
|
|
19
|
+
TrendingUp,
|
|
20
|
+
TrendingDown,
|
|
21
|
+
Cloud,
|
|
22
|
+
Server,
|
|
23
|
+
Database,
|
|
24
|
+
HardDrive,
|
|
25
|
+
Network,
|
|
26
|
+
Cpu,
|
|
27
|
+
AlertTriangle,
|
|
28
|
+
ArrowUpRight,
|
|
29
|
+
ArrowDownRight,
|
|
30
|
+
BarChart3,
|
|
31
|
+
PieChart,
|
|
32
|
+
Calendar,
|
|
33
|
+
Download,
|
|
34
|
+
Filter,
|
|
35
|
+
RefreshCw,
|
|
36
|
+
Zap,
|
|
37
|
+
Globe,
|
|
38
|
+
Container,
|
|
39
|
+
Shield,
|
|
40
|
+
} from "lucide-react"
|
|
41
|
+
|
|
42
|
+
export type CloudProvider = "aws" | "gcp" | "azure" | "all"
|
|
43
|
+
export type CostCategory = "compute" | "storage" | "database" | "network" | "security" | "other"
|
|
44
|
+
export type TimeRange = "day" | "week" | "month" | "quarter" | "year"
|
|
45
|
+
|
|
46
|
+
export interface CostItem {
|
|
47
|
+
id: string
|
|
48
|
+
name: string
|
|
49
|
+
category: CostCategory
|
|
50
|
+
provider: CloudProvider
|
|
51
|
+
cost: number
|
|
52
|
+
previousCost?: number
|
|
53
|
+
budget?: number
|
|
54
|
+
usage?: number
|
|
55
|
+
unit?: string
|
|
56
|
+
trend?: number
|
|
57
|
+
tags?: string[]
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface CostSummary {
|
|
61
|
+
totalCost: number
|
|
62
|
+
previousTotalCost?: number
|
|
63
|
+
budget?: number
|
|
64
|
+
forecast?: number
|
|
65
|
+
byProvider: Record<CloudProvider, number>
|
|
66
|
+
byCategory: Record<CostCategory, number>
|
|
67
|
+
topServices: CostItem[]
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface CostAnomaly {
|
|
71
|
+
id: string
|
|
72
|
+
service: string
|
|
73
|
+
expectedCost: number
|
|
74
|
+
actualCost: number
|
|
75
|
+
deviation: number
|
|
76
|
+
timestamp: Date
|
|
77
|
+
status: "new" | "acknowledged" | "resolved"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CloudCostDashboardProps {
|
|
81
|
+
summary: CostSummary
|
|
82
|
+
items: CostItem[]
|
|
83
|
+
anomalies?: CostAnomaly[]
|
|
84
|
+
timeRange?: TimeRange
|
|
85
|
+
onTimeRangeChange?: (range: TimeRange) => void
|
|
86
|
+
onProviderFilter?: (provider: CloudProvider) => void
|
|
87
|
+
onExport?: () => void
|
|
88
|
+
onRefresh?: () => void
|
|
89
|
+
className?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const categoryConfig: Record<CostCategory, { icon: React.ElementType; color: string; label: string }> = {
|
|
93
|
+
compute: { icon: Cpu, color: "text-blue-500", label: "Compute" },
|
|
94
|
+
storage: { icon: HardDrive, color: "text-green-500", label: "Storage" },
|
|
95
|
+
database: { icon: Database, color: "text-purple-500", label: "Database" },
|
|
96
|
+
network: { icon: Network, color: "text-cyan-500", label: "Network" },
|
|
97
|
+
security: { icon: Shield, color: "text-red-500", label: "Security" },
|
|
98
|
+
other: { icon: Cloud, color: "text-gray-500", label: "Other" },
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const providerConfig: Record<CloudProvider, { label: string; color: string }> = {
|
|
102
|
+
aws: { label: "AWS", color: "text-orange-500" },
|
|
103
|
+
gcp: { label: "GCP", color: "text-blue-500" },
|
|
104
|
+
azure: { label: "Azure", color: "text-cyan-500" },
|
|
105
|
+
all: { label: "All Providers", color: "text-gray-500" },
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function formatCurrency(amount: number): string {
|
|
109
|
+
return new Intl.NumberFormat("en-US", {
|
|
110
|
+
style: "currency",
|
|
111
|
+
currency: "USD",
|
|
112
|
+
minimumFractionDigits: 0,
|
|
113
|
+
maximumFractionDigits: 0,
|
|
114
|
+
}).format(amount)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function formatPercentage(value: number): string {
|
|
118
|
+
return `${value >= 0 ? "+" : ""}${value.toFixed(1)}%`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function CostMetricCard({
|
|
122
|
+
title,
|
|
123
|
+
value,
|
|
124
|
+
previousValue,
|
|
125
|
+
icon: Icon,
|
|
126
|
+
trend,
|
|
127
|
+
budget,
|
|
128
|
+
forecast,
|
|
129
|
+
}: {
|
|
130
|
+
title: string
|
|
131
|
+
value: number
|
|
132
|
+
previousValue?: number
|
|
133
|
+
icon: React.ElementType
|
|
134
|
+
trend?: number
|
|
135
|
+
budget?: number
|
|
136
|
+
forecast?: number
|
|
137
|
+
}) {
|
|
138
|
+
const trendValue = previousValue !== undefined
|
|
139
|
+
? ((value - previousValue) / previousValue) * 100
|
|
140
|
+
: trend
|
|
141
|
+
|
|
142
|
+
const isOverBudget = budget !== undefined && value > budget
|
|
143
|
+
const budgetPercentage = budget !== undefined ? (value / budget) * 100 : undefined
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<Card className={cn(isOverBudget && "border-red-500/50")}>
|
|
147
|
+
<CardContent className="p-4">
|
|
148
|
+
<div className="flex items-start justify-between">
|
|
149
|
+
<div className="p-2 rounded-lg bg-muted">
|
|
150
|
+
<Icon className="h-5 w-5 text-muted-foreground" />
|
|
151
|
+
</div>
|
|
152
|
+
{trendValue !== undefined && (
|
|
153
|
+
<Badge
|
|
154
|
+
variant="outline"
|
|
155
|
+
className={cn(
|
|
156
|
+
trendValue > 0 ? "text-red-500" : "text-green-500"
|
|
157
|
+
)}
|
|
158
|
+
>
|
|
159
|
+
{trendValue > 0 ? (
|
|
160
|
+
<ArrowUpRight className="h-3 w-3 mr-1" />
|
|
161
|
+
) : (
|
|
162
|
+
<ArrowDownRight className="h-3 w-3 mr-1" />
|
|
163
|
+
)}
|
|
164
|
+
{formatPercentage(Math.abs(trendValue))}
|
|
165
|
+
</Badge>
|
|
166
|
+
)}
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<div className="mt-3">
|
|
170
|
+
<div className="text-2xl font-bold">{formatCurrency(value)}</div>
|
|
171
|
+
<div className="text-sm text-muted-foreground">{title}</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
{budgetPercentage !== undefined && (
|
|
175
|
+
<div className="mt-3">
|
|
176
|
+
<div className="flex items-center justify-between text-xs mb-1">
|
|
177
|
+
<span className="text-muted-foreground">Budget</span>
|
|
178
|
+
<span className={cn(isOverBudget && "text-red-500")}>
|
|
179
|
+
{formatCurrency(value)} / {formatCurrency(budget!)}
|
|
180
|
+
</span>
|
|
181
|
+
</div>
|
|
182
|
+
<Progress
|
|
183
|
+
value={Math.min(budgetPercentage, 100)}
|
|
184
|
+
className={cn(
|
|
185
|
+
"h-2",
|
|
186
|
+
budgetPercentage > 100 && "[&>div]:bg-red-500",
|
|
187
|
+
budgetPercentage > 80 && budgetPercentage <= 100 && "[&>div]:bg-yellow-500"
|
|
188
|
+
)}
|
|
189
|
+
/>
|
|
190
|
+
</div>
|
|
191
|
+
)}
|
|
192
|
+
|
|
193
|
+
{forecast !== undefined && (
|
|
194
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
195
|
+
Forecast: {formatCurrency(forecast)}
|
|
196
|
+
</div>
|
|
197
|
+
)}
|
|
198
|
+
</CardContent>
|
|
199
|
+
</Card>
|
|
200
|
+
)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function CategoryBreakdown({ data }: { data: Record<CostCategory, number> }) {
|
|
204
|
+
const total = Object.values(data).reduce((a, b) => a + b, 0)
|
|
205
|
+
const sorted = Object.entries(data)
|
|
206
|
+
.sort(([, a], [, b]) => b - a)
|
|
207
|
+
.filter(([, value]) => value > 0) as [CostCategory, number][]
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
<Card>
|
|
211
|
+
<CardHeader className="pb-2">
|
|
212
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
213
|
+
<PieChart className="h-4 w-4" />
|
|
214
|
+
Cost by Category
|
|
215
|
+
</CardTitle>
|
|
216
|
+
</CardHeader>
|
|
217
|
+
<CardContent className="space-y-3">
|
|
218
|
+
{sorted.map(([category, value]) => {
|
|
219
|
+
const config = categoryConfig[category]
|
|
220
|
+
const Icon = config.icon
|
|
221
|
+
const percentage = (value / total) * 100
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div key={category}>
|
|
225
|
+
<div className="flex items-center justify-between mb-1">
|
|
226
|
+
<div className="flex items-center gap-2">
|
|
227
|
+
<Icon className={cn("h-4 w-4", config.color)} />
|
|
228
|
+
<span className="text-sm">{config.label}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div className="text-sm font-medium">
|
|
231
|
+
{formatCurrency(value)}
|
|
232
|
+
<span className="text-muted-foreground ml-1">
|
|
233
|
+
({percentage.toFixed(0)}%)
|
|
234
|
+
</span>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
<Progress value={percentage} className="h-2" />
|
|
238
|
+
</div>
|
|
239
|
+
)
|
|
240
|
+
})}
|
|
241
|
+
</CardContent>
|
|
242
|
+
</Card>
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function ProviderBreakdown({ data }: { data: Record<CloudProvider, number> }) {
|
|
247
|
+
const total = Object.values(data).reduce((a, b) => a + b, 0)
|
|
248
|
+
const sorted = Object.entries(data)
|
|
249
|
+
.filter(([key]) => key !== "all")
|
|
250
|
+
.sort(([, a], [, b]) => b - a)
|
|
251
|
+
.filter(([, value]) => value > 0) as [CloudProvider, number][]
|
|
252
|
+
|
|
253
|
+
return (
|
|
254
|
+
<Card>
|
|
255
|
+
<CardHeader className="pb-2">
|
|
256
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
257
|
+
<Cloud className="h-4 w-4" />
|
|
258
|
+
Cost by Provider
|
|
259
|
+
</CardTitle>
|
|
260
|
+
</CardHeader>
|
|
261
|
+
<CardContent className="space-y-3">
|
|
262
|
+
{sorted.map(([provider, value]) => {
|
|
263
|
+
const config = providerConfig[provider]
|
|
264
|
+
const percentage = (value / total) * 100
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div key={provider}>
|
|
268
|
+
<div className="flex items-center justify-between mb-1">
|
|
269
|
+
<span className={cn("text-sm font-medium", config.color)}>
|
|
270
|
+
{config.label}
|
|
271
|
+
</span>
|
|
272
|
+
<div className="text-sm font-medium">
|
|
273
|
+
{formatCurrency(value)}
|
|
274
|
+
<span className="text-muted-foreground ml-1">
|
|
275
|
+
({percentage.toFixed(0)}%)
|
|
276
|
+
</span>
|
|
277
|
+
</div>
|
|
278
|
+
</div>
|
|
279
|
+
<Progress value={percentage} className="h-2" />
|
|
280
|
+
</div>
|
|
281
|
+
)
|
|
282
|
+
})}
|
|
283
|
+
</CardContent>
|
|
284
|
+
</Card>
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function TopServicesTable({ items }: { items: CostItem[] }) {
|
|
289
|
+
return (
|
|
290
|
+
<Card>
|
|
291
|
+
<CardHeader className="pb-2">
|
|
292
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
293
|
+
<BarChart3 className="h-4 w-4" />
|
|
294
|
+
Top Services by Cost
|
|
295
|
+
</CardTitle>
|
|
296
|
+
</CardHeader>
|
|
297
|
+
<CardContent>
|
|
298
|
+
<ScrollArea className="h-[300px]">
|
|
299
|
+
<div className="space-y-2">
|
|
300
|
+
{items.map((item, index) => {
|
|
301
|
+
const categoryConf = categoryConfig[item.category]
|
|
302
|
+
const CategoryIcon = categoryConf.icon
|
|
303
|
+
const providerConf = providerConfig[item.provider]
|
|
304
|
+
const trendValue = item.previousCost !== undefined
|
|
305
|
+
? ((item.cost - item.previousCost) / item.previousCost) * 100
|
|
306
|
+
: item.trend
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<div
|
|
310
|
+
key={item.id}
|
|
311
|
+
className="flex items-center gap-3 p-2 rounded-lg hover:bg-muted/50 transition-colors"
|
|
312
|
+
>
|
|
313
|
+
<div className="text-lg font-bold text-muted-foreground w-6 text-center">
|
|
314
|
+
{index + 1}
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div className={cn("p-1.5 rounded", `${categoryConf.color.replace("text-", "bg-")}/10`)}>
|
|
318
|
+
<CategoryIcon className={cn("h-4 w-4", categoryConf.color)} />
|
|
319
|
+
</div>
|
|
320
|
+
|
|
321
|
+
<div className="flex-1 min-w-0">
|
|
322
|
+
<div className="font-medium truncate">{item.name}</div>
|
|
323
|
+
<div className="text-xs text-muted-foreground flex items-center gap-2">
|
|
324
|
+
<span className={providerConf.color}>{providerConf.label}</span>
|
|
325
|
+
<span>•</span>
|
|
326
|
+
<span>{categoryConf.label}</span>
|
|
327
|
+
{item.usage !== undefined && item.unit && (
|
|
328
|
+
<>
|
|
329
|
+
<span>•</span>
|
|
330
|
+
<span>{item.usage} {item.unit}</span>
|
|
331
|
+
</>
|
|
332
|
+
)}
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<div className="text-right shrink-0">
|
|
337
|
+
<div className="font-semibold">{formatCurrency(item.cost)}</div>
|
|
338
|
+
{trendValue !== undefined && (
|
|
339
|
+
<div className={cn(
|
|
340
|
+
"text-xs flex items-center justify-end gap-0.5",
|
|
341
|
+
trendValue > 0 ? "text-red-500" : "text-green-500"
|
|
342
|
+
)}>
|
|
343
|
+
{trendValue > 0 ? (
|
|
344
|
+
<TrendingUp className="h-3 w-3" />
|
|
345
|
+
) : (
|
|
346
|
+
<TrendingDown className="h-3 w-3" />
|
|
347
|
+
)}
|
|
348
|
+
{formatPercentage(Math.abs(trendValue))}
|
|
349
|
+
</div>
|
|
350
|
+
)}
|
|
351
|
+
</div>
|
|
352
|
+
</div>
|
|
353
|
+
)
|
|
354
|
+
})}
|
|
355
|
+
</div>
|
|
356
|
+
</ScrollArea>
|
|
357
|
+
</CardContent>
|
|
358
|
+
</Card>
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function AnomaliesCard({
|
|
363
|
+
anomalies,
|
|
364
|
+
onAcknowledge,
|
|
365
|
+
}: {
|
|
366
|
+
anomalies: CostAnomaly[]
|
|
367
|
+
onAcknowledge?: (id: string) => void
|
|
368
|
+
}) {
|
|
369
|
+
const newAnomalies = anomalies.filter((a) => a.status === "new")
|
|
370
|
+
|
|
371
|
+
return (
|
|
372
|
+
<Card className={cn(newAnomalies.length > 0 && "border-yellow-500/50")}>
|
|
373
|
+
<CardHeader className="pb-2">
|
|
374
|
+
<div className="flex items-center justify-between">
|
|
375
|
+
<CardTitle className="text-base flex items-center gap-2">
|
|
376
|
+
<AlertTriangle className="h-4 w-4 text-yellow-500" />
|
|
377
|
+
Cost Anomalies
|
|
378
|
+
</CardTitle>
|
|
379
|
+
{newAnomalies.length > 0 && (
|
|
380
|
+
<Badge className="bg-yellow-500">{newAnomalies.length} new</Badge>
|
|
381
|
+
)}
|
|
382
|
+
</div>
|
|
383
|
+
</CardHeader>
|
|
384
|
+
<CardContent>
|
|
385
|
+
{anomalies.length === 0 ? (
|
|
386
|
+
<div className="text-center text-muted-foreground py-4">
|
|
387
|
+
<Zap className="h-8 w-8 mx-auto mb-2 text-green-500" />
|
|
388
|
+
<p>No anomalies detected</p>
|
|
389
|
+
</div>
|
|
390
|
+
) : (
|
|
391
|
+
<div className="space-y-2">
|
|
392
|
+
{anomalies.slice(0, 5).map((anomaly) => (
|
|
393
|
+
<div
|
|
394
|
+
key={anomaly.id}
|
|
395
|
+
className={cn(
|
|
396
|
+
"flex items-center gap-3 p-2 rounded-lg",
|
|
397
|
+
anomaly.status === "new" && "bg-yellow-500/10"
|
|
398
|
+
)}
|
|
399
|
+
>
|
|
400
|
+
<AlertTriangle className={cn(
|
|
401
|
+
"h-4 w-4 shrink-0",
|
|
402
|
+
anomaly.deviation > 100 ? "text-red-500" : "text-yellow-500"
|
|
403
|
+
)} />
|
|
404
|
+
|
|
405
|
+
<div className="flex-1 min-w-0">
|
|
406
|
+
<div className="font-medium truncate">{anomaly.service}</div>
|
|
407
|
+
<div className="text-xs text-muted-foreground">
|
|
408
|
+
Expected {formatCurrency(anomaly.expectedCost)} • Actual {formatCurrency(anomaly.actualCost)}
|
|
409
|
+
</div>
|
|
410
|
+
</div>
|
|
411
|
+
|
|
412
|
+
<Badge variant="outline" className="text-red-500 shrink-0">
|
|
413
|
+
+{anomaly.deviation.toFixed(0)}%
|
|
414
|
+
</Badge>
|
|
415
|
+
</div>
|
|
416
|
+
))}
|
|
417
|
+
</div>
|
|
418
|
+
)}
|
|
419
|
+
</CardContent>
|
|
420
|
+
</Card>
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function CloudCostDashboard({
|
|
425
|
+
summary,
|
|
426
|
+
items,
|
|
427
|
+
anomalies = [],
|
|
428
|
+
timeRange = "month",
|
|
429
|
+
onTimeRangeChange,
|
|
430
|
+
onProviderFilter,
|
|
431
|
+
onExport,
|
|
432
|
+
onRefresh,
|
|
433
|
+
className,
|
|
434
|
+
}: CloudCostDashboardProps) {
|
|
435
|
+
const [selectedProvider, setSelectedProvider] = React.useState<CloudProvider>("all")
|
|
436
|
+
|
|
437
|
+
const handleProviderChange = (provider: CloudProvider) => {
|
|
438
|
+
setSelectedProvider(provider)
|
|
439
|
+
onProviderFilter?.(provider)
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const filteredItems = React.useMemo(() => {
|
|
443
|
+
if (selectedProvider === "all") return items
|
|
444
|
+
return items.filter((item) => item.provider === selectedProvider)
|
|
445
|
+
}, [items, selectedProvider])
|
|
446
|
+
|
|
447
|
+
const trendValue = summary.previousTotalCost !== undefined
|
|
448
|
+
? ((summary.totalCost - summary.previousTotalCost) / summary.previousTotalCost) * 100
|
|
449
|
+
: undefined
|
|
450
|
+
|
|
451
|
+
return (
|
|
452
|
+
<div className={cn("space-y-6", className)}>
|
|
453
|
+
{/* Header */}
|
|
454
|
+
<Card>
|
|
455
|
+
<CardHeader>
|
|
456
|
+
<div className="flex items-center justify-between">
|
|
457
|
+
<div className="flex items-center gap-3">
|
|
458
|
+
<DollarSign className="h-6 w-6" />
|
|
459
|
+
<div>
|
|
460
|
+
<CardTitle>Cloud Cost Dashboard</CardTitle>
|
|
461
|
+
<p className="text-sm text-muted-foreground">
|
|
462
|
+
Monitor and optimize your cloud spending
|
|
463
|
+
</p>
|
|
464
|
+
</div>
|
|
465
|
+
</div>
|
|
466
|
+
|
|
467
|
+
<div className="flex items-center gap-2">
|
|
468
|
+
<Select value={timeRange} onValueChange={(v) => onTimeRangeChange?.(v as TimeRange)}>
|
|
469
|
+
<SelectTrigger className="w-32">
|
|
470
|
+
<Calendar className="h-4 w-4 mr-2" />
|
|
471
|
+
<SelectValue />
|
|
472
|
+
</SelectTrigger>
|
|
473
|
+
<SelectContent>
|
|
474
|
+
<SelectItem value="day">Today</SelectItem>
|
|
475
|
+
<SelectItem value="week">This Week</SelectItem>
|
|
476
|
+
<SelectItem value="month">This Month</SelectItem>
|
|
477
|
+
<SelectItem value="quarter">This Quarter</SelectItem>
|
|
478
|
+
<SelectItem value="year">This Year</SelectItem>
|
|
479
|
+
</SelectContent>
|
|
480
|
+
</Select>
|
|
481
|
+
|
|
482
|
+
<Select value={selectedProvider} onValueChange={(v) => handleProviderChange(v as CloudProvider)}>
|
|
483
|
+
<SelectTrigger className="w-32">
|
|
484
|
+
<Cloud className="h-4 w-4 mr-2" />
|
|
485
|
+
<SelectValue />
|
|
486
|
+
</SelectTrigger>
|
|
487
|
+
<SelectContent>
|
|
488
|
+
<SelectItem value="all">All Providers</SelectItem>
|
|
489
|
+
<SelectItem value="aws">AWS</SelectItem>
|
|
490
|
+
<SelectItem value="gcp">GCP</SelectItem>
|
|
491
|
+
<SelectItem value="azure">Azure</SelectItem>
|
|
492
|
+
</SelectContent>
|
|
493
|
+
</Select>
|
|
494
|
+
|
|
495
|
+
{onRefresh && (
|
|
496
|
+
<Button variant="outline" size="icon" onClick={onRefresh}>
|
|
497
|
+
<RefreshCw className="h-4 w-4" />
|
|
498
|
+
</Button>
|
|
499
|
+
)}
|
|
500
|
+
|
|
501
|
+
{onExport && (
|
|
502
|
+
<Button variant="outline" onClick={onExport}>
|
|
503
|
+
<Download className="h-4 w-4 mr-1" />
|
|
504
|
+
Export
|
|
505
|
+
</Button>
|
|
506
|
+
)}
|
|
507
|
+
</div>
|
|
508
|
+
</div>
|
|
509
|
+
</CardHeader>
|
|
510
|
+
</Card>
|
|
511
|
+
|
|
512
|
+
{/* Key metrics */}
|
|
513
|
+
<div className="grid grid-cols-4 gap-4">
|
|
514
|
+
<CostMetricCard
|
|
515
|
+
title="Total Cost"
|
|
516
|
+
value={summary.totalCost}
|
|
517
|
+
previousValue={summary.previousTotalCost}
|
|
518
|
+
icon={DollarSign}
|
|
519
|
+
budget={summary.budget}
|
|
520
|
+
/>
|
|
521
|
+
<CostMetricCard
|
|
522
|
+
title="Compute"
|
|
523
|
+
value={summary.byCategory.compute}
|
|
524
|
+
icon={Cpu}
|
|
525
|
+
trend={-5.2}
|
|
526
|
+
/>
|
|
527
|
+
<CostMetricCard
|
|
528
|
+
title="Storage"
|
|
529
|
+
value={summary.byCategory.storage}
|
|
530
|
+
icon={HardDrive}
|
|
531
|
+
trend={12.3}
|
|
532
|
+
/>
|
|
533
|
+
<CostMetricCard
|
|
534
|
+
title="Forecast (EOM)"
|
|
535
|
+
value={summary.forecast || summary.totalCost * 1.1}
|
|
536
|
+
icon={TrendingUp}
|
|
537
|
+
budget={summary.budget}
|
|
538
|
+
/>
|
|
539
|
+
</div>
|
|
540
|
+
|
|
541
|
+
{/* Main content */}
|
|
542
|
+
<div className="grid grid-cols-3 gap-6">
|
|
543
|
+
<div className="col-span-2 space-y-6">
|
|
544
|
+
<TopServicesTable items={filteredItems.slice(0, 10)} />
|
|
545
|
+
</div>
|
|
546
|
+
|
|
547
|
+
<div className="space-y-6">
|
|
548
|
+
<CategoryBreakdown data={summary.byCategory} />
|
|
549
|
+
<ProviderBreakdown data={summary.byProvider} />
|
|
550
|
+
<AnomaliesCard anomalies={anomalies} />
|
|
551
|
+
</div>
|
|
552
|
+
</div>
|
|
553
|
+
</div>
|
|
554
|
+
)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Default sample data
|
|
558
|
+
export const defaultCloudCostSummary: CostSummary = {
|
|
559
|
+
totalCost: 45230,
|
|
560
|
+
previousTotalCost: 42100,
|
|
561
|
+
budget: 50000,
|
|
562
|
+
forecast: 48500,
|
|
563
|
+
byProvider: {
|
|
564
|
+
aws: 28500,
|
|
565
|
+
gcp: 12300,
|
|
566
|
+
azure: 4430,
|
|
567
|
+
all: 45230,
|
|
568
|
+
},
|
|
569
|
+
byCategory: {
|
|
570
|
+
compute: 22000,
|
|
571
|
+
storage: 8500,
|
|
572
|
+
database: 7200,
|
|
573
|
+
network: 4100,
|
|
574
|
+
security: 1800,
|
|
575
|
+
other: 1630,
|
|
576
|
+
},
|
|
577
|
+
topServices: [],
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export const defaultCloudCostItems: CostItem[] = [
|
|
581
|
+
{ id: "1", name: "EC2 Instances", category: "compute", provider: "aws", cost: 12500, previousCost: 11800, usage: 450, unit: "hours" },
|
|
582
|
+
{ id: "2", name: "RDS PostgreSQL", category: "database", provider: "aws", cost: 4200, previousCost: 4000 },
|
|
583
|
+
{ id: "3", name: "S3 Storage", category: "storage", provider: "aws", cost: 3800, previousCost: 3200, usage: 12, unit: "TB" },
|
|
584
|
+
{ id: "4", name: "GKE Clusters", category: "compute", provider: "gcp", cost: 6500, previousCost: 6800 },
|
|
585
|
+
{ id: "5", name: "Cloud SQL", category: "database", provider: "gcp", cost: 2800, previousCost: 2600 },
|
|
586
|
+
{ id: "6", name: "BigQuery", category: "database", provider: "gcp", cost: 1800, previousCost: 1500, usage: 45, unit: "TB scanned" },
|
|
587
|
+
{ id: "7", name: "Azure VMs", category: "compute", provider: "azure", cost: 2400, previousCost: 2200 },
|
|
588
|
+
{ id: "8", name: "Blob Storage", category: "storage", provider: "azure", cost: 1200, previousCost: 1100 },
|
|
589
|
+
{ id: "9", name: "CloudFront CDN", category: "network", provider: "aws", cost: 2100, previousCost: 1900, usage: 85, unit: "TB" },
|
|
590
|
+
{ id: "10", name: "Lambda Functions", category: "compute", provider: "aws", cost: 850, previousCost: 720, usage: 12000000, unit: "invocations" },
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
export const defaultCostAnomalies: CostAnomaly[] = [
|
|
594
|
+
{ id: "1", service: "EC2 Spot Instances", expectedCost: 500, actualCost: 1250, deviation: 150, timestamp: new Date(), status: "new" },
|
|
595
|
+
{ id: "2", service: "Data Transfer", expectedCost: 800, actualCost: 1400, deviation: 75, timestamp: new Date(Date.now() - 3600000), status: "new" },
|
|
596
|
+
{ id: "3", service: "Lambda", expectedCost: 200, actualCost: 320, deviation: 60, timestamp: new Date(Date.now() - 7200000), status: "acknowledged" },
|
|
597
|
+
]
|