astryx-dracula 0.1.3 → 0.2.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/BRAND.md +1 -1
- package/README.md +2 -0
- package/USAGE.md +13 -0
- package/astryx-dracula.js +4 -4
- package/astryx.integration.mjs +3 -0
- package/package.json +4 -2
- package/templates/ai-chat-landing.template.mjs +6 -0
- package/templates/ai-chat-landing.tsx +476 -0
- package/templates/ai-chat.template.mjs +6 -0
- package/templates/ai-chat.tsx +762 -0
- package/templates/blank.template.mjs +6 -0
- package/templates/blank.tsx +16 -0
- package/templates/centered-hero.template.mjs +6 -0
- package/templates/centered-hero.tsx +188 -0
- package/templates/classic-gallery.template.mjs +6 -0
- package/templates/classic-gallery.tsx +181 -0
- package/templates/contact-form.template.mjs +6 -0
- package/templates/contact-form.tsx +292 -0
- package/templates/dashboard-portfolio.template.mjs +6 -0
- package/templates/dashboard-portfolio.tsx +800 -0
- package/templates/dashboard.template.mjs +6 -0
- package/templates/dashboard.tsx +774 -0
- package/templates/detail-page.template.mjs +6 -0
- package/templates/detail-page.tsx +695 -0
- package/templates/documentation-design.template.mjs +6 -0
- package/templates/documentation-design.tsx +751 -0
- package/templates/documentation-technical.template.mjs +6 -0
- package/templates/documentation-technical.tsx +292 -0
- package/templates/documentation.template.mjs +6 -0
- package/templates/documentation.tsx +347 -0
- package/templates/editor.template.mjs +6 -0
- package/templates/editor.tsx +959 -0
- package/templates/file-explorer.template.mjs +6 -0
- package/templates/file-explorer.tsx +577 -0
- package/templates/form-two-column.template.mjs +6 -0
- package/templates/form-two-column.tsx +314 -0
- package/templates/gallery-hero.template.mjs +6 -0
- package/templates/gallery-hero.tsx +287 -0
- package/templates/ide.template.mjs +6 -0
- package/templates/ide.tsx +533 -0
- package/templates/incident-console.template.mjs +6 -0
- package/templates/incident-console.tsx +584 -0
- package/templates/kanban-board.template.mjs +6 -0
- package/templates/kanban-board.tsx +746 -0
- package/templates/library.template.mjs +6 -0
- package/templates/library.tsx +489 -0
- package/templates/login-card.template.mjs +6 -0
- package/templates/login-card.tsx +200 -0
- package/templates/login-split.template.mjs +6 -0
- package/templates/login-split.tsx +318 -0
- package/templates/login-sso.template.mjs +6 -0
- package/templates/login-sso.tsx +277 -0
- package/templates/login.template.mjs +6 -0
- package/templates/login.tsx +98 -0
- package/templates/messaging-shell.template.mjs +6 -0
- package/templates/messaging-shell.tsx +675 -0
- package/templates/mixed-gallery.template.mjs +6 -0
- package/templates/mixed-gallery.tsx +190 -0
- package/templates/payment-form.template.mjs +6 -0
- package/templates/payment-form.tsx +990 -0
- package/templates/product-detail.template.mjs +6 -0
- package/templates/product-detail.tsx +366 -0
- package/templates/product-gallery.template.mjs +6 -0
- package/templates/product-gallery.tsx +173 -0
- package/templates/settings-dialog.template.mjs +6 -0
- package/templates/settings-dialog.tsx +920 -0
- package/templates/settings-sidebar.template.mjs +6 -0
- package/templates/settings-sidebar.tsx +952 -0
- package/templates/settings.template.mjs +6 -0
- package/templates/settings.tsx +235 -0
- package/templates/shell-nav.template.mjs +6 -0
- package/templates/shell-nav.tsx +306 -0
- package/templates/shell-side-nav.template.mjs +6 -0
- package/templates/shell-side-nav.tsx +238 -0
- package/templates/shell-top-nav.template.mjs +6 -0
- package/templates/shell-top-nav.tsx +243 -0
- package/templates/side-gallery.template.mjs +6 -0
- package/templates/side-gallery.tsx +179 -0
- package/templates/table-grouped.template.mjs +6 -0
- package/templates/table-grouped.tsx +1174 -0
- package/templates/table-page-chart.template.mjs +6 -0
- package/templates/table-page-chart.tsx +693 -0
- package/templates/table-page-heatmap-status.template.mjs +6 -0
- package/templates/table-page-heatmap-status.tsx +547 -0
- package/templates/table-page-shoe-store-heatmap.template.mjs +6 -0
- package/templates/table-page-shoe-store-heatmap.tsx +1038 -0
- package/templates/table-page.template.mjs +6 -0
- package/templates/table-page.tsx +445 -0
- package/templates/table.template.mjs +6 -0
- package/templates/table.tsx +84 -0
- package/templates/theme-showcase.template.mjs +6 -0
- package/templates/theme-showcase.tsx +1282 -0
- package/theme.css +4 -4
- package/tokens.css +4 -4
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
VStack,
|
|
5
|
+
HStack,
|
|
6
|
+
StackItem,
|
|
7
|
+
Layout,
|
|
8
|
+
LayoutContent,
|
|
9
|
+
LayoutHeader,
|
|
10
|
+
} from '@astryxdesign/core/Layout';
|
|
11
|
+
import {Text, Heading} from '@astryxdesign/core/Text';
|
|
12
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
13
|
+
import {IconButton} from '@astryxdesign/core/IconButton';
|
|
14
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
15
|
+
import {Avatar} from '@astryxdesign/core/Avatar';
|
|
16
|
+
import {Badge} from '@astryxdesign/core/Badge';
|
|
17
|
+
import {Card} from '@astryxdesign/core/Card';
|
|
18
|
+
import {Link} from '@astryxdesign/core/Link';
|
|
19
|
+
import {Table, proportional, pixel} from '@astryxdesign/core/Table';
|
|
20
|
+
import type {TableColumn} from '@astryxdesign/core/Table';
|
|
21
|
+
import {Filter, Download, RotateCw} from 'lucide-react';
|
|
22
|
+
|
|
23
|
+
// ============= ICONS (verified lucide-react exports) =============
|
|
24
|
+
// Filter ← FunnelIcon, Download ← ArrowDownTrayIcon, RotateCw ← ArrowPathIcon.
|
|
25
|
+
|
|
26
|
+
// ============= DATA =============
|
|
27
|
+
|
|
28
|
+
type Severity = 'critical' | 'major' | 'minor' | 'resolved';
|
|
29
|
+
type ProductName =
|
|
30
|
+
| 'Ads Manager'
|
|
31
|
+
| 'Business Suite'
|
|
32
|
+
| 'Commerce Manager'
|
|
33
|
+
| 'Pages'
|
|
34
|
+
| 'Messenger API'
|
|
35
|
+
| 'Graph API'
|
|
36
|
+
| 'WhatsApp Business'
|
|
37
|
+
| 'Instagram API';
|
|
38
|
+
|
|
39
|
+
interface IncidentRow extends Record<string, unknown> {
|
|
40
|
+
id: string;
|
|
41
|
+
product: ProductName;
|
|
42
|
+
title: string;
|
|
43
|
+
severity: Severity;
|
|
44
|
+
status: 'ongoing' | 'identified' | 'monitoring' | 'resolved';
|
|
45
|
+
oncall: string;
|
|
46
|
+
startTime: string;
|
|
47
|
+
duration: string;
|
|
48
|
+
date: string;
|
|
49
|
+
dayOfWeek: number;
|
|
50
|
+
hour: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const incidents: IncidentRow[] = [
|
|
54
|
+
// Active — ongoing
|
|
55
|
+
{
|
|
56
|
+
id: 'INC-4008',
|
|
57
|
+
product: 'Business Suite',
|
|
58
|
+
title:
|
|
59
|
+
'Notification delivery delays affecting mobile push and email channels — queue saturation under investigation',
|
|
60
|
+
severity: 'major',
|
|
61
|
+
oncall: 'Carlos Mendez',
|
|
62
|
+
status: 'ongoing',
|
|
63
|
+
startTime: '09:15',
|
|
64
|
+
duration: 'ongoing',
|
|
65
|
+
date: '2025-01-15',
|
|
66
|
+
dayOfWeek: 3,
|
|
67
|
+
hour: 9,
|
|
68
|
+
},
|
|
69
|
+
// Tue (2) 3pm — the only major outage cluster (3 incidents)
|
|
70
|
+
{
|
|
71
|
+
id: 'INC-4001',
|
|
72
|
+
product: 'Ads Manager',
|
|
73
|
+
title:
|
|
74
|
+
'Campaign creation timing out for ~12% of advertisers in EMEA region — root caused to elevated DB write latency',
|
|
75
|
+
severity: 'critical',
|
|
76
|
+
oncall: 'Sarah Chen',
|
|
77
|
+
status: 'resolved',
|
|
78
|
+
startTime: '15:10',
|
|
79
|
+
duration: '2h 15m',
|
|
80
|
+
date: '2025-01-14',
|
|
81
|
+
dayOfWeek: 2,
|
|
82
|
+
hour: 15,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'INC-4002',
|
|
86
|
+
product: 'Graph API',
|
|
87
|
+
title:
|
|
88
|
+
'Elevated 5xx error rates on /me and /me/accounts endpoints affecting third-party integrations globally',
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
oncall: 'David Kim',
|
|
91
|
+
status: 'resolved',
|
|
92
|
+
startTime: '15:30',
|
|
93
|
+
duration: '3h 20m',
|
|
94
|
+
date: '2025-01-14',
|
|
95
|
+
dayOfWeek: 2,
|
|
96
|
+
hour: 15,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
id: 'INC-4003',
|
|
100
|
+
product: 'Instagram API',
|
|
101
|
+
title:
|
|
102
|
+
'Media upload failures for image and video content via /media endpoint affecting ~8% of publishers',
|
|
103
|
+
severity: 'major',
|
|
104
|
+
oncall: 'Priya Sharma',
|
|
105
|
+
status: 'resolved',
|
|
106
|
+
startTime: '15:05',
|
|
107
|
+
duration: '1h 50m',
|
|
108
|
+
date: '2025-01-14',
|
|
109
|
+
dayOfWeek: 2,
|
|
110
|
+
hour: 15,
|
|
111
|
+
},
|
|
112
|
+
// Tue (2) 4pm — spillover (2 incidents)
|
|
113
|
+
{
|
|
114
|
+
id: 'INC-4004',
|
|
115
|
+
product: 'Ads Manager',
|
|
116
|
+
title:
|
|
117
|
+
'Audience insights tab returning empty results due to cache invalidation cascade from earlier outage',
|
|
118
|
+
severity: 'minor',
|
|
119
|
+
oncall: 'Marcus Rivera',
|
|
120
|
+
status: 'resolved',
|
|
121
|
+
startTime: '16:00',
|
|
122
|
+
duration: '45m',
|
|
123
|
+
date: '2025-01-14',
|
|
124
|
+
dayOfWeek: 2,
|
|
125
|
+
hour: 16,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
id: 'INC-4005',
|
|
129
|
+
product: 'WhatsApp Business',
|
|
130
|
+
title:
|
|
131
|
+
'Outbound message delivery delays of 2-5 minutes for high-volume senders due to queue backpressure',
|
|
132
|
+
severity: 'major',
|
|
133
|
+
oncall: 'Elena Volkov',
|
|
134
|
+
status: 'resolved',
|
|
135
|
+
startTime: '16:20',
|
|
136
|
+
duration: '1h 15m',
|
|
137
|
+
date: '2025-01-14',
|
|
138
|
+
dayOfWeek: 2,
|
|
139
|
+
hour: 16,
|
|
140
|
+
},
|
|
141
|
+
// Thu (4) 10am — single incident
|
|
142
|
+
{
|
|
143
|
+
id: 'INC-4006',
|
|
144
|
+
product: 'Commerce Manager',
|
|
145
|
+
title:
|
|
146
|
+
'Product catalog sync failures from Shopify and BigCommerce integrations after schema migration deployed',
|
|
147
|
+
severity: 'major',
|
|
148
|
+
oncall: 'Noah Williams',
|
|
149
|
+
status: 'resolved',
|
|
150
|
+
startTime: '10:15',
|
|
151
|
+
duration: '2h 40m',
|
|
152
|
+
date: '2025-01-16',
|
|
153
|
+
dayOfWeek: 4,
|
|
154
|
+
hour: 10,
|
|
155
|
+
},
|
|
156
|
+
// Fri (5) 4pm — single incident
|
|
157
|
+
{
|
|
158
|
+
id: 'INC-4007',
|
|
159
|
+
product: 'Messenger API',
|
|
160
|
+
title:
|
|
161
|
+
'Webhook delivery failures for subscribed page events — retries succeeded but signaled false alerts',
|
|
162
|
+
severity: 'minor',
|
|
163
|
+
oncall: 'Mei Lin',
|
|
164
|
+
status: 'resolved',
|
|
165
|
+
startTime: '16:30',
|
|
166
|
+
duration: '45m',
|
|
167
|
+
date: '2025-01-17',
|
|
168
|
+
dayOfWeek: 5,
|
|
169
|
+
hour: 16,
|
|
170
|
+
},
|
|
171
|
+
// Active incident
|
|
172
|
+
// Scattered minor blips — light pink cells
|
|
173
|
+
{
|
|
174
|
+
id: 'INC-4009',
|
|
175
|
+
product: 'Pages',
|
|
176
|
+
title:
|
|
177
|
+
'Image upload timeouts for posts larger than 4MB — auto-recovered after CDN region failover',
|
|
178
|
+
severity: 'minor',
|
|
179
|
+
oncall: 'Fatima Al-Rashid',
|
|
180
|
+
status: 'resolved',
|
|
181
|
+
startTime: '11:00',
|
|
182
|
+
duration: '20m',
|
|
183
|
+
date: '2025-01-12',
|
|
184
|
+
dayOfWeek: 0,
|
|
185
|
+
hour: 11,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
id: 'INC-4010',
|
|
189
|
+
product: 'Graph API',
|
|
190
|
+
title:
|
|
191
|
+
'Intermittent 503 errors on a single load balancer in us-east-1 — auto-healed by health check rotation',
|
|
192
|
+
severity: 'minor',
|
|
193
|
+
oncall: 'Lucas Andersson',
|
|
194
|
+
status: 'resolved',
|
|
195
|
+
startTime: '14:30',
|
|
196
|
+
duration: '15m',
|
|
197
|
+
date: '2025-01-13',
|
|
198
|
+
dayOfWeek: 1,
|
|
199
|
+
hour: 14,
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
id: 'INC-4011',
|
|
203
|
+
product: 'WhatsApp Business',
|
|
204
|
+
title:
|
|
205
|
+
'Template message rejections due to stale category cache for newly approved templates',
|
|
206
|
+
severity: 'minor',
|
|
207
|
+
oncall: 'Sofia Garcia',
|
|
208
|
+
status: 'resolved',
|
|
209
|
+
startTime: '13:00',
|
|
210
|
+
duration: '30m',
|
|
211
|
+
date: '2025-01-16',
|
|
212
|
+
dayOfWeek: 4,
|
|
213
|
+
hour: 13,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 'INC-4012',
|
|
217
|
+
product: 'Commerce Manager',
|
|
218
|
+
title:
|
|
219
|
+
'Inventory count mismatch between checkout and catalog services for ~200 SKUs in test region',
|
|
220
|
+
severity: 'minor',
|
|
221
|
+
oncall: 'Raj Kapoor',
|
|
222
|
+
status: 'resolved',
|
|
223
|
+
startTime: '12:00',
|
|
224
|
+
duration: '1h 00m',
|
|
225
|
+
date: '2025-01-17',
|
|
226
|
+
dayOfWeek: 5,
|
|
227
|
+
hour: 12,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 'INC-4013',
|
|
231
|
+
product: 'Instagram API',
|
|
232
|
+
title:
|
|
233
|
+
'Story insights data delayed by 30-60 minutes for accounts with >100K followers',
|
|
234
|
+
severity: 'minor',
|
|
235
|
+
oncall: 'Emma Thompson',
|
|
236
|
+
status: 'resolved',
|
|
237
|
+
startTime: '10:00',
|
|
238
|
+
duration: '45m',
|
|
239
|
+
date: '2025-01-18',
|
|
240
|
+
dayOfWeek: 6,
|
|
241
|
+
hour: 10,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 'INC-4014',
|
|
245
|
+
product: 'Business Suite',
|
|
246
|
+
title:
|
|
247
|
+
'Slow inbox loading times (>3s p95) for accounts with large message histories — index rebuild in progress',
|
|
248
|
+
severity: 'minor',
|
|
249
|
+
oncall: 'Andre Santos',
|
|
250
|
+
status: 'resolved',
|
|
251
|
+
startTime: '15:00',
|
|
252
|
+
duration: '25m',
|
|
253
|
+
date: '2025-01-15',
|
|
254
|
+
dayOfWeek: 3,
|
|
255
|
+
hour: 15,
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
|
|
259
|
+
const DAYS = [
|
|
260
|
+
'Jan 12',
|
|
261
|
+
'Jan 13',
|
|
262
|
+
'Jan 14',
|
|
263
|
+
'Jan 15',
|
|
264
|
+
'Jan 16',
|
|
265
|
+
'Jan 17',
|
|
266
|
+
'Jan 18',
|
|
267
|
+
];
|
|
268
|
+
const HOURS = [
|
|
269
|
+
'9am',
|
|
270
|
+
'10am',
|
|
271
|
+
'11am',
|
|
272
|
+
'12pm',
|
|
273
|
+
'1pm',
|
|
274
|
+
'2pm',
|
|
275
|
+
'3pm',
|
|
276
|
+
'4pm',
|
|
277
|
+
'5pm',
|
|
278
|
+
];
|
|
279
|
+
|
|
280
|
+
function buildHeatmapData(data: IncidentRow[]) {
|
|
281
|
+
return HOURS.flatMap((hour, hi) =>
|
|
282
|
+
DAYS.map((day, di) => {
|
|
283
|
+
const hourValue = 9 + hi;
|
|
284
|
+
const count = data.filter(
|
|
285
|
+
o => o.dayOfWeek === di && o.hour === hourValue,
|
|
286
|
+
).length;
|
|
287
|
+
return {day, hour, incidents: count};
|
|
288
|
+
}),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const statusVariant: Record<string, 'error' | 'warning' | 'info' | 'success'> =
|
|
293
|
+
{
|
|
294
|
+
ongoing: 'error',
|
|
295
|
+
identified: 'warning',
|
|
296
|
+
monitoring: 'info',
|
|
297
|
+
resolved: 'success',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
const columns: TableColumn<IncidentRow>[] = [
|
|
301
|
+
{
|
|
302
|
+
key: 'id',
|
|
303
|
+
header: 'Incident',
|
|
304
|
+
width: pixel(110),
|
|
305
|
+
renderCell: (item: IncidentRow) => (
|
|
306
|
+
<Link href="#/templates/table-page-heatmap-status" isStandalone>
|
|
307
|
+
{item.id}
|
|
308
|
+
</Link>
|
|
309
|
+
),
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
key: 'product',
|
|
313
|
+
header: 'Product',
|
|
314
|
+
width: proportional(2),
|
|
315
|
+
renderCell: (item: IncidentRow) => <Text type="body">{item.product}</Text>,
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
key: 'title',
|
|
319
|
+
header: 'Description',
|
|
320
|
+
width: proportional(5),
|
|
321
|
+
renderCell: (item: IncidentRow) => <Text type="body">{item.title}</Text>,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
key: 'startTime',
|
|
325
|
+
header: 'Started',
|
|
326
|
+
width: pixel(96),
|
|
327
|
+
renderCell: (item: IncidentRow) => (
|
|
328
|
+
<Text type="body" hasTabularNumbers>
|
|
329
|
+
{item.startTime}
|
|
330
|
+
</Text>
|
|
331
|
+
),
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
key: 'duration',
|
|
335
|
+
header: 'Duration',
|
|
336
|
+
width: pixel(100),
|
|
337
|
+
renderCell: (item: IncidentRow) => (
|
|
338
|
+
<Text type="body" hasTabularNumbers>
|
|
339
|
+
{item.duration}
|
|
340
|
+
</Text>
|
|
341
|
+
),
|
|
342
|
+
},
|
|
343
|
+
{
|
|
344
|
+
key: 'oncall',
|
|
345
|
+
header: 'On-call',
|
|
346
|
+
width: proportional(2),
|
|
347
|
+
renderCell: (item: IncidentRow) => (
|
|
348
|
+
<HStack gap={2} vAlign="center">
|
|
349
|
+
<Avatar name={item.oncall} size="sm" />
|
|
350
|
+
<Text type="body">{item.oncall}</Text>
|
|
351
|
+
</HStack>
|
|
352
|
+
),
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
key: 'status',
|
|
356
|
+
header: 'Status',
|
|
357
|
+
width: pixel(120),
|
|
358
|
+
renderCell: (item: IncidentRow) => (
|
|
359
|
+
<Badge
|
|
360
|
+
label={item.status.charAt(0).toUpperCase() + item.status.slice(1)}
|
|
361
|
+
variant={statusVariant[item.status]}
|
|
362
|
+
/>
|
|
363
|
+
),
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
key: 'date',
|
|
367
|
+
header: 'Date',
|
|
368
|
+
width: pixel(110),
|
|
369
|
+
renderCell: (item: IncidentRow) => (
|
|
370
|
+
<Text type="body" hasTabularNumbers>
|
|
371
|
+
{item.date}
|
|
372
|
+
</Text>
|
|
373
|
+
),
|
|
374
|
+
},
|
|
375
|
+
];
|
|
376
|
+
|
|
377
|
+
// ============= HEATMAP (hand SVG, Dracula ramp) =============
|
|
378
|
+
|
|
379
|
+
// Incident-count ramp: quiet days rest on the Dracula surface, busier cells
|
|
380
|
+
// climb green → orange → red.
|
|
381
|
+
function heatFill(count: number): string {
|
|
382
|
+
if (count >= 3) {
|
|
383
|
+
return 'var(--dracula-red)';
|
|
384
|
+
}
|
|
385
|
+
if (count === 2) {
|
|
386
|
+
return 'var(--dracula-orange)';
|
|
387
|
+
}
|
|
388
|
+
if (count === 1) {
|
|
389
|
+
return 'var(--dracula-green)';
|
|
390
|
+
}
|
|
391
|
+
return 'var(--dracula-bg-light)';
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const HEAT_LEGEND = [
|
|
395
|
+
{label: '0', fill: 'var(--dracula-bg-light)'},
|
|
396
|
+
{label: '1', fill: 'var(--dracula-green)'},
|
|
397
|
+
{label: '2', fill: 'var(--dracula-orange)'},
|
|
398
|
+
{label: '3+', fill: 'var(--dracula-red)'},
|
|
399
|
+
];
|
|
400
|
+
|
|
401
|
+
function OutageHeatmap() {
|
|
402
|
+
const heatmapData = buildHeatmapData(incidents);
|
|
403
|
+
const cellW = 56;
|
|
404
|
+
const cellH = 24;
|
|
405
|
+
const gap = 4;
|
|
406
|
+
const labelW = 44;
|
|
407
|
+
const labelH = 22;
|
|
408
|
+
const W = labelW + DAYS.length * (cellW + gap);
|
|
409
|
+
const H = labelH + HOURS.length * (cellH + gap);
|
|
410
|
+
const countFor = (day: string, hour: string) =>
|
|
411
|
+
heatmapData.find(d => d.day === day && d.hour === hour)?.incidents ?? 0;
|
|
412
|
+
return (
|
|
413
|
+
<VStack gap={3}>
|
|
414
|
+
<Card
|
|
415
|
+
padding={3}
|
|
416
|
+
style={{
|
|
417
|
+
backgroundColor: 'var(--color-background)',
|
|
418
|
+
border: 'var(--border-width) solid var(--color-separator)',
|
|
419
|
+
}}>
|
|
420
|
+
<svg
|
|
421
|
+
viewBox={`0 0 ${W} ${H}`}
|
|
422
|
+
width="100%"
|
|
423
|
+
role="img"
|
|
424
|
+
aria-label="Incidents by day and hour, January 12 to 18">
|
|
425
|
+
{DAYS.map((day, di) => (
|
|
426
|
+
<text
|
|
427
|
+
key={day}
|
|
428
|
+
x={labelW + di * (cellW + gap) + cellW / 2}
|
|
429
|
+
y={14}
|
|
430
|
+
textAnchor="middle"
|
|
431
|
+
fontSize={9}
|
|
432
|
+
fill="var(--color-text-paragraph)"
|
|
433
|
+
fontFamily="var(--font-family-mono)">
|
|
434
|
+
{day}
|
|
435
|
+
</text>
|
|
436
|
+
))}
|
|
437
|
+
{HOURS.map((hour, hi) => (
|
|
438
|
+
<g key={hour}>
|
|
439
|
+
<text
|
|
440
|
+
x={labelW - 6}
|
|
441
|
+
y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
|
|
442
|
+
textAnchor="end"
|
|
443
|
+
fontSize={9}
|
|
444
|
+
fill="var(--color-text-paragraph)"
|
|
445
|
+
fontFamily="var(--font-family-mono)">
|
|
446
|
+
{hour}
|
|
447
|
+
</text>
|
|
448
|
+
{DAYS.map((day, di) => {
|
|
449
|
+
const count = countFor(day, hour);
|
|
450
|
+
return (
|
|
451
|
+
<g key={day}>
|
|
452
|
+
<rect
|
|
453
|
+
x={labelW + di * (cellW + gap)}
|
|
454
|
+
y={labelH + hi * (cellH + gap)}
|
|
455
|
+
width={cellW}
|
|
456
|
+
height={cellH}
|
|
457
|
+
rx={4}
|
|
458
|
+
fill={heatFill(count)}
|
|
459
|
+
/>
|
|
460
|
+
{count > 0 && (
|
|
461
|
+
<text
|
|
462
|
+
x={labelW + di * (cellW + gap) + cellW / 2}
|
|
463
|
+
y={labelH + hi * (cellH + gap) + cellH / 2 + 3}
|
|
464
|
+
textAnchor="middle"
|
|
465
|
+
fontSize={9}
|
|
466
|
+
fill="var(--dracula-bg-dark)"
|
|
467
|
+
fontFamily="var(--font-family-mono)">
|
|
468
|
+
{count}
|
|
469
|
+
</text>
|
|
470
|
+
)}
|
|
471
|
+
</g>
|
|
472
|
+
);
|
|
473
|
+
})}
|
|
474
|
+
</g>
|
|
475
|
+
))}
|
|
476
|
+
</svg>
|
|
477
|
+
</Card>
|
|
478
|
+
<Text type="supporting" color="secondary">
|
|
479
|
+
Incidents by day and hour · Jan 12–18
|
|
480
|
+
</Text>
|
|
481
|
+
<HStack gap={4} vAlign="center">
|
|
482
|
+
{HEAT_LEGEND.map(entry => (
|
|
483
|
+
<HStack key={entry.label} gap={2} vAlign="center">
|
|
484
|
+
<svg
|
|
485
|
+
width={12}
|
|
486
|
+
height={12}
|
|
487
|
+
role="img"
|
|
488
|
+
aria-label={`${entry.label} incidents`}>
|
|
489
|
+
<rect width={12} height={12} rx={4} fill={entry.fill} />
|
|
490
|
+
</svg>
|
|
491
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
492
|
+
{entry.label}
|
|
493
|
+
</Text>
|
|
494
|
+
</HStack>
|
|
495
|
+
))}
|
|
496
|
+
</HStack>
|
|
497
|
+
</VStack>
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// ============= PAGE =============
|
|
502
|
+
|
|
503
|
+
export default function HeatmapTable() {
|
|
504
|
+
return (
|
|
505
|
+
<Layout
|
|
506
|
+
height="fill"
|
|
507
|
+
header={
|
|
508
|
+
<LayoutHeader hasDivider>
|
|
509
|
+
<HStack gap={2} vAlign="center">
|
|
510
|
+
<StackItem size="fill">
|
|
511
|
+
<Heading level={1}>Status</Heading>
|
|
512
|
+
</StackItem>
|
|
513
|
+
<IconButton
|
|
514
|
+
label="Filter"
|
|
515
|
+
icon={<Icon icon={Filter} size="sm" />}
|
|
516
|
+
variant="ghost"
|
|
517
|
+
tooltip="Filter"
|
|
518
|
+
/>
|
|
519
|
+
<IconButton
|
|
520
|
+
label="Export"
|
|
521
|
+
icon={<Icon icon={Download} size="sm" />}
|
|
522
|
+
variant="ghost"
|
|
523
|
+
tooltip="Export"
|
|
524
|
+
/>
|
|
525
|
+
<Button label="Refresh" icon={<Icon icon={RotateCw} size="sm" />} />
|
|
526
|
+
</HStack>
|
|
527
|
+
</LayoutHeader>
|
|
528
|
+
}
|
|
529
|
+
content={
|
|
530
|
+
<LayoutContent padding={3}>
|
|
531
|
+
<VStack gap={4}>
|
|
532
|
+
<OutageHeatmap />
|
|
533
|
+
|
|
534
|
+
<Table<IncidentRow>
|
|
535
|
+
data={incidents}
|
|
536
|
+
columns={columns}
|
|
537
|
+
idKey="id"
|
|
538
|
+
density="balanced"
|
|
539
|
+
dividers="rows"
|
|
540
|
+
hasHover
|
|
541
|
+
/>
|
|
542
|
+
</VStack>
|
|
543
|
+
</LayoutContent>
|
|
544
|
+
}
|
|
545
|
+
/>
|
|
546
|
+
);
|
|
547
|
+
}
|