react-dashstream 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,1193 +1,1245 @@
1
- # React DashStream
2
-
3
- Holographic 3D infrastructure monitoring dashboard for React. Pure CSS-3D — no WebGL, no canvas, no dependencies beyond React.
4
-
5
- ```
6
- npm install react-dashstream
7
- ```
8
-
9
- ---
10
-
11
- ## Quick start
12
-
13
- ```tsx
14
- import "react-dashstream/dist/index.css";
15
- import { AIOPsDashboard, Service, ServerNode, DatabaseNode } from "react-dashstream";
16
- import type { ServiceMeta } from "react-dashstream";
17
-
18
- const services: ServiceMeta[] = [
19
- {
20
- name: "My Service",
21
- status: "online",
22
- metrics: [
23
- { label: "Service Health", value: "99.9%", color: "#00ff88" },
24
- { label: "Avg Response Time", value: "14ms", color: "#00e5ff" },
25
- ],
26
- alerts: [{ level: "info", message: "All Systems Nominal" }],
27
- },
28
- ];
29
-
30
- export default function App() {
31
- return (
32
- <AIOPsDashboard brandName="MY DASHBOARD" services={services}>
33
- <Service
34
- name="My Service"
35
- status="online"
36
- connections={[
37
- { from: [330, 200], to: [200, 380], visibleAtPhase: 3 },
38
- { from: [330, 200], to: [460, 380], visibleAtPhase: 3 },
39
- ]}
40
- >
41
- <ServerNode
42
- ex={200}
43
- ey={380}
44
- compactOffset={{ x: -30, y: -20 }}
45
- zIndex={8}
46
- name="SRV-01"
47
- subLabel="APP SERVER"
48
- status="online"
49
- cpuLoad={42}
50
- memLoad={60}
51
- />
52
- <DatabaseNode
53
- ex={460}
54
- ey={380}
55
- compactOffset={{ x: 30, y: -20 }}
56
- zIndex={7}
57
- name="DB-01"
58
- subLabel="PRIMARY"
59
- status="online"
60
- capacity={55}
61
- />
62
- </Service>
63
- </AIOPsDashboard>
64
- );
65
- }
66
- ```
67
-
68
- Click a service to expand its topology. Click a component to drill into its internals.
69
-
70
- ---
71
-
72
- ## Theme (light / dark)
73
-
74
- The dashboard ships with **light** and **dark** visual modes. `AIOPsDashboard` defaults to **light**; the header includes a control to toggle modes.
75
-
76
- ### Background images
77
-
78
- | Prop | When used |
79
- | ---------------------- | -------------------------------------------------- |
80
- | `backgroundImage` | Dark mode, and light mode if no light asset is set |
81
- | `lightBackgroundImage` | Light mode when provided |
82
-
83
- ### Controlled theme (sync with your app shell)
84
-
85
- Pass **`theme`** and **`onThemeChange`** to drive the dashboard from parent state (for example, to keep tabs, sidebars, and `EventView` in sync):
86
-
87
- ```tsx
88
- import { useState } from "react";
89
- import { AIOPsDashboard, ThemeProvider, type DashboardTheme, type ServiceMeta } from "react-dashstream";
90
- import lightBg from "./light.webp";
91
- import darkBg from "./dark.webp";
92
-
93
- const services: ServiceMeta[] = [
94
- /* …same shape as Quick start… */
95
- ];
96
-
97
- export default function App() {
98
- const [theme, setTheme] = useState<DashboardTheme>("light");
99
-
100
- return (
101
- <ThemeProvider value={theme}>
102
- <AIOPsDashboard
103
- theme={theme}
104
- onThemeChange={setTheme}
105
- lightBackgroundImage={lightBg}
106
- backgroundImage={darkBg}
107
- brandName="MY DASHBOARD"
108
- services={services}
109
- >
110
- {/* Service / node tree — see Quick start */}
111
- </AIOPsDashboard>
112
- </ThemeProvider>
113
- );
114
- }
115
- ```
116
-
117
- When both props are set, the dashboard is **controlled**; the header toggle calls `onThemeChange`. Omit them to use **internal** theme state.
118
-
119
- ### `ThemeProvider`, `EventView`, and the credentials modal
120
-
121
- **`ServiceDialog`**, **`ComponentDialog`**, **`EventView`**, and **`CredentialsModal`** (access-key prompt) read the active mode from **`useTheme()`**.
122
-
123
- - Wrap any subtree that includes **`EventView`** or standalone credential prompts in **`ThemeProvider`** with the same `value` as your dashboard so the event console and lock screen match.
124
- - Optionally pass **`theme="light"` \| `"dark"`** on **`EventView`** to override context (useful when embedding without a provider).
125
-
126
- Exports: `ThemeProvider`, `useTheme`, type `DashboardTheme`.
127
-
128
- ---
129
-
130
- ## Full example — multi-service, multi-layer
131
-
132
- See `example/Dashboard.tsx` in this package for a complete two-service example with Payment Gateway and Auth Service topologies rotating in a 3D carousel.
133
-
134
- ---
135
-
136
- ## Event Console
137
-
138
- The `EventView` component provides a full-screen operations event console with severity filtering, sortable columns, search, and pagination styled to match the holographic theme. It fetches events from an external API using the same `access-key` / `access-secret-key` authentication as the 3D dashboard.
139
-
140
- ### Quick start API mode
141
-
142
- ```tsx
143
- import "react-dashstream/dist/index.css";
144
- import { EventView } from "react-dashstream";
145
- import type { EventApiConfig } from "react-dashstream";
146
-
147
- const eventApiConfig: EventApiConfig = {
148
- baseUrl: "https://your-monitoring-server.example.com",
149
- // endpoint defaults to "/tsws/monitoring/api/v1.0/events/search"
150
- payload: {
151
- filter: {},
152
- sortBy: "date_reception",
153
- sortOrder: "DESC",
154
- },
155
- fieldMapping: {
156
- id: "mc_ueid",
157
- occurrence: "date_reception",
158
- severityLastModified: "severity_last_modified",
159
- severity: "severity",
160
- owner: "owner",
161
- class: "class",
162
- host: "mc_host",
163
- message: "msg",
164
- remedySupportGroup: "ara_remedy_support_group",
165
- incidentId: "ara_incident_id",
166
- smsStatus: "ara_sms_status",
167
- onCallNumber: "ara_on_call_number",
168
- hostedApplication: "ara_hosted_application",
169
- monitoringCategory: "ara_hosted_app_monitoring",
170
- applicationSupportUnit: "ara_application_support_unit",
171
- },
172
- };
173
-
174
- export default function Events() {
175
- return <EventView apiConfig={eventApiConfig} />;
176
- }
177
- ```
178
-
179
- On first load a credentials modal appears (same as the 3D dashboard). After authentication, EventView sends a `POST` to `{baseUrl}/tsws/monitoring/api/v1.0/events/search` with the payload and polls every 60 seconds.
180
-
181
- ### How it works
182
-
183
- 1. **Authentication** — uses `access-key` and `access-secret-key` HTTP headers, identical to the 3D dashboard. If EventView is inside a `DataProvider` (e.g. alongside `AIOPsDashboard` with `liveData`), it reuses those credentials automatically — no second login.
184
- 2. **POST request** — sends `apiConfig.payload` as the JSON body.
185
- 3. **Response format** — expects:
186
- ```json
187
- {
188
- "eventSeverityCount": { "MAJOR": 1, "CRITICAL": 2, "MINOR": 3 },
189
- "totalCount": 6,
190
- "eventList": [{ "mc_ueid": "...", "msg": "...", ... }]
191
- }
192
- ```
193
- 4. **Field mapping** — each object in `eventList` is mapped to an `AIOpsEvent` using `apiConfig.fieldMapping`. Every `AIOpsEvent` field must have a corresponding API field name.
194
- 5. **Severity mapping** — raw severity strings (e.g. `"CRITICAL"`) are mapped to `EventSeverity` via `apiConfig.severityMap` (defaults to `{ CRITICAL: "Critical", MAJOR: "Major", MINOR: "Minor" }`).
195
-
196
- ### Pre-fetched events mode
197
-
198
- If you already have events data, pass them directly — no API call is made:
199
-
200
- ```tsx
201
- <EventView events={myEvents} title="My Event Console" />
202
- ```
203
-
204
- ### EventView props
205
-
206
- | Prop | Type | Default | Description |
207
- | -------------------- | --------------------- | ----------------- | ---------------------------------------------------------------------------- |
208
- | `apiConfig` | `EventApiConfig` | — | API configuration (base URL, payload, field mapping). Required for API mode |
209
- | `events` | `AIOpsEvent[]` | — | Pre-fetched events. When provided, `apiConfig` is not used |
210
- | `credentials` | `Credentials` | — | Explicit credentials. Falls back to DataProvider context, then modal |
211
- | `columnWidthsCookie` | `string` | `"ev_col_widths"` | Cookie name used to persist user-defined column widths |
212
- | `title` | `string` | `"Event Console"` | Title displayed in the component header |
213
- | `theme` | `"light"` \| `"dark"` | — | Optional override; otherwise uses `ThemeProvider` / defaults to dark context |
214
-
215
- ### EventApiConfig
216
-
217
- | Field | Type | Default | Description |
218
- | ----------------- | ------------------------------- | ---------------------------------------------------------- | ------------------------------------------------- |
219
- | `baseUrl` | `string` | (required) | Protocol + host, e.g. `"https://mon.example.com"` |
220
- | `endpoint` | `string` | `"/tsws/monitoring/api/v1.0/events/search"` | Path appended to `baseUrl` |
221
- | `payload` | `Record<string, unknown>` | (required) | POST body sent with every request |
222
- | `fieldMapping` | `EventFieldMapping` | (required) | Maps API field names → `AIOpsEvent` fields |
223
- | `severityMap` | `Record<string, EventSeverity>` | `{ CRITICAL: "Critical", MAJOR: "Major", MINOR: "Minor" }` | Maps raw severity → `EventSeverity` |
224
- | `refreshInterval` | `number` | `60000` | Polling interval in milliseconds |
225
-
226
- ### Field mapping
227
-
228
- The `fieldMapping` object maps every `AIOpsEvent` property to the key name the API returns. All fields are required:
229
-
230
- ```ts
231
- const fieldMapping: EventFieldMapping = {
232
- id: "mc_ueid", // unique event identifier
233
- occurrence: "date_reception", // datetime
234
- severityLastModified: "severity_last_modified",
235
- severity: "severity", // mapped through severityMap
236
- owner: "owner",
237
- class: "class", // Self-monitoring, SCOM, etc.
238
- host: "mc_host",
239
- message: "msg",
240
- remedySupportGroup: "ara_remedy_support_group",
241
- incidentId: "ara_incident_id",
242
- smsStatus: "ara_sms_status", // "", "SUCCESS", "FAILED"
243
- onCallNumber: "ara_on_call_number",
244
- hostedApplication: "ara_hosted_application",
245
- monitoringCategory: "ara_hosted_app_monitoring", // "24/7" or "Office Hours"
246
- applicationSupportUnit: "ara_application_support_unit",
247
- };
248
- ```
249
-
250
- ### Credential resolution order
251
-
252
- 1. `credentials` prop (if provided)
253
- 2. `DataProvider` context credentials (when inside `AIOPsDashboard` with `liveData`)
254
- 3. Built-in credentials modal (same UI as the 3D dashboard)
255
-
256
- ### Severity colors
257
-
258
- | Severity | Color | Usage |
259
- | -------- | --------- | ------------------ |
260
- | Minor | `#ffb800` | Amber, low urgency |
261
- | Major | `#ff6600` | Orange, attention |
262
- | Critical | `#ff2255` | Red, immediate |
263
-
264
- ### Features
265
-
266
- - **Live API polling** — POST to configurable endpoint with auto-refresh
267
- - **Field mapping** — maps arbitrary API response keys to typed event columns
268
- - **Shared authentication** — reuses credentials from `DataProvider` context when available
269
- - **Severity filter chips** — toggle Critical, Major, Minor with live counts
270
- - **Free-text search** filters across message, host, owner, and incident ID
271
- - **Sortable columns** click any header to cycle ascending / descending / none
272
- - **Infinite scroll** — all matching events render in a single scrollable table
273
- - **Resizable columns** drag column edges to resize; widths are persisted in cookies
274
- - **Loading / error states** spinner, error badge, last-refresh timestamp, manual refresh button
275
- - **Theming** dark holographic styling or **light** mode (panels, table, and credentials modal follow `ThemeProvider` / optional `theme` prop)
276
-
277
- ---
278
-
279
- ## Event-to-dashboard bridge
280
-
281
- When the same `EventApiConfig` is passed to `AIOPsDashboard`, events are fetched in the background and **automatically highlight nodes** in the 3D topology whose hostname matches an open event. No extra code, no manual wiring — just pass the config.
282
-
283
- ### Quick start
284
-
285
- ```tsx
286
- <AIOPsDashboard
287
- brandName="MY DASHBOARD"
288
- services={services}
289
- liveData
290
- dataEndpoint="https://prometheus.example.com/api/v1/query"
291
- dataBindings={dataBindings}
292
- eventApiConfig={eventApiConfig} // ← same config used for EventView
293
- >
294
- <MyService name="My Service" />
295
- </AIOPsDashboard>
296
- ```
297
-
298
- ### How it works
299
-
300
- 1. `AIOPsDashboard` wraps its children in an `EventAlertsProvider` that fetches events in the background (using the same polling interval and credentials as the Event Console).
301
- 2. Each fetched event has a `host` field (mapped via `fieldMapping`).
302
- 3. Every `ServiceNode` in the topology checks if its `componentInfo.name` matches any event host (**case-insensitive**).
303
- 4. If a match is found, the node's severity is updated and a `NodeCallout` appears with the event message.
304
-
305
- ### Severity mapping
306
-
307
- | Event severity | Dashboard status | Callout color |
308
- | -------------- | ---------------- | ------------- |
309
- | Critical | `"critical"` | Red |
310
- | Major | `"warning"` | Orange |
311
- | Minor | `"warning"` | Orange |
312
-
313
- When multiple events exist on the same host, the highest severity wins and the callout shows `"N events – <message>"`.
314
-
315
- The event alert is a **third severity source** alongside the node's own `status` prop and metric threshold breaches. The highest severity among all three always wins.
316
-
317
- ### Node name matching
318
-
319
- For the bridge to work, the `name` prop on your nodes must match the `host` field in the event data:
320
-
321
- ```tsx
322
- // If your events API returns host: "SAP-PRD-APP01", name your node:
323
- <ServerNode name="SAP-PRD-APP01" ... />
324
-
325
- // Matching is case-insensitive, so these also work:
326
- <ServerNode name="sap-prd-app01" ... />
327
- <ServerNode name="Sap-Prd-App01" ... />
328
- ```
329
-
330
- ### Credential resolution
331
-
332
- The `EventAlertsProvider` inside `AIOPsDashboard` resolves credentials in this order:
333
-
334
- 1. Parent `DataProvider` context (when `liveData` is enabled — **same credentials, single login**)
335
- 2. Built-in credentials modal (when `liveData` is not enabled)
336
-
337
- ### Opt-in behavior
338
-
339
- - **Without `eventApiConfig`** — no events are fetched, no alerts are injected, zero overhead.
340
- - **Without `EventView`** — the bridge still works. You can use event-based alerts on the 3D dashboard without ever rendering an `EventView`.
341
-
342
- ---
343
-
344
- ## External data source monitoring
345
-
346
- DashStream can connect to any HTTP monitoring endpoint (Prometheus, Grafana, custom APIs) and feed live values into your dashboard. This section covers **every** data path — from node props to service dialogs, component dialogs, alerts, drill-down internals, and graphs.
347
-
348
- ### How it works
349
-
350
- 1. Set `liveData={true}` on `AIOPsDashboard` with `dataEndpoint` and `dataBindings`.
351
- 2. A **credentials modal** appears on first load asking for `access-key` and `access-secret-key` (stored in memory only).
352
- 3. The dashboard polls `GET <endpoint>?query=<encodedQuery>` for each unique query with credentials as HTTP headers.
353
- 4. Resolved values are injected as **props** into child service components matched by `name`.
354
- 5. The header shows **"DATA REFRESH FAILED"** if any queries fail.
355
-
356
- ### Live data props
357
-
358
- | Prop | Type | Default | Description |
359
- | --------------------- | ---------------------------------------- | ------------- | ------------------------------------------------------ |
360
- | `liveData` | `boolean` | `false` | Enable the live data pipeline. |
361
- | `dataEndpoint` | `string` | | Base URL. Queries sent as `GET <url>?query=<encoded>`. |
362
- | `dataBindings` | `DataBindings` | | Maps service → prop → query. See below. |
363
- | `dataTransform` | `(raw) => unknown` | Numeric parse | Global transform for raw responses. |
364
- | `dataRefreshInterval` | `number` | `60000` | Polling interval in ms. |
365
- | `serviceDataBindings` | `Record<string, ServiceMetricBinding[]>` | — | Live metrics for the service stats dialog. |
366
-
367
- ### Endpoint contract
368
-
369
- | Aspect | Requirement |
370
- | ------------ | --------------------------------------------------------------- |
371
- | **Method** | `GET` |
372
- | **URL** | `<dataEndpoint>?query=<urlEncodedQuery>` |
373
- | **Headers** | `access-key` and `access-secret-key` |
374
- | **Response** | Plain text body (trimmed). Default transform parses as number. |
375
- | **Errors** | Non-2xx counted as failure. Partial failures shown in header. |
376
-
377
- For JSON responses (e.g. Prometheus), provide a custom `dataTransform`:
378
-
379
- ```tsx
380
- dataTransform={(raw) => {
381
- const parsed = JSON.parse(String(raw));
382
- return parsed?.data?.result?.[0]?.value?.[1] ?? raw;
383
- }}
384
- ```
385
-
386
- ---
387
-
388
- ### 1. Data bindings — injecting live props into nodes
389
-
390
- `dataBindings` maps **service name → prop name → query**. Each key in the inner object must match a prop on your service component. The service name must match the child's `name` prop.
391
-
392
- A binding can be a bare query string or an object with a custom transform:
393
-
394
- ```ts
395
- type DataBinding = string | { query: string; transform?: (raw: unknown) => unknown };
396
- type DataBindings = Record<string, Record<string, DataBinding>>;
397
- ```
398
-
399
- **Example — one service with a server and database:**
400
-
401
- ```tsx
402
- import "react-dashstream/dist/index.css";
403
- import { AIOPsDashboard, Service, ServerNode, DatabaseNode } from "react-dashstream";
404
-
405
- function statusFromValue(raw: unknown) {
406
- const n = Number(raw);
407
- if (n >= 85) return "critical";
408
- if (n >= 70) return "warning";
409
- return "online";
410
- }
411
-
412
- export default function App() {
413
- return (
414
- <AIOPsDashboard
415
- brandName="LIVE DASHBOARD"
416
- services={[{ name: "My Service", status: "online" }]}
417
- liveData={true}
418
- dataEndpoint="https://prometheus.example.com/api/v1/query"
419
- dataRefreshInterval={10000}
420
- dataBindings={{
421
- "My Service": {
422
- // bare string parsed as number automatically
423
- cpuLoad: 'cpu_usage{instance="srv-01"}',
424
- memLoad: 'memory_usage{instance="srv-01"}',
425
-
426
- // object with transform convert number to status string
427
- status: {
428
- query: 'up{instance="srv-01"}',
429
- transform: statusFromValue,
430
- },
431
-
432
- // bind database props too
433
- dbCapacity: 'disk_capacity{instance="db-01"}',
434
- dbStatus: {
435
- query: 'disk_capacity{instance="db-01"}',
436
- transform: statusFromValue,
437
- },
438
- },
439
- }}
440
- >
441
- <MyService name="My Service" />
442
- </AIOPsDashboard>
443
- );
444
- }
445
- ```
446
-
447
- The component receives `cpuLoad`, `memLoad`, `status`, `dbCapacity`, and `dbStatus` as live props, overriding their defaults.
448
-
449
- #### Multi-component services
450
-
451
- When a service has multiple nodes of the same type (e.g. three servers), use a flat naming convention with prefixes. Each binding maps one prop to one query — there is no array or object binding support.
452
-
453
- ```tsx
454
- // Service component — accepts prefixed props for each server
455
- interface ServiceXProps {
456
- name: string;
457
- status?: ComponentStatus;
458
- srv1CpuLoad?: number;
459
- srv1MemLoad?: number;
460
- srv1Status?: ComponentStatus;
461
- srv2CpuLoad?: number;
462
- srv2MemLoad?: number;
463
- srv2Status?: ComponentStatus;
464
- srv3CpuLoad?: number;
465
- srv3MemLoad?: number;
466
- srv3Status?: ComponentStatus;
467
- dbCapacity?: number;
468
- dbStatus?: ComponentStatus;
469
- }
470
-
471
- function ServiceX({
472
- name, status = "online",
473
- srv1CpuLoad = 54, srv1MemLoad = 58, srv1Status = "online",
474
- srv2CpuLoad = 63, srv2MemLoad = 66, srv2Status = "online",
475
- srv3CpuLoad = 78, srv3MemLoad = 71, srv3Status = "online",
476
- dbCapacity = 68, dbStatus = "online",
477
- }: ServiceXProps) {
478
- return (
479
- <Service name={name} status={status} connections={[/* ... */]}>
480
- <ServerNode name="SRV-X1" status={srv1Status}
481
- cpuLoad={srv1CpuLoad} memLoad={srv1MemLoad} ... />
482
- <ServerNode name="SRV-X2" status={srv2Status}
483
- cpuLoad={srv2CpuLoad} memLoad={srv2MemLoad} ... />
484
- <ServerNode name="SRV-X3" status={srv3Status}
485
- cpuLoad={srv3CpuLoad} memLoad={srv3MemLoad} ... />
486
- <DatabaseNode name="DB-X1" status={dbStatus}
487
- capacity={dbCapacity} ... />
488
- </Service>
489
- );
490
- }
491
-
492
- // Dashboard — bind each prefixed prop to its query
493
- <AIOPsDashboard
494
- liveData={true}
495
- dataEndpoint="https://prometheus.example.com/api/v1/query"
496
- dataBindings={{
497
- ServiceX: {
498
- status: { query: 'status{instance="svcx"}', transform: statusFromValue },
499
- srv1CpuLoad: 'cpu_usage{instance="srvx-01"}',
500
- srv1MemLoad: 'memory_usage{instance="srvx-01"}',
501
- srv1Status: { query: 'status{instance="srvx-01"}', transform: statusFromValue },
502
- srv2CpuLoad: 'cpu_usage{instance="srvx-02"}',
503
- srv2MemLoad: 'memory_usage{instance="srvx-02"}',
504
- srv2Status: { query: 'status{instance="srvx-02"}', transform: statusFromValue },
505
- srv3CpuLoad: 'cpu_usage{instance="srvx-03"}',
506
- srv3MemLoad: 'memory_usage{instance="srvx-03"}',
507
- srv3Status: { query: 'status{instance="srvx-03"}', transform: statusFromValue },
508
- dbCapacity: 'disk_capacity{instance="dbx-01"}',
509
- dbStatus: { query: 'status{instance="dbx-01"}', transform: statusFromValue },
510
- },
511
- }}
512
- services={[{ name: "ServiceX", status: "online" }]}
513
- >
514
- <ServiceX name="ServiceX" />
515
- </AIOPsDashboard>
516
- ```
517
-
518
- Use a consistent naming convention like `srv1CpuLoad`, `srv2CpuLoad`, etc. The dashboard injects each prefixed prop individually. The service component maps each prop to the correct node.
519
-
520
- ---
521
-
522
- ### 2. Service dialog — live KPI metrics
523
-
524
- The **ServiceDialog** is the stats panel that appears when you click a service. It shows KPI rows and alerts.
525
-
526
- **Static way** pass `ServiceMeta` with hardcoded values:
527
-
528
- ```tsx
529
- const services: ServiceMeta[] = [
530
- {
531
- name: "My Service",
532
- status: "online",
533
- metrics: [
534
- { label: "Uptime", value: "99.99%", color: "#00ff88" },
535
- { label: "Avg Latency", value: "8ms", color: "#00e5ff" },
536
- { label: "Error Rate", value: "0.02%", color: "#00ff88" },
537
- ],
538
- alerts: [{ level: "info", message: "All Systems Nominal" }],
539
- },
540
- ];
541
- ```
542
-
543
- **Live way** — use `serviceDataBindings` to fetch values from your endpoint:
544
-
545
- ```tsx
546
- <AIOPsDashboard
547
- services={[{ name: "My Service", status: "online" }]}
548
- liveData={true}
549
- dataEndpoint="https://prometheus.example.com/api/v1/query"
550
- dataBindings={{ /* ... */ }}
551
- serviceDataBindings={{
552
- "My Service": [
553
- { label: "CPU Load", query: 'cpu_usage{instance="srv-01"}', unit: "%", color: "#00e5ff" },
554
- { label: "Memory", query: 'memory_usage{instance="srv-01"}', unit: "%", color: "#bb55ff" },
555
- { label: "Disk", query: 'disk_capacity{instance="db-01"}', unit: "%", color: "#ff8c00" },
556
- ],
557
- }}
558
- >
559
- ```
560
-
561
- Each `ServiceMetricBinding`:
562
-
563
- ```ts
564
- interface ServiceMetricBinding {
565
- label: string; // Row label
566
- query: string; // PromQL query
567
- unit?: string; // Suffix (e.g. "%", "ms")
568
- color?: string; // Accent color
569
- transform?: (raw: unknown) => string; // Custom formatter
570
- }
571
- ```
572
-
573
- When `serviceDataBindings` is provided for a service, live values **replace** the static `ServiceMeta.metrics`.
574
-
575
- ---
576
-
577
- ### 3. Component dialog — custom gauges
578
-
579
- The **ComponentDialog** appears when you click a node (server, database, etc.) inside an expanded service. By default it shows CPU, Memory, and Storage gauges derived from the node's props.
580
-
581
- **Override these gauges** with the `dialogMetrics` prop on any compound node:
582
-
583
- ```tsx
584
- <ServerNode
585
- ex={200}
586
- ey={380}
587
- compactOffset={{ x: -30, y: -20 }}
588
- zIndex={8}
589
- name="SRV-01"
590
- subLabel="APP SERVER"
591
- status="online"
592
- cpuLoad={67}
593
- memLoad={72}
594
- dialogMetrics={[
595
- { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: 67, unit: "%" },
596
- { id: "mem", label: "MEMORY", sublabel: "HEAP USAGE", value: 72, unit: "%" },
597
- { id: "iops", label: "IOPS", sublabel: "DISK OPS", value: 45, unit: "k/s", icon: "disk" },
598
- {
599
- id: "threads",
600
- label: "THREADS",
601
- sublabel: "ACTIVE",
602
- value: 82,
603
- unit: "%",
604
- warnAt: 60,
605
- critAt: 80,
606
- icon: "cpu",
607
- },
608
- ]}
609
- />
610
- ```
611
-
612
- Each `ComponentDialogMetric`:
613
-
614
- ```ts
615
- interface ComponentDialogMetric {
616
- id: string; // Unique key
617
- label: string; // Upper label (e.g. "CPU")
618
- sublabel: string; // Lower label (e.g. "PROCESSOR")
619
- value: number; // 0–100 gauge value
620
- unit?: string; // Suffix (default "%")
621
- icon?: "cpu" | "mem" | "disk"; // Gauge icon (default "cpu")
622
- warnAt?: number; // Orange threshold (default 70)
623
- critAt?: number; // Red threshold (default 85)
624
- color?: string; // Override bar color (bypasses thresholds)
625
- }
626
- ```
627
-
628
- All compound nodes (`ServerNode`, `DatabaseNode`, `WebDispatcherNode`, `MessageServerNode`) accept `dialogMetrics`.
629
-
630
- **Live component dialog metrics** — to feed live values into custom gauges, expose each metric value as a prop on your service component and use `dataBindings` to inject them. Then build the `dialogMetrics` array from those live props:
631
-
632
- ```tsx
633
- // 1. Define your service component with props for each custom metric
634
- import { Service, ServerNode } from "react-dashstream";
635
- import type { ComponentStatus } from "react-dashstream";
636
-
637
- interface MyServiceProps {
638
- name: string;
639
- status?: ComponentStatus;
640
- cpuLoad?: number;
641
- memLoad?: number;
642
- iops?: number;
643
- threadCount?: number;
644
- }
645
-
646
- function MyService({
647
- name,
648
- status = "online",
649
- cpuLoad = 42,
650
- memLoad = 60,
651
- iops = 20,
652
- threadCount = 45,
653
- }: MyServiceProps) {
654
- return (
655
- <Service
656
- name={name}
657
- status={status}
658
- connections={
659
- [
660
- /* ... */
661
- ]
662
- }
663
- >
664
- <ServerNode
665
- ex={200}
666
- ey={380}
667
- compactOffset={{ x: -30, y: -20 }}
668
- zIndex={8}
669
- name="SRV-01"
670
- subLabel="APP SERVER"
671
- status={status}
672
- cpuLoad={cpuLoad}
673
- memLoad={memLoad}
674
- dialogMetrics={[
675
- { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: cpuLoad },
676
- { id: "mem", label: "MEMORY", sublabel: "HEAP", value: memLoad },
677
- {
678
- id: "iops",
679
- label: "IOPS",
680
- sublabel: "DISK OPS",
681
- value: iops,
682
- icon: "disk",
683
- warnAt: 50,
684
- critAt: 80,
685
- },
686
- {
687
- id: "threads",
688
- label: "THREADS",
689
- sublabel: "ACTIVE",
690
- value: threadCount,
691
- icon: "cpu",
692
- warnAt: 60,
693
- critAt: 85,
694
- },
695
- ]}
696
- alert={{ offsetX: -160, offsetY: -60, align: "left" }}
697
- />
698
- </Service>
699
- );
700
- }
701
-
702
- // 2. Bind each prop to a live query
703
- <AIOPsDashboard
704
- liveData={true}
705
- dataEndpoint="https://prometheus.example.com/api/v1/query"
706
- dataBindings={{
707
- "My Service": {
708
- cpuLoad: 'cpu_usage{instance="srv-01"}',
709
- memLoad: 'memory_usage{instance="srv-01"}',
710
- iops: 'disk_iops{instance="srv-01"}',
711
- threadCount: 'active_threads{instance="srv-01"}',
712
- status: { query: 'cpu_usage{instance="srv-01"}', transform: statusFromValue },
713
- },
714
- }}
715
- services={[{ name: "My Service", status: "online" }]}
716
- >
717
- <MyService name="My Service" />
718
- </AIOPsDashboard>;
719
- ```
720
-
721
- The dashboard injects `cpuLoad`, `memLoad`, `iops`, and `threadCount` as live props into `MyService`. Those flow into the `dialogMetrics` array, so the component dialog gauges update automatically on every poll cycle. This works for any number of custom metrics — just add a prop for each one.
722
-
723
- ---
724
-
725
- ### 4. Alerts — automatic threshold detection
726
-
727
- Nodes **automatically** show alert callouts when metrics breach thresholds:
728
-
729
- - **Warning** at 70% (orange callout)
730
- - **Critical** at 85% (red callout)
731
-
732
- This works from `cpuLoad`, `memLoad`, `traffic`, `queueDepth`, or `capacity` — no extra code needed:
733
-
734
- ```tsx
735
- // This server shows a critical alert automatically because cpuLoad > 85
736
- <ServerNode
737
- ex={200}
738
- ey={380}
739
- compactOffset={{ x: -30, y: -20 }}
740
- zIndex={8}
741
- name="SRV-01"
742
- subLabel="PROCESSOR"
743
- status="online"
744
- cpuLoad={92}
745
- memLoad={64}
746
- />
747
- ```
748
-
749
- **Position the callout:**
750
-
751
- ```tsx
752
- <ServerNode
753
- cpuLoad={92}
754
- alert={{ offsetX: -160, offsetY: -60, align: "left" }}
755
- ...
756
- />
757
- ```
758
-
759
- **Custom alert message:**
760
-
761
- ```tsx
762
- <ServerNode
763
- cpuLoad={92}
764
- alert={{ msg: "CPU overload — scale out", offsetX: -160, offsetY: -60, align: "left" }}
765
- ...
766
- />
767
- ```
768
-
769
- **Custom thresholds** via `dialogMetrics`:
770
-
771
- ```tsx
772
- <ServerNode
773
- dialogMetrics={[
774
- { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: 67,
775
- warnAt: 50, critAt: 75 }, // Alert triggers at 67% because critAt is 75
776
- ]}
777
- alert={{ offsetX: -160, offsetY: -60, align: "left" }}
778
- ...
779
- />
780
- ```
781
-
782
- Alert `align` options: `"left"`, `"right"`, `"top"`, `"bottom"`.
783
-
784
- ---
785
-
786
- ### 5. Drill-down internals — custom sub-components
787
-
788
- When you click a node in expanded view, a **drill-down** opens showing internal sub-components (CPUs, memory sticks, drives, etc.). By default these are auto-generated based on node type.
789
-
790
- Provide **custom sub-components** via the `subComponents` prop:
791
-
792
- ```tsx
793
- import { ServerNode, CPU3D, Memory3D, DriveBay3D, ThreadPool3D } from "react-dashstream";
794
- import type { SubComponentConfig } from "react-dashstream";
795
-
796
- const internals: SubComponentConfig[] = [
797
- {
798
- id: "cpu-0",
799
- label: "CPU-0",
800
- status: "online",
801
- element: <CPU3D label="CPU-0" load={67} color="#00e5ff" />,
802
- },
803
- {
804
- id: "cpu-1",
805
- label: "CPU-1",
806
- status: "warning",
807
- element: <CPU3D label="CPU-1" load={88} color="#ff8c00" status="warning" />,
808
- },
809
- {
810
- id: "heap",
811
- label: "HEAP",
812
- status: "online",
813
- element: <Memory3D label="HEAP" usedPercent={72} color="#8855ee" />,
814
- },
815
- {
816
- id: "drive",
817
- label: "DRIVE-0",
818
- status: "online",
819
- element: <DriveBay3D label="DRIVE-0" color="#00e5ff" activity={true} />,
820
- },
821
- {
822
- id: "threads",
823
- label: "THREADS",
824
- status: "online",
825
- element: <ThreadPool3D label="THREADS" color="#00e5ff" />,
826
- },
827
- ];
828
-
829
- <ServerNode
830
- ex={200}
831
- ey={380}
832
- compactOffset={{ x: -30, y: -20 }}
833
- zIndex={8}
834
- name="SRV-01"
835
- subLabel="APP SERVER"
836
- status="online"
837
- cpuLoad={67}
838
- memLoad={72}
839
- subComponents={internals}
840
- />;
841
- ```
842
-
843
- Each `SubComponentConfig`:
844
-
845
- ```ts
846
- interface SubComponentConfig {
847
- id: string; // Unique key
848
- label: string; // Display name
849
- status: ComponentStatus; // Drives LED color
850
- detail?: string; // Shown on fault
851
- element: ReactNode; // The 3D element to render
852
- }
853
- ```
854
-
855
- Available internal 3D components: `CPU3D`, `Memory3D`, `DriveBay3D`, `NetworkBlock3D`, `ThreadPool3D`, `Platter3D`, `Port3D`.
856
-
857
- ---
858
-
859
- ### 6. Graph series custom sparklines
860
-
861
- The drill-down also shows a **historical graph panel** with sparklines. By default, graphs are auto-generated from mock data.
862
-
863
- Provide **custom graph data** via the `graphSeries` prop:
864
-
865
- ```tsx
866
- import type { GraphSeries } from "react-dashstream";
867
-
868
- const graphs: GraphSeries[] = [
869
- { id: "cpu", label: "CPU-0", unit: "%", color: "#00e5ff", data: [45, 52, 67, 71, 68, 55, 62] },
870
- { id: "mem", label: "HEAP", unit: "%", color: "#8855ee", data: [60, 65, 72, 78, 82, 75, 70] },
871
- { id: "iops", label: "DISK", unit: "k/s", color: "#ff8c00", data: [12, 15, 22, 18, 25, 20, 16] },
872
- ];
873
-
874
- <ServerNode
875
- ex={200}
876
- ey={380}
877
- compactOffset={{ x: -30, y: -20 }}
878
- zIndex={8}
879
- name="SRV-01"
880
- subLabel="APP SERVER"
881
- status="online"
882
- cpuLoad={67}
883
- memLoad={72}
884
- graphSeries={graphs}
885
- />;
886
- ```
887
-
888
- Each `GraphSeries`:
889
-
890
- ```ts
891
- interface GraphSeries {
892
- id: string; // Unique key
893
- label: string; // Label above the sparkline
894
- unit: string; // Suffix (e.g. "%", "kbps")
895
- color: string; // Sparkline color
896
- data: number[]; // Data points (most recent last)
897
- }
898
- ```
899
-
900
- ---
901
-
902
- ### 7. Putting it all together
903
-
904
- A complete example with live data, service dialog metrics, component dialog gauges, sub-components, graphs, and alert positioning:
905
-
906
- ```tsx
907
- import "react-dashstream/dist/index.css";
908
- import { AIOPsDashboard, Service, ServerNode, DatabaseNode, CPU3D, Memory3D } from "react-dashstream";
909
- import type { ServiceMeta, DataBindings, ServiceMetricBinding } from "react-dashstream";
910
-
911
- const services: ServiceMeta[] = [
912
- {
913
- name: "My Service",
914
- status: "online",
915
- metrics: [{ label: "Uptime", value: "99.99%", color: "#00ff88" }],
916
- alerts: [{ level: "info", message: "All Systems Nominal" }],
917
- },
918
- ];
919
-
920
- function statusFromValue(raw: unknown) {
921
- const n = Number(raw);
922
- if (n >= 85) return "critical";
923
- if (n >= 70) return "warning";
924
- return "online";
925
- }
926
-
927
- const dataBindings: DataBindings = {
928
- "My Service": {
929
- cpuLoad: 'cpu_usage{instance="srv-01"}',
930
- memLoad: 'memory_usage{instance="srv-01"}',
931
- status: { query: 'cpu_usage{instance="srv-01"}', transform: statusFromValue },
932
- dbCapacity: 'disk_capacity{instance="db-01"}',
933
- dbStatus: { query: 'disk_capacity{instance="db-01"}', transform: statusFromValue },
934
- },
935
- };
936
-
937
- const serviceDataBindings: Record<string, ServiceMetricBinding[]> = {
938
- "My Service": [
939
- { label: "CPU Load", query: 'cpu_usage{instance="srv-01"}', unit: "%", color: "#00e5ff" },
940
- { label: "Memory", query: 'memory_usage{instance="srv-01"}', unit: "%", color: "#bb55ff" },
941
- { label: "Disk", query: 'disk_capacity{instance="db-01"}', unit: "%", color: "#ff8c00" },
942
- ],
943
- };
944
-
945
- function MyService({ name, status, cpuLoad, memLoad, dbCapacity, dbStatus }: any) {
946
- return (
947
- <Service
948
- name={name}
949
- status={status ?? "online"}
950
- connections={[
951
- { from: [330, 200], to: [200, 380], visibleAtPhase: 3 },
952
- { from: [330, 200], to: [460, 380], visibleAtPhase: 3 },
953
- ]}
954
- >
955
- <ServerNode
956
- ex={200}
957
- ey={380}
958
- compactOffset={{ x: -30, y: -20 }}
959
- zIndex={8}
960
- name="SRV-01"
961
- subLabel="APP SERVER"
962
- status={status ?? "online"}
963
- cpuLoad={cpuLoad ?? 42}
964
- memLoad={memLoad ?? 60}
965
- alert={{ offsetX: -160, offsetY: -60, align: "left" }}
966
- subComponents={[
967
- {
968
- id: "cpu",
969
- label: "CPU-0",
970
- status: "online",
971
- element: <CPU3D label="CPU-0" load={cpuLoad ?? 42} />,
972
- },
973
- {
974
- id: "mem",
975
- label: "HEAP",
976
- status: "online",
977
- element: <Memory3D label="HEAP" usedPercent={memLoad ?? 60} />,
978
- },
979
- ]}
980
- graphSeries={[
981
- { id: "cpu", label: "CPU", unit: "%", color: "#00e5ff", data: [45, 52, 67, 71, 68] },
982
- ]}
983
- />
984
- <DatabaseNode
985
- ex={460}
986
- ey={380}
987
- compactOffset={{ x: 30, y: -20 }}
988
- zIndex={7}
989
- name="DB-01"
990
- subLabel="PRIMARY"
991
- status={dbStatus ?? "online"}
992
- capacity={dbCapacity ?? 55}
993
- alert={{ offsetX: 160, offsetY: -60, align: "right" }}
994
- />
995
- </Service>
996
- );
997
- }
998
-
999
- export default function App() {
1000
- return (
1001
- <AIOPsDashboard
1002
- brandName="MY DASHBOARD"
1003
- services={services}
1004
- liveData={true}
1005
- dataEndpoint="https://prometheus.example.com/api/v1/query"
1006
- dataRefreshInterval={10000}
1007
- dataBindings={dataBindings}
1008
- serviceDataBindings={serviceDataBindings}
1009
- >
1010
- <MyService name="My Service" />
1011
- </AIOPsDashboard>
1012
- );
1013
- }
1014
- ```
1015
-
1016
- ---
1017
-
1018
- ### 8. Data hooks
1019
-
1020
- Access live data anywhere inside the dashboard tree:
1021
-
1022
- ```tsx
1023
- import { useAIOpsData, useAIOpsDataOptional, useQueryResult } from "react-dashstream";
1024
-
1025
- function CustomWidget() {
1026
- const { data, isRefreshing, lastRefreshError } = useAIOpsData();
1027
- const cpu = useQueryResult('cpu_usage{instance="srv-01"}');
1028
-
1029
- return (
1030
- <div>
1031
- {isRefreshing && <span>Refreshing...</span>}
1032
- {lastRefreshError && <span>{lastRefreshError}</span>}
1033
- <span>CPU: {String(cpu)}</span>
1034
- </div>
1035
- );
1036
- }
1037
- ```
1038
-
1039
- | Hook | Returns | Throws? |
1040
- | ------------------------ | -------------------------- | ---------------------------- |
1041
- | `useAIOpsData()` | Full `DataContextValue` | Yes — outside `DataProvider` |
1042
- | `useAIOpsDataOptional()` | `DataContextValue \| null` | No |
1043
- | `useQueryResult(query)` | Raw response or `null` | Yes — outside `DataProvider` |
1044
-
1045
- ### 9. Standalone DataProvider
1046
-
1047
- Use the data layer without the full dashboard shell:
1048
-
1049
- ```tsx
1050
- import { DataProvider, useAIOpsData } from "react-dashstream";
1051
-
1052
- function MyApp() {
1053
- return (
1054
- <DataProvider
1055
- config={{
1056
- endpoint: "https://prometheus.example.com/api/v1/query",
1057
- queries: ['cpu_usage{instance="srv-01"}'],
1058
- refreshInterval: 15000,
1059
- }}
1060
- >
1061
- <MyContent />
1062
- </DataProvider>
1063
- );
1064
- }
1065
- ```
1066
-
1067
- ---
1068
-
1069
- ## Components
1070
-
1071
- ### Layout
1072
-
1073
- | Component | Description |
1074
- | ---------------- | -------------------------------------------------------------------------------------------------------------- |
1075
- | `AIOPsDashboard` | Full dashboard shell — header, carousel, and state management. Drop services in as children. |
1076
- | `Service` | Service container — positions child nodes on a 3D orbit and draws connection lines between them. |
1077
- | `ServiceNode` | Low-level positioned wrapper with floating animation, scan line, and labels. Used by the compound nodes below. |
1078
-
1079
- ### Compound nodes (recommended)
1080
-
1081
- These combine `ServiceNode` + 3D model + `componentInfo` into a single element:
1082
-
1083
- | Component | Key props | Description |
1084
- | ------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
1085
- | `ServerNode` | `status`, `cpuLoad`, `memLoad`, `brandLabel`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | App server tower with LEDs and CPU/memory bars. |
1086
- | `DatabaseNode` | `status`, `capacity`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Three-platter database cylinder with capacity bar. |
1087
- | `WebDispatcherNode` | `status`, `traffic`, `activeRoutes`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Network appliance with 8 port LEDs and traffic metrics. |
1088
- | `MessageServerNode` | `status`, `queueDepth`, `msgsPerSec`, `instances`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Message server with instance LEDs and queue metrics. |
1089
- | `HumanNode` | `status`, `scale` | SVG wireframe person icon for user/actor nodes. |
1090
-
1091
- All compound nodes share: `ex`, `ey`, `compactOffset`, `zIndex`, `name`, `subLabel`, `color`, `delay`, `visibleAtPhase`.
1092
-
1093
- ### 3D models (low-level)
1094
-
1095
- If you need full control, use the raw 3D models inside a `ServiceNode`:
1096
-
1097
- `Server3D`, `Database3D`, `WebDispatcher3D`, `MessageServer3D`, `Human3D`
1098
-
1099
- All 3D models accept: `rotateX`, `rotateY`, `rotateZ`, `scale`, `autoRotate`.
1100
-
1101
- ### Status indicators
1102
-
1103
- | Component | Props | Description |
1104
- | --------------- | ------------------------------------ | ---------------------------------------------------------------- |
1105
- | `SyncBridge` | `synced`, `latencyMs` | Database replication bridge between primary and standby. |
1106
- | `NodeCallout` | `status`, `title`, `msg`, `ex`, `ey` | Alert callout with leader line (auto-rendered by `ServiceNode`). |
1107
- | `HoloBase` | `size`, `color`, `widthRatio` | Neon holographic base platform (auto-rendered by `Service`). |
1108
- | `SvgConnection` | `x1`, `y1`, `x2`, `y2`, `show` | Animated dashed SVG connection line. |
1109
-
1110
- ### Dialogs
1111
-
1112
- | Component | Description |
1113
- | ----------------- | ----------------------------------------------------------------------------------------------- |
1114
- | `ServiceDialog` | Service-level stats panel — shows metrics and alerts. Auto-rendered when a service is expanded. |
1115
- | `ComponentDialog` | Component drill-down with sub-component internals and sparkline graphs. |
1116
-
1117
- ### Drill-down internals
1118
-
1119
- Rendered inside `ComponentDialog` when a component is inspected:
1120
-
1121
- `CPU3D`, `Memory3D`, `DriveBay3D`, `NetworkBlock3D`, `ThreadPool3D`, `Platter3D`, `Port3D`, `HistoricalGraphPanel`, `ComponentDrillView`
1122
-
1123
- ### Pre-built services
1124
-
1125
- | Component | Topology |
1126
- | ----------------- | --------------------------------------------------------------------------------- |
1127
- | `SAPService` | Users Web Dispatcher + Message Server 3 App Servers Primary DB + Standby DB |
1128
- | `ExchangeService` | Users Dispatcher 3 App Servers Primary DB + Standby DB |
1129
-
1130
- ---
1131
-
1132
- ## Building a custom service
1133
-
1134
- Compose compound nodes inside a `Service` container. Each node needs:
1135
-
1136
- - **`ex`, `ey`** Position in the expanded topology (pixels from top-left of scene).
1137
- - **`compactOffset`** Offset from the service center in the compact carousel view.
1138
- - **`zIndex`** Stacking order (higher tiers get higher values).
1139
- - **`visibleAtPhase`** When the node fades in during expansion (0–6). Use `2` for top-tier, `3` for middle, etc.
1140
-
1141
- Define connection lines between nodes via the `connections` prop on `Service`:
1142
-
1143
- ```tsx
1144
- connections={[
1145
- { from: [x1, y1], to: [x2, y2], visibleAtPhase: 3 },
1146
- { from: [x1, y1], to: [x2, y2], visibleAtPhase: 4, color: "#ff8c00" },
1147
- ]}
1148
- ```
1149
-
1150
- ---
1151
-
1152
- ## Status types
1153
-
1154
- ```ts
1155
- type ComponentStatus = "online" | "warning" | "critical" | "offline";
1156
- ```
1157
-
1158
- | Status | Color | Glow |
1159
- | ---------- | --------- | -------- |
1160
- | `online` | `#00e5ff` | cyan |
1161
- | `warning` | `#ff8c00` | orange |
1162
- | `critical` | `#ff2255` | red |
1163
- | `offline` | `#1e3a5a` | dim blue |
1164
-
1165
- ---
1166
-
1167
- ## Theme constants
1168
-
1169
- ```ts
1170
- import {
1171
- STATUS_CFG,
1172
- HOLO_CYAN,
1173
- HOLO_BLUE,
1174
- HOLO_SURFACE,
1175
- HOLO_GLASS,
1176
- makeFaceStyles,
1177
- ThemeProvider,
1178
- useTheme,
1179
- type DashboardTheme,
1180
- } from "react-dashstream";
1181
- ```
1182
-
1183
- - `STATUS_CFG` — status-to-color lookup table
1184
- - `HOLO_CYAN` / `HOLO_BLUE` — accent colors
1185
- - `HOLO_SURFACE` / `HOLO_GLASS` — CSS gradient backgrounds for 3D faces
1186
- - `makeFaceStyles(W, H, D)` generates CSS transforms for the 6 faces of a 3D box
1187
- - `ThemeProvider`, `useTheme`, `DashboardTheme` — light/dark for dashboard UI, dialogs, `EventView`, and credentials modal (see **Theme (light / dark)** above)
1188
-
1189
- ---
1190
-
1191
- ## License
1192
-
1193
- MIT
1
+ # React DashStream
2
+
3
+ Holographic 3D infrastructure monitoring dashboard for React. Pure CSS-3D — no WebGL, no canvas, no dependencies beyond React.
4
+
5
+ ```
6
+ npm install react-dashstream
7
+ ```
8
+
9
+ ---
10
+
11
+ ## Quick start
12
+
13
+ ```tsx
14
+ import "react-dashstream/dist/index.css";
15
+ import { AIOPsDashboard, Service, ServerNode, DatabaseNode } from "react-dashstream";
16
+ import type { ServiceMeta } from "react-dashstream";
17
+
18
+ const services: ServiceMeta[] = [
19
+ {
20
+ name: "My Service",
21
+ status: "online",
22
+ metrics: [
23
+ { label: "Service Health", value: "99.9%", color: "#00ff88" },
24
+ { label: "Avg Response Time", value: "14ms", color: "#00e5ff" },
25
+ ],
26
+ alerts: [{ level: "info", message: "All Systems Nominal" }],
27
+ },
28
+ ];
29
+
30
+ export default function App() {
31
+ return (
32
+ <AIOPsDashboard brandName="MY DASHBOARD" services={services}>
33
+ <Service
34
+ name="My Service"
35
+ status="online"
36
+ connections={[
37
+ { from: [330, 200], to: [200, 380], visibleAtPhase: 3 },
38
+ { from: [330, 200], to: [460, 380], visibleAtPhase: 3 },
39
+ ]}
40
+ >
41
+ <ServerNode
42
+ ex={200}
43
+ ey={380}
44
+ compactOffset={{ x: -30, y: -20 }}
45
+ zIndex={8}
46
+ name="SRV-01"
47
+ subLabel="APP SERVER"
48
+ status="online"
49
+ cpuLoad={42}
50
+ memLoad={60}
51
+ />
52
+ <DatabaseNode
53
+ ex={460}
54
+ ey={380}
55
+ compactOffset={{ x: 30, y: -20 }}
56
+ zIndex={7}
57
+ name="DB-01"
58
+ subLabel="PRIMARY"
59
+ status="online"
60
+ capacity={55}
61
+ />
62
+ </Service>
63
+ </AIOPsDashboard>
64
+ );
65
+ }
66
+ ```
67
+
68
+ Click a service to expand its topology. Click a component to drill into its internals.
69
+
70
+ ---
71
+
72
+ ## Theme (light / dark)
73
+
74
+ The dashboard ships with **light** and **dark** visual modes. `AIOPsDashboard` defaults to **light**; the header includes a control to toggle modes.
75
+
76
+ ### Background images
77
+
78
+ | Prop | When used |
79
+ | ---------------------- | -------------------------------------------------- |
80
+ | `backgroundImage` | Dark mode, and light mode if no light asset is set |
81
+ | `lightBackgroundImage` | Light mode when provided |
82
+
83
+ ### Controlled theme (sync with your app shell)
84
+
85
+ Pass **`theme`** and **`onThemeChange`** to drive the dashboard from parent state (for example, to keep tabs, sidebars, and `EventView` in sync):
86
+
87
+ ```tsx
88
+ import { useState } from "react";
89
+ import { AIOPsDashboard, ThemeProvider, type DashboardTheme, type ServiceMeta } from "react-dashstream";
90
+ import lightBg from "./light.webp";
91
+ import darkBg from "./dark.webp";
92
+
93
+ const services: ServiceMeta[] = [
94
+ /* …same shape as Quick start… */
95
+ ];
96
+
97
+ export default function App() {
98
+ const [theme, setTheme] = useState<DashboardTheme>("light");
99
+
100
+ return (
101
+ <ThemeProvider value={theme}>
102
+ <AIOPsDashboard
103
+ theme={theme}
104
+ onThemeChange={setTheme}
105
+ lightBackgroundImage={lightBg}
106
+ backgroundImage={darkBg}
107
+ brandName="MY DASHBOARD"
108
+ services={services}
109
+ >
110
+ {/* Service / node tree — see Quick start */}
111
+ </AIOPsDashboard>
112
+ </ThemeProvider>
113
+ );
114
+ }
115
+ ```
116
+
117
+ When both props are set, the dashboard is **controlled**; the header toggle calls `onThemeChange`. Omit them to use **internal** theme state.
118
+
119
+ ### `ThemeProvider`, `EventView`, and the credentials modal
120
+
121
+ **`ServiceDialog`**, **`ComponentDialog`**, **`EventView`**, and **`CredentialsModal`** (access-key prompt) read the active mode from **`useTheme()`**.
122
+
123
+ - Wrap any subtree that includes **`EventView`** or standalone credential prompts in **`ThemeProvider`** with the same `value` as your dashboard so the event console and lock screen match.
124
+ - Optionally pass **`theme="light"` \| `"dark"`** on **`EventView`** to override context (useful when embedding without a provider).
125
+
126
+ Exports: `ThemeProvider`, `useTheme`, type `DashboardTheme`.
127
+
128
+ ---
129
+
130
+ ## Full example — multi-service, multi-layer
131
+
132
+ See `example/Dashboard.tsx` in this package for a complete two-service example with Payment Gateway and Auth Service topologies rotating in a 3D carousel.
133
+
134
+ ---
135
+
136
+ ## Datacenter topology map (`DatacenterView`)
137
+
138
+ `DatacenterView` renders an SVG **topology** (or optional **geographic outline** if you pass an SVG path `d` string), **CSS 3D** datacenter markers, link lines, and a **zoom transition** into a nested **`AIOPsDashboard`** for the selected building. Define **multiple buildings** under one **`siteId`** to get a single site marker, aggregated KPIs, and a multi-building cluster. Metrics can stay **mock** (`dataCenters[].metrics`) or use **PromQL** via **`metricBindings`** (building id → metric key`DataBinding`) together with **`DataProvider`** and **`extractDatacenterMetricQueries`**.
139
+
140
+ The demo app (`src/App.tsx`) uses `example/SaudiMapView.tsx`, a thin wrapper around `DatacenterView`. Scenario data lives in `example/saudiMapDemoData.tsx`; optional mock metric sliders use `example/DemoMetricPanel.tsx` + `example/DemoMetricPanel.css` (not part of the published package).
141
+
142
+ **Agent-oriented reference:** see **`dashstream-skill.md`** (section **DatacenterView — topology / geography map**) for props, types, mock vs live data, and composition with `DataProvider`.
143
+
144
+ ### Minimal import
145
+
146
+ ```tsx
147
+ import "react-dashstream/dist/index.css";
148
+ import { DatacenterView, DataProvider, extractDatacenterMetricQueries } from "react-dashstream";
149
+ import type { DatacenterBuildingConfig, DatacenterMetricBindings } from "react-dashstream";
150
+
151
+ const dataCenters: DatacenterBuildingConfig[] = [
152
+ {
153
+ id: "dc-a",
154
+ name: "DC A",
155
+ subtitle: "Primary",
156
+ x: 30,
157
+ y: 40,
158
+ status: "online",
159
+ variant: "tower",
160
+ metrics: {
161
+ carbonEmissions: 100,
162
+ powerUtilization: 5000,
163
+ cooling: 1200,
164
+ pue: 1.2,
165
+ uptime: 99.9,
166
+ activeServers: 200,
167
+ temperature: 22,
168
+ networkThroughput: 100,
169
+ },
170
+ services: [],
171
+ renderServices: () => null,
172
+ },
173
+ ];
174
+
175
+ const metricBindings: DatacenterMetricBindings = {
176
+ "dc-a": { powerUtilization: 'avg(scrape_duration_seconds)' },
177
+ };
178
+
179
+ const queries = extractDatacenterMetricQueries(metricBindings);
180
+
181
+ <DataProvider config={{ endpoint: "https://prom.example.com/api/v1/query", queries }}>
182
+ <DatacenterView dataCenters={dataCenters} metricBindings={metricBindings} />
183
+ </DataProvider>;
184
+ ```
185
+
186
+ ---
187
+
188
+ ## Event Console
189
+
190
+ The `EventView` component provides a full-screen operations event console with severity filtering, sortable columns, search, and pagination — styled to match the holographic theme. It fetches events from an external API using the same `access-key` / `access-secret-key` authentication as the 3D dashboard.
191
+
192
+ ### Quick start — API mode
193
+
194
+ ```tsx
195
+ import "react-dashstream/dist/index.css";
196
+ import { EventView } from "react-dashstream";
197
+ import type { EventApiConfig } from "react-dashstream";
198
+
199
+ const eventApiConfig: EventApiConfig = {
200
+ baseUrl: "https://your-monitoring-server.example.com",
201
+ // endpoint defaults to "/tsws/monitoring/api/v1.0/events/search"
202
+ payload: {
203
+ filter: {},
204
+ sortBy: "date_reception",
205
+ sortOrder: "DESC",
206
+ },
207
+ fieldMapping: {
208
+ id: "mc_ueid",
209
+ occurrence: "date_reception",
210
+ severityLastModified: "severity_last_modified",
211
+ severity: "severity",
212
+ owner: "owner",
213
+ class: "class",
214
+ host: "mc_host",
215
+ message: "msg",
216
+ remedySupportGroup: "ara_remedy_support_group",
217
+ incidentId: "ara_incident_id",
218
+ smsStatus: "ara_sms_status",
219
+ onCallNumber: "ara_on_call_number",
220
+ hostedApplication: "ara_hosted_application",
221
+ monitoringCategory: "ara_hosted_app_monitoring",
222
+ applicationSupportUnit: "ara_application_support_unit",
223
+ },
224
+ };
225
+
226
+ export default function Events() {
227
+ return <EventView apiConfig={eventApiConfig} />;
228
+ }
229
+ ```
230
+
231
+ On first load a credentials modal appears (same as the 3D dashboard). After authentication, EventView sends a `POST` to `{baseUrl}/tsws/monitoring/api/v1.0/events/search` with the payload and polls every 60 seconds.
232
+
233
+ ### How it works
234
+
235
+ 1. **Authentication** — uses `access-key` and `access-secret-key` HTTP headers, identical to the 3D dashboard. If EventView is inside a `DataProvider` (e.g. alongside `AIOPsDashboard` with `liveData`), it reuses those credentials automatically — no second login.
236
+ 2. **POST request** — sends `apiConfig.payload` as the JSON body.
237
+ 3. **Response format** expects:
238
+ ```json
239
+ {
240
+ "eventSeverityCount": { "MAJOR": 1, "CRITICAL": 2, "MINOR": 3 },
241
+ "totalCount": 6,
242
+ "eventList": [{ "mc_ueid": "...", "msg": "...", ... }]
243
+ }
244
+ ```
245
+ 4. **Field mapping** each object in `eventList` is mapped to an `AIOpsEvent` using `apiConfig.fieldMapping`. Every `AIOpsEvent` field must have a corresponding API field name.
246
+ 5. **Severity mapping** — raw severity strings (e.g. `"CRITICAL"`) are mapped to `EventSeverity` via `apiConfig.severityMap` (defaults to `{ CRITICAL: "Critical", MAJOR: "Major", MINOR: "Minor" }`).
247
+
248
+ ### Pre-fetched events mode
249
+
250
+ If you already have events data, pass them directly — no API call is made:
251
+
252
+ ```tsx
253
+ <EventView events={myEvents} title="My Event Console" />
254
+ ```
255
+
256
+ ### EventView props
257
+
258
+ | Prop | Type | Default | Description |
259
+ | -------------------- | --------------------- | ----------------- | ---------------------------------------------------------------------------- |
260
+ | `apiConfig` | `EventApiConfig` | | API configuration (base URL, payload, field mapping). Required for API mode |
261
+ | `events` | `AIOpsEvent[]` | | Pre-fetched events. When provided, `apiConfig` is not used |
262
+ | `credentials` | `Credentials` | | Explicit credentials. Falls back to DataProvider context, then modal |
263
+ | `columnWidthsCookie` | `string` | `"ev_col_widths"` | Cookie name used to persist user-defined column widths |
264
+ | `title` | `string` | `"Event Console"` | Title displayed in the component header |
265
+ | `theme` | `"light"` \| `"dark"` | — | Optional override; otherwise uses `ThemeProvider` / defaults to dark context |
266
+
267
+ ### EventApiConfig
268
+
269
+ | Field | Type | Default | Description |
270
+ | ----------------- | ------------------------------- | ---------------------------------------------------------- | ------------------------------------------------- |
271
+ | `baseUrl` | `string` | (required) | Protocol + host, e.g. `"https://mon.example.com"` |
272
+ | `endpoint` | `string` | `"/tsws/monitoring/api/v1.0/events/search"` | Path appended to `baseUrl` |
273
+ | `payload` | `Record<string, unknown>` | (required) | POST body sent with every request |
274
+ | `fieldMapping` | `EventFieldMapping` | (required) | Maps API field names `AIOpsEvent` fields |
275
+ | `severityMap` | `Record<string, EventSeverity>` | `{ CRITICAL: "Critical", MAJOR: "Major", MINOR: "Minor" }` | Maps raw severity `EventSeverity` |
276
+ | `refreshInterval` | `number` | `60000` | Polling interval in milliseconds |
277
+
278
+ ### Field mapping
279
+
280
+ The `fieldMapping` object maps every `AIOpsEvent` property to the key name the API returns. All fields are required:
281
+
282
+ ```ts
283
+ const fieldMapping: EventFieldMapping = {
284
+ id: "mc_ueid", // unique event identifier
285
+ occurrence: "date_reception", // datetime
286
+ severityLastModified: "severity_last_modified",
287
+ severity: "severity", // mapped through severityMap
288
+ owner: "owner",
289
+ class: "class", // Self-monitoring, SCOM, etc.
290
+ host: "mc_host",
291
+ message: "msg",
292
+ remedySupportGroup: "ara_remedy_support_group",
293
+ incidentId: "ara_incident_id",
294
+ smsStatus: "ara_sms_status", // "", "SUCCESS", "FAILED"
295
+ onCallNumber: "ara_on_call_number",
296
+ hostedApplication: "ara_hosted_application",
297
+ monitoringCategory: "ara_hosted_app_monitoring", // "24/7" or "Office Hours"
298
+ applicationSupportUnit: "ara_application_support_unit",
299
+ };
300
+ ```
301
+
302
+ ### Credential resolution order
303
+
304
+ 1. `credentials` prop (if provided)
305
+ 2. `DataProvider` context credentials (when inside `AIOPsDashboard` with `liveData`)
306
+ 3. Built-in credentials modal (same UI as the 3D dashboard)
307
+
308
+ ### Severity colors
309
+
310
+ | Severity | Color | Usage |
311
+ | -------- | --------- | ------------------ |
312
+ | Minor | `#ffb800` | Amber, low urgency |
313
+ | Major | `#ff6600` | Orange, attention |
314
+ | Critical | `#ff2255` | Red, immediate |
315
+
316
+ ### Features
317
+
318
+ - **Live API polling** — POST to configurable endpoint with auto-refresh
319
+ - **Field mapping** maps arbitrary API response keys to typed event columns
320
+ - **Shared authentication** — reuses credentials from `DataProvider` context when available
321
+ - **Severity filter chips** — toggle Critical, Major, Minor with live counts
322
+ - **Free-text search** filters across message, host, owner, and incident ID
323
+ - **Sortable columns** — click any header to cycle ascending / descending / none
324
+ - **Infinite scroll** — all matching events render in a single scrollable table
325
+ - **Resizable columns** drag column edges to resize; widths are persisted in cookies
326
+ - **Loading / error states** — spinner, error badge, last-refresh timestamp, manual refresh button
327
+ - **Theming** — dark holographic styling or **light** mode (panels, table, and credentials modal follow `ThemeProvider` / optional `theme` prop)
328
+
329
+ ---
330
+
331
+ ## Event-to-dashboard bridge
332
+
333
+ When the same `EventApiConfig` is passed to `AIOPsDashboard`, events are fetched in the background and **automatically highlight nodes** in the 3D topology whose hostname matches an open event. No extra code, no manual wiring — just pass the config.
334
+
335
+ ### Quick start
336
+
337
+ ```tsx
338
+ <AIOPsDashboard
339
+ brandName="MY DASHBOARD"
340
+ services={services}
341
+ liveData
342
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
343
+ dataBindings={dataBindings}
344
+ eventApiConfig={eventApiConfig} // same config used for EventView
345
+ >
346
+ <MyService name="My Service" />
347
+ </AIOPsDashboard>
348
+ ```
349
+
350
+ ### How it works
351
+
352
+ 1. `AIOPsDashboard` wraps its children in an `EventAlertsProvider` that fetches events in the background (using the same polling interval and credentials as the Event Console).
353
+ 2. Each fetched event has a `host` field (mapped via `fieldMapping`).
354
+ 3. Every `ServiceNode` in the topology checks if its `componentInfo.name` matches any event host (**case-insensitive**).
355
+ 4. If a match is found, the node's severity is updated and a `NodeCallout` appears with the event message.
356
+
357
+ ### Severity mapping
358
+
359
+ | Event severity | Dashboard status | Callout color |
360
+ | -------------- | ---------------- | ------------- |
361
+ | Critical | `"critical"` | Red |
362
+ | Major | `"warning"` | Orange |
363
+ | Minor | `"warning"` | Orange |
364
+
365
+ When multiple events exist on the same host, the highest severity wins and the callout shows `"N events – <message>"`.
366
+
367
+ The event alert is a **third severity source** alongside the node's own `status` prop and metric threshold breaches. The highest severity among all three always wins.
368
+
369
+ ### Node name matching
370
+
371
+ For the bridge to work, the `name` prop on your nodes must match the `host` field in the event data:
372
+
373
+ ```tsx
374
+ // If your events API returns host: "SAP-PRD-APP01", name your node:
375
+ <ServerNode name="SAP-PRD-APP01" ... />
376
+
377
+ // Matching is case-insensitive, so these also work:
378
+ <ServerNode name="sap-prd-app01" ... />
379
+ <ServerNode name="Sap-Prd-App01" ... />
380
+ ```
381
+
382
+ ### Credential resolution
383
+
384
+ The `EventAlertsProvider` inside `AIOPsDashboard` resolves credentials in this order:
385
+
386
+ 1. Parent `DataProvider` context (when `liveData` is enabled — **same credentials, single login**)
387
+ 2. Built-in credentials modal (when `liveData` is not enabled)
388
+
389
+ ### Opt-in behavior
390
+
391
+ - **Without `eventApiConfig`** — no events are fetched, no alerts are injected, zero overhead.
392
+ - **Without `EventView`** — the bridge still works. You can use event-based alerts on the 3D dashboard without ever rendering an `EventView`.
393
+
394
+ ---
395
+
396
+ ## External data source monitoring
397
+
398
+ DashStream can connect to any HTTP monitoring endpoint (Prometheus, Grafana, custom APIs) and feed live values into your dashboard. This section covers **every** data path — from node props to service dialogs, component dialogs, alerts, drill-down internals, and graphs.
399
+
400
+ ### How it works
401
+
402
+ 1. Set `liveData={true}` on `AIOPsDashboard` with `dataEndpoint` and `dataBindings`.
403
+ 2. A **credentials modal** appears on first load asking for `access-key` and `access-secret-key` (stored in memory only).
404
+ 3. The dashboard polls `GET <endpoint>?query=<encodedQuery>` for each unique query with credentials as HTTP headers.
405
+ 4. Resolved values are injected as **props** into child service components matched by `name`.
406
+ 5. The header shows **"DATA REFRESH FAILED"** if any queries fail.
407
+
408
+ ### Live data props
409
+
410
+ | Prop | Type | Default | Description |
411
+ | --------------------- | ---------------------------------------- | ------------- | ------------------------------------------------------ |
412
+ | `liveData` | `boolean` | `false` | Enable the live data pipeline. |
413
+ | `dataEndpoint` | `string` | — | Base URL. Queries sent as `GET <url>?query=<encoded>`. |
414
+ | `dataBindings` | `DataBindings` | — | Maps service → prop → query. See below. |
415
+ | `dataTransform` | `(raw) => unknown` | Numeric parse | Global transform for raw responses. |
416
+ | `dataRefreshInterval` | `number` | `60000` | Polling interval in ms. |
417
+ | `serviceDataBindings` | `Record<string, ServiceMetricBinding[]>` | — | Live metrics for the service stats dialog. |
418
+
419
+ ### Endpoint contract
420
+
421
+ | Aspect | Requirement |
422
+ | ------------ | --------------------------------------------------------------- |
423
+ | **Method** | `GET` |
424
+ | **URL** | `<dataEndpoint>?query=<urlEncodedQuery>` |
425
+ | **Headers** | `access-key` and `access-secret-key` |
426
+ | **Response** | Plain text body (trimmed). Default transform parses as number. |
427
+ | **Errors** | Non-2xx → counted as failure. Partial failures shown in header. |
428
+
429
+ For JSON responses (e.g. Prometheus), provide a custom `dataTransform`:
430
+
431
+ ```tsx
432
+ dataTransform={(raw) => {
433
+ const parsed = JSON.parse(String(raw));
434
+ return parsed?.data?.result?.[0]?.value?.[1] ?? raw;
435
+ }}
436
+ ```
437
+
438
+ ---
439
+
440
+ ### 1. Data bindings — injecting live props into nodes
441
+
442
+ `dataBindings` maps **service name → prop name → query**. Each key in the inner object must match a prop on your service component. The service name must match the child's `name` prop.
443
+
444
+ A binding can be a bare query string or an object with a custom transform:
445
+
446
+ ```ts
447
+ type DataBinding = string | { query: string; transform?: (raw: unknown) => unknown };
448
+ type DataBindings = Record<string, Record<string, DataBinding>>;
449
+ ```
450
+
451
+ **Example one service with a server and database:**
452
+
453
+ ```tsx
454
+ import "react-dashstream/dist/index.css";
455
+ import { AIOPsDashboard, Service, ServerNode, DatabaseNode } from "react-dashstream";
456
+
457
+ function statusFromValue(raw: unknown) {
458
+ const n = Number(raw);
459
+ if (n >= 85) return "critical";
460
+ if (n >= 70) return "warning";
461
+ return "online";
462
+ }
463
+
464
+ export default function App() {
465
+ return (
466
+ <AIOPsDashboard
467
+ brandName="LIVE DASHBOARD"
468
+ services={[{ name: "My Service", status: "online" }]}
469
+ liveData={true}
470
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
471
+ dataRefreshInterval={10000}
472
+ dataBindings={{
473
+ "My Service": {
474
+ // bare string parsed as number automatically
475
+ cpuLoad: 'cpu_usage{instance="srv-01"}',
476
+ memLoad: 'memory_usage{instance="srv-01"}',
477
+
478
+ // object with transform → convert number to status string
479
+ status: {
480
+ query: 'up{instance="srv-01"}',
481
+ transform: statusFromValue,
482
+ },
483
+
484
+ // bind database props too
485
+ dbCapacity: 'disk_capacity{instance="db-01"}',
486
+ dbStatus: {
487
+ query: 'disk_capacity{instance="db-01"}',
488
+ transform: statusFromValue,
489
+ },
490
+ },
491
+ }}
492
+ >
493
+ <MyService name="My Service" />
494
+ </AIOPsDashboard>
495
+ );
496
+ }
497
+ ```
498
+
499
+ The component receives `cpuLoad`, `memLoad`, `status`, `dbCapacity`, and `dbStatus` as live props, overriding their defaults.
500
+
501
+ #### Multi-component services
502
+
503
+ When a service has multiple nodes of the same type (e.g. three servers), use a flat naming convention with prefixes. Each binding maps one prop to one query — there is no array or object binding support.
504
+
505
+ ```tsx
506
+ // Service component — accepts prefixed props for each server
507
+ interface ServiceXProps {
508
+ name: string;
509
+ status?: ComponentStatus;
510
+ srv1CpuLoad?: number;
511
+ srv1MemLoad?: number;
512
+ srv1Status?: ComponentStatus;
513
+ srv2CpuLoad?: number;
514
+ srv2MemLoad?: number;
515
+ srv2Status?: ComponentStatus;
516
+ srv3CpuLoad?: number;
517
+ srv3MemLoad?: number;
518
+ srv3Status?: ComponentStatus;
519
+ dbCapacity?: number;
520
+ dbStatus?: ComponentStatus;
521
+ }
522
+
523
+ function ServiceX({
524
+ name, status = "online",
525
+ srv1CpuLoad = 54, srv1MemLoad = 58, srv1Status = "online",
526
+ srv2CpuLoad = 63, srv2MemLoad = 66, srv2Status = "online",
527
+ srv3CpuLoad = 78, srv3MemLoad = 71, srv3Status = "online",
528
+ dbCapacity = 68, dbStatus = "online",
529
+ }: ServiceXProps) {
530
+ return (
531
+ <Service name={name} status={status} connections={[/* ... */]}>
532
+ <ServerNode name="SRV-X1" status={srv1Status}
533
+ cpuLoad={srv1CpuLoad} memLoad={srv1MemLoad} ... />
534
+ <ServerNode name="SRV-X2" status={srv2Status}
535
+ cpuLoad={srv2CpuLoad} memLoad={srv2MemLoad} ... />
536
+ <ServerNode name="SRV-X3" status={srv3Status}
537
+ cpuLoad={srv3CpuLoad} memLoad={srv3MemLoad} ... />
538
+ <DatabaseNode name="DB-X1" status={dbStatus}
539
+ capacity={dbCapacity} ... />
540
+ </Service>
541
+ );
542
+ }
543
+
544
+ // Dashboard — bind each prefixed prop to its query
545
+ <AIOPsDashboard
546
+ liveData={true}
547
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
548
+ dataBindings={{
549
+ ServiceX: {
550
+ status: { query: 'status{instance="svcx"}', transform: statusFromValue },
551
+ srv1CpuLoad: 'cpu_usage{instance="srvx-01"}',
552
+ srv1MemLoad: 'memory_usage{instance="srvx-01"}',
553
+ srv1Status: { query: 'status{instance="srvx-01"}', transform: statusFromValue },
554
+ srv2CpuLoad: 'cpu_usage{instance="srvx-02"}',
555
+ srv2MemLoad: 'memory_usage{instance="srvx-02"}',
556
+ srv2Status: { query: 'status{instance="srvx-02"}', transform: statusFromValue },
557
+ srv3CpuLoad: 'cpu_usage{instance="srvx-03"}',
558
+ srv3MemLoad: 'memory_usage{instance="srvx-03"}',
559
+ srv3Status: { query: 'status{instance="srvx-03"}', transform: statusFromValue },
560
+ dbCapacity: 'disk_capacity{instance="dbx-01"}',
561
+ dbStatus: { query: 'status{instance="dbx-01"}', transform: statusFromValue },
562
+ },
563
+ }}
564
+ services={[{ name: "ServiceX", status: "online" }]}
565
+ >
566
+ <ServiceX name="ServiceX" />
567
+ </AIOPsDashboard>
568
+ ```
569
+
570
+ Use a consistent naming convention like `srv1CpuLoad`, `srv2CpuLoad`, etc. The dashboard injects each prefixed prop individually. The service component maps each prop to the correct node.
571
+
572
+ ---
573
+
574
+ ### 2. Service dialog — live KPI metrics
575
+
576
+ The **ServiceDialog** is the stats panel that appears when you click a service. It shows KPI rows and alerts.
577
+
578
+ **Static way** — pass `ServiceMeta` with hardcoded values:
579
+
580
+ ```tsx
581
+ const services: ServiceMeta[] = [
582
+ {
583
+ name: "My Service",
584
+ status: "online",
585
+ metrics: [
586
+ { label: "Uptime", value: "99.99%", color: "#00ff88" },
587
+ { label: "Avg Latency", value: "8ms", color: "#00e5ff" },
588
+ { label: "Error Rate", value: "0.02%", color: "#00ff88" },
589
+ ],
590
+ alerts: [{ level: "info", message: "All Systems Nominal" }],
591
+ },
592
+ ];
593
+ ```
594
+
595
+ **Live way** use `serviceDataBindings` to fetch values from your endpoint:
596
+
597
+ ```tsx
598
+ <AIOPsDashboard
599
+ services={[{ name: "My Service", status: "online" }]}
600
+ liveData={true}
601
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
602
+ dataBindings={{ /* ... */ }}
603
+ serviceDataBindings={{
604
+ "My Service": [
605
+ { label: "CPU Load", query: 'cpu_usage{instance="srv-01"}', unit: "%", color: "#00e5ff" },
606
+ { label: "Memory", query: 'memory_usage{instance="srv-01"}', unit: "%", color: "#bb55ff" },
607
+ { label: "Disk", query: 'disk_capacity{instance="db-01"}', unit: "%", color: "#ff8c00" },
608
+ ],
609
+ }}
610
+ >
611
+ ```
612
+
613
+ Each `ServiceMetricBinding`:
614
+
615
+ ```ts
616
+ interface ServiceMetricBinding {
617
+ label: string; // Row label
618
+ query: string; // PromQL query
619
+ unit?: string; // Suffix (e.g. "%", "ms")
620
+ color?: string; // Accent color
621
+ transform?: (raw: unknown) => string; // Custom formatter
622
+ }
623
+ ```
624
+
625
+ When `serviceDataBindings` is provided for a service, live values **replace** the static `ServiceMeta.metrics`.
626
+
627
+ ---
628
+
629
+ ### 3. Component dialog — custom gauges
630
+
631
+ The **ComponentDialog** appears when you click a node (server, database, etc.) inside an expanded service. By default it shows CPU, Memory, and Storage gauges derived from the node's props.
632
+
633
+ **Override these gauges** with the `dialogMetrics` prop on any compound node:
634
+
635
+ ```tsx
636
+ <ServerNode
637
+ ex={200}
638
+ ey={380}
639
+ compactOffset={{ x: -30, y: -20 }}
640
+ zIndex={8}
641
+ name="SRV-01"
642
+ subLabel="APP SERVER"
643
+ status="online"
644
+ cpuLoad={67}
645
+ memLoad={72}
646
+ dialogMetrics={[
647
+ { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: 67, unit: "%" },
648
+ { id: "mem", label: "MEMORY", sublabel: "HEAP USAGE", value: 72, unit: "%" },
649
+ { id: "iops", label: "IOPS", sublabel: "DISK OPS", value: 45, unit: "k/s", icon: "disk" },
650
+ {
651
+ id: "threads",
652
+ label: "THREADS",
653
+ sublabel: "ACTIVE",
654
+ value: 82,
655
+ unit: "%",
656
+ warnAt: 60,
657
+ critAt: 80,
658
+ icon: "cpu",
659
+ },
660
+ ]}
661
+ />
662
+ ```
663
+
664
+ Each `ComponentDialogMetric`:
665
+
666
+ ```ts
667
+ interface ComponentDialogMetric {
668
+ id: string; // Unique key
669
+ label: string; // Upper label (e.g. "CPU")
670
+ sublabel: string; // Lower label (e.g. "PROCESSOR")
671
+ value: number; // 0–100 gauge value
672
+ unit?: string; // Suffix (default "%")
673
+ icon?: "cpu" | "mem" | "disk"; // Gauge icon (default "cpu")
674
+ warnAt?: number; // Orange threshold (default 70)
675
+ critAt?: number; // Red threshold (default 85)
676
+ color?: string; // Override bar color (bypasses thresholds)
677
+ }
678
+ ```
679
+
680
+ All compound nodes (`ServerNode`, `DatabaseNode`, `WebDispatcherNode`, `MessageServerNode`) accept `dialogMetrics`.
681
+
682
+ **Live component dialog metrics** — to feed live values into custom gauges, expose each metric value as a prop on your service component and use `dataBindings` to inject them. Then build the `dialogMetrics` array from those live props:
683
+
684
+ ```tsx
685
+ // 1. Define your service component with props for each custom metric
686
+ import { Service, ServerNode } from "react-dashstream";
687
+ import type { ComponentStatus } from "react-dashstream";
688
+
689
+ interface MyServiceProps {
690
+ name: string;
691
+ status?: ComponentStatus;
692
+ cpuLoad?: number;
693
+ memLoad?: number;
694
+ iops?: number;
695
+ threadCount?: number;
696
+ }
697
+
698
+ function MyService({
699
+ name,
700
+ status = "online",
701
+ cpuLoad = 42,
702
+ memLoad = 60,
703
+ iops = 20,
704
+ threadCount = 45,
705
+ }: MyServiceProps) {
706
+ return (
707
+ <Service
708
+ name={name}
709
+ status={status}
710
+ connections={
711
+ [
712
+ /* ... */
713
+ ]
714
+ }
715
+ >
716
+ <ServerNode
717
+ ex={200}
718
+ ey={380}
719
+ compactOffset={{ x: -30, y: -20 }}
720
+ zIndex={8}
721
+ name="SRV-01"
722
+ subLabel="APP SERVER"
723
+ status={status}
724
+ cpuLoad={cpuLoad}
725
+ memLoad={memLoad}
726
+ dialogMetrics={[
727
+ { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: cpuLoad },
728
+ { id: "mem", label: "MEMORY", sublabel: "HEAP", value: memLoad },
729
+ {
730
+ id: "iops",
731
+ label: "IOPS",
732
+ sublabel: "DISK OPS",
733
+ value: iops,
734
+ icon: "disk",
735
+ warnAt: 50,
736
+ critAt: 80,
737
+ },
738
+ {
739
+ id: "threads",
740
+ label: "THREADS",
741
+ sublabel: "ACTIVE",
742
+ value: threadCount,
743
+ icon: "cpu",
744
+ warnAt: 60,
745
+ critAt: 85,
746
+ },
747
+ ]}
748
+ alert={{ offsetX: -160, offsetY: -60, align: "left" }}
749
+ />
750
+ </Service>
751
+ );
752
+ }
753
+
754
+ // 2. Bind each prop to a live query
755
+ <AIOPsDashboard
756
+ liveData={true}
757
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
758
+ dataBindings={{
759
+ "My Service": {
760
+ cpuLoad: 'cpu_usage{instance="srv-01"}',
761
+ memLoad: 'memory_usage{instance="srv-01"}',
762
+ iops: 'disk_iops{instance="srv-01"}',
763
+ threadCount: 'active_threads{instance="srv-01"}',
764
+ status: { query: 'cpu_usage{instance="srv-01"}', transform: statusFromValue },
765
+ },
766
+ }}
767
+ services={[{ name: "My Service", status: "online" }]}
768
+ >
769
+ <MyService name="My Service" />
770
+ </AIOPsDashboard>;
771
+ ```
772
+
773
+ The dashboard injects `cpuLoad`, `memLoad`, `iops`, and `threadCount` as live props into `MyService`. Those flow into the `dialogMetrics` array, so the component dialog gauges update automatically on every poll cycle. This works for any number of custom metrics — just add a prop for each one.
774
+
775
+ ---
776
+
777
+ ### 4. Alerts automatic threshold detection
778
+
779
+ Nodes **automatically** show alert callouts when metrics breach thresholds:
780
+
781
+ - **Warning** at 70% (orange callout)
782
+ - **Critical** at 85% (red callout)
783
+
784
+ This works from `cpuLoad`, `memLoad`, `traffic`, `queueDepth`, or `capacity` — no extra code needed:
785
+
786
+ ```tsx
787
+ // This server shows a critical alert automatically because cpuLoad > 85
788
+ <ServerNode
789
+ ex={200}
790
+ ey={380}
791
+ compactOffset={{ x: -30, y: -20 }}
792
+ zIndex={8}
793
+ name="SRV-01"
794
+ subLabel="PROCESSOR"
795
+ status="online"
796
+ cpuLoad={92}
797
+ memLoad={64}
798
+ />
799
+ ```
800
+
801
+ **Position the callout:**
802
+
803
+ ```tsx
804
+ <ServerNode
805
+ cpuLoad={92}
806
+ alert={{ offsetX: -160, offsetY: -60, align: "left" }}
807
+ ...
808
+ />
809
+ ```
810
+
811
+ **Custom alert message:**
812
+
813
+ ```tsx
814
+ <ServerNode
815
+ cpuLoad={92}
816
+ alert={{ msg: "CPU overload — scale out", offsetX: -160, offsetY: -60, align: "left" }}
817
+ ...
818
+ />
819
+ ```
820
+
821
+ **Custom thresholds** via `dialogMetrics`:
822
+
823
+ ```tsx
824
+ <ServerNode
825
+ dialogMetrics={[
826
+ { id: "cpu", label: "CPU", sublabel: "PROCESSOR", value: 67,
827
+ warnAt: 50, critAt: 75 }, // Alert triggers at 67% because critAt is 75
828
+ ]}
829
+ alert={{ offsetX: -160, offsetY: -60, align: "left" }}
830
+ ...
831
+ />
832
+ ```
833
+
834
+ Alert `align` options: `"left"`, `"right"`, `"top"`, `"bottom"`.
835
+
836
+ ---
837
+
838
+ ### 5. Drill-down internals — custom sub-components
839
+
840
+ When you click a node in expanded view, a **drill-down** opens showing internal sub-components (CPUs, memory sticks, drives, etc.). By default these are auto-generated based on node type.
841
+
842
+ Provide **custom sub-components** via the `subComponents` prop:
843
+
844
+ ```tsx
845
+ import { ServerNode, CPU3D, Memory3D, DriveBay3D, ThreadPool3D } from "react-dashstream";
846
+ import type { SubComponentConfig } from "react-dashstream";
847
+
848
+ const internals: SubComponentConfig[] = [
849
+ {
850
+ id: "cpu-0",
851
+ label: "CPU-0",
852
+ status: "online",
853
+ element: <CPU3D label="CPU-0" load={67} color="#00e5ff" />,
854
+ },
855
+ {
856
+ id: "cpu-1",
857
+ label: "CPU-1",
858
+ status: "warning",
859
+ element: <CPU3D label="CPU-1" load={88} color="#ff8c00" status="warning" />,
860
+ },
861
+ {
862
+ id: "heap",
863
+ label: "HEAP",
864
+ status: "online",
865
+ element: <Memory3D label="HEAP" usedPercent={72} color="#8855ee" />,
866
+ },
867
+ {
868
+ id: "drive",
869
+ label: "DRIVE-0",
870
+ status: "online",
871
+ element: <DriveBay3D label="DRIVE-0" color="#00e5ff" activity={true} />,
872
+ },
873
+ {
874
+ id: "threads",
875
+ label: "THREADS",
876
+ status: "online",
877
+ element: <ThreadPool3D label="THREADS" color="#00e5ff" />,
878
+ },
879
+ ];
880
+
881
+ <ServerNode
882
+ ex={200}
883
+ ey={380}
884
+ compactOffset={{ x: -30, y: -20 }}
885
+ zIndex={8}
886
+ name="SRV-01"
887
+ subLabel="APP SERVER"
888
+ status="online"
889
+ cpuLoad={67}
890
+ memLoad={72}
891
+ subComponents={internals}
892
+ />;
893
+ ```
894
+
895
+ Each `SubComponentConfig`:
896
+
897
+ ```ts
898
+ interface SubComponentConfig {
899
+ id: string; // Unique key
900
+ label: string; // Display name
901
+ status: ComponentStatus; // Drives LED color
902
+ detail?: string; // Shown on fault
903
+ element: ReactNode; // The 3D element to render
904
+ }
905
+ ```
906
+
907
+ Available internal 3D components: `CPU3D`, `Memory3D`, `DriveBay3D`, `NetworkBlock3D`, `ThreadPool3D`, `Platter3D`, `Port3D`.
908
+
909
+ ---
910
+
911
+ ### 6. Graph series — custom sparklines
912
+
913
+ The drill-down also shows a **historical graph panel** with sparklines. By default, graphs are auto-generated from mock data.
914
+
915
+ Provide **custom graph data** via the `graphSeries` prop:
916
+
917
+ ```tsx
918
+ import type { GraphSeries } from "react-dashstream";
919
+
920
+ const graphs: GraphSeries[] = [
921
+ { id: "cpu", label: "CPU-0", unit: "%", color: "#00e5ff", data: [45, 52, 67, 71, 68, 55, 62] },
922
+ { id: "mem", label: "HEAP", unit: "%", color: "#8855ee", data: [60, 65, 72, 78, 82, 75, 70] },
923
+ { id: "iops", label: "DISK", unit: "k/s", color: "#ff8c00", data: [12, 15, 22, 18, 25, 20, 16] },
924
+ ];
925
+
926
+ <ServerNode
927
+ ex={200}
928
+ ey={380}
929
+ compactOffset={{ x: -30, y: -20 }}
930
+ zIndex={8}
931
+ name="SRV-01"
932
+ subLabel="APP SERVER"
933
+ status="online"
934
+ cpuLoad={67}
935
+ memLoad={72}
936
+ graphSeries={graphs}
937
+ />;
938
+ ```
939
+
940
+ Each `GraphSeries`:
941
+
942
+ ```ts
943
+ interface GraphSeries {
944
+ id: string; // Unique key
945
+ label: string; // Label above the sparkline
946
+ unit: string; // Suffix (e.g. "%", "kbps")
947
+ color: string; // Sparkline color
948
+ data: number[]; // Data points (most recent last)
949
+ }
950
+ ```
951
+
952
+ ---
953
+
954
+ ### 7. Putting it all together
955
+
956
+ A complete example with live data, service dialog metrics, component dialog gauges, sub-components, graphs, and alert positioning:
957
+
958
+ ```tsx
959
+ import "react-dashstream/dist/index.css";
960
+ import { AIOPsDashboard, Service, ServerNode, DatabaseNode, CPU3D, Memory3D } from "react-dashstream";
961
+ import type { ServiceMeta, DataBindings, ServiceMetricBinding } from "react-dashstream";
962
+
963
+ const services: ServiceMeta[] = [
964
+ {
965
+ name: "My Service",
966
+ status: "online",
967
+ metrics: [{ label: "Uptime", value: "99.99%", color: "#00ff88" }],
968
+ alerts: [{ level: "info", message: "All Systems Nominal" }],
969
+ },
970
+ ];
971
+
972
+ function statusFromValue(raw: unknown) {
973
+ const n = Number(raw);
974
+ if (n >= 85) return "critical";
975
+ if (n >= 70) return "warning";
976
+ return "online";
977
+ }
978
+
979
+ const dataBindings: DataBindings = {
980
+ "My Service": {
981
+ cpuLoad: 'cpu_usage{instance="srv-01"}',
982
+ memLoad: 'memory_usage{instance="srv-01"}',
983
+ status: { query: 'cpu_usage{instance="srv-01"}', transform: statusFromValue },
984
+ dbCapacity: 'disk_capacity{instance="db-01"}',
985
+ dbStatus: { query: 'disk_capacity{instance="db-01"}', transform: statusFromValue },
986
+ },
987
+ };
988
+
989
+ const serviceDataBindings: Record<string, ServiceMetricBinding[]> = {
990
+ "My Service": [
991
+ { label: "CPU Load", query: 'cpu_usage{instance="srv-01"}', unit: "%", color: "#00e5ff" },
992
+ { label: "Memory", query: 'memory_usage{instance="srv-01"}', unit: "%", color: "#bb55ff" },
993
+ { label: "Disk", query: 'disk_capacity{instance="db-01"}', unit: "%", color: "#ff8c00" },
994
+ ],
995
+ };
996
+
997
+ function MyService({ name, status, cpuLoad, memLoad, dbCapacity, dbStatus }: any) {
998
+ return (
999
+ <Service
1000
+ name={name}
1001
+ status={status ?? "online"}
1002
+ connections={[
1003
+ { from: [330, 200], to: [200, 380], visibleAtPhase: 3 },
1004
+ { from: [330, 200], to: [460, 380], visibleAtPhase: 3 },
1005
+ ]}
1006
+ >
1007
+ <ServerNode
1008
+ ex={200}
1009
+ ey={380}
1010
+ compactOffset={{ x: -30, y: -20 }}
1011
+ zIndex={8}
1012
+ name="SRV-01"
1013
+ subLabel="APP SERVER"
1014
+ status={status ?? "online"}
1015
+ cpuLoad={cpuLoad ?? 42}
1016
+ memLoad={memLoad ?? 60}
1017
+ alert={{ offsetX: -160, offsetY: -60, align: "left" }}
1018
+ subComponents={[
1019
+ {
1020
+ id: "cpu",
1021
+ label: "CPU-0",
1022
+ status: "online",
1023
+ element: <CPU3D label="CPU-0" load={cpuLoad ?? 42} />,
1024
+ },
1025
+ {
1026
+ id: "mem",
1027
+ label: "HEAP",
1028
+ status: "online",
1029
+ element: <Memory3D label="HEAP" usedPercent={memLoad ?? 60} />,
1030
+ },
1031
+ ]}
1032
+ graphSeries={[
1033
+ { id: "cpu", label: "CPU", unit: "%", color: "#00e5ff", data: [45, 52, 67, 71, 68] },
1034
+ ]}
1035
+ />
1036
+ <DatabaseNode
1037
+ ex={460}
1038
+ ey={380}
1039
+ compactOffset={{ x: 30, y: -20 }}
1040
+ zIndex={7}
1041
+ name="DB-01"
1042
+ subLabel="PRIMARY"
1043
+ status={dbStatus ?? "online"}
1044
+ capacity={dbCapacity ?? 55}
1045
+ alert={{ offsetX: 160, offsetY: -60, align: "right" }}
1046
+ />
1047
+ </Service>
1048
+ );
1049
+ }
1050
+
1051
+ export default function App() {
1052
+ return (
1053
+ <AIOPsDashboard
1054
+ brandName="MY DASHBOARD"
1055
+ services={services}
1056
+ liveData={true}
1057
+ dataEndpoint="https://prometheus.example.com/api/v1/query"
1058
+ dataRefreshInterval={10000}
1059
+ dataBindings={dataBindings}
1060
+ serviceDataBindings={serviceDataBindings}
1061
+ >
1062
+ <MyService name="My Service" />
1063
+ </AIOPsDashboard>
1064
+ );
1065
+ }
1066
+ ```
1067
+
1068
+ ---
1069
+
1070
+ ### 8. Data hooks
1071
+
1072
+ Access live data anywhere inside the dashboard tree:
1073
+
1074
+ ```tsx
1075
+ import { useAIOpsData, useAIOpsDataOptional, useQueryResult } from "react-dashstream";
1076
+
1077
+ function CustomWidget() {
1078
+ const { data, isRefreshing, lastRefreshError } = useAIOpsData();
1079
+ const cpu = useQueryResult('cpu_usage{instance="srv-01"}');
1080
+
1081
+ return (
1082
+ <div>
1083
+ {isRefreshing && <span>Refreshing...</span>}
1084
+ {lastRefreshError && <span>{lastRefreshError}</span>}
1085
+ <span>CPU: {String(cpu)}</span>
1086
+ </div>
1087
+ );
1088
+ }
1089
+ ```
1090
+
1091
+ | Hook | Returns | Throws? |
1092
+ | ------------------------ | -------------------------- | ---------------------------- |
1093
+ | `useAIOpsData()` | Full `DataContextValue` | Yes — outside `DataProvider` |
1094
+ | `useAIOpsDataOptional()` | `DataContextValue \| null` | No |
1095
+ | `useQueryResult(query)` | Raw response or `null` | Yes outside `DataProvider` |
1096
+
1097
+ ### 9. Standalone DataProvider
1098
+
1099
+ Use the data layer without the full dashboard shell:
1100
+
1101
+ ```tsx
1102
+ import { DataProvider, useAIOpsData } from "react-dashstream";
1103
+
1104
+ function MyApp() {
1105
+ return (
1106
+ <DataProvider
1107
+ config={{
1108
+ endpoint: "https://prometheus.example.com/api/v1/query",
1109
+ queries: ['cpu_usage{instance="srv-01"}'],
1110
+ refreshInterval: 15000,
1111
+ }}
1112
+ >
1113
+ <MyContent />
1114
+ </DataProvider>
1115
+ );
1116
+ }
1117
+ ```
1118
+
1119
+ ---
1120
+
1121
+ ## Components
1122
+
1123
+ ### Layout
1124
+
1125
+ | Component | Description |
1126
+ | ---------------- | -------------------------------------------------------------------------------------------------------------- |
1127
+ | `AIOPsDashboard` | Full dashboard shell header, carousel, and state management. Drop services in as children. |
1128
+ | `Service` | Service container positions child nodes on a 3D orbit and draws connection lines between them. |
1129
+ | `ServiceNode` | Low-level positioned wrapper with floating animation, scan line, and labels. Used by the compound nodes below. |
1130
+
1131
+ ### Compound nodes (recommended)
1132
+
1133
+ These combine `ServiceNode` + 3D model + `componentInfo` into a single element:
1134
+
1135
+ | Component | Key props | Description |
1136
+ | ------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
1137
+ | `ServerNode` | `status`, `cpuLoad`, `memLoad`, `brandLabel`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | App server tower with LEDs and CPU/memory bars. |
1138
+ | `DatabaseNode` | `status`, `capacity`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Three-platter database cylinder with capacity bar. |
1139
+ | `WebDispatcherNode` | `status`, `traffic`, `activeRoutes`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Network appliance with 8 port LEDs and traffic metrics. |
1140
+ | `MessageServerNode` | `status`, `queueDepth`, `msgsPerSec`, `instances`, `dialogMetrics`, `subComponents`, `graphSeries`, `alert` | Message server with instance LEDs and queue metrics. |
1141
+ | `HumanNode` | `status`, `scale` | SVG wireframe person icon for user/actor nodes. |
1142
+
1143
+ All compound nodes share: `ex`, `ey`, `compactOffset`, `zIndex`, `name`, `subLabel`, `color`, `delay`, `visibleAtPhase`.
1144
+
1145
+ ### 3D models (low-level)
1146
+
1147
+ If you need full control, use the raw 3D models inside a `ServiceNode`:
1148
+
1149
+ `Server3D`, `Database3D`, `WebDispatcher3D`, `MessageServer3D`, `Human3D`
1150
+
1151
+ All 3D models accept: `rotateX`, `rotateY`, `rotateZ`, `scale`, `autoRotate`.
1152
+
1153
+ ### Status indicators
1154
+
1155
+ | Component | Props | Description |
1156
+ | --------------- | ------------------------------------ | ---------------------------------------------------------------- |
1157
+ | `SyncBridge` | `synced`, `latencyMs` | Database replication bridge between primary and standby. |
1158
+ | `NodeCallout` | `status`, `title`, `msg`, `ex`, `ey` | Alert callout with leader line (auto-rendered by `ServiceNode`). |
1159
+ | `HoloBase` | `size`, `color`, `widthRatio` | Neon holographic base platform (auto-rendered by `Service`). |
1160
+ | `SvgConnection` | `x1`, `y1`, `x2`, `y2`, `show` | Animated dashed SVG connection line. |
1161
+
1162
+ ### Dialogs
1163
+
1164
+ | Component | Description |
1165
+ | ----------------- | ----------------------------------------------------------------------------------------------- |
1166
+ | `ServiceDialog` | Service-level stats panel — shows metrics and alerts. Auto-rendered when a service is expanded. |
1167
+ | `ComponentDialog` | Component drill-down with sub-component internals and sparkline graphs. |
1168
+
1169
+ ### Drill-down internals
1170
+
1171
+ Rendered inside `ComponentDialog` when a component is inspected:
1172
+
1173
+ `CPU3D`, `Memory3D`, `DriveBay3D`, `NetworkBlock3D`, `ThreadPool3D`, `Platter3D`, `Port3D`, `HistoricalGraphPanel`, `ComponentDrillView`
1174
+
1175
+ ### Pre-built services
1176
+
1177
+ | Component | Topology |
1178
+ | ----------------- | --------------------------------------------------------------------------------- |
1179
+ | `SAPService` | Users → Web Dispatcher + Message Server → 3 App Servers → Primary DB + Standby DB |
1180
+ | `ExchangeService` | Users → Dispatcher → 3 App Servers → Primary DB + Standby DB |
1181
+
1182
+ ---
1183
+
1184
+ ## Building a custom service
1185
+
1186
+ Compose compound nodes inside a `Service` container. Each node needs:
1187
+
1188
+ - **`ex`, `ey`** — Position in the expanded topology (pixels from top-left of scene).
1189
+ - **`compactOffset`** — Offset from the service center in the compact carousel view.
1190
+ - **`zIndex`** — Stacking order (higher tiers get higher values).
1191
+ - **`visibleAtPhase`** — When the node fades in during expansion (0–6). Use `2` for top-tier, `3` for middle, etc.
1192
+
1193
+ Define connection lines between nodes via the `connections` prop on `Service`:
1194
+
1195
+ ```tsx
1196
+ connections={[
1197
+ { from: [x1, y1], to: [x2, y2], visibleAtPhase: 3 },
1198
+ { from: [x1, y1], to: [x2, y2], visibleAtPhase: 4, color: "#ff8c00" },
1199
+ ]}
1200
+ ```
1201
+
1202
+ ---
1203
+
1204
+ ## Status types
1205
+
1206
+ ```ts
1207
+ type ComponentStatus = "online" | "warning" | "critical" | "offline";
1208
+ ```
1209
+
1210
+ | Status | Color | Glow |
1211
+ | ---------- | --------- | -------- |
1212
+ | `online` | `#00e5ff` | cyan |
1213
+ | `warning` | `#ff8c00` | orange |
1214
+ | `critical` | `#ff2255` | red |
1215
+ | `offline` | `#1e3a5a` | dim blue |
1216
+
1217
+ ---
1218
+
1219
+ ## Theme constants
1220
+
1221
+ ```ts
1222
+ import {
1223
+ STATUS_CFG,
1224
+ HOLO_CYAN,
1225
+ HOLO_BLUE,
1226
+ HOLO_SURFACE,
1227
+ HOLO_GLASS,
1228
+ makeFaceStyles,
1229
+ ThemeProvider,
1230
+ useTheme,
1231
+ type DashboardTheme,
1232
+ } from "react-dashstream";
1233
+ ```
1234
+
1235
+ - `STATUS_CFG` — status-to-color lookup table
1236
+ - `HOLO_CYAN` / `HOLO_BLUE` — accent colors
1237
+ - `HOLO_SURFACE` / `HOLO_GLASS` — CSS gradient backgrounds for 3D faces
1238
+ - `makeFaceStyles(W, H, D)` — generates CSS transforms for the 6 faces of a 3D box
1239
+ - `ThemeProvider`, `useTheme`, `DashboardTheme` — light/dark for dashboard UI, dialogs, `EventView`, and credentials modal (see **Theme (light / dark)** above)
1240
+
1241
+ ---
1242
+
1243
+ ## License
1244
+
1245
+ MIT