@oneuptime/common 10.5.3 → 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 (28) hide show
  1. package/Models/DatabaseModels/AlertGroupingRule.ts +76 -0
  2. package/Models/DatabaseModels/IncidentGroupingRule.ts +76 -0
  3. package/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.ts +37 -0
  4. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  5. package/Server/Services/AlertGroupingEngineService.ts +83 -0
  6. package/Server/Services/IncidentGroupingEngineService.ts +99 -0
  7. package/Tests/Server/Services/AlertGroupingEngineService.test.ts +28 -0
  8. package/Tests/Server/Services/AlertGroupingRuleService.test.ts +14 -0
  9. package/UI/Components/Accordion/Accordion.tsx +12 -4
  10. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js +78 -0
  11. package/build/dist/Models/DatabaseModels/AlertGroupingRule.js.map +1 -1
  12. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js +78 -0
  13. package/build/dist/Models/DatabaseModels/IncidentGroupingRule.js.map +1 -1
  14. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js +18 -0
  15. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1779971548393-AddLabelGroupByToGroupingRules.js.map +1 -0
  16. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  17. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  18. package/build/dist/Server/Services/AlertGroupingEngineService.js +85 -0
  19. package/build/dist/Server/Services/AlertGroupingEngineService.js.map +1 -1
  20. package/build/dist/Server/Services/IncidentGroupingEngineService.js +95 -0
  21. package/build/dist/Server/Services/IncidentGroupingEngineService.js.map +1 -1
  22. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js +21 -0
  23. package/build/dist/Tests/Server/Services/AlertGroupingEngineService.test.js.map +1 -1
  24. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js +12 -0
  25. package/build/dist/Tests/Server/Services/AlertGroupingRuleService.test.js.map +1 -1
  26. package/build/dist/UI/Components/Accordion/Accordion.js +4 -4
  27. package/build/dist/UI/Components/Accordion/Accordion.js.map +1 -1
  28. package/package.json +1 -1
@@ -811,6 +811,82 @@ export default class AlertGroupingRule extends BaseModel {
811
811
  })
812
812
  public groupByAlertTitle?: boolean = undefined;
813
813
 
814
+ @ColumnAccessControl({
815
+ create: [
816
+ Permission.ProjectOwner,
817
+ Permission.ProjectAdmin,
818
+ Permission.CreateAlertGroupingRule,
819
+ ],
820
+ read: [
821
+ Permission.ProjectOwner,
822
+ Permission.ProjectAdmin,
823
+ Permission.ProjectMember,
824
+ Permission.Viewer,
825
+ Permission.AlertAdmin,
826
+ Permission.AlertMember,
827
+ Permission.AlertViewer,
828
+ Permission.ReadAlertGroupingRule,
829
+ ],
830
+ update: [
831
+ Permission.ProjectOwner,
832
+ Permission.ProjectAdmin,
833
+ Permission.EditAlertGroupingRule,
834
+ ],
835
+ })
836
+ @TableColumn({
837
+ required: false,
838
+ type: TableColumnType.Boolean,
839
+ title: "Group By Alert Labels",
840
+ description:
841
+ "When enabled, alerts with different sets of labels will be grouped into separate episodes (exact set match). When disabled, alert labels are ignored for grouping.",
842
+ defaultValue: false,
843
+ isDefaultValueColumn: true,
844
+ })
845
+ @Column({
846
+ type: ColumnType.Boolean,
847
+ nullable: false,
848
+ default: false,
849
+ })
850
+ public groupByAlertLabels?: boolean = undefined;
851
+
852
+ @ColumnAccessControl({
853
+ create: [
854
+ Permission.ProjectOwner,
855
+ Permission.ProjectAdmin,
856
+ Permission.CreateAlertGroupingRule,
857
+ ],
858
+ read: [
859
+ Permission.ProjectOwner,
860
+ Permission.ProjectAdmin,
861
+ Permission.ProjectMember,
862
+ Permission.Viewer,
863
+ Permission.AlertAdmin,
864
+ Permission.AlertMember,
865
+ Permission.AlertViewer,
866
+ Permission.ReadAlertGroupingRule,
867
+ ],
868
+ update: [
869
+ Permission.ProjectOwner,
870
+ Permission.ProjectAdmin,
871
+ Permission.EditAlertGroupingRule,
872
+ ],
873
+ })
874
+ @TableColumn({
875
+ required: false,
876
+ type: TableColumnType.Boolean,
877
+ title: "Group By Monitor Labels",
878
+ description:
879
+ "When enabled, alerts whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
880
+ defaultValue: false,
881
+ isDefaultValueColumn: true,
882
+ })
883
+ @Column({
884
+ type: ColumnType.Boolean,
885
+ nullable: false,
886
+ default: false,
887
+ })
888
+ public groupByMonitorLabels?: boolean = undefined;
889
+
814
890
  @ColumnAccessControl({
815
891
  create: [
816
892
  Permission.ProjectOwner,
@@ -815,6 +815,82 @@ export default class IncidentGroupingRule extends BaseModel {
815
815
  })
816
816
  public groupByIncidentTitle?: boolean = undefined;
817
817
 
818
+ @ColumnAccessControl({
819
+ create: [
820
+ Permission.ProjectOwner,
821
+ Permission.ProjectAdmin,
822
+ Permission.CreateIncidentGroupingRule,
823
+ ],
824
+ read: [
825
+ Permission.ProjectOwner,
826
+ Permission.ProjectAdmin,
827
+ Permission.ProjectMember,
828
+ Permission.Viewer,
829
+ Permission.IncidentAdmin,
830
+ Permission.IncidentMember,
831
+ Permission.IncidentViewer,
832
+ Permission.ReadIncidentGroupingRule,
833
+ ],
834
+ update: [
835
+ Permission.ProjectOwner,
836
+ Permission.ProjectAdmin,
837
+ Permission.EditIncidentGroupingRule,
838
+ ],
839
+ })
840
+ @TableColumn({
841
+ required: false,
842
+ type: TableColumnType.Boolean,
843
+ title: "Group By Incident Labels",
844
+ description:
845
+ "When enabled, incidents with different sets of labels will be grouped into separate episodes (exact set match). When disabled, incident labels are ignored for grouping.",
846
+ defaultValue: false,
847
+ isDefaultValueColumn: true,
848
+ })
849
+ @Column({
850
+ type: ColumnType.Boolean,
851
+ nullable: false,
852
+ default: false,
853
+ })
854
+ public groupByIncidentLabels?: boolean = undefined;
855
+
856
+ @ColumnAccessControl({
857
+ create: [
858
+ Permission.ProjectOwner,
859
+ Permission.ProjectAdmin,
860
+ Permission.CreateIncidentGroupingRule,
861
+ ],
862
+ read: [
863
+ Permission.ProjectOwner,
864
+ Permission.ProjectAdmin,
865
+ Permission.ProjectMember,
866
+ Permission.Viewer,
867
+ Permission.IncidentAdmin,
868
+ Permission.IncidentMember,
869
+ Permission.IncidentViewer,
870
+ Permission.ReadIncidentGroupingRule,
871
+ ],
872
+ update: [
873
+ Permission.ProjectOwner,
874
+ Permission.ProjectAdmin,
875
+ Permission.EditIncidentGroupingRule,
876
+ ],
877
+ })
878
+ @TableColumn({
879
+ required: false,
880
+ type: TableColumnType.Boolean,
881
+ title: "Group By Monitor Labels",
882
+ description:
883
+ "When enabled, incidents whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
884
+ defaultValue: false,
885
+ isDefaultValueColumn: true,
886
+ })
887
+ @Column({
888
+ type: ColumnType.Boolean,
889
+ nullable: false,
890
+ default: false,
891
+ })
892
+ public groupByMonitorLabels?: boolean = undefined;
893
+
818
894
  @ColumnAccessControl({
819
895
  create: [
820
896
  Permission.ProjectOwner,
@@ -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
+ }
@@ -356,6 +356,7 @@ import { ExpandOwnerRuleInheritFlags1779823516881 } from "./1779823516881-Expand
356
356
  import { RenameStatusPageZhToZhCN1779827700000 } from "./1779827700000-RenameStatusPageZhToZhCN";
357
357
  import { MigrationName1779879993421 } from "./1779879993421-MigrationName";
358
358
  import { MigrationName1779882573463 } from "./1779882573463-MigrationName";
359
+ import { AddLabelGroupByToGroupingRules1779971548393 } from "./1779971548393-AddLabelGroupByToGroupingRules";
359
360
 
360
361
  export default [
361
362
  InitialMigration,
@@ -716,4 +717,5 @@ export default [
716
717
  RenameStatusPageZhToZhCN1779827700000,
717
718
  MigrationName1779879993421,
718
719
  MigrationName1779882573463,
720
+ AddLabelGroupByToGroupingRules1779971548393,
719
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
@@ -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", () => {
@@ -75,7 +75,9 @@ const Accordion: FunctionComponent<ComponentProps> = (
75
75
  <div className={className}>
76
76
  <div>
77
77
  <div
78
- className={`flex justify-between items-start gap-3 cursor-pointer group/accordion-header rounded-lg -mx-2 px-2 py-2 transition-colors ${
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 ${
79
81
  isOpen ? "" : "hover:bg-gray-50/80"
80
82
  }`}
81
83
  role="button"
@@ -87,10 +89,16 @@ const Accordion: FunctionComponent<ComponentProps> = (
87
89
  }}
88
90
  onKeyDown={handleKeyDown}
89
91
  >
90
- <div className="flex items-start min-w-0 flex-1">
92
+ <div
93
+ className={`flex ${
94
+ props.description ? "items-start" : "items-center"
95
+ } min-w-0 flex-1`}
96
+ >
91
97
  {props.title && (
92
98
  <div
93
- className={`flex-shrink-0 mt-0.5 flex items-center justify-center w-6 h-6 rounded-md transition-all duration-200 ${
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 ${
94
102
  isOpen
95
103
  ? "bg-gray-900/5 text-gray-700"
96
104
  : "text-gray-400 group-hover/accordion-header:bg-gray-900/5 group-hover/accordion-header:text-gray-700"
@@ -126,7 +134,7 @@ const Accordion: FunctionComponent<ComponentProps> = (
126
134
  )}
127
135
  </div>
128
136
  {!isOpen && props.rightElement && (
129
- <div className="flex-shrink-0 mt-0.5">{props.rightElement}</div>
137
+ <div className="flex-shrink-0">{props.rightElement}</div>
130
138
  )}
131
139
  </div>
132
140
  {isOpen && (
@@ -54,6 +54,8 @@ let AlertGroupingRule = class AlertGroupingRule extends BaseModel {
54
54
  this.groupByMonitor = undefined;
55
55
  this.groupBySeverity = undefined;
56
56
  this.groupByAlertTitle = undefined;
57
+ this.groupByAlertLabels = undefined;
58
+ this.groupByMonitorLabels = undefined;
57
59
  this.enableTimeWindow = undefined;
58
60
  this.timeWindowMinutes = undefined;
59
61
  this.groupByFields = undefined;
@@ -780,6 +782,82 @@ __decorate([
780
782
  }),
781
783
  __metadata("design:type", Boolean)
782
784
  ], AlertGroupingRule.prototype, "groupByAlertTitle", void 0);
785
+ __decorate([
786
+ ColumnAccessControl({
787
+ create: [
788
+ Permission.ProjectOwner,
789
+ Permission.ProjectAdmin,
790
+ Permission.CreateAlertGroupingRule,
791
+ ],
792
+ read: [
793
+ Permission.ProjectOwner,
794
+ Permission.ProjectAdmin,
795
+ Permission.ProjectMember,
796
+ Permission.Viewer,
797
+ Permission.AlertAdmin,
798
+ Permission.AlertMember,
799
+ Permission.AlertViewer,
800
+ Permission.ReadAlertGroupingRule,
801
+ ],
802
+ update: [
803
+ Permission.ProjectOwner,
804
+ Permission.ProjectAdmin,
805
+ Permission.EditAlertGroupingRule,
806
+ ],
807
+ }),
808
+ TableColumn({
809
+ required: false,
810
+ type: TableColumnType.Boolean,
811
+ title: "Group By Alert Labels",
812
+ description: "When enabled, alerts with different sets of labels will be grouped into separate episodes (exact set match). When disabled, alert labels are ignored for grouping.",
813
+ defaultValue: false,
814
+ isDefaultValueColumn: true,
815
+ }),
816
+ Column({
817
+ type: ColumnType.Boolean,
818
+ nullable: false,
819
+ default: false,
820
+ }),
821
+ __metadata("design:type", Boolean)
822
+ ], AlertGroupingRule.prototype, "groupByAlertLabels", void 0);
823
+ __decorate([
824
+ ColumnAccessControl({
825
+ create: [
826
+ Permission.ProjectOwner,
827
+ Permission.ProjectAdmin,
828
+ Permission.CreateAlertGroupingRule,
829
+ ],
830
+ read: [
831
+ Permission.ProjectOwner,
832
+ Permission.ProjectAdmin,
833
+ Permission.ProjectMember,
834
+ Permission.Viewer,
835
+ Permission.AlertAdmin,
836
+ Permission.AlertMember,
837
+ Permission.AlertViewer,
838
+ Permission.ReadAlertGroupingRule,
839
+ ],
840
+ update: [
841
+ Permission.ProjectOwner,
842
+ Permission.ProjectAdmin,
843
+ Permission.EditAlertGroupingRule,
844
+ ],
845
+ }),
846
+ TableColumn({
847
+ required: false,
848
+ type: TableColumnType.Boolean,
849
+ title: "Group By Monitor Labels",
850
+ description: "When enabled, alerts whose monitors have different sets of labels will be grouped into separate episodes (exact set match). When disabled, monitor labels are ignored for grouping.",
851
+ defaultValue: false,
852
+ isDefaultValueColumn: true,
853
+ }),
854
+ Column({
855
+ type: ColumnType.Boolean,
856
+ nullable: false,
857
+ default: false,
858
+ }),
859
+ __metadata("design:type", Boolean)
860
+ ], AlertGroupingRule.prototype, "groupByMonitorLabels", void 0);
783
861
  __decorate([
784
862
  ColumnAccessControl({
785
863
  create: [