astryx-dracula 0.5.0 → 0.6.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/BRAND.md +3 -3
- package/README.md +7 -4
- package/USAGE.md +49 -49
- package/astryx-dracula.d.ts +2 -2
- package/astryx-dracula.js +25 -9
- package/astryx-dracula.variants.d.ts +2 -2
- package/astryx.integration.mjs +1 -1
- package/icons.mjs +21 -17
- package/icons.tsx +69 -62
- package/package.json +125 -120
- package/shared/auth-chrome-config.ts +6 -6
- package/shared/auth-chrome.tsx +143 -144
- package/shared/auth-copy.ts +14 -14
- package/shared/chaptered-doc-config.ts +48 -51
- package/shared/chaptered-doc.tsx +267 -289
- package/shared/chart-hues.ts +25 -10
- package/shared/chart-labels.tsx +31 -30
- package/shared/chart-legend.tsx +55 -56
- package/shared/chart-panel-style.ts +3 -3
- package/shared/data-bar.tsx +97 -102
- package/shared/format-money.ts +1 -1
- package/shared/gallery-image.ts +6 -6
- package/shared/login-demo.ts +1 -1
- package/shared/metric-card.tsx +101 -105
- package/shared/metric-delta.tsx +28 -27
- package/shared/order-desk.tsx +205 -216
- package/shared/revenue-chart.tsx +145 -165
- package/shared/scene-castle.tsx +206 -291
- package/shared/scene-frame.tsx +33 -32
- package/shared/scene-hues.ts +17 -17
- package/shared/scene-tile.tsx +88 -92
- package/shared/settings-data.ts +119 -125
- package/shared/settings-rows.tsx +122 -159
- package/shared/sparkline.tsx +75 -88
- package/shared/sso-icons.tsx +10 -22
- package/shared/stars.tsx +16 -25
- package/templates/ai-chat-landing.template.mjs +5 -4
- package/templates/ai-chat-landing.tsx +473 -460
- package/templates/ai-chat.template.mjs +5 -4
- package/templates/ai-chat.tsx +543 -612
- package/templates/blank.template.mjs +5 -4
- package/templates/blank.tsx +18 -14
- package/templates/centered-hero.template.mjs +5 -4
- package/templates/centered-hero.tsx +55 -68
- package/templates/classic-gallery.template.mjs +5 -4
- package/templates/classic-gallery.tsx +97 -104
- package/templates/contact-form.template.mjs +5 -4
- package/templates/contact-form.tsx +247 -282
- package/templates/dashboard-portfolio.template.mjs +5 -4
- package/templates/dashboard-portfolio.tsx +717 -548
- package/templates/dashboard.template.mjs +5 -4
- package/templates/dashboard.tsx +596 -567
- package/templates/detail-page.template.mjs +5 -4
- package/templates/detail-page.tsx +546 -603
- package/templates/documentation-design.template.mjs +5 -4
- package/templates/documentation-design.tsx +231 -243
- package/templates/documentation-technical.template.mjs +5 -4
- package/templates/documentation-technical.tsx +236 -239
- package/templates/documentation.template.mjs +5 -4
- package/templates/documentation.tsx +267 -279
- package/templates/editor.template.mjs +5 -4
- package/templates/editor.tsx +844 -877
- package/templates/file-explorer.template.mjs +5 -4
- package/templates/file-explorer.tsx +566 -590
- package/templates/form-two-column.template.mjs +5 -4
- package/templates/form-two-column.tsx +195 -210
- package/templates/gallery-hero.template.mjs +5 -4
- package/templates/gallery-hero.tsx +93 -114
- package/templates/ide.template.mjs +5 -4
- package/templates/ide.tsx +407 -453
- package/templates/incident-console.template.mjs +5 -4
- package/templates/incident-console.tsx +593 -575
- package/templates/kanban-board.template.mjs +5 -4
- package/templates/kanban-board.tsx +665 -683
- package/templates/library.template.mjs +5 -4
- package/templates/library.tsx +409 -430
- package/templates/login-card.template.mjs +5 -4
- package/templates/login-card.tsx +63 -72
- package/templates/login-split.template.mjs +5 -4
- package/templates/login-split.tsx +192 -193
- package/templates/login-sso.template.mjs +5 -4
- package/templates/login-sso.tsx +257 -258
- package/templates/login.template.mjs +5 -4
- package/templates/login.tsx +53 -49
- package/templates/messaging-shell.template.mjs +5 -4
- package/templates/messaging-shell.tsx +556 -574
- package/templates/mixed-gallery.template.mjs +5 -4
- package/templates/mixed-gallery.tsx +60 -58
- package/templates/payment-form.template.mjs +5 -4
- package/templates/payment-form.tsx +882 -991
- package/templates/product-detail.template.mjs +5 -4
- package/templates/product-detail.tsx +261 -272
- package/templates/product-gallery.template.mjs +5 -4
- package/templates/product-gallery.tsx +126 -137
- package/templates/product-tour.template.mjs +5 -4
- package/templates/product-tour.tsx +741 -773
- package/templates/settings-dialog.template.mjs +5 -4
- package/templates/settings-dialog.tsx +688 -717
- package/templates/settings-sidebar.template.mjs +5 -4
- package/templates/settings-sidebar.tsx +762 -807
- package/templates/settings.template.mjs +5 -4
- package/templates/settings.tsx +289 -304
- package/templates/shell-nav.template.mjs +5 -4
- package/templates/shell-nav.tsx +212 -219
- package/templates/shell-side-nav.template.mjs +5 -4
- package/templates/shell-side-nav.tsx +135 -157
- package/templates/shell-top-nav.template.mjs +5 -4
- package/templates/shell-top-nav.tsx +182 -203
- package/templates/side-gallery.template.mjs +5 -4
- package/templates/side-gallery.tsx +85 -90
- package/templates/table-grouped.template.mjs +5 -4
- package/templates/table-grouped.tsx +1000 -1053
- package/templates/table-page-chart.template.mjs +5 -4
- package/templates/table-page-chart.tsx +400 -398
- package/templates/table-page-heatmap-status.template.mjs +5 -4
- package/templates/table-page-heatmap-status.tsx +483 -512
- package/templates/table-page-shoe-store-heatmap.template.mjs +5 -4
- package/templates/table-page-shoe-store-heatmap.tsx +748 -746
- package/templates/table-page.template.mjs +5 -4
- package/templates/table-page.tsx +396 -405
- package/templates/table.template.mjs +5 -4
- package/templates/table.tsx +92 -93
- package/templates/tech-report.template.mjs +5 -4
- package/templates/tech-report.tsx +1316 -1304
- package/templates/theme-showcase.template.mjs +5 -4
- package/templates/theme-showcase.tsx +871 -951
- package/theme.css +35 -77
- package/theme.css.d.ts +9 -0
- package/tokens.css +373 -380
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
2
|
-
// XLE (canonical structure
|
|
2
|
+
// XLE (canonical structure of the tree below; the CLI had no XLE validator
|
|
3
|
+
// after `astryx layout` was removed in 0.7.0, so this is a reading aid and a
|
|
4
|
+
// human drift check — update it with the file, never alone):
|
|
3
5
|
// NOTE on `t=display-2`: the h1 asked for display-2 from the start, but
|
|
4
6
|
// core 0.3.0's Heading emitted no `data-type`, and the 0.3.0 CLI emitted no
|
|
5
7
|
// `.astryx-heading[data-type]` rule, so --text-display-2-size never reached a
|
|
@@ -8,638 +10,654 @@
|
|
|
8
10
|
// pixel-diffing the pre-upgrade build against this one at 1440 and 390: the
|
|
9
11
|
// only regression was this header detaching at 390, fixed by stacking the
|
|
10
12
|
// title group under `isNarrow`.
|
|
11
|
-
// L > (LH > H[
|
|
13
|
+
// L[h=fill] > (LH[divider p=6] > (H[g=3 a=center] > (SI[fill] > H[g=2 a=center wrap] > Hd"Night watch"[level=1 t=display-2] + Tx"3 investigating"[t=supporting]) + SG"Filter by status" + B"Declare incident")) + (LC[p=0 !scroll] > V[g=0] > (H > (SI[fill] > PS"Search the night watch…")) + (SI[fill] > (V[g=0] > (V[g=0] > (H[g=2 a=center] > Tx"Investigating"[t=label] + Tx"3"[t=supporting]) + (UL[density=compact hasDividers] > (LI > SB)*3)) + (V[g=0] > (H[g=2 a=center] > Tx"Mitigated"[t=label] + Tx"2"[t=supporting]) + (UL[density=compact hasDividers] > (LI > SB)*2)) + (V[g=0] > (H[g=2 a=center] > Tx"Resolved"[t=label] + Tx"2"[t=supporting]) + (UL[density=compact hasDividers] > (LI > SB)*2))))) + (LP[w=380 p=0] > V[g=4] > (V[g=2] > (H[g=1 a=center] > SB + Tx"INC-2417"[t=supporting] + Tk"Investigating") + Hd"Checkout API elevated 5xx rate"[level=2]) + (H[g=2 wrap] > B.primary"Mark mitigated" + B.secondary"Escalate") + D + ML + D + (V[g=2] > Hd"Timeline"[level=3] + (UL[density=compact] > LI*3)))) ;; Drawer"Incident details"[side=end w=380] > (DrawerHeader"Incident details" + (V[g=4] > (V[g2] > (H[g=1 a=center] > SB + Tx"INC-2417"[t=supporting] + Tk"Investigating") + Hd"Checkout API elevated 5xx rate"[level=2]) + (H[g=2 wrap] > B.primary"Mark mitigated" + B.secondary"Escalate") + D + ML + D + (V[g=2] > Hd"Timeline"[level=3] + (UL[density=compact] > LI*3))))
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* Incident Console — an on-call incident response tool for the night watch. Frame-first layout (see `bunx astryx docs layout`), distilled fro (Frame/responsive/container: see XLE header above.)
|
|
15
17
|
*/
|
|
16
18
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
LayoutHeader,
|
|
24
|
-
StackItem,
|
|
25
|
-
VStack,
|
|
26
|
-
} from '@astryxdesign/core/Layout';
|
|
27
|
-
import {ResizeHandle, useResizable} from '@astryxdesign/core/Resizable';
|
|
28
|
-
import {LayoutPanel} from '@astryxdesign/core/Layout';
|
|
29
|
-
import {Heading, Text} from '@astryxdesign/core/Text';
|
|
30
|
-
import {Button} from '@astryxdesign/core/Button';
|
|
31
|
-
import {Icon} from '@astryxdesign/core/Icon';
|
|
32
|
-
import {Divider} from '@astryxdesign/core/Divider';
|
|
33
|
-
import {EmptyState} from '@astryxdesign/core/EmptyState';
|
|
34
|
-
import {List, ListItem} from '@astryxdesign/core/List';
|
|
35
|
-
import {MetadataList, MetadataListItem} from '@astryxdesign/core/MetadataList';
|
|
19
|
+
import { Button } from "@astryxdesign/core/Button";
|
|
20
|
+
import { Divider } from "@astryxdesign/core/Divider";
|
|
21
|
+
import { Drawer, DrawerHeader } from "@astryxdesign/core/Drawer";
|
|
22
|
+
import { EmptyState } from "@astryxdesign/core/EmptyState";
|
|
23
|
+
import { useMediaQuery } from "@astryxdesign/core/hooks";
|
|
24
|
+
import { Icon } from "@astryxdesign/core/Icon";
|
|
36
25
|
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
} from
|
|
45
|
-
import {
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
48
|
-
import {
|
|
49
|
-
|
|
26
|
+
HStack,
|
|
27
|
+
Layout,
|
|
28
|
+
LayoutContent,
|
|
29
|
+
LayoutHeader,
|
|
30
|
+
LayoutPanel,
|
|
31
|
+
StackItem,
|
|
32
|
+
VStack,
|
|
33
|
+
} from "@astryxdesign/core/Layout";
|
|
34
|
+
import { List, ListItem } from "@astryxdesign/core/List";
|
|
35
|
+
import { MetadataList, MetadataListItem } from "@astryxdesign/core/MetadataList";
|
|
36
|
+
import type { PowerSearchFilter } from "@astryxdesign/core/PowerSearch";
|
|
37
|
+
import { PowerSearch, usePowerSearchConfig } from "@astryxdesign/core/PowerSearch";
|
|
38
|
+
import { ResizeHandle, useResizable } from "@astryxdesign/core/Resizable";
|
|
39
|
+
import { SegmentedControl, SegmentedControlItem } from "@astryxdesign/core/SegmentedControl";
|
|
40
|
+
import { StatusDot } from "@astryxdesign/core/StatusDot";
|
|
41
|
+
import { Heading, Text } from "@astryxdesign/core/Text";
|
|
42
|
+
import { Timestamp } from "@astryxdesign/core/Timestamp";
|
|
43
|
+
import { Token } from "@astryxdesign/core/Token";
|
|
50
44
|
// ============= ICONS (verified lucide-react exports) =============
|
|
51
45
|
// BellRing ← BellAlertIcon, Plus ← PlusIcon.
|
|
52
|
-
import {BellRing, Plus} from
|
|
46
|
+
import { BellRing, Plus } from "lucide-react";
|
|
47
|
+
import { type CSSProperties, useMemo, useState } from "react";
|
|
53
48
|
|
|
54
49
|
const styles: Record<string, CSSProperties> = {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
50
|
+
contentFill: {
|
|
51
|
+
height: "100%",
|
|
52
|
+
minHeight: 0,
|
|
53
|
+
},
|
|
54
|
+
searchRow: {
|
|
55
|
+
padding: "var(--spacing-3)",
|
|
56
|
+
},
|
|
57
|
+
groupHeader: {
|
|
58
|
+
padding: "var(--spacing-2) var(--spacing-3)",
|
|
59
|
+
backgroundColor: "var(--color-background-muted)",
|
|
60
|
+
},
|
|
61
|
+
// All FOUR edges, not block-only. With `overflowY: auto` and no `overflowX`,
|
|
62
|
+
// the inline axis computes to `auto` too, so this box clips horizontally as
|
|
63
|
+
// well as vertically. A block-only padding lands green and still leaves the
|
|
64
|
+
// inline half of the ring clipped.
|
|
65
|
+
rows: {
|
|
66
|
+
overflowY: "auto",
|
|
67
|
+
minHeight: 0,
|
|
68
|
+
padding: "var(--spacing-1)",
|
|
69
|
+
},
|
|
70
|
+
inspector: {
|
|
71
|
+
padding: "var(--spacing-4)",
|
|
72
|
+
height: "100%",
|
|
73
|
+
},
|
|
74
|
+
controlsRow: {
|
|
75
|
+
// The four-option filter plus the action never fit a phone header; the
|
|
76
|
+
// row scrolls instead of clipping options the pointer cannot reach.
|
|
77
|
+
overflowX: "auto",
|
|
78
|
+
},
|
|
84
79
|
};
|
|
85
80
|
|
|
86
|
-
type Severity =
|
|
87
|
-
type Status =
|
|
81
|
+
type Severity = "sev1" | "sev2" | "sev3";
|
|
82
|
+
type Status = "investigating" | "mitigated" | "resolved";
|
|
88
83
|
|
|
89
84
|
interface TimelineEvent {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
85
|
+
at: string;
|
|
86
|
+
label: string;
|
|
87
|
+
detail: string;
|
|
93
88
|
}
|
|
94
89
|
|
|
95
90
|
interface Incident extends Record<string, unknown> {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
91
|
+
id: string;
|
|
92
|
+
title: string;
|
|
93
|
+
service: string;
|
|
94
|
+
severity: Severity;
|
|
95
|
+
status: Status;
|
|
96
|
+
commander: string;
|
|
97
|
+
startedAt: string;
|
|
98
|
+
impact: string;
|
|
99
|
+
timeline: TimelineEvent[];
|
|
105
100
|
}
|
|
106
101
|
|
|
107
102
|
// Deterministic fixtures: fixed ISO timestamps, no clocks, no randomness.
|
|
108
103
|
const INCIDENTS: Incident[] = [
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
104
|
+
{
|
|
105
|
+
id: "INC-2417",
|
|
106
|
+
title: "Checkout API elevated 5xx rate",
|
|
107
|
+
service: "checkout-api",
|
|
108
|
+
severity: "sev1",
|
|
109
|
+
status: "investigating",
|
|
110
|
+
commander: "Priya Raman",
|
|
111
|
+
startedAt: "2026-06-30T21:14:00Z",
|
|
112
|
+
impact: "12% of checkout requests failing in us-east",
|
|
113
|
+
timeline: [
|
|
114
|
+
{
|
|
115
|
+
at: "2026-06-30T21:14:00Z",
|
|
116
|
+
label: "Incident declared",
|
|
117
|
+
detail: "Paging monitor: checkout-api 5xx > 5% for 10m",
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
at: "2026-06-30T21:19:00Z",
|
|
121
|
+
label: "Commander assigned",
|
|
122
|
+
detail: "Priya Raman took command",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
at: "2026-06-30T21:31:00Z",
|
|
126
|
+
label: "Suspect identified",
|
|
127
|
+
detail: "Deploy 8f31c2 correlates with error onset",
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "INC-2416",
|
|
133
|
+
title: "Search indexing lag above 30 minutes",
|
|
134
|
+
service: "search-indexer",
|
|
135
|
+
severity: "sev2",
|
|
136
|
+
status: "investigating",
|
|
137
|
+
commander: "Marcus Webb",
|
|
138
|
+
startedAt: "2026-06-30T19:42:00Z",
|
|
139
|
+
impact: "New listings not searchable; stale results served",
|
|
140
|
+
timeline: [
|
|
141
|
+
{
|
|
142
|
+
at: "2026-06-30T19:42:00Z",
|
|
143
|
+
label: "Incident declared",
|
|
144
|
+
detail: "Indexing lag alarm crossed 30m threshold",
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
at: "2026-06-30T20:05:00Z",
|
|
148
|
+
label: "Mitigation attempted",
|
|
149
|
+
detail: "Doubled indexer worker pool; lag still climbing",
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
id: "INC-2415",
|
|
155
|
+
title: "Webhook delivery retries exhausting queue",
|
|
156
|
+
service: "webhooks",
|
|
157
|
+
severity: "sev3",
|
|
158
|
+
status: "investigating",
|
|
159
|
+
commander: "Ana Duarte",
|
|
160
|
+
startedAt: "2026-06-30T18:20:00Z",
|
|
161
|
+
impact: "Third-party webhook delivery delayed up to 15m",
|
|
162
|
+
timeline: [
|
|
163
|
+
{
|
|
164
|
+
at: "2026-06-30T18:20:00Z",
|
|
165
|
+
label: "Incident declared",
|
|
166
|
+
detail: "Queue depth alarm on webhook-delivery",
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
id: "INC-2414",
|
|
172
|
+
title: "Payments settlement job stalled",
|
|
173
|
+
service: "payments",
|
|
174
|
+
severity: "sev1",
|
|
175
|
+
status: "mitigated",
|
|
176
|
+
commander: "Priya Raman",
|
|
177
|
+
startedAt: "2026-06-30T14:03:00Z",
|
|
178
|
+
impact: "Settlement batch delayed; no customer-visible impact",
|
|
179
|
+
timeline: [
|
|
180
|
+
{
|
|
181
|
+
at: "2026-06-30T14:03:00Z",
|
|
182
|
+
label: "Incident declared",
|
|
183
|
+
detail: "Settlement job heartbeat missed twice",
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
at: "2026-06-30T14:26:00Z",
|
|
187
|
+
label: "Mitigated",
|
|
188
|
+
detail: "Job restarted on standby worker; batch draining",
|
|
189
|
+
},
|
|
190
|
+
],
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
id: "INC-2413",
|
|
194
|
+
title: "CDN cache hit ratio degraded in eu-west",
|
|
195
|
+
service: "edge-cdn",
|
|
196
|
+
severity: "sev2",
|
|
197
|
+
status: "mitigated",
|
|
198
|
+
commander: "Tom Okafor",
|
|
199
|
+
startedAt: "2026-06-30T11:47:00Z",
|
|
200
|
+
impact: "Origin load 3x baseline; p95 latency +180ms in EU",
|
|
201
|
+
timeline: [
|
|
202
|
+
{
|
|
203
|
+
at: "2026-06-30T11:47:00Z",
|
|
204
|
+
label: "Incident declared",
|
|
205
|
+
detail: "Cache hit ratio dropped below 80%",
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
at: "2026-06-30T12:12:00Z",
|
|
209
|
+
label: "Mitigated",
|
|
210
|
+
detail: "Rolled back cache-key config change",
|
|
211
|
+
},
|
|
212
|
+
],
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "INC-2412",
|
|
216
|
+
title: "Login rate limiting misfiring for SSO users",
|
|
217
|
+
service: "auth",
|
|
218
|
+
severity: "sev2",
|
|
219
|
+
status: "resolved",
|
|
220
|
+
commander: "Ana Duarte",
|
|
221
|
+
startedAt: "2026-06-29T22:31:00Z",
|
|
222
|
+
impact: "SSO users intermittently blocked at login",
|
|
223
|
+
timeline: [
|
|
224
|
+
{
|
|
225
|
+
at: "2026-06-29T22:31:00Z",
|
|
226
|
+
label: "Incident declared",
|
|
227
|
+
detail: "Support surge: SSO login failures",
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
at: "2026-06-29T23:02:00Z",
|
|
231
|
+
label: "Mitigated",
|
|
232
|
+
detail: "Raised limiter threshold for SSO IdP ranges",
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
at: "2026-06-30T09:15:00Z",
|
|
236
|
+
label: "Resolved",
|
|
237
|
+
detail: "Limiter keying fixed to per-user; monitors green 8h",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
id: "INC-2411",
|
|
243
|
+
title: "Notification digests sent twice",
|
|
244
|
+
service: "notifications",
|
|
245
|
+
severity: "sev3",
|
|
246
|
+
status: "resolved",
|
|
247
|
+
commander: "Marcus Webb",
|
|
248
|
+
startedAt: "2026-06-29T08:12:00Z",
|
|
249
|
+
impact: "Duplicate daily digest for ~40k users",
|
|
250
|
+
timeline: [
|
|
251
|
+
{
|
|
252
|
+
at: "2026-06-29T08:12:00Z",
|
|
253
|
+
label: "Incident declared",
|
|
254
|
+
detail: "Duplicate-send reports from support",
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
at: "2026-06-29T10:40:00Z",
|
|
258
|
+
label: "Resolved",
|
|
259
|
+
detail: "Dedup key restored in scheduler; backfill verified",
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
},
|
|
268
263
|
];
|
|
269
264
|
|
|
270
|
-
const STATUS_ORDER: Status[] = [
|
|
265
|
+
const STATUS_ORDER: Status[] = ["investigating", "mitigated", "resolved"];
|
|
271
266
|
|
|
272
267
|
// One union: label for group headings + row tokens, color for row tokens.
|
|
273
268
|
// SEVERITY_DOT stays separate — it keys on severity (sev1/2/3), a different
|
|
274
269
|
// axis than status, so merging would conflate two domains in one map.
|
|
275
|
-
const STATUS: Record<
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
investigating: {label: 'Investigating', tokenColor: 'cyan'},
|
|
280
|
-
mitigated: {label: 'Mitigated', tokenColor: 'yellow'},
|
|
281
|
-
resolved: {label: 'Resolved', tokenColor: 'green'},
|
|
270
|
+
const STATUS: Record<Status, { label: string; tokenColor: "cyan" | "yellow" | "green" }> = {
|
|
271
|
+
investigating: { label: "Investigating", tokenColor: "cyan" },
|
|
272
|
+
mitigated: { label: "Mitigated", tokenColor: "yellow" },
|
|
273
|
+
resolved: { label: "Resolved", tokenColor: "green" },
|
|
282
274
|
};
|
|
283
275
|
|
|
284
|
-
const SEVERITY_DOT: Record<Severity,
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
276
|
+
const SEVERITY_DOT: Record<Severity, "error" | "warning" | "neutral"> = {
|
|
277
|
+
sev1: "error",
|
|
278
|
+
sev2: "warning",
|
|
279
|
+
sev3: "neutral",
|
|
288
280
|
};
|
|
289
281
|
|
|
290
282
|
// SEV badge shared by rows and the inspector: dot + uppercase word travel
|
|
291
283
|
// together (a bare dot leaves SEV1/2/3 as hue alone; StatusDot's label is
|
|
292
284
|
// aria-only on a role="img" span and paints nothing).
|
|
293
285
|
function SeverityBadge({
|
|
294
|
-
|
|
295
|
-
|
|
286
|
+
severity,
|
|
287
|
+
isPulsing = false,
|
|
296
288
|
}: {
|
|
297
|
-
|
|
298
|
-
|
|
289
|
+
severity: Severity;
|
|
290
|
+
isPulsing?: boolean;
|
|
299
291
|
}) {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
292
|
+
return (
|
|
293
|
+
<HStack gap={1} vAlign="center">
|
|
294
|
+
<StatusDot
|
|
295
|
+
variant={SEVERITY_DOT[severity]}
|
|
296
|
+
label={severity.toUpperCase()}
|
|
297
|
+
isPulsing={isPulsing}
|
|
298
|
+
/>
|
|
299
|
+
<Text type="supporting" color="secondary">
|
|
300
|
+
{severity.toUpperCase()}
|
|
301
|
+
</Text>
|
|
302
|
+
</HStack>
|
|
303
|
+
);
|
|
312
304
|
}
|
|
313
305
|
|
|
314
306
|
const SERVICE_VALUES = [
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
307
|
+
{ value: "checkout-api", label: "checkout-api" },
|
|
308
|
+
{ value: "search-indexer", label: "search-indexer" },
|
|
309
|
+
{ value: "webhooks", label: "webhooks" },
|
|
310
|
+
{ value: "payments", label: "payments" },
|
|
311
|
+
{ value: "edge-cdn", label: "edge-cdn" },
|
|
312
|
+
{ value: "auth", label: "auth" },
|
|
313
|
+
{ value: "notifications", label: "notifications" },
|
|
322
314
|
];
|
|
323
315
|
|
|
324
316
|
const SEVERITY_VALUES = [
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
317
|
+
{ value: "sev1", label: "SEV1" },
|
|
318
|
+
{ value: "sev2", label: "SEV2" },
|
|
319
|
+
{ value: "sev3", label: "SEV3" },
|
|
328
320
|
];
|
|
329
321
|
|
|
330
322
|
const fieldDefs = [
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
323
|
+
{ key: "title", type: "string", label: "Title" },
|
|
324
|
+
{ key: "service", type: "enum", label: "Service", enumValues: SERVICE_VALUES },
|
|
325
|
+
{
|
|
326
|
+
key: "severity",
|
|
327
|
+
type: "enum",
|
|
328
|
+
label: "Severity",
|
|
329
|
+
enumValues: SEVERITY_VALUES,
|
|
330
|
+
},
|
|
331
|
+
{ key: "commander", type: "string", label: "Commander" },
|
|
340
332
|
] as const;
|
|
341
333
|
|
|
342
334
|
function IncidentRows({
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
335
|
+
incidents,
|
|
336
|
+
selectedId,
|
|
337
|
+
onSelect,
|
|
346
338
|
}: {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
339
|
+
incidents: Incident[];
|
|
340
|
+
selectedId: string | null;
|
|
341
|
+
onSelect: (id: string) => void;
|
|
350
342
|
}) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
343
|
+
const groups = STATUS_ORDER.map((status) => ({
|
|
344
|
+
status,
|
|
345
|
+
items: incidents.filter((incident) => incident.status === status),
|
|
346
|
+
})).filter((group) => group.items.length > 0);
|
|
347
|
+
|
|
348
|
+
if (groups.length === 0) {
|
|
349
|
+
return (
|
|
350
|
+
<EmptyState
|
|
351
|
+
title="Quiet in the crypt"
|
|
352
|
+
description="No incidents match. Adjust the status filter or clear search filters."
|
|
353
|
+
icon={<Icon icon={BellRing} size="lg" color="secondary" />}
|
|
354
|
+
/>
|
|
355
|
+
);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return (
|
|
359
|
+
<VStack gap={0}>
|
|
360
|
+
{groups.map((group) => (
|
|
361
|
+
<VStack gap={0} key={group.status}>
|
|
362
|
+
<HStack gap={2} vAlign="center" style={styles.groupHeader}>
|
|
363
|
+
{/* Primary, not secondary: --color-text-secondary #9AA1BC on
|
|
372
364
|
--color-background-muted #44475A is 3.57:1, under the 4.5:1
|
|
373
365
|
floor. The group heading is the string this sidebar is scanned
|
|
374
366
|
for. Hierarchy comes from the two type roles (principle 6),
|
|
375
367
|
not from dimming the count. */}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
368
|
+
<Text type="label">{STATUS[group.status].label}</Text>
|
|
369
|
+
<Text type="supporting" hasTabularNumbers>
|
|
370
|
+
{group.items.length}
|
|
371
|
+
</Text>
|
|
372
|
+
</HStack>
|
|
373
|
+
<List density="compact" hasDividers>
|
|
374
|
+
{group.items.map((incident) => (
|
|
375
|
+
<ListItem
|
|
376
|
+
key={incident.id}
|
|
377
|
+
label={incident.title}
|
|
378
|
+
description={`${incident.id} · ${incident.service} · ${incident.impact}`}
|
|
379
|
+
// Incident titles wrap to two lines. The severity badge
|
|
380
|
+
// and the status token belong on the TITLE's first
|
|
381
|
+
// line, not floating at the middle of the block.
|
|
382
|
+
align="first-line"
|
|
383
|
+
startContent={
|
|
384
|
+
<SeverityBadge
|
|
385
|
+
severity={incident.severity}
|
|
386
|
+
isPulsing={incident.status === "investigating"}
|
|
387
|
+
/>
|
|
388
|
+
}
|
|
389
|
+
endContent={
|
|
390
|
+
<HStack gap={3} vAlign="center">
|
|
391
|
+
<Token
|
|
392
|
+
size="sm"
|
|
393
|
+
color={STATUS[incident.status].tokenColor}
|
|
394
|
+
label={STATUS[incident.status].label}
|
|
395
|
+
/>
|
|
396
|
+
<Timestamp value={incident.startedAt} format="relative" color="secondary" />
|
|
397
|
+
</HStack>
|
|
398
|
+
}
|
|
399
|
+
onClick={() => onSelect(incident.id)}
|
|
400
|
+
isSelected={incident.id === selectedId}
|
|
401
|
+
/>
|
|
402
|
+
))}
|
|
403
|
+
</List>
|
|
404
|
+
</VStack>
|
|
405
|
+
))}
|
|
406
|
+
</VStack>
|
|
407
|
+
);
|
|
416
408
|
}
|
|
417
409
|
|
|
418
|
-
function IncidentInspector({incident}: {incident: Incident}) {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
</VStack>
|
|
489
|
-
);
|
|
410
|
+
function IncidentInspector({ incident }: { incident: Incident }) {
|
|
411
|
+
const nextAction =
|
|
412
|
+
incident.status === "investigating"
|
|
413
|
+
? "Mark mitigated"
|
|
414
|
+
: incident.status === "mitigated"
|
|
415
|
+
? "Resolve"
|
|
416
|
+
: "Reopen";
|
|
417
|
+
|
|
418
|
+
return (
|
|
419
|
+
<VStack gap={4} style={styles.inspector}>
|
|
420
|
+
<VStack gap={2}>
|
|
421
|
+
<HStack gap={1} vAlign="center">
|
|
422
|
+
<SeverityBadge
|
|
423
|
+
severity={incident.severity}
|
|
424
|
+
isPulsing={incident.status === "investigating"}
|
|
425
|
+
/>
|
|
426
|
+
<Text type="supporting" color="secondary">
|
|
427
|
+
{incident.id}
|
|
428
|
+
</Text>
|
|
429
|
+
<Token
|
|
430
|
+
size="sm"
|
|
431
|
+
color={STATUS[incident.status].tokenColor}
|
|
432
|
+
label={STATUS[incident.status].label}
|
|
433
|
+
/>
|
|
434
|
+
</HStack>
|
|
435
|
+
<Heading level={2}>{incident.title}</Heading>
|
|
436
|
+
</VStack>
|
|
437
|
+
|
|
438
|
+
<HStack gap={2} wrap="wrap">
|
|
439
|
+
<Button label={nextAction} variant="primary" />
|
|
440
|
+
<Button label="Escalate" variant="secondary" />
|
|
441
|
+
</HStack>
|
|
442
|
+
|
|
443
|
+
<Divider />
|
|
444
|
+
|
|
445
|
+
<MetadataList columns="single" label={{ position: "start", width: 96 }}>
|
|
446
|
+
<MetadataListItem label="Service">
|
|
447
|
+
<Text type="body">{incident.service}</Text>
|
|
448
|
+
</MetadataListItem>
|
|
449
|
+
<MetadataListItem label="Severity">
|
|
450
|
+
<Text type="body">{incident.severity.toUpperCase()}</Text>
|
|
451
|
+
</MetadataListItem>
|
|
452
|
+
<MetadataListItem label="Commander">
|
|
453
|
+
<Text type="body">{incident.commander}</Text>
|
|
454
|
+
</MetadataListItem>
|
|
455
|
+
<MetadataListItem label="Started">
|
|
456
|
+
<Timestamp value={incident.startedAt} format="date_time" />
|
|
457
|
+
</MetadataListItem>
|
|
458
|
+
<MetadataListItem label="Impact">
|
|
459
|
+
<Text type="body">{incident.impact}</Text>
|
|
460
|
+
</MetadataListItem>
|
|
461
|
+
</MetadataList>
|
|
462
|
+
|
|
463
|
+
<Divider />
|
|
464
|
+
|
|
465
|
+
<VStack gap={2}>
|
|
466
|
+
<Heading level={3}>Timeline</Heading>
|
|
467
|
+
<List density="compact">
|
|
468
|
+
{incident.timeline.map((event) => (
|
|
469
|
+
<ListItem
|
|
470
|
+
key={`${event.at}-${event.label}`}
|
|
471
|
+
label={event.label}
|
|
472
|
+
description={event.detail}
|
|
473
|
+
endContent={<Timestamp value={event.at} format="time" color="secondary" />}
|
|
474
|
+
/>
|
|
475
|
+
))}
|
|
476
|
+
</List>
|
|
477
|
+
</VStack>
|
|
478
|
+
</VStack>
|
|
479
|
+
);
|
|
490
480
|
}
|
|
491
481
|
|
|
492
482
|
export default function IncidentConsole() {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
483
|
+
const [statusFilter, setStatusFilter] = useState("all");
|
|
484
|
+
const [filters, setFilters] = useState<PowerSearchFilter[]>([]);
|
|
485
|
+
const [selectedId, setSelectedId] = useState<string | null>(INCIDENTS[0].id);
|
|
486
|
+
// Mobile inspector drawer. The inspector is a separate surface at <= 1024px
|
|
487
|
+
// (it is the `end` slot of the layout there), so it is a Drawer: a panel
|
|
488
|
+
// that slides in over the row list instead of a column that compresses it.
|
|
489
|
+
const [isInspectorOpen, setInspectorOpen] = useState(false);
|
|
490
|
+
const { config, applyFilters } = usePowerSearchConfig(fieldDefs, "Incidents");
|
|
491
|
+
|
|
492
|
+
// Responsive contract: the inspector is hidden at <= 1024px so the row
|
|
493
|
+
// list keeps its full width (never compress dense rows).
|
|
494
|
+
const isNarrow = useMediaQuery("(max-width: 1024px)");
|
|
495
|
+
|
|
496
|
+
const inspectorPanel = useResizable({
|
|
497
|
+
defaultSize: 380,
|
|
498
|
+
minSize: 320,
|
|
499
|
+
maxSize: 480,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const visible = useMemo(() => {
|
|
503
|
+
const searched = applyFilters(filters, INCIDENTS);
|
|
504
|
+
return statusFilter === "all"
|
|
505
|
+
? searched
|
|
506
|
+
: searched.filter((incident) => incident.status === statusFilter);
|
|
507
|
+
}, [filters, applyFilters, statusFilter]);
|
|
508
|
+
|
|
509
|
+
const selected = visible.find((incident) => incident.id === selectedId) ?? null;
|
|
510
|
+
|
|
511
|
+
const openCount = INCIDENTS.filter((incident) => incident.status === "investigating").length;
|
|
512
|
+
|
|
513
|
+
// Extracted so a narrow header can drop them onto their own scrollable row
|
|
514
|
+
// instead of squeezing the title, the filter and the action into one line.
|
|
515
|
+
const statusFilterControl = (
|
|
516
|
+
<SegmentedControl
|
|
517
|
+
label="Filter by status"
|
|
518
|
+
value={statusFilter}
|
|
519
|
+
onChange={setStatusFilter}
|
|
520
|
+
size="sm"
|
|
521
|
+
>
|
|
522
|
+
<SegmentedControlItem label="All" value="all" />
|
|
523
|
+
<SegmentedControlItem label="Investigating" value="investigating" />
|
|
524
|
+
<SegmentedControlItem label="Mitigated" value="mitigated" />
|
|
525
|
+
<SegmentedControlItem label="Resolved" value="resolved" />
|
|
526
|
+
</SegmentedControl>
|
|
527
|
+
);
|
|
528
|
+
const declareButton = <Button label="Declare incident" icon={<Icon icon={Plus} size="sm" />} />;
|
|
529
|
+
// Stacks below 1024px. The H1 asks for `display-2` (35px), which is the
|
|
530
|
+
// theme's own declared value but only started rendering in core 0.6.3 —
|
|
531
|
+
// see the note in the XLE header. At 35px the title wraps to two lines on a
|
|
532
|
+
// phone, and the counter beside it then wraps too and detaches into the
|
|
533
|
+
// corner. Stacking keeps the counter on the title's own line at any width
|
|
534
|
+
// the title needs more than one, and costs nothing at desktop.
|
|
535
|
+
const titleGroup = (
|
|
536
|
+
<HStack gap={2} vAlign="center" wrap="wrap">
|
|
537
|
+
<Heading level={1} type="display-2">
|
|
538
|
+
Night watch
|
|
539
|
+
</Heading>
|
|
540
|
+
<Text type="supporting" color="secondary" hasTabularNumbers>
|
|
541
|
+
{openCount} investigating
|
|
542
|
+
</Text>
|
|
543
|
+
</HStack>
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
return (
|
|
547
|
+
<>
|
|
548
|
+
<Layout
|
|
549
|
+
height="fill"
|
|
550
|
+
header={
|
|
551
|
+
<LayoutHeader hasDivider padding={6}>
|
|
552
|
+
{isNarrow ? (
|
|
553
|
+
<VStack gap={2}>
|
|
554
|
+
{titleGroup}
|
|
555
|
+
<HStack
|
|
556
|
+
gap={2}
|
|
557
|
+
vAlign="center"
|
|
558
|
+
style={styles.controlsRow}
|
|
559
|
+
tabIndex={0}
|
|
560
|
+
role="region"
|
|
561
|
+
aria-label="Incident filter actions"
|
|
562
|
+
>
|
|
563
|
+
{statusFilterControl}
|
|
564
|
+
{declareButton}
|
|
565
|
+
</HStack>
|
|
566
|
+
</VStack>
|
|
567
|
+
) : (
|
|
568
|
+
<HStack gap={3} vAlign="center">
|
|
569
|
+
<StackItem size="fill">{titleGroup}</StackItem>
|
|
570
|
+
{statusFilterControl}
|
|
571
|
+
{declareButton}
|
|
572
|
+
</HStack>
|
|
573
|
+
)}
|
|
574
|
+
</LayoutHeader>
|
|
575
|
+
}
|
|
576
|
+
content={
|
|
577
|
+
<LayoutContent padding={0} isScrollable={false}>
|
|
578
|
+
<VStack gap={0} style={styles.contentFill}>
|
|
579
|
+
<HStack style={styles.searchRow}>
|
|
580
|
+
<StackItem size="fill">
|
|
581
|
+
<PowerSearch
|
|
582
|
+
config={config}
|
|
583
|
+
filters={filters}
|
|
584
|
+
onChange={(newFilters) => setFilters([...newFilters])}
|
|
585
|
+
placeholder="Search the night watch…"
|
|
586
|
+
resultCount={visible.length}
|
|
587
|
+
/>
|
|
588
|
+
</StackItem>
|
|
589
|
+
</HStack>
|
|
590
|
+
<StackItem
|
|
591
|
+
size="fill"
|
|
592
|
+
style={styles.rows}
|
|
593
|
+
tabIndex={0}
|
|
594
|
+
role="region"
|
|
595
|
+
aria-label="Incident rows"
|
|
596
|
+
>
|
|
597
|
+
<IncidentRows
|
|
598
|
+
incidents={visible}
|
|
599
|
+
selectedId={selected?.id ?? null}
|
|
600
|
+
onSelect={(id) => {
|
|
601
|
+
setSelectedId(id);
|
|
602
|
+
// On a narrow viewport the inspector is the Drawer, so
|
|
603
|
+
// picking a row is what opens it.
|
|
604
|
+
if (isNarrow) setInspectorOpen(true);
|
|
605
|
+
}}
|
|
606
|
+
/>
|
|
607
|
+
</StackItem>
|
|
608
|
+
</VStack>
|
|
609
|
+
</LayoutContent>
|
|
610
|
+
}
|
|
611
|
+
end={
|
|
612
|
+
isNarrow ? undefined : (
|
|
613
|
+
<>
|
|
614
|
+
<ResizeHandle
|
|
615
|
+
direction="horizontal"
|
|
616
|
+
hasDivider
|
|
617
|
+
isAlwaysVisible={false}
|
|
618
|
+
resizable={inspectorPanel.props}
|
|
619
|
+
label="Resize inspector"
|
|
620
|
+
/>
|
|
621
|
+
<LayoutPanel width={inspectorPanel.size} padding={0} label="Incident details">
|
|
622
|
+
{selected ? (
|
|
623
|
+
<IncidentInspector incident={selected} />
|
|
624
|
+
) : (
|
|
625
|
+
<EmptyState
|
|
626
|
+
title="No incident under the lens"
|
|
627
|
+
description="Select an incident from the rows to inspect its timeline."
|
|
628
|
+
icon={<Icon icon={BellRing} size="lg" color="secondary" />}
|
|
629
|
+
isCompact
|
|
630
|
+
/>
|
|
631
|
+
)}
|
|
632
|
+
</LayoutPanel>
|
|
633
|
+
</>
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
/>
|
|
637
|
+
{/* Narrow viewport: the inspector is a Drawer, not a column. It is
|
|
638
|
+
non-modal (hasScrim={false}) so the row list stays visible behind it —
|
|
639
|
+
the point of a peek-at-the-detail surface. The close button comes from
|
|
640
|
+
DrawerHeader, which only renders it when it is given onOpenChange. */}
|
|
641
|
+
<Drawer
|
|
642
|
+
side="end"
|
|
643
|
+
label="Incident details"
|
|
644
|
+
isOpen={isNarrow && isInspectorOpen}
|
|
645
|
+
onOpenChange={setInspectorOpen}
|
|
646
|
+
hasScrim={false}
|
|
647
|
+
width={inspectorPanel.size}
|
|
648
|
+
>
|
|
649
|
+
<DrawerHeader title="Incident details" onOpenChange={setInspectorOpen} />
|
|
650
|
+
{selected ? (
|
|
651
|
+
<IncidentInspector incident={selected} />
|
|
652
|
+
) : (
|
|
653
|
+
<EmptyState
|
|
654
|
+
title="No incident under the lens"
|
|
655
|
+
description="Select an incident from the rows to inspect its timeline."
|
|
656
|
+
icon={<Icon icon={BellRing} size="lg" color="secondary" />}
|
|
657
|
+
isCompact
|
|
658
|
+
/>
|
|
659
|
+
)}
|
|
660
|
+
</Drawer>
|
|
661
|
+
</>
|
|
662
|
+
);
|
|
645
663
|
}
|