@oneuptime/common 10.0.71 → 10.0.72

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.
Files changed (97) hide show
  1. package/Models/DatabaseModels/Alert.ts +55 -0
  2. package/Models/DatabaseModels/Incident.ts +55 -0
  3. package/Models/DatabaseModels/StatusPage.ts +80 -0
  4. package/Server/API/StatusPageAPI.ts +4 -0
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.ts +41 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.ts +25 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  8. package/Server/Services/AnalyticsDatabaseService.ts +17 -7
  9. package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +175 -29
  10. package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +71 -0
  11. package/Server/Utils/Monitor/MonitorAlert.ts +91 -7
  12. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +171 -2
  13. package/Server/Utils/Monitor/MonitorIncident.ts +133 -8
  14. package/Server/Utils/Monitor/MonitorMetricUtil.ts +423 -1
  15. package/Server/Utils/Monitor/MonitorResource.ts +2 -0
  16. package/Server/Utils/Monitor/MonitorTemplateUtil.ts +99 -0
  17. package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.ts +268 -0
  18. package/Types/Infrastructure/BasicMetrics.ts +75 -0
  19. package/Types/Metrics/MetricQueryData.ts +11 -0
  20. package/Types/Monitor/CriteriaFilter.ts +10 -0
  21. package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +11 -0
  22. package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +10 -0
  23. package/Types/Monitor/MetricMonitor/MetricSeriesResult.ts +20 -0
  24. package/Types/Monitor/MonitorMetricType.ts +34 -0
  25. package/Types/Monitor/ServerMonitor/ServerMonitorResponse.ts +8 -0
  26. package/Types/Probe/ProbeApiIngestResponse.ts +25 -0
  27. package/Types/StatusPage/StatusPageLanguage.ts +29 -0
  28. package/UI/Components/Charts/Area/AreaChart.tsx +17 -12
  29. package/UI/Components/Charts/Bar/BarChart.tsx +16 -11
  30. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +23 -0
  31. package/UI/Components/Charts/Line/LineChart.tsx +16 -11
  32. package/UI/Components/Filters/FiltersForm.tsx +26 -2
  33. package/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.ts +453 -0
  34. package/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.tsx +229 -0
  35. package/Utils/Metrics/MetricSeriesFingerprint.ts +97 -0
  36. package/Utils/Monitor/MonitorMetricType.ts +309 -19
  37. package/build/dist/Models/DatabaseModels/Alert.js +57 -0
  38. package/build/dist/Models/DatabaseModels/Alert.js.map +1 -1
  39. package/build/dist/Models/DatabaseModels/Incident.js +57 -0
  40. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  41. package/build/dist/Models/DatabaseModels/StatusPage.js +82 -0
  42. package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
  43. package/build/dist/Server/API/StatusPageAPI.js +4 -0
  44. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  45. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js +22 -0
  46. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776940714709-MigrationName.js.map +1 -0
  47. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js +14 -0
  48. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776971364783-AddStatusPageLanguageSettings.js.map +1 -0
  49. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  50. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  51. package/build/dist/Server/Services/AnalyticsDatabaseService.js +14 -4
  52. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  53. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +132 -30
  54. package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js.map +1 -1
  55. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +58 -7
  56. package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
  57. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +66 -12
  58. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  59. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +112 -0
  60. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  61. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +91 -15
  62. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  63. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js +373 -0
  64. package/build/dist/Server/Utils/Monitor/MonitorMetricUtil.js.map +1 -1
  65. package/build/dist/Server/Utils/Monitor/MonitorResource.js +2 -0
  66. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  67. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js +65 -0
  68. package/build/dist/Server/Utils/Monitor/MonitorTemplateUtil.js.map +1 -1
  69. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js +199 -0
  70. package/build/dist/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.test.js.map +1 -1
  71. package/build/dist/Types/Monitor/CriteriaFilter.js +10 -0
  72. package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
  73. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js +2 -0
  74. package/build/dist/Types/Monitor/MetricMonitor/MetricSeriesResult.js.map +1 -0
  75. package/build/dist/Types/Monitor/MonitorMetricType.js +28 -0
  76. package/build/dist/Types/Monitor/MonitorMetricType.js.map +1 -1
  77. package/build/dist/Types/StatusPage/StatusPageLanguage.js +21 -0
  78. package/build/dist/Types/StatusPage/StatusPageLanguage.js.map +1 -0
  79. package/build/dist/UI/Components/Charts/Area/AreaChart.js +13 -12
  80. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  81. package/build/dist/UI/Components/Charts/Bar/BarChart.js +12 -11
  82. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  83. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +11 -3
  84. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  85. package/build/dist/UI/Components/Charts/Line/LineChart.js +12 -11
  86. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  87. package/build/dist/UI/Components/Filters/FiltersForm.js +6 -2
  88. package/build/dist/UI/Components/Filters/FiltersForm.js.map +1 -1
  89. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js +383 -0
  90. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesCatalog.js.map +1 -0
  91. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js +109 -0
  92. package/build/dist/UI/Components/MonitorTemplateVariables/TemplateVariablesModal.js.map +1 -0
  93. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js +81 -0
  94. package/build/dist/Utils/Metrics/MetricSeriesFingerprint.js.map +1 -0
  95. package/build/dist/Utils/Monitor/MonitorMetricType.js +287 -19
  96. package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
  97. package/package.json +1 -1
@@ -0,0 +1,453 @@
1
+ import MonitorType from "../../../Types/Monitor/MonitorType";
2
+
3
+ export interface TemplateVariable {
4
+ /** The placeholder users type, e.g. "monitorName" → renders as {{monitorName}}. */
5
+ key: string;
6
+ /** One-line explanation of what the variable resolves to at render time. */
7
+ description: string;
8
+ /** Example rendered value, shown in the "Example" column so users can picture the output. */
9
+ example?: string | undefined;
10
+ }
11
+
12
+ export interface TemplateVariableGroup {
13
+ title: string;
14
+ description?: string | undefined;
15
+ variables: Array<TemplateVariable>;
16
+ }
17
+
18
+ /**
19
+ * Single source of truth for the variables available to incident/alert
20
+ * template strings (`{{…}}`), mirroring what
21
+ * `MonitorTemplateUtil.buildTemplateStorageMap` exposes on the server.
22
+ *
23
+ * Grouped by monitor type so the template-variables modal can render a
24
+ * categorized, skimmable list that matches the actual monitor the user
25
+ * is editing — users don't see SSL fields while configuring a Metric
26
+ * monitor, and vice versa.
27
+ *
28
+ * Keep in sync with `Common/Server/Utils/Monitor/MonitorTemplateUtil.ts`.
29
+ */
30
+ export default class TemplateVariablesCatalog {
31
+ public static getVariables(input: {
32
+ monitorType: MonitorType;
33
+ /**
34
+ * Attribute keys the user has configured on the metric query
35
+ * (e.g. ["host.name", "region"]). For metric/kubernetes/docker
36
+ * monitors, these become per-series template variables — one
37
+ * incident fires per unique value combination, and each incident
38
+ * can reference the label values via `{{host.name}}` etc.
39
+ */
40
+ seriesAttributeKeys?: Array<string> | undefined;
41
+ }): Array<TemplateVariableGroup> {
42
+ const groups: Array<TemplateVariableGroup> = [];
43
+
44
+ groups.push(TemplateVariablesCatalog.monitorIdentityGroup());
45
+
46
+ const perTypeGroup: TemplateVariableGroup | null =
47
+ TemplateVariablesCatalog.perTypeGroup(input.monitorType);
48
+ if (perTypeGroup) {
49
+ groups.push(perTypeGroup);
50
+ }
51
+
52
+ if (
53
+ input.monitorType === MonitorType.Metrics ||
54
+ input.monitorType === MonitorType.Kubernetes ||
55
+ input.monitorType === MonitorType.Docker
56
+ ) {
57
+ groups.push(
58
+ TemplateVariablesCatalog.seriesLabelsGroup(input.seriesAttributeKeys),
59
+ );
60
+ }
61
+
62
+ return groups;
63
+ }
64
+
65
+ private static monitorIdentityGroup(): TemplateVariableGroup {
66
+ return {
67
+ title: "Monitor",
68
+ description:
69
+ "Identity of the monitor that triggered the incident or alert.",
70
+ variables: [
71
+ {
72
+ key: "monitorName",
73
+ description: "Human-readable name of the monitor.",
74
+ example: "Production API",
75
+ },
76
+ {
77
+ key: "monitorId",
78
+ description: "UUID of the monitor.",
79
+ example: "a0f78958-da0a-4775-9fd9-c9fc63d3456f",
80
+ },
81
+ ],
82
+ };
83
+ }
84
+
85
+ private static seriesLabelsGroup(
86
+ attributeKeys: Array<string> | undefined,
87
+ ): TemplateVariableGroup {
88
+ const keys: Array<string> = attributeKeys || [];
89
+
90
+ if (keys.length === 0) {
91
+ return {
92
+ title: "Series Labels (per-host / per-container)",
93
+ description:
94
+ "When you configure 'Group By' attributes on the metric query, this monitor fires one incident per unique group (e.g. one per host). Each group's label values become template variables here. Add attributes under 'Group By' to see variables.",
95
+ variables: [],
96
+ };
97
+ }
98
+
99
+ return {
100
+ title: "Series Labels (per-host / per-container)",
101
+ description:
102
+ "One incident fires per unique combination of these values. Reference the triggering series' values in titles, descriptions, and remediation notes.",
103
+ variables: keys.map((key: string): TemplateVariable => {
104
+ return {
105
+ key,
106
+ description: `Value of \`${key}\` for the series that breached the threshold.`,
107
+ example:
108
+ key === "host.name"
109
+ ? "prod-db-01"
110
+ : key === "resource.k8s.container.name"
111
+ ? "mariadb"
112
+ : undefined,
113
+ };
114
+ }),
115
+ };
116
+ }
117
+
118
+ private static perTypeGroup(
119
+ monitorType: MonitorType,
120
+ ): TemplateVariableGroup | null {
121
+ switch (monitorType) {
122
+ case MonitorType.API:
123
+ case MonitorType.Website:
124
+ return {
125
+ title: "Response",
126
+ description:
127
+ "Details of the HTTP response from the monitored endpoint.",
128
+ variables: [
129
+ {
130
+ key: "isOnline",
131
+ description: "True if the endpoint responded successfully.",
132
+ example: "true",
133
+ },
134
+ {
135
+ key: "responseStatusCode",
136
+ description: "HTTP status code returned.",
137
+ example: "503",
138
+ },
139
+ {
140
+ key: "responseTimeInMs",
141
+ description: "Round-trip response time in milliseconds.",
142
+ example: "812",
143
+ },
144
+ {
145
+ key: "responseBody",
146
+ description:
147
+ "Parsed response body. Use dot paths like `{{responseBody.data.status}}` for JSON responses.",
148
+ example: '{"status":"degraded"}',
149
+ },
150
+ {
151
+ key: "responseHeaders",
152
+ description:
153
+ "Response headers. Use `{{responseHeaders.content-type}}` for a specific header.",
154
+ },
155
+ ],
156
+ };
157
+
158
+ case MonitorType.IncomingRequest:
159
+ return {
160
+ title: "Incoming Request",
161
+ variables: [
162
+ {
163
+ key: "requestMethod",
164
+ description: "HTTP method of the incoming request.",
165
+ example: "POST",
166
+ },
167
+ {
168
+ key: "requestBody",
169
+ description:
170
+ "Parsed request body. Use dot paths like `{{requestBody.event.type}}`.",
171
+ },
172
+ {
173
+ key: "requestHeaders",
174
+ description: "Request headers.",
175
+ },
176
+ {
177
+ key: "incomingRequestReceivedAt",
178
+ description: "Timestamp the request was received.",
179
+ },
180
+ ],
181
+ };
182
+
183
+ case MonitorType.Ping:
184
+ case MonitorType.IP:
185
+ case MonitorType.Port:
186
+ return {
187
+ title: "Connectivity",
188
+ variables: [
189
+ {
190
+ key: "isOnline",
191
+ description: "True if the target responded.",
192
+ example: "false",
193
+ },
194
+ {
195
+ key: "responseTimeInMs",
196
+ description: "Response time in milliseconds.",
197
+ example: "42",
198
+ },
199
+ {
200
+ key: "isTimeout",
201
+ description: "True if the check timed out.",
202
+ },
203
+ {
204
+ key: "failureCause",
205
+ description: "Reason the check failed, if any.",
206
+ example: "Connection refused",
207
+ },
208
+ ],
209
+ };
210
+
211
+ case MonitorType.SSLCertificate:
212
+ return {
213
+ title: "SSL Certificate",
214
+ variables: [
215
+ {
216
+ key: "commonName",
217
+ description: "Common name (CN) of the certificate.",
218
+ example: "*.example.com",
219
+ },
220
+ { key: "organization", description: "Issuing organization." },
221
+ { key: "organizationalUnit", description: "Organizational unit." },
222
+ { key: "locality", description: "Locality (city)." },
223
+ { key: "state", description: "State / region." },
224
+ { key: "country", description: "Country code." },
225
+ {
226
+ key: "expiresAt",
227
+ description: "Certificate expiration date.",
228
+ example: "2026-07-14T00:00:00.000Z",
229
+ },
230
+ { key: "createdAt", description: "Issue date." },
231
+ {
232
+ key: "isSelfSigned",
233
+ description: "True if the certificate is self-signed.",
234
+ },
235
+ { key: "serialNumber", description: "Serial number." },
236
+ { key: "fingerprint", description: "SHA-1 fingerprint." },
237
+ { key: "fingerprint256", description: "SHA-256 fingerprint." },
238
+ {
239
+ key: "isOnline",
240
+ description: "True if the SSL endpoint responded.",
241
+ },
242
+ {
243
+ key: "failureCause",
244
+ description: "Reason the SSL check failed, if any.",
245
+ },
246
+ ],
247
+ };
248
+
249
+ case MonitorType.Server:
250
+ return {
251
+ title: "Server Metrics",
252
+ description: "Basic host metrics reported by the OneUptime agent.",
253
+ variables: [
254
+ {
255
+ key: "hostname",
256
+ description: "Host name reported by the agent.",
257
+ example: "web-03.prod",
258
+ },
259
+ {
260
+ key: "cpuUsagePercent",
261
+ description: "Current CPU usage, 0-100.",
262
+ example: "87",
263
+ },
264
+ { key: "cpuCores", description: "Number of CPU cores." },
265
+ {
266
+ key: "memoryUsagePercent",
267
+ description: "Current memory usage, 0-100.",
268
+ example: "74",
269
+ },
270
+ {
271
+ key: "memoryFreePercent",
272
+ description: "Free memory percentage.",
273
+ },
274
+ {
275
+ key: "memoryTotalBytes",
276
+ description: "Total memory on the host.",
277
+ },
278
+ {
279
+ key: "diskMetrics",
280
+ description:
281
+ "Array of disks with {diskPath, usagePercent, freePercent, totalBytes}. Iterate with Handlebars or index: `{{diskMetrics.0.usagePercent}}`.",
282
+ },
283
+ {
284
+ key: "processes",
285
+ description:
286
+ "Array of {pid, name, command} for tracked processes.",
287
+ },
288
+ { key: "requestReceivedAt", description: "Heartbeat timestamp." },
289
+ {
290
+ key: "failureCause",
291
+ description: "Failure reason if the heartbeat failed.",
292
+ },
293
+ ],
294
+ };
295
+
296
+ case MonitorType.CustomJavaScriptCode:
297
+ return {
298
+ title: "Custom Code",
299
+ variables: [
300
+ {
301
+ key: "result",
302
+ description:
303
+ "Return value of your script. Dot-accessible if it's an object: `{{result.status}}`.",
304
+ },
305
+ {
306
+ key: "executionTimeInMs",
307
+ description: "Script runtime in milliseconds.",
308
+ },
309
+ {
310
+ key: "scriptError",
311
+ description: "Runtime error message, if any.",
312
+ },
313
+ {
314
+ key: "logMessages",
315
+ description: "Array of `console.log` output from the script.",
316
+ },
317
+ {
318
+ key: "failureCause",
319
+ description: "High-level failure reason.",
320
+ },
321
+ ],
322
+ };
323
+
324
+ case MonitorType.SyntheticMonitor:
325
+ return {
326
+ title: "Synthetic Monitor",
327
+ description:
328
+ "One entry per browser × screen-size run. Use `{{syntheticResponses.0.scriptError}}` or a `{{#each syntheticResponses}}` loop.",
329
+ variables: [
330
+ {
331
+ key: "syntheticResponses",
332
+ description:
333
+ "Array of {result, scriptError, executionTimeInMs, logMessages, screenshots, browserType, screenSizeType} — one per run.",
334
+ },
335
+ {
336
+ key: "failureCause",
337
+ description: "Overall failure reason if all runs failed.",
338
+ },
339
+ ],
340
+ };
341
+
342
+ case MonitorType.SNMP:
343
+ return {
344
+ title: "SNMP",
345
+ variables: [
346
+ {
347
+ key: "isOnline",
348
+ description: "True if the SNMP agent responded.",
349
+ },
350
+ {
351
+ key: "responseTimeInMs",
352
+ description: "Response time in milliseconds.",
353
+ },
354
+ {
355
+ key: "isTimeout",
356
+ description: "True if the SNMP request timed out.",
357
+ },
358
+ {
359
+ key: "failureCause",
360
+ description: "Failure reason.",
361
+ },
362
+ {
363
+ key: "oidResponses",
364
+ description:
365
+ "Array of {oid, name, value, type}. Each named OID is also exposed directly — e.g. a `.sysUpTime` OID resolves as `{{sysUpTime}}`.",
366
+ },
367
+ ],
368
+ };
369
+
370
+ case MonitorType.DNS:
371
+ return {
372
+ title: "DNS",
373
+ variables: [
374
+ { key: "isOnline", description: "True if the DNS query resolved." },
375
+ { key: "responseTimeInMs", description: "Query time." },
376
+ { key: "isTimeout", description: "True if the query timed out." },
377
+ { key: "isDnssecValid", description: "DNSSEC validation result." },
378
+ { key: "failureCause", description: "Failure reason." },
379
+ {
380
+ key: "records",
381
+ description: "Array of {type, value, ttl}.",
382
+ },
383
+ {
384
+ key: "recordValues",
385
+ description: "Flat array of record values for quick display.",
386
+ },
387
+ ],
388
+ };
389
+
390
+ case MonitorType.Domain:
391
+ return {
392
+ title: "Domain",
393
+ variables: [
394
+ { key: "isOnline", description: "True if WHOIS lookup succeeded." },
395
+ { key: "domainName", description: "Domain queried." },
396
+ { key: "registrar", description: "Registrar name." },
397
+ { key: "createdDate", description: "Domain registration date." },
398
+ { key: "updatedDate", description: "Last WHOIS update." },
399
+ { key: "expiresDate", description: "Expiration date." },
400
+ { key: "nameServers", description: "Array of nameservers." },
401
+ { key: "domainStatus", description: "EPP status codes." },
402
+ { key: "dnssec", description: "DNSSEC status." },
403
+ { key: "responseTimeInMs", description: "Lookup time." },
404
+ { key: "failureCause", description: "Failure reason." },
405
+ ],
406
+ };
407
+
408
+ case MonitorType.ExternalStatusPage:
409
+ return {
410
+ title: "External Status Page",
411
+ variables: [
412
+ {
413
+ key: "overallStatus",
414
+ description: "Overall status reported by the status page.",
415
+ example: "major_outage",
416
+ },
417
+ {
418
+ key: "activeIncidentCount",
419
+ description: "Number of active incidents on the status page.",
420
+ },
421
+ {
422
+ key: "componentStatuses",
423
+ description: "Array of {name, status, description}.",
424
+ },
425
+ {
426
+ key: "isOnline",
427
+ description: "True if the status page responded.",
428
+ },
429
+ { key: "responseTimeInMs", description: "Fetch time." },
430
+ { key: "failureCause", description: "Failure reason." },
431
+ ],
432
+ };
433
+
434
+ case MonitorType.Metrics:
435
+ case MonitorType.Kubernetes:
436
+ case MonitorType.Docker:
437
+ return {
438
+ title: "Metric",
439
+ variables: [
440
+ {
441
+ key: "metricName",
442
+ description:
443
+ "The OTel metric name the monitor is watching (e.g. container.cpu.time).",
444
+ example: "container.cpu.time",
445
+ },
446
+ ],
447
+ };
448
+
449
+ default:
450
+ return null;
451
+ }
452
+ }
453
+ }
@@ -0,0 +1,229 @@
1
+ import Modal, { ModalWidth } from "../Modal/Modal";
2
+ import Icon, { SizeProp } from "../Icon/Icon";
3
+ import IconProp from "../../../Types/Icon/IconProp";
4
+ import MonitorType from "../../../Types/Monitor/MonitorType";
5
+ import React, {
6
+ FunctionComponent,
7
+ ReactElement,
8
+ useMemo,
9
+ useState,
10
+ } from "react";
11
+ import TemplateVariablesCatalog, {
12
+ TemplateVariable,
13
+ TemplateVariableGroup,
14
+ } from "./TemplateVariablesCatalog";
15
+
16
+ export interface ComponentProps {
17
+ monitorType: MonitorType;
18
+ /**
19
+ * Group-by attribute keys from the metric query, if any. Used to
20
+ * populate the "Series Labels" section with per-host / per-container
21
+ * variables like `{{host.name}}`.
22
+ */
23
+ seriesAttributeKeys?: Array<string> | undefined;
24
+ onClose: () => void;
25
+ }
26
+
27
+ /**
28
+ * Modal that surfaces the dynamic template variables available to the
29
+ * current monitor's incident/alert titles, descriptions, and
30
+ * remediation notes. Organized by monitor type so a metric monitor
31
+ * doesn't show SSL variables (and vice versa), with a live search
32
+ * that scales when a user groups by many attributes.
33
+ *
34
+ * Click a variable to copy `{{var}}` to the clipboard; the chip flips
35
+ * to a "Copied" state briefly so the user gets feedback without a
36
+ * toast.
37
+ */
38
+ const TemplateVariablesModal: FunctionComponent<ComponentProps> = (
39
+ props: ComponentProps,
40
+ ): ReactElement => {
41
+ const [search, setSearch] = useState<string>("");
42
+ const [copiedKey, setCopiedKey] = useState<string | null>(null);
43
+
44
+ const groups: Array<TemplateVariableGroup> = useMemo(() => {
45
+ return TemplateVariablesCatalog.getVariables({
46
+ monitorType: props.monitorType,
47
+ seriesAttributeKeys: props.seriesAttributeKeys,
48
+ });
49
+ }, [props.monitorType, props.seriesAttributeKeys]);
50
+
51
+ const normalized: string = search.trim().toLowerCase();
52
+
53
+ const filteredGroups: Array<TemplateVariableGroup> = useMemo(() => {
54
+ if (!normalized) {
55
+ return groups;
56
+ }
57
+ return groups
58
+ .map((group: TemplateVariableGroup): TemplateVariableGroup => {
59
+ return {
60
+ ...group,
61
+ variables: group.variables.filter((v: TemplateVariable) => {
62
+ return (
63
+ v.key.toLowerCase().includes(normalized) ||
64
+ v.description.toLowerCase().includes(normalized)
65
+ );
66
+ }),
67
+ };
68
+ })
69
+ .filter((group: TemplateVariableGroup) => {
70
+ return group.variables.length > 0;
71
+ });
72
+ }, [groups, normalized]);
73
+
74
+ const copyToClipboard: (key: string) => void = (key: string): void => {
75
+ const token: string = `{{${key}}}`;
76
+ if (
77
+ typeof navigator !== "undefined" &&
78
+ navigator.clipboard &&
79
+ typeof navigator.clipboard.writeText === "function"
80
+ ) {
81
+ void navigator.clipboard.writeText(token);
82
+ }
83
+ setCopiedKey(key);
84
+ setTimeout(() => {
85
+ setCopiedKey((current: string | null): string | null => {
86
+ return current === key ? null : current;
87
+ });
88
+ }, 1500);
89
+ };
90
+
91
+ const renderVariableRow: (v: TemplateVariable) => ReactElement = (
92
+ v: TemplateVariable,
93
+ ): ReactElement => {
94
+ const isCopied: boolean = copiedKey === v.key;
95
+ return (
96
+ <button
97
+ key={v.key}
98
+ type="button"
99
+ onClick={(): void => {
100
+ copyToClipboard(v.key);
101
+ }}
102
+ className="group w-full rounded-md border border-gray-200 bg-white px-3 py-2.5 text-left transition hover:border-indigo-300 hover:bg-indigo-50/40"
103
+ title={`Click to copy {{${v.key}}}`}
104
+ >
105
+ <div className="flex items-start justify-between gap-3">
106
+ <div className="min-w-0 flex-1">
107
+ <code className="font-mono text-sm text-indigo-700 break-all">
108
+ {`{{${v.key}}}`}
109
+ </code>
110
+ <p className="mt-1 text-xs leading-snug text-gray-600">
111
+ {v.description}
112
+ </p>
113
+ {v.example ? (
114
+ <p className="mt-1 text-xs text-gray-400">
115
+ Example:{" "}
116
+ <span className="font-mono text-gray-500">{v.example}</span>
117
+ </p>
118
+ ) : null}
119
+ </div>
120
+ <span
121
+ className={`shrink-0 inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide transition ${
122
+ isCopied
123
+ ? "bg-emerald-100 text-emerald-700"
124
+ : "bg-gray-100 text-gray-500 group-hover:bg-indigo-100 group-hover:text-indigo-700"
125
+ }`}
126
+ >
127
+ {isCopied ? "Copied" : "Copy"}
128
+ </span>
129
+ </div>
130
+ </button>
131
+ );
132
+ };
133
+
134
+ const renderGroup: (group: TemplateVariableGroup) => ReactElement = (
135
+ group: TemplateVariableGroup,
136
+ ): ReactElement => {
137
+ return (
138
+ <div key={group.title} className="space-y-2">
139
+ <div className="border-b border-gray-200 pb-1.5">
140
+ <h3 className="text-sm font-semibold text-gray-900">{group.title}</h3>
141
+ {group.description ? (
142
+ <p className="mt-0.5 text-xs text-gray-500">{group.description}</p>
143
+ ) : null}
144
+ </div>
145
+ {group.variables.length > 0 ? (
146
+ <div className="grid grid-cols-1 gap-2 sm:grid-cols-2">
147
+ {group.variables.map(renderVariableRow)}
148
+ </div>
149
+ ) : (
150
+ <p className="text-xs italic text-gray-400">
151
+ No variables in this section yet.
152
+ </p>
153
+ )}
154
+ </div>
155
+ );
156
+ };
157
+
158
+ const totalVariables: number = groups.reduce(
159
+ (sum: number, group: TemplateVariableGroup) => {
160
+ return sum + group.variables.length;
161
+ },
162
+ 0,
163
+ );
164
+
165
+ return (
166
+ <Modal
167
+ title="Dynamic Template Variables"
168
+ description={`Use these variables in incident and alert titles, descriptions, and remediation notes. Click any variable to copy it. ${totalVariables} variables available for this monitor type.`}
169
+ onClose={props.onClose}
170
+ onSubmit={props.onClose}
171
+ submitButtonText="Done"
172
+ modalWidth={ModalWidth.Large}
173
+ >
174
+ <div className="space-y-4">
175
+ <div className="relative">
176
+ <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
177
+ <Icon
178
+ icon={IconProp.Search}
179
+ size={SizeProp.Smaller}
180
+ className="h-4 w-4 text-gray-400"
181
+ />
182
+ </div>
183
+ <input
184
+ type="text"
185
+ value={search}
186
+ onChange={(e: React.ChangeEvent<HTMLInputElement>): void => {
187
+ setSearch(e.target.value);
188
+ }}
189
+ placeholder="Filter variables…"
190
+ className="block w-full rounded-md border border-gray-300 bg-white py-2 pl-9 pr-3 text-sm shadow-sm focus:border-indigo-500 focus:outline-none focus:ring-1 focus:ring-indigo-500"
191
+ autoFocus
192
+ />
193
+ </div>
194
+
195
+ {filteredGroups.length === 0 ? (
196
+ <div className="rounded-md border border-gray-200 bg-gray-50 p-6 text-center text-sm text-gray-500">
197
+ No variables match <code className="font-mono">{search}</code>.
198
+ </div>
199
+ ) : (
200
+ <div className="space-y-5 max-h-[60vh] overflow-y-auto pr-1">
201
+ {filteredGroups.map(renderGroup)}
202
+ </div>
203
+ )}
204
+
205
+ <div className="rounded-md border border-blue-100 bg-blue-50 p-3 text-xs text-blue-900">
206
+ <p className="font-medium">Syntax tips</p>
207
+ <ul className="mt-1 list-disc space-y-1 pl-4">
208
+ <li>
209
+ Wrap the variable name in double braces:{" "}
210
+ <code className="font-mono">{"{{monitorName}}"}</code>
211
+ </li>
212
+ <li>
213
+ Use dot paths for nested values:{" "}
214
+ <code className="font-mono">
215
+ {"{{responseBody.data.status}}"}
216
+ </code>
217
+ </li>
218
+ <li>
219
+ Missing values render as empty strings — they will not break the
220
+ template.
221
+ </li>
222
+ </ul>
223
+ </div>
224
+ </div>
225
+ </Modal>
226
+ );
227
+ };
228
+
229
+ export default TemplateVariablesModal;