@oneuptime/common 10.5.2 → 10.5.4

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 (55) hide show
  1. package/Models/DatabaseModels/AlertGroupingRule.ts +76 -0
  2. package/Models/DatabaseModels/IncidentGroupingRule.ts +76 -0
  3. package/Models/DatabaseModels/StatusPageGroup.ts +212 -0
  4. package/Models/DatabaseModels/StatusPageResource.ts +86 -0
  5. package/Server/API/StatusPageAPI.ts +15 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.ts +61 -13
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.ts +65 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.ts +37 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -3
  10. package/Server/Services/AlertGroupingEngineService.ts +83 -0
  11. package/Server/Services/IncidentGroupingEngineService.ts +99 -0
  12. package/Server/Services/StatusPageService.ts +5 -0
  13. package/Tests/Server/Services/AlertGroupingEngineService.test.ts +28 -0
  14. package/Tests/Server/Services/AlertGroupingRuleService.test.ts +14 -0
  15. package/Types/Monitor/MonitorStep.ts +85 -0
  16. package/Types/StatusPage/StatusPageGroupViewMode.ts +6 -0
  17. package/UI/Components/Accordion/Accordion.tsx +40 -26
  18. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +78 -0
  19. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -1
  20. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +78 -0
  21. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  22. package/build/dist/Models/DatabaseModels/StatusPageGroup.js +217 -0
  23. package/build/dist/Models/DatabaseModels/StatusPageGroup.js.map +1 -1
  24. package/build/dist/Models/DatabaseModels/StatusPageResource.js +88 -0
  25. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  26. package/build/dist/Server/API/StatusPageAPI.js +15 -0
  27. package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
  28. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js +29 -5
  29. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779879993421-MigrationName.js.map +1 -1
  30. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js +28 -0
  31. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779882573463-MigrationName.js.map +1 -0
  32. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js +18 -0
  33. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js.map +1 -0
  34. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +5 -3
  35. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  36. package/build/dist/Server/Services/AlertGroupingEngineService.js +85 -0
  37. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  38. package/build/dist/Server/Services/IncidentGroupingEngineService.js +95 -0
  39. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  40. package/build/dist/Server/Services/StatusPageService.js +5 -0
  41. package/build/dist/Server/Services/StatusPageService.js.map +1 -1
  42. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +21 -0
  43. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -1
  44. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +12 -0
  45. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -1
  46. package/build/dist/Types/Monitor/MonitorStep.js +59 -0
  47. package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
  48. package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js +7 -0
  49. package/build/dist/Types/StatusPage/StatusPageGroupViewMode.js.map +1 -0
  50. package/build/dist/UI/Components/Accordion/Accordion.js +11 -11
  51. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  52. package/package.json +1 -1
  53. package/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.ts +0 -115
  54. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js +0 -106
  55. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex.js.map +0 -1
@@ -0,0 +1,37 @@
1
+ import { MigrationInterface, QueryRunner } from "typeorm";
2
+
3
+ export class AddLabelGroupByToGroupingRules1779971548393
4
+ implements MigrationInterface
5
+ {
6
+ public name: string = "AddLabelGroupByToGroupingRules1779971548393";
7
+
8
+ public async up(queryRunner: QueryRunner): Promise<void> {
9
+ await queryRunner.query(
10
+ `ALTER TABLE "IncidentGroupingRule" ADD "groupByIncidentLabels" boolean NOT NULL DEFAULT false`,
11
+ );
12
+ await queryRunner.query(
13
+ `ALTER TABLE "IncidentGroupingRule" ADD "groupByMonitorLabels" boolean NOT NULL DEFAULT false`,
14
+ );
15
+ await queryRunner.query(
16
+ `ALTER TABLE "AlertGroupingRule" ADD "groupByAlertLabels" boolean NOT NULL DEFAULT false`,
17
+ );
18
+ await queryRunner.query(
19
+ `ALTER TABLE "AlertGroupingRule" ADD "groupByMonitorLabels" boolean NOT NULL DEFAULT false`,
20
+ );
21
+ }
22
+
23
+ public async down(queryRunner: QueryRunner): Promise<void> {
24
+ await queryRunner.query(
25
+ `ALTER TABLE "AlertGroupingRule" DROP COLUMN "groupByMonitorLabels"`,
26
+ );
27
+ await queryRunner.query(
28
+ `ALTER TABLE "AlertGroupingRule" DROP COLUMN "groupByAlertLabels"`,
29
+ );
30
+ await queryRunner.query(
31
+ `ALTER TABLE "IncidentGroupingRule" DROP COLUMN "groupByMonitorLabels"`,
32
+ );
33
+ await queryRunner.query(
34
+ `ALTER TABLE "IncidentGroupingRule" DROP COLUMN "groupByIncidentLabels"`,
35
+ );
36
+ }
37
+ }
@@ -355,7 +355,9 @@ import { MigrationName1779790539196 } from "./1779790539196-MigrationName";
355
355
  import { ExpandOwnerRuleInheritFlags1779823516881 } from "./1779823516881-ExpandOwnerRuleInheritFlags";
356
356
  import { RenameStatusPageZhToZhCN1779827700000 } from "./1779827700000-RenameStatusPageZhToZhCN";
357
357
  import { MigrationName1779879993421 } from "./1779879993421-MigrationName";
358
- import { DedupeTelemetryExceptionsAndAddUniqueIndex1779900000000 } from "./1779900000000-DedupeTelemetryExceptionsAndAddUniqueIndex";
358
+ import { MigrationName1779882573463 } from "./1779882573463-MigrationName";
359
+ import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-AddLabelGroupByToGroupingRules";
360
+
359
361
  export default [
360
362
  InitialMigration,
361
363
  MigrationName1717678334852,
@@ -713,6 +715,7 @@ export default [
713
715
  MigrationName1779790539196,
714
716
  ExpandOwnerRuleInheritFlags1779823516881,
715
717
  RenameStatusPageZhToZhCN1779827700000,
716
- DedupeTelemetryExceptionsAndAddUniqueIndex1779900000000,
717
- MigrationName1779879993421
718
+ MigrationName1779879993421,
719
+ MigrationName1779882573463,
720
+ AddLabelGroupByToGroupingRules1779971548393,
718
721
  ];
@@ -16,6 +16,7 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
16
16
  import OneUptimeDate from "../../Types/Date";
17
17
  import QueryHelper from "../Types/Database/QueryHelper";
18
18
  import AlertGroupingRuleService from "./AlertGroupingRuleService";
19
+ import AlertService from "./AlertService";
19
20
  import AlertEpisodeService from "./AlertEpisodeService";
20
21
  import AlertEpisodeMemberService from "./AlertEpisodeMemberService";
21
22
  import AlertEpisodeOwnerUserService from "./AlertEpisodeOwnerUserService";
@@ -91,6 +92,8 @@ class AlertGroupingEngineServiceClass {
91
92
  groupByMonitor: true,
92
93
  groupBySeverity: true,
93
94
  groupByAlertTitle: true,
95
+ groupByAlertLabels: true,
96
+ groupByMonitorLabels: true,
94
97
  // Time settings
95
98
  enableTimeWindow: true,
96
99
  timeWindowMinutes: true,
@@ -555,10 +558,84 @@ class AlertGroupingEngineServiceClass {
555
558
  parts.push(`title:${normalizedTitle}`);
556
559
  }
557
560
 
561
+ // Group by alert labels (exact set match) - only if explicitly enabled
562
+ if (rule.groupByAlertLabels && alert.id) {
563
+ const alertLabels: Array<Label> = await this.getAlertLabels(alert);
564
+ const sortedLabelIds: Array<string> = alertLabels
565
+ .map((l: Label) => {
566
+ return l.id?.toString() || "";
567
+ })
568
+ .filter((id: string) => {
569
+ return id.length > 0;
570
+ })
571
+ .sort();
572
+ parts.push(`alertLabels:${sortedLabelIds.join(",")}`);
573
+ }
574
+
575
+ // Group by monitor labels (exact set match) - only if explicitly enabled
576
+ if (rule.groupByMonitorLabels && alert.monitorId) {
577
+ const monitorLabels: Array<Label> = await this.getMonitorLabels(
578
+ alert.monitorId,
579
+ );
580
+ const sortedLabelIds: Array<string> = monitorLabels
581
+ .map((l: Label) => {
582
+ return l.id?.toString() || "";
583
+ })
584
+ .filter((id: string) => {
585
+ return id.length > 0;
586
+ })
587
+ .sort();
588
+ parts.push(`monitorLabels:${sortedLabelIds.join(",")}`);
589
+ }
590
+
558
591
  // If no group by options are enabled, all matching alerts go into a single episode
559
592
  return parts.join("|") || "default";
560
593
  }
561
594
 
595
+ @CaptureSpan()
596
+ private async getAlertLabels(alert: Alert): Promise<Array<Label>> {
597
+ // If labels are already loaded on the alert, use them
598
+ if (alert.labels && Array.isArray(alert.labels)) {
599
+ return alert.labels;
600
+ }
601
+
602
+ if (!alert.id) {
603
+ return [];
604
+ }
605
+
606
+ // Re-load alert with labels
607
+ const reloadedAlert: Alert | null = await AlertService.findOneById({
608
+ id: alert.id,
609
+ select: {
610
+ labels: {
611
+ _id: true,
612
+ },
613
+ },
614
+ props: {
615
+ isRoot: true,
616
+ },
617
+ });
618
+
619
+ return reloadedAlert?.labels || [];
620
+ }
621
+
622
+ @CaptureSpan()
623
+ private async getMonitorLabels(monitorId: ObjectID): Promise<Array<Label>> {
624
+ const monitor: Monitor | null = await MonitorService.findOneById({
625
+ id: monitorId,
626
+ select: {
627
+ labels: {
628
+ _id: true,
629
+ },
630
+ },
631
+ props: {
632
+ isRoot: true,
633
+ },
634
+ });
635
+
636
+ return monitor?.labels || [];
637
+ }
638
+
562
639
  @CaptureSpan()
563
640
  private async findMatchingActiveEpisode(
564
641
  projectId: ObjectID,
@@ -793,6 +870,12 @@ class AlertGroupingEngineServiceClass {
793
870
  if (rule.groupByAlertTitle) {
794
871
  groupByParts.push("Alert Title");
795
872
  }
873
+ if (rule.groupByAlertLabels) {
874
+ groupByParts.push("Alert Labels");
875
+ }
876
+ if (rule.groupByMonitorLabels) {
877
+ groupByParts.push("Monitor Labels");
878
+ }
796
879
 
797
880
  const groupByDescription: string =
798
881
  groupByParts.length > 0
@@ -17,6 +17,7 @@ import SortOrder from "../../Types/BaseDatabase/SortOrder";
17
17
  import OneUptimeDate from "../../Types/Date";
18
18
  import QueryHelper from "../Types/Database/QueryHelper";
19
19
  import IncidentGroupingRuleService from "./IncidentGroupingRuleService";
20
+ import IncidentService from "./IncidentService";
20
21
  import IncidentEpisodeService from "./IncidentEpisodeService";
21
22
  import IncidentEpisodeMemberService from "./IncidentEpisodeMemberService";
22
23
  import IncidentEpisodeOwnerUserService from "./IncidentEpisodeOwnerUserService";
@@ -93,6 +94,8 @@ class IncidentGroupingEngineServiceClass {
93
94
  groupByMonitor: true,
94
95
  groupBySeverity: true,
95
96
  groupByIncidentTitle: true,
97
+ groupByIncidentLabels: true,
98
+ groupByMonitorLabels: true,
96
99
  // Time settings
97
100
  enableTimeWindow: true,
98
101
  timeWindowMinutes: true,
@@ -613,10 +616,100 @@ class IncidentGroupingEngineServiceClass {
613
616
  parts.push(`title:${normalizedTitle}`);
614
617
  }
615
618
 
619
+ // Group by incident labels (exact set match) - only if explicitly enabled
620
+ if (rule.groupByIncidentLabels && incident.id) {
621
+ const incidentLabels: Array<Label> =
622
+ await this.getIncidentLabels(incident);
623
+ const sortedLabelIds: Array<string> = incidentLabels
624
+ .map((l: Label) => {
625
+ return l.id?.toString() || "";
626
+ })
627
+ .filter((id: string) => {
628
+ return id.length > 0;
629
+ })
630
+ .sort();
631
+ parts.push(`incidentLabels:${sortedLabelIds.join(",")}`);
632
+ }
633
+
634
+ /*
635
+ * Group by monitor labels (exact set match) - only if explicitly enabled
636
+ * Incidents can be associated with multiple monitors; we union and dedupe labels across all of them.
637
+ */
638
+ if (
639
+ rule.groupByMonitorLabels &&
640
+ incident.monitors &&
641
+ incident.monitors.length > 0
642
+ ) {
643
+ const monitorLabelIdSet: Set<string> = new Set<string>();
644
+ for (const incidentMonitor of incident.monitors) {
645
+ if (!incidentMonitor || !incidentMonitor.id) {
646
+ continue;
647
+ }
648
+ const monitorLabels: Array<Label> = await this.getMonitorLabels(
649
+ incidentMonitor.id,
650
+ );
651
+ for (const label of monitorLabels) {
652
+ const labelIdStr: string = label.id?.toString() || "";
653
+ if (labelIdStr.length > 0) {
654
+ monitorLabelIdSet.add(labelIdStr);
655
+ }
656
+ }
657
+ }
658
+ const sortedLabelIds: Array<string> =
659
+ Array.from(monitorLabelIdSet).sort();
660
+ parts.push(`monitorLabels:${sortedLabelIds.join(",")}`);
661
+ }
662
+
616
663
  // If no group by options are enabled, all matching incidents go into a single episode
617
664
  return parts.join("|") || "default";
618
665
  }
619
666
 
667
+ @CaptureSpan()
668
+ private async getIncidentLabels(incident: Incident): Promise<Array<Label>> {
669
+ // If labels are already loaded on the incident, use them
670
+ if (incident.labels && Array.isArray(incident.labels)) {
671
+ return incident.labels;
672
+ }
673
+
674
+ if (!incident.id) {
675
+ return [];
676
+ }
677
+
678
+ // Re-load incident with labels
679
+ const reloadedIncident: Incident | null = await IncidentService.findOneById(
680
+ {
681
+ id: incident.id,
682
+ select: {
683
+ labels: {
684
+ _id: true,
685
+ },
686
+ },
687
+ props: {
688
+ isRoot: true,
689
+ },
690
+ },
691
+ );
692
+
693
+ return reloadedIncident?.labels || [];
694
+ }
695
+
696
+ @CaptureSpan()
697
+ private async getMonitorLabels(monitorId: ObjectID): Promise<Array<Label>> {
698
+ const monitor: Monitor | null = await MonitorService.findOneById({
699
+ id: monitorId,
700
+ select: {
701
+ labels: {
702
+ _id: true,
703
+ },
704
+ },
705
+ props: {
706
+ isRoot: true,
707
+ },
708
+ });
709
+
710
+ return monitor?.labels || [];
711
+ }
712
+
620
713
  @CaptureSpan()
621
714
  private async findMatchingActiveEpisode(
622
715
  projectId: ObjectID,
@@ -887,6 +980,12 @@ class IncidentGroupingEngineServiceClass {
887
980
  if (rule.groupByIncidentTitle) {
888
981
  groupByParts.push("Incident Title");
889
982
  }
983
+ if (rule.groupByIncidentLabels) {
984
+ groupByParts.push("Incident Labels");
985
+ }
986
+ if (rule.groupByMonitorLabels) {
987
+ groupByParts.push("Monitor Labels");
988
+ }
890
989
 
891
990
  const groupByDescription: string =
892
991
  groupByParts.length > 0
@@ -1247,11 +1247,16 @@ export class Service extends DatabaseService<StatusPage> {
1247
1247
  statusPageGroupId: true,
1248
1248
  statusPageGroup: {
1249
1249
  name: true,
1250
+ viewMode: true,
1251
+ rowAxisLabel: true,
1252
+ columnAxisLabel: true,
1250
1253
  },
1251
1254
  monitorId: true,
1252
1255
  displayTooltip: true,
1253
1256
  displayDescription: true,
1254
1257
  displayName: true,
1258
+ rowAxisValue: true,
1259
+ columnAxisValue: true,
1255
1260
  monitor: {
1256
1261
  _id: true,
1257
1262
  currentMonitorStatusId: true,
@@ -232,6 +232,34 @@ describe("AlertGroupingEngineService Models", () => {
232
232
  );
233
233
  expect(groupingKey).toContain(`title:${mockAlert.title}`);
234
234
  });
235
+
236
+ test("should produce identical label key regardless of label order (exact set match)", () => {
237
+ // Simulates buildGroupingKey's label-sorting logic.
238
+ const labelIdsA: string[] = ["lbl-c", "lbl-a", "lbl-b"];
239
+ const labelIdsB: string[] = ["lbl-b", "lbl-c", "lbl-a"];
240
+
241
+ const keyA: string = `alertLabels:${[...labelIdsA].sort().join(",")}`;
242
+ const keyB: string = `alertLabels:${[...labelIdsB].sort().join(",")}`;
243
+
244
+ expect(keyA).toBe(keyB);
245
+ expect(keyA).toBe("alertLabels:lbl-a,lbl-b,lbl-c");
246
+ });
247
+
248
+ test("should produce different label keys for different label sets (exact set match)", () => {
249
+ // Alerts with [A,B] and [A,C] must NOT group together under exact set match.
250
+ const keyAB: string = `alertLabels:${["lbl-a", "lbl-b"].sort().join(",")}`;
251
+ const keyAC: string = `alertLabels:${["lbl-a", "lbl-c"].sort().join(",")}`;
252
+
253
+ expect(keyAB).not.toBe(keyAC);
254
+ });
255
+
256
+ test("should emit empty label key when alert has no labels", () => {
257
+ // Alerts with no labels should produce the same (empty) label key.
258
+ const labelIds: string[] = [];
259
+ const key: string = `alertLabels:${[...labelIds].sort().join(",")}`;
260
+
261
+ expect(key).toBe("alertLabels:");
262
+ });
235
263
  });
236
264
 
237
265
  describe("Time Window Configuration", () => {
@@ -143,14 +143,28 @@ describe("AlertGroupingRule Model", () => {
143
143
  expect(rule.groupByAlertTitle).toBe(true);
144
144
  });
145
145
 
146
+ test("should set and get groupByAlertLabels correctly", () => {
147
+ rule.groupByAlertLabels = true;
148
+ expect(rule.groupByAlertLabels).toBe(true);
149
+ });
150
+
151
+ test("should set and get groupByMonitorLabels correctly", () => {
152
+ rule.groupByMonitorLabels = true;
153
+ expect(rule.groupByMonitorLabels).toBe(true);
154
+ });
155
+
146
156
  test("should handle all groupBy options as false", () => {
147
157
  rule.groupByMonitor = false;
148
158
  rule.groupBySeverity = false;
149
159
  rule.groupByAlertTitle = false;
160
+ rule.groupByAlertLabels = false;
161
+ rule.groupByMonitorLabels = false;
150
162
 
151
163
  expect(rule.groupByMonitor).toBe(false);
152
164
  expect(rule.groupBySeverity).toBe(false);
153
165
  expect(rule.groupByAlertTitle).toBe(false);
166
+ expect(rule.groupByAlertLabels).toBe(false);
167
+ expect(rule.groupByMonitorLabels).toBe(false);
154
168
  });
155
169
 
156
170
  test("should handle combination of groupBy options", () => {
@@ -52,6 +52,39 @@ import MonitorStepDockerMonitor, {
52
52
  } from "./MonitorStepDockerMonitor";
53
53
  import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
54
54
 
55
+ /*
56
+ * Caps and defaults for per-step request timeout and retry settings.
57
+ * Users may lower these via the UI; values higher than the cap are clamped.
58
+ */
59
+ export const MAX_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000; // 60 seconds
60
+ export const DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS: number = 60000;
61
+ export const MAX_MONITOR_RETRY_COUNT: number = 3;
62
+ export const DEFAULT_MONITOR_RETRY_COUNT: number = 3;
63
+
64
+ export const clampMonitorRequestTimeoutInMs: (value: number) => number = (
65
+ value: number,
66
+ ): number => {
67
+ if (!value || value <= 0) {
68
+ return DEFAULT_MONITOR_REQUEST_TIMEOUT_IN_MS;
69
+ }
70
+ if (value > MAX_MONITOR_REQUEST_TIMEOUT_IN_MS) {
71
+ return MAX_MONITOR_REQUEST_TIMEOUT_IN_MS;
72
+ }
73
+ return value;
74
+ };
75
+
76
+ export const clampMonitorRetryCount: (value: number) => number = (
77
+ value: number,
78
+ ): number => {
79
+ if (value === undefined || value === null || isNaN(value) || value < 0) {
80
+ return DEFAULT_MONITOR_RETRY_COUNT;
81
+ }
82
+ if (value > MAX_MONITOR_RETRY_COUNT) {
83
+ return MAX_MONITOR_RETRY_COUNT;
84
+ }
85
+ return value;
86
+ };
87
+
55
88
  export interface MonitorStepType {
56
89
  id: string;
57
90
  monitorDestination?: URL | IP | Hostname | undefined;
@@ -88,6 +121,19 @@ export interface MonitorStepType {
88
121
  // retry count for synthetic monitors - number of times to retry on error
89
122
  retryCountOnError?: number | undefined;
90
123
 
124
+ /*
125
+ * Per-step request timeout in milliseconds for probe-based monitors
126
+ * (Website, API, Ping, IP, Port, SSLCertificate). Defaults to and is
127
+ * capped at 60000 ms (60 seconds).
128
+ */
129
+ requestTimeoutInMs?: number | undefined;
130
+
131
+ /*
132
+ * Per-step retry count for probe-based monitors when a check fails.
133
+ * Defaults to and is capped at 3.
134
+ */
135
+ retryCount?: number | undefined;
136
+
91
137
  // Log monitor type.
92
138
  logMonitor?: MonitorStepLogMonitor | undefined;
93
139
 
@@ -148,6 +194,8 @@ export default class MonitorStep extends DatabaseProperty {
148
194
  screenSizeTypes: undefined,
149
195
  browserTypes: undefined,
150
196
  retryCountOnError: undefined,
197
+ requestTimeoutInMs: undefined,
198
+ retryCount: undefined,
151
199
  logMonitor: undefined,
152
200
  traceMonitor: undefined,
153
201
  metricMonitor: undefined,
@@ -190,6 +238,8 @@ export default class MonitorStep extends DatabaseProperty {
190
238
  screenSizeTypes: undefined,
191
239
  browserTypes: undefined,
192
240
  retryCountOnError: undefined,
241
+ requestTimeoutInMs: undefined,
242
+ retryCount: undefined,
193
243
  logMonitor: undefined,
194
244
  traceMonitor: undefined,
195
245
  metricMonitor: undefined,
@@ -294,6 +344,27 @@ export default class MonitorStep extends DatabaseProperty {
294
344
  return this;
295
345
  }
296
346
 
347
+ public setRequestTimeoutInMs(
348
+ requestTimeoutInMs: number | undefined,
349
+ ): MonitorStep {
350
+ if (requestTimeoutInMs === undefined) {
351
+ this.data!.requestTimeoutInMs = undefined;
352
+ return this;
353
+ }
354
+ this.data!.requestTimeoutInMs =
355
+ clampMonitorRequestTimeoutInMs(requestTimeoutInMs);
356
+ return this;
357
+ }
358
+
359
+ public setRetryCount(retryCount: number | undefined): MonitorStep {
360
+ if (retryCount === undefined) {
361
+ this.data!.retryCount = undefined;
362
+ return this;
363
+ }
364
+ this.data!.retryCount = clampMonitorRetryCount(retryCount);
365
+ return this;
366
+ }
367
+
297
368
  public setLogMonitor(logMonitor: MonitorStepLogMonitor): MonitorStep {
298
369
  this.data!.logMonitor = logMonitor;
299
370
  return this;
@@ -400,6 +471,8 @@ export default class MonitorStep extends DatabaseProperty {
400
471
  screenSizeTypes: undefined,
401
472
  browserTypes: undefined,
402
473
  retryCountOnError: undefined,
474
+ requestTimeoutInMs: undefined,
475
+ retryCount: undefined,
403
476
  logMonitor: undefined,
404
477
  exceptionMonitor: undefined,
405
478
  kubernetesMonitor: undefined,
@@ -597,6 +670,11 @@ export default class MonitorStep extends DatabaseProperty {
597
670
  screenSizeTypes: this.data.screenSizeTypes || undefined,
598
671
  browserTypes: this.data.browserTypes || undefined,
599
672
  retryCountOnError: this.data.retryCountOnError || undefined,
673
+ requestTimeoutInMs: this.data.requestTimeoutInMs || undefined,
674
+ retryCount:
675
+ this.data.retryCount === undefined
676
+ ? undefined
677
+ : this.data.retryCount,
600
678
  logMonitor: this.data.logMonitor
601
679
  ? MonitorStepLogMonitorUtil.toJSON(
602
680
  this.data.logMonitor || MonitorStepLogMonitorUtil.getDefault(),
@@ -745,6 +823,11 @@ export default class MonitorStep extends DatabaseProperty {
745
823
  (json["screenSizeTypes"] as Array<ScreenSizeType>) || undefined,
746
824
  browserTypes: (json["browserTypes"] as Array<BrowserType>) || undefined,
747
825
  retryCountOnError: (json["retryCountOnError"] as number) || undefined,
826
+ requestTimeoutInMs: (json["requestTimeoutInMs"] as number) || undefined,
827
+ retryCount:
828
+ json["retryCount"] === undefined || json["retryCount"] === null
829
+ ? undefined
830
+ : (json["retryCount"] as number),
748
831
  logMonitor: json["logMonitor"]
749
832
  ? (json["logMonitor"] as JSONObject)
750
833
  : undefined,
@@ -806,6 +889,8 @@ export default class MonitorStep extends DatabaseProperty {
806
889
  screenSizeTypes: Zod.any().optional(),
807
890
  browserTypes: Zod.any().optional(),
808
891
  retryCountOnError: Zod.number().optional(),
892
+ requestTimeoutInMs: Zod.number().optional(),
893
+ retryCount: Zod.number().optional(),
809
894
  logMonitor: Zod.any().optional(),
810
895
  traceMonitor: Zod.any().optional(),
811
896
  metricMonitor: Zod.any().optional(),
@@ -0,0 +1,6 @@
1
+ enum StatusPageGroupViewMode {
2
+ List = "List",
3
+ Grid = "Grid",
4
+ }
5
+
6
+ export default StatusPageGroupViewMode;
@@ -75,7 +75,11 @@ const Accordion: FunctionComponent<ComponentProps> = (
75
75
  <div className={className}>
76
76
  <div>
77
77
  <div
78
- className={`flex justify-between cursor-pointer`}
78
+ className={`flex justify-between ${
79
+ props.description ? "items-start" : "items-center"
80
+ } gap-3 cursor-pointer group/accordion-header rounded-lg -mx-2 px-2 py-2 transition-colors ${
81
+ isOpen ? "" : "hover:bg-gray-50/80"
82
+ }`}
79
83
  role="button"
80
84
  tabIndex={0}
81
85
  aria-expanded={isOpen}
@@ -85,43 +89,53 @@ const Accordion: FunctionComponent<ComponentProps> = (
85
89
  }}
86
90
  onKeyDown={handleKeyDown}
87
91
  >
88
- <div className="flex">
92
+ <div
93
+ className={`flex ${
94
+ props.description ? "items-start" : "items-center"
95
+ } min-w-0 flex-1`}
96
+ >
89
97
  {props.title && (
90
- <div>
91
- {isOpen && (
92
- <Icon
93
- className="h-4 w-4 text-gray-500"
94
- icon={IconProp.ChevronDown}
95
- thick={ThickProp.Thick}
96
- />
97
- )}
98
- {!isOpen && (
99
- <Icon
100
- className="h-4 w-4 text-gray-500"
101
- icon={IconProp.ChevronRight}
102
- thick={ThickProp.Thick}
103
- />
104
- )}
98
+ <div
99
+ className={`flex-shrink-0 ${
100
+ props.description ? "mt-0.5" : ""
101
+ } flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 ${
102
+ isOpen
103
+ ? "bg-gray-900/5 text-gray-700"
104
+ : "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
105
+ }`}
106
+ aria-hidden="true"
107
+ >
108
+ <Icon
109
+ className={`h-3.5 w-3.5 transition-transform duration-200 ease-out ${
110
+ isOpen ? "rotate-90" : ""
111
+ }`}
112
+ icon={IconProp.ChevronRight}
113
+ thick={ThickProp.Thick}
114
+ />
105
115
  </div>
106
116
  )}
107
117
  {props.title && (
108
118
  <div
109
- className={`ml-1 -mt-1 ${
119
+ className={`ml-2.5 min-w-0 flex-1 ${
110
120
  props.onClick ? "cursor-pointer" : ""
111
121
  }`}
112
122
  >
113
- <div className={`text-gray-500 ${props.titleClassName}`}>
114
- {props.title}{" "}
115
- </div>
116
- <div className="mb-2 text-sm">
117
- {props.description && (
118
- <MarkdownViewer text={props.description || ""} />
119
- )}
123
+ <div
124
+ className={`text-gray-900 leading-snug ${props.titleClassName || ""}`}
125
+ >
126
+ {props.title}
120
127
  </div>
128
+ {props.description && (
129
+ <div className="mt-1 text-sm text-gray-500 leading-relaxed">
130
+ <MarkdownViewer text={props.description} />
131
+ </div>
132
+ )}
121
133
  </div>
122
134
  )}
123
135
  </div>
124
- {!isOpen && <div className="">{props.rightElement}</div>}
136
+ {!isOpen && props.rightElement && (
137
+ <div className="flex-shrink-0">{props.rightElement}</div>
138
+ )}
125
139
  </div>
126
140
  {isOpen && (
127
141
  <div