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,584 @@
|
|
|
1
|
+
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Incident Console — an on-call incident response tool for the night watch.
|
|
5
|
+
*
|
|
6
|
+
* Frame-first layout (see `npx astryx docs layout`), distilled from
|
|
7
|
+
* product-scale apps built with the design system:
|
|
8
|
+
*
|
|
9
|
+
* Frame: header | grouped incident rows (fill) | inspector 380 (resizable)
|
|
10
|
+
*
|
|
11
|
+
* Responsive contract:
|
|
12
|
+
* > 1024px header | rows | inspector 380
|
|
13
|
+
* <= 1024px inspector hidden; rows keep full width
|
|
14
|
+
*
|
|
15
|
+
* Container policy (tracker archetype): dense data renders as rows —
|
|
16
|
+
* edge-to-edge lists grouped by status, zero cards. Status is carried by
|
|
17
|
+
* StatusDot (severity) and Token (state), not decorative badges.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {useMemo, useState, type CSSProperties} from 'react';
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
HStack,
|
|
24
|
+
Layout,
|
|
25
|
+
LayoutContent,
|
|
26
|
+
LayoutHeader,
|
|
27
|
+
StackItem,
|
|
28
|
+
VStack,
|
|
29
|
+
} from '@astryxdesign/core/Layout';
|
|
30
|
+
import {ResizeHandle, useResizable} from '@astryxdesign/core/Resizable';
|
|
31
|
+
import {LayoutPanel} from '@astryxdesign/core/Layout';
|
|
32
|
+
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
33
|
+
import {Button} from '@astryxdesign/core/Button';
|
|
34
|
+
import {Icon} from '@astryxdesign/core/Icon';
|
|
35
|
+
import {Divider} from '@astryxdesign/core/Divider';
|
|
36
|
+
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
37
|
+
import {List, ListItem} from '@astryxdesign/core/List';
|
|
38
|
+
import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
|
|
39
|
+
import {
|
|
40
|
+
PowerSearch,
|
|
41
|
+
usePowerSearchConfig,
|
|
42
|
+
} from '@astryxdesign/core/PowerSearch';
|
|
43
|
+
import type {PowerSearchFilter} from '@astryxdesign/core/PowerSearch';
|
|
44
|
+
import {
|
|
45
|
+
SegmentedControl,
|
|
46
|
+
SegmentedControlItem,
|
|
47
|
+
} from '@astryxdesign/core/SegmentedControl';
|
|
48
|
+
import {StatusDot} from '@astryxdesign/core/StatusDot';
|
|
49
|
+
import {Timestamp} from '@astryxdesign/core/Timestamp';
|
|
50
|
+
import {Token} from '@astryxdesign/core/Token';
|
|
51
|
+
import {useMediaQuery} from '@astryxdesign/core/hooks';
|
|
52
|
+
|
|
53
|
+
// ============= ICONS (verified lucide-react exports) =============
|
|
54
|
+
// BellRing ← BellAlertIcon, Plus ← PlusIcon.
|
|
55
|
+
import {BellRing, Plus} from 'lucide-react';
|
|
56
|
+
|
|
57
|
+
const styles: Record<string, CSSProperties> = {
|
|
58
|
+
contentFill: {
|
|
59
|
+
height: '100%',
|
|
60
|
+
minHeight: 0,
|
|
61
|
+
},
|
|
62
|
+
searchRow: {
|
|
63
|
+
padding: 'var(--spacing-3)',
|
|
64
|
+
},
|
|
65
|
+
groupHeader: {
|
|
66
|
+
padding: 'var(--spacing-2) var(--spacing-3)',
|
|
67
|
+
backgroundColor: 'var(--color-background-muted)',
|
|
68
|
+
},
|
|
69
|
+
rows: {
|
|
70
|
+
overflowY: 'auto',
|
|
71
|
+
minHeight: 0,
|
|
72
|
+
},
|
|
73
|
+
inspector: {
|
|
74
|
+
padding: 'var(--spacing-4)',
|
|
75
|
+
height: '100%',
|
|
76
|
+
overflowY: 'auto',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type Severity = 'sev1' | 'sev2' | 'sev3';
|
|
81
|
+
type Status = 'investigating' | 'mitigated' | 'resolved';
|
|
82
|
+
|
|
83
|
+
interface TimelineEvent {
|
|
84
|
+
at: string;
|
|
85
|
+
label: string;
|
|
86
|
+
detail: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
interface Incident extends Record<string, unknown> {
|
|
90
|
+
id: string;
|
|
91
|
+
title: string;
|
|
92
|
+
service: string;
|
|
93
|
+
severity: Severity;
|
|
94
|
+
status: Status;
|
|
95
|
+
commander: string;
|
|
96
|
+
startedAt: string;
|
|
97
|
+
impact: string;
|
|
98
|
+
timeline: TimelineEvent[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Deterministic fixtures: fixed ISO timestamps, no clocks, no randomness.
|
|
102
|
+
const INCIDENTS: Incident[] = [
|
|
103
|
+
{
|
|
104
|
+
id: 'INC-2417',
|
|
105
|
+
title: 'Checkout API elevated 5xx rate',
|
|
106
|
+
service: 'checkout-api',
|
|
107
|
+
severity: 'sev1',
|
|
108
|
+
status: 'investigating',
|
|
109
|
+
commander: 'Priya Raman',
|
|
110
|
+
startedAt: '2026-06-30T21:14:00Z',
|
|
111
|
+
impact: '12% of checkout requests failing in us-east',
|
|
112
|
+
timeline: [
|
|
113
|
+
{
|
|
114
|
+
at: '2026-06-30T21:14:00Z',
|
|
115
|
+
label: 'Incident declared',
|
|
116
|
+
detail: 'Paging monitor: checkout-api 5xx > 5% for 10m',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
at: '2026-06-30T21:19:00Z',
|
|
120
|
+
label: 'Commander assigned',
|
|
121
|
+
detail: 'Priya Raman took command',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
at: '2026-06-30T21:31:00Z',
|
|
125
|
+
label: 'Suspect identified',
|
|
126
|
+
detail: 'Deploy 8f31c2 correlates with error onset',
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
id: 'INC-2416',
|
|
132
|
+
title: 'Search indexing lag above 30 minutes',
|
|
133
|
+
service: 'search-indexer',
|
|
134
|
+
severity: 'sev2',
|
|
135
|
+
status: 'investigating',
|
|
136
|
+
commander: 'Marcus Webb',
|
|
137
|
+
startedAt: '2026-06-30T19:42:00Z',
|
|
138
|
+
impact: 'New listings not searchable; stale results served',
|
|
139
|
+
timeline: [
|
|
140
|
+
{
|
|
141
|
+
at: '2026-06-30T19:42:00Z',
|
|
142
|
+
label: 'Incident declared',
|
|
143
|
+
detail: 'Indexing lag alarm crossed 30m threshold',
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
at: '2026-06-30T20:05:00Z',
|
|
147
|
+
label: 'Mitigation attempted',
|
|
148
|
+
detail: 'Doubled indexer worker pool; lag still climbing',
|
|
149
|
+
},
|
|
150
|
+
],
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
id: 'INC-2415',
|
|
154
|
+
title: 'Webhook delivery retries exhausting queue',
|
|
155
|
+
service: 'webhooks',
|
|
156
|
+
severity: 'sev3',
|
|
157
|
+
status: 'investigating',
|
|
158
|
+
commander: 'Ana Duarte',
|
|
159
|
+
startedAt: '2026-06-30T18:20:00Z',
|
|
160
|
+
impact: 'Third-party webhook delivery delayed up to 15m',
|
|
161
|
+
timeline: [
|
|
162
|
+
{
|
|
163
|
+
at: '2026-06-30T18:20:00Z',
|
|
164
|
+
label: 'Incident declared',
|
|
165
|
+
detail: 'Queue depth alarm on webhook-delivery',
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'INC-2414',
|
|
171
|
+
title: 'Payments settlement job stalled',
|
|
172
|
+
service: 'payments',
|
|
173
|
+
severity: 'sev1',
|
|
174
|
+
status: 'mitigated',
|
|
175
|
+
commander: 'Priya Raman',
|
|
176
|
+
startedAt: '2026-06-30T14:03:00Z',
|
|
177
|
+
impact: 'Settlement batch delayed; no customer-visible impact',
|
|
178
|
+
timeline: [
|
|
179
|
+
{
|
|
180
|
+
at: '2026-06-30T14:03:00Z',
|
|
181
|
+
label: 'Incident declared',
|
|
182
|
+
detail: 'Settlement job heartbeat missed twice',
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
at: '2026-06-30T14:26:00Z',
|
|
186
|
+
label: 'Mitigated',
|
|
187
|
+
detail: 'Job restarted on standby worker; batch draining',
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
id: 'INC-2413',
|
|
193
|
+
title: 'CDN cache hit ratio degraded in eu-west',
|
|
194
|
+
service: 'edge-cdn',
|
|
195
|
+
severity: 'sev2',
|
|
196
|
+
status: 'mitigated',
|
|
197
|
+
commander: 'Tom Okafor',
|
|
198
|
+
startedAt: '2026-06-30T11:47:00Z',
|
|
199
|
+
impact: 'Origin load 3x baseline; p95 latency +180ms in EU',
|
|
200
|
+
timeline: [
|
|
201
|
+
{
|
|
202
|
+
at: '2026-06-30T11:47:00Z',
|
|
203
|
+
label: 'Incident declared',
|
|
204
|
+
detail: 'Cache hit ratio dropped below 80%',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
at: '2026-06-30T12:12:00Z',
|
|
208
|
+
label: 'Mitigated',
|
|
209
|
+
detail: 'Rolled back cache-key config change',
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'INC-2412',
|
|
215
|
+
title: 'Login rate limiting misfiring for SSO users',
|
|
216
|
+
service: 'auth',
|
|
217
|
+
severity: 'sev2',
|
|
218
|
+
status: 'resolved',
|
|
219
|
+
commander: 'Ana Duarte',
|
|
220
|
+
startedAt: '2026-06-29T22:31:00Z',
|
|
221
|
+
impact: 'SSO users intermittently blocked at login',
|
|
222
|
+
timeline: [
|
|
223
|
+
{
|
|
224
|
+
at: '2026-06-29T22:31:00Z',
|
|
225
|
+
label: 'Incident declared',
|
|
226
|
+
detail: 'Support surge: SSO login failures',
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
at: '2026-06-29T23:02:00Z',
|
|
230
|
+
label: 'Mitigated',
|
|
231
|
+
detail: 'Raised limiter threshold for SSO IdP ranges',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
at: '2026-06-30T09:15:00Z',
|
|
235
|
+
label: 'Resolved',
|
|
236
|
+
detail: 'Limiter keying fixed to per-user; monitors green 8h',
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
id: 'INC-2411',
|
|
242
|
+
title: 'Notification digests sent twice',
|
|
243
|
+
service: 'notifications',
|
|
244
|
+
severity: 'sev3',
|
|
245
|
+
status: 'resolved',
|
|
246
|
+
commander: 'Marcus Webb',
|
|
247
|
+
startedAt: '2026-06-29T08:12:00Z',
|
|
248
|
+
impact: 'Duplicate daily digest for ~40k users',
|
|
249
|
+
timeline: [
|
|
250
|
+
{
|
|
251
|
+
at: '2026-06-29T08:12:00Z',
|
|
252
|
+
label: 'Incident declared',
|
|
253
|
+
detail: 'Duplicate-send reports from support',
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
at: '2026-06-29T10:40:00Z',
|
|
257
|
+
label: 'Resolved',
|
|
258
|
+
detail: 'Dedup key restored in scheduler; backfill verified',
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
},
|
|
262
|
+
];
|
|
263
|
+
|
|
264
|
+
const STATUS_ORDER: Status[] = ['investigating', 'mitigated', 'resolved'];
|
|
265
|
+
|
|
266
|
+
const STATUS_LABEL: Record<Status, string> = {
|
|
267
|
+
investigating: 'Investigating',
|
|
268
|
+
mitigated: 'Mitigated',
|
|
269
|
+
resolved: 'Resolved',
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const STATUS_TOKEN_COLOR: Record<Status, 'red' | 'yellow' | 'green'> = {
|
|
273
|
+
investigating: 'red',
|
|
274
|
+
mitigated: 'yellow',
|
|
275
|
+
resolved: 'green',
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const SEVERITY_DOT: Record<Severity, 'error' | 'warning' | 'neutral'> = {
|
|
279
|
+
sev1: 'error',
|
|
280
|
+
sev2: 'warning',
|
|
281
|
+
sev3: 'neutral',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const SERVICE_VALUES = [
|
|
285
|
+
{value: 'checkout-api', label: 'checkout-api'},
|
|
286
|
+
{value: 'search-indexer', label: 'search-indexer'},
|
|
287
|
+
{value: 'webhooks', label: 'webhooks'},
|
|
288
|
+
{value: 'payments', label: 'payments'},
|
|
289
|
+
{value: 'edge-cdn', label: 'edge-cdn'},
|
|
290
|
+
{value: 'auth', label: 'auth'},
|
|
291
|
+
{value: 'notifications', label: 'notifications'},
|
|
292
|
+
];
|
|
293
|
+
|
|
294
|
+
const SEVERITY_VALUES = [
|
|
295
|
+
{value: 'sev1', label: 'SEV1'},
|
|
296
|
+
{value: 'sev2', label: 'SEV2'},
|
|
297
|
+
{value: 'sev3', label: 'SEV3'},
|
|
298
|
+
];
|
|
299
|
+
|
|
300
|
+
const fieldDefs = [
|
|
301
|
+
{key: 'title', type: 'string', label: 'Title'},
|
|
302
|
+
{key: 'service', type: 'enum', label: 'Service', enumValues: SERVICE_VALUES},
|
|
303
|
+
{
|
|
304
|
+
key: 'severity',
|
|
305
|
+
type: 'enum',
|
|
306
|
+
label: 'Severity',
|
|
307
|
+
enumValues: SEVERITY_VALUES,
|
|
308
|
+
},
|
|
309
|
+
{key: 'commander', type: 'string', label: 'Commander'},
|
|
310
|
+
] as const;
|
|
311
|
+
|
|
312
|
+
function IncidentRows({
|
|
313
|
+
incidents,
|
|
314
|
+
selectedId,
|
|
315
|
+
onSelect,
|
|
316
|
+
}: {
|
|
317
|
+
incidents: Incident[];
|
|
318
|
+
selectedId: string | null;
|
|
319
|
+
onSelect: (id: string) => void;
|
|
320
|
+
}) {
|
|
321
|
+
const groups = STATUS_ORDER.map(status => ({
|
|
322
|
+
status,
|
|
323
|
+
items: incidents.filter(incident => incident.status === status),
|
|
324
|
+
})).filter(group => group.items.length > 0);
|
|
325
|
+
|
|
326
|
+
if (groups.length === 0) {
|
|
327
|
+
return (
|
|
328
|
+
<EmptyState
|
|
329
|
+
title="Quiet in the crypt"
|
|
330
|
+
description="No incidents match — adjust the status filter or clear search filters."
|
|
331
|
+
icon={<Icon icon={BellRing} size="lg" />}
|
|
332
|
+
/>
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<VStack gap={0}>
|
|
338
|
+
{groups.map(group => (
|
|
339
|
+
<VStack gap={0} key={group.status}>
|
|
340
|
+
<HStack gap={2} vAlign="center" style={styles.groupHeader}>
|
|
341
|
+
<Text type="label" color="secondary">
|
|
342
|
+
{STATUS_LABEL[group.status]}
|
|
343
|
+
</Text>
|
|
344
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
345
|
+
{group.items.length}
|
|
346
|
+
</Text>
|
|
347
|
+
</HStack>
|
|
348
|
+
<List density="compact" hasDividers>
|
|
349
|
+
{group.items.map(incident => (
|
|
350
|
+
<ListItem
|
|
351
|
+
key={incident.id}
|
|
352
|
+
label={incident.title}
|
|
353
|
+
description={`${incident.id} · ${incident.service} · ${incident.impact}`}
|
|
354
|
+
startContent={
|
|
355
|
+
<StatusDot
|
|
356
|
+
variant={SEVERITY_DOT[incident.severity]}
|
|
357
|
+
label={incident.severity.toUpperCase()}
|
|
358
|
+
isPulsing={
|
|
359
|
+
incident.severity === 'sev1' &&
|
|
360
|
+
incident.status === 'investigating'
|
|
361
|
+
}
|
|
362
|
+
/>
|
|
363
|
+
}
|
|
364
|
+
endContent={
|
|
365
|
+
<HStack gap={3} vAlign="center">
|
|
366
|
+
<Token
|
|
367
|
+
size="sm"
|
|
368
|
+
color={STATUS_TOKEN_COLOR[incident.status]}
|
|
369
|
+
label={STATUS_LABEL[incident.status]}
|
|
370
|
+
/>
|
|
371
|
+
<Timestamp
|
|
372
|
+
value={incident.startedAt}
|
|
373
|
+
format="relative"
|
|
374
|
+
color="secondary"
|
|
375
|
+
/>
|
|
376
|
+
</HStack>
|
|
377
|
+
}
|
|
378
|
+
onClick={() => onSelect(incident.id)}
|
|
379
|
+
isSelected={incident.id === selectedId}
|
|
380
|
+
/>
|
|
381
|
+
))}
|
|
382
|
+
</List>
|
|
383
|
+
</VStack>
|
|
384
|
+
))}
|
|
385
|
+
</VStack>
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function IncidentInspector({incident}: {incident: Incident}) {
|
|
390
|
+
const nextAction =
|
|
391
|
+
incident.status === 'investigating'
|
|
392
|
+
? 'Mark mitigated'
|
|
393
|
+
: incident.status === 'mitigated'
|
|
394
|
+
? 'Resolve'
|
|
395
|
+
: 'Reopen';
|
|
396
|
+
|
|
397
|
+
return (
|
|
398
|
+
<VStack gap={4} style={styles.inspector}>
|
|
399
|
+
<VStack gap={2}>
|
|
400
|
+
<HStack gap={2} vAlign="center">
|
|
401
|
+
<StatusDot
|
|
402
|
+
variant={SEVERITY_DOT[incident.severity]}
|
|
403
|
+
label={incident.severity.toUpperCase()}
|
|
404
|
+
/>
|
|
405
|
+
<Text type="supporting" color="secondary">
|
|
406
|
+
{incident.id}
|
|
407
|
+
</Text>
|
|
408
|
+
<Token
|
|
409
|
+
size="sm"
|
|
410
|
+
color={STATUS_TOKEN_COLOR[incident.status]}
|
|
411
|
+
label={STATUS_LABEL[incident.status]}
|
|
412
|
+
/>
|
|
413
|
+
</HStack>
|
|
414
|
+
<Heading level={2}>{incident.title}</Heading>
|
|
415
|
+
</VStack>
|
|
416
|
+
|
|
417
|
+
<HStack gap={2}>
|
|
418
|
+
<Button label={nextAction} size="sm" />
|
|
419
|
+
<Button label="Escalate" variant="secondary" size="sm" />
|
|
420
|
+
</HStack>
|
|
421
|
+
|
|
422
|
+
<Divider />
|
|
423
|
+
|
|
424
|
+
<MetadataList columns="single" label={{position: 'start', width: 96}}>
|
|
425
|
+
<MetadataListItem label="Service">
|
|
426
|
+
<Text type="body">{incident.service}</Text>
|
|
427
|
+
</MetadataListItem>
|
|
428
|
+
<MetadataListItem label="Severity">
|
|
429
|
+
<Text type="body">{incident.severity.toUpperCase()}</Text>
|
|
430
|
+
</MetadataListItem>
|
|
431
|
+
<MetadataListItem label="Commander">
|
|
432
|
+
<Text type="body">{incident.commander}</Text>
|
|
433
|
+
</MetadataListItem>
|
|
434
|
+
<MetadataListItem label="Started">
|
|
435
|
+
<Timestamp value={incident.startedAt} format="date_time" />
|
|
436
|
+
</MetadataListItem>
|
|
437
|
+
<MetadataListItem label="Impact">
|
|
438
|
+
<Text type="body">{incident.impact}</Text>
|
|
439
|
+
</MetadataListItem>
|
|
440
|
+
</MetadataList>
|
|
441
|
+
|
|
442
|
+
<Divider />
|
|
443
|
+
|
|
444
|
+
<VStack gap={2}>
|
|
445
|
+
<Heading level={3}>Timeline</Heading>
|
|
446
|
+
<List density="compact">
|
|
447
|
+
{incident.timeline.map(event => (
|
|
448
|
+
<ListItem
|
|
449
|
+
key={`${event.at}-${event.label}`}
|
|
450
|
+
label={event.label}
|
|
451
|
+
description={event.detail}
|
|
452
|
+
endContent={
|
|
453
|
+
<Timestamp value={event.at} format="time" color="secondary" />
|
|
454
|
+
}
|
|
455
|
+
/>
|
|
456
|
+
))}
|
|
457
|
+
</List>
|
|
458
|
+
</VStack>
|
|
459
|
+
</VStack>
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export default function IncidentConsole() {
|
|
464
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
465
|
+
const [filters, setFilters] = useState<PowerSearchFilter[]>([]);
|
|
466
|
+
const [selectedId, setSelectedId] = useState<string | null>(INCIDENTS[0].id);
|
|
467
|
+
const {config, applyFilters} = usePowerSearchConfig(fieldDefs, 'Incidents');
|
|
468
|
+
|
|
469
|
+
// Responsive contract: the inspector is hidden at <= 1024px so the row
|
|
470
|
+
// list keeps its full width (never compress dense rows).
|
|
471
|
+
const isNarrow = useMediaQuery('(max-width: 1024px)');
|
|
472
|
+
|
|
473
|
+
const inspectorPanel = useResizable({
|
|
474
|
+
defaultSize: 380,
|
|
475
|
+
minSizePx: 320,
|
|
476
|
+
maxSizePx: 480,
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const visible = useMemo(() => {
|
|
480
|
+
const searched = applyFilters(filters, INCIDENTS);
|
|
481
|
+
return statusFilter === 'all'
|
|
482
|
+
? searched
|
|
483
|
+
: searched.filter(incident => incident.status === statusFilter);
|
|
484
|
+
}, [filters, applyFilters, statusFilter]);
|
|
485
|
+
|
|
486
|
+
const selected = visible.find(incident => incident.id === selectedId) ?? null;
|
|
487
|
+
|
|
488
|
+
const openCount = INCIDENTS.filter(
|
|
489
|
+
incident => incident.status === 'investigating',
|
|
490
|
+
).length;
|
|
491
|
+
|
|
492
|
+
return (
|
|
493
|
+
<Layout
|
|
494
|
+
height="fill"
|
|
495
|
+
header={
|
|
496
|
+
<LayoutHeader hasDivider>
|
|
497
|
+
<HStack gap={3} vAlign="center">
|
|
498
|
+
<StackItem size="fill">
|
|
499
|
+
<HStack gap={2} vAlign="center">
|
|
500
|
+
<Heading level={1}>Night watch</Heading>
|
|
501
|
+
<Text
|
|
502
|
+
type="supporting"
|
|
503
|
+
color="secondary"
|
|
504
|
+
hasTabularNumbers>
|
|
505
|
+
{openCount} investigating
|
|
506
|
+
</Text>
|
|
507
|
+
</HStack>
|
|
508
|
+
</StackItem>
|
|
509
|
+
<SegmentedControl
|
|
510
|
+
label="Filter by status"
|
|
511
|
+
value={statusFilter}
|
|
512
|
+
onChange={setStatusFilter}
|
|
513
|
+
size="sm">
|
|
514
|
+
<SegmentedControlItem label="All" value="all" />
|
|
515
|
+
<SegmentedControlItem
|
|
516
|
+
label="Investigating"
|
|
517
|
+
value="investigating"
|
|
518
|
+
/>
|
|
519
|
+
<SegmentedControlItem label="Mitigated" value="mitigated" />
|
|
520
|
+
<SegmentedControlItem label="Resolved" value="resolved" />
|
|
521
|
+
</SegmentedControl>
|
|
522
|
+
<Button
|
|
523
|
+
label="Declare incident"
|
|
524
|
+
icon={<Icon icon={Plus} size="sm" />}
|
|
525
|
+
size="sm"
|
|
526
|
+
/>
|
|
527
|
+
</HStack>
|
|
528
|
+
</LayoutHeader>
|
|
529
|
+
}
|
|
530
|
+
content={
|
|
531
|
+
<LayoutContent padding={0}>
|
|
532
|
+
<VStack gap={0} style={styles.contentFill}>
|
|
533
|
+
<HStack style={styles.searchRow}>
|
|
534
|
+
<StackItem size="fill">
|
|
535
|
+
<PowerSearch
|
|
536
|
+
config={config}
|
|
537
|
+
filters={filters}
|
|
538
|
+
onChange={newFilters => setFilters([...newFilters])}
|
|
539
|
+
placeholder="Search the night watch..."
|
|
540
|
+
resultCount={visible.length}
|
|
541
|
+
/>
|
|
542
|
+
</StackItem>
|
|
543
|
+
</HStack>
|
|
544
|
+
<StackItem size="fill" style={styles.rows}>
|
|
545
|
+
<IncidentRows
|
|
546
|
+
incidents={visible}
|
|
547
|
+
selectedId={selected?.id ?? null}
|
|
548
|
+
onSelect={setSelectedId}
|
|
549
|
+
/>
|
|
550
|
+
</StackItem>
|
|
551
|
+
</VStack>
|
|
552
|
+
</LayoutContent>
|
|
553
|
+
}
|
|
554
|
+
end={
|
|
555
|
+
isNarrow ? undefined : (
|
|
556
|
+
<>
|
|
557
|
+
<ResizeHandle
|
|
558
|
+
direction="horizontal"
|
|
559
|
+
hasDivider
|
|
560
|
+
isAlwaysVisible={false}
|
|
561
|
+
resizable={inspectorPanel.props}
|
|
562
|
+
label="Resize inspector"
|
|
563
|
+
/>
|
|
564
|
+
<LayoutPanel
|
|
565
|
+
width={inspectorPanel.size}
|
|
566
|
+
padding={0}
|
|
567
|
+
label="Incident details">
|
|
568
|
+
{selected ? (
|
|
569
|
+
<IncidentInspector incident={selected} />
|
|
570
|
+
) : (
|
|
571
|
+
<EmptyState
|
|
572
|
+
title="No incident under the lens"
|
|
573
|
+
description="Select an incident from the rows to inspect its timeline."
|
|
574
|
+
icon={<Icon icon={BellRing} size="lg" />}
|
|
575
|
+
isCompact
|
|
576
|
+
/>
|
|
577
|
+
)}
|
|
578
|
+
</LayoutPanel>
|
|
579
|
+
</>
|
|
580
|
+
)
|
|
581
|
+
}
|
|
582
|
+
/>
|
|
583
|
+
);
|
|
584
|
+
}
|