@oneuptime/common 11.3.5 → 11.3.6

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 (59) hide show
  1. package/Server/Infrastructure/Queue.ts +103 -16
  2. package/Server/Services/AnalyticsDatabaseService.ts +9 -5
  3. package/Server/Services/TeamMemberService.ts +38 -1
  4. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +31 -0
  5. package/Server/Utils/Monitor/IncomingRequestIncidentGrouping.ts +428 -0
  6. package/Server/Utils/Monitor/MonitorAlert.ts +158 -8
  7. package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +26 -0
  8. package/Server/Utils/Monitor/MonitorIncident.ts +147 -2
  9. package/Server/Utils/Monitor/MonitorResource.ts +74 -0
  10. package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +2 -1
  11. package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +32 -0
  12. package/Tests/Server/Utils/Monitor/IncomingRequestIncidentGrouping.test.ts +389 -0
  13. package/Tests/Server/Utils/Monitor/SeriesAbsenceResolutionGuard.test.ts +177 -0
  14. package/Tests/Types/Events/Recurring.test.ts +475 -0
  15. package/Tests/Types/Monitor/CriteriaFilter.test.ts +348 -0
  16. package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +331 -0
  17. package/Tests/UI/Components/Alert.test.tsx +23 -15
  18. package/Tests/UI/Components/Breadcrumbs.test.tsx +18 -6
  19. package/Tests/UI/Components/Button.test.tsx +3 -3
  20. package/Tests/UI/Components/DictionaryFilterOperator.test.ts +174 -0
  21. package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +4 -3
  22. package/Tests/UI/Components/Input.test.tsx +3 -2
  23. package/Tests/UI/Components/Pill.test.tsx +4 -2
  24. package/Tests/UI/Components/SideMenuItem.test.tsx +18 -8
  25. package/Tests/UI/Components/TextArea.test.tsx +6 -4
  26. package/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.ts +74 -0
  27. package/Types/Monitor/MonitorCriteriaInstance.ts +22 -0
  28. package/UI/Components/Dictionary/Dictionary.tsx +123 -50
  29. package/UI/Components/Dictionary/DictionaryFilterOperator.ts +96 -1
  30. package/UI/Components/MasterPage/MasterPage.tsx +5 -3
  31. package/build/dist/Server/Infrastructure/Queue.js +83 -15
  32. package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
  33. package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -5
  34. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  35. package/build/dist/Server/Services/TeamMemberService.js +37 -1
  36. package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
  37. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -0
  38. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  39. package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js +288 -0
  40. package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js.map +1 -0
  41. package/build/dist/Server/Utils/Monitor/MonitorAlert.js +119 -8
  42. package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
  43. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +24 -0
  44. package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
  45. package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -2
  46. package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
  47. package/build/dist/Server/Utils/Monitor/MonitorResource.js +62 -0
  48. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  49. package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js +8 -0
  50. package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js.map +1 -0
  51. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
  52. package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
  53. package/build/dist/UI/Components/Dictionary/Dictionary.js +60 -9
  54. package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
  55. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +69 -0
  56. package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
  57. package/build/dist/UI/Components/MasterPage/MasterPage.js +5 -3
  58. package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
  59. package/package.json +1 -1
@@ -8,7 +8,7 @@ describe("alert tests", () => {
8
8
  test("it should render all props passed", () => {
9
9
  const handleClick: undefined | (() => void) = jest.fn();
10
10
  const handleClose: (() => void) | undefined = jest.fn();
11
- render(
11
+ const { container } = render(
12
12
  <Alert
13
13
  title="title"
14
14
  strongTitle="strong"
@@ -18,8 +18,8 @@ describe("alert tests", () => {
18
18
  />,
19
19
  );
20
20
 
21
- const icon: HTMLElement = screen.getByRole("icon");
22
- expect(icon).toBeInTheDocument();
21
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
22
+ expect(container.querySelector("svg")).not.toBeNull();
23
23
 
24
24
  const alert: HTMLElement = screen.getByRole("alert");
25
25
  expect(alert).toBeInTheDocument();
@@ -34,30 +34,38 @@ describe("alert tests", () => {
34
34
  expect(handleClose).toBeCalled();
35
35
  });
36
36
  test("it should show icon when alert type is equal to success", () => {
37
- render(<Alert dataTestId="test-id" type={AlertType.SUCCESS} />);
38
- const icon: HTMLElement = screen.getByRole("icon");
39
- expect(icon).toBeInTheDocument();
37
+ const { container } = render(
38
+ <Alert dataTestId="test-id" type={AlertType.SUCCESS} />,
39
+ );
40
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
41
+ expect(container.querySelector("svg")).not.toBeNull();
40
42
  const testId: HTMLElement = screen.getByTestId("test-id");
41
43
  expect(testId).toHaveClass("rounded-md bg-gray-700 p-4");
42
44
  });
43
45
  test("it should show icon when alert type is equal to info", () => {
44
- render(<Alert dataTestId="test-id" type={AlertType.INFO} />);
45
- const icon: HTMLElement = screen.getByRole("icon");
46
- expect(icon).toBeInTheDocument();
46
+ const { container } = render(
47
+ <Alert dataTestId="test-id" type={AlertType.INFO} />,
48
+ );
49
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
50
+ expect(container.querySelector("svg")).not.toBeNull();
47
51
  const testId: HTMLElement = screen.getByTestId("test-id");
48
52
  expect(testId).toHaveClass("rounded-md bg-gray-700 p-4");
49
53
  });
50
54
  test("it should show icon when alert type is equal to warning", () => {
51
- render(<Alert dataTestId="test-id" type={AlertType.WARNING} />);
52
- const icon: HTMLElement = screen.getByRole("icon");
53
- expect(icon).toBeInTheDocument();
55
+ const { container } = render(
56
+ <Alert dataTestId="test-id" type={AlertType.WARNING} />,
57
+ );
58
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
59
+ expect(container.querySelector("svg")).not.toBeNull();
54
60
  const testId: HTMLElement = screen.getByTestId("test-id");
55
61
  expect(testId).toHaveClass("rounded-md bg-gray-700 p-4");
56
62
  });
57
63
  test("it should show icon when alert type is equal to danger", () => {
58
- render(<Alert dataTestId="test-id" type={AlertType.DANGER} />);
59
- const icon: HTMLElement = screen.getByRole("icon");
60
- expect(icon).toBeInTheDocument();
64
+ const { container } = render(
65
+ <Alert dataTestId="test-id" type={AlertType.DANGER} />,
66
+ );
67
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
68
+ expect(container.querySelector("svg")).not.toBeNull();
61
69
  const testId: HTMLElement = screen.getByTestId("test-id");
62
70
  expect(testId).toHaveClass("rounded-md bg-red-700 p-4");
63
71
  });
@@ -63,20 +63,32 @@ describe("Breadcrumbs", () => {
63
63
  <Breadcrumbs links={links} />,
64
64
  );
65
65
  const testInstance: ReactTestInstance = testRenderer.root;
66
- // Assert cursor style
66
+ /*
67
+ * The current page is not linked: it renders as a non-interactive <span>
68
+ * (cursor-default), not an <a> (WCAG 4.1.2). Only the other crumbs are
69
+ * rendered as anchors.
70
+ */
67
71
  const anchors: ReactTestInstance[] = testInstance.findAllByType("a");
72
+ expect(anchors).toHaveLength(1);
68
73
  expect(anchors[0]?.props["className"]).toContain("cursor-pointer");
69
- expect(anchors[1]?.props["className"]).toContain("cursor-default");
74
+ // The current page ("Projects") renders as a cursor-default span.
75
+ const currentPageSpans: ReactTestInstance[] = testInstance.findAll(
76
+ (node: ReactTestInstance) => {
77
+ return (
78
+ node.type === "span" &&
79
+ typeof node.props["className"] === "string" &&
80
+ node.props["className"].includes("cursor-default")
81
+ );
82
+ },
83
+ );
84
+ expect(currentPageSpans.length).toBeGreaterThan(0);
70
85
  // Set up spy on navigation
71
86
  jest.spyOn(Navigation, "navigate");
72
87
  // Create a mock event with preventDefault
73
88
  const mockEvent: { preventDefault: ReturnType<typeof jest.fn> } = {
74
89
  preventDefault: jest.fn(),
75
90
  };
76
- // Assert the second link does not navigate
77
- anchors[1]?.props["onClick"](mockEvent);
78
- expect(Navigation.navigate).not.toHaveBeenCalled();
79
- // Assert the first link navigates
91
+ // The first (non-current) link navigates when clicked.
80
92
  anchors[0]?.props["onClick"](mockEvent);
81
93
  expect(Navigation.navigate).toHaveBeenCalledTimes(1);
82
94
  });
@@ -12,7 +12,7 @@ import { describe, expect, jest } from "@jest/globals";
12
12
 
13
13
  describe("Button", () => {
14
14
  test("it should render correctly with title and icon", () => {
15
- render(
15
+ const { container } = render(
16
16
  <Button
17
17
  dataTestId="test-id"
18
18
  title="sample title"
@@ -28,8 +28,8 @@ describe("Button", () => {
28
28
  expect(testId).toBeInTheDocument();
29
29
  expect(testId).toHaveAttribute("type", "button");
30
30
  expect(testId).toHaveAttribute("disabled");
31
- const icon: HTMLElement = screen.getByRole("icon");
32
- expect(icon).toBeInTheDocument();
31
+ // The icon renders an inline <svg> (no invalid role="icon"; WCAG 4.1.2).
32
+ expect(container.querySelector("svg")).not.toBeNull();
33
33
  });
34
34
 
35
35
  test("it should have shortcutKey Setting", () => {
@@ -0,0 +1,174 @@
1
+ import {
2
+ DictionaryEntryValue,
3
+ DictionaryFilterOperator,
4
+ buildDictionaryValue,
5
+ detectOperatorFromValue,
6
+ } from "../../../UI/Components/Dictionary/DictionaryFilterOperator";
7
+ import Includes from "../../../Types/BaseDatabase/Includes";
8
+ import IncludesNone from "../../../Types/BaseDatabase/IncludesNone";
9
+ import { ObjectType } from "../../../Types/JSON";
10
+ import { describe, expect, it } from "@jest/globals";
11
+
12
+ describe("DictionaryFilterOperator - IsAnyOf (multi-value membership)", () => {
13
+ it("builds an Includes wrapper from a multi-value selection", () => {
14
+ const built: DictionaryEntryValue = buildDictionaryValue({
15
+ operator: DictionaryFilterOperator.IsAnyOf,
16
+ rawValue: "",
17
+ rawValues: ["system", "user"],
18
+ });
19
+
20
+ expect(built).toBeInstanceOf(Includes);
21
+ expect((built as Includes).values).toEqual(["system", "user"]);
22
+ });
23
+
24
+ it("drops empty-string entries so an empty pick becomes an empty Includes (treated as 'All' downstream)", () => {
25
+ const built: DictionaryEntryValue = buildDictionaryValue({
26
+ operator: DictionaryFilterOperator.IsAnyOf,
27
+ rawValue: "",
28
+ rawValues: ["system", "", "user"],
29
+ });
30
+
31
+ expect((built as Includes).values).toEqual(["system", "user"]);
32
+ });
33
+
34
+ it("builds an empty Includes when no values are supplied", () => {
35
+ const built: DictionaryEntryValue = buildDictionaryValue({
36
+ operator: DictionaryFilterOperator.IsAnyOf,
37
+ rawValue: "",
38
+ });
39
+
40
+ expect(built).toBeInstanceOf(Includes);
41
+ expect((built as Includes).values).toEqual([]);
42
+ });
43
+
44
+ it("detects a hydrated Includes instance as IsAnyOf with a joined display value and structured array", () => {
45
+ const detected: {
46
+ operator: DictionaryFilterOperator;
47
+ rawValue: string;
48
+ rawValues?: Array<string> | undefined;
49
+ } = detectOperatorFromValue(new Includes(["system", "user"]));
50
+
51
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsAnyOf);
52
+ expect(detected.rawValue).toBe("system, user");
53
+ expect(detected.rawValues).toEqual(["system", "user"]);
54
+ });
55
+
56
+ it("detects the round-tripped `{_type, value}` JSON shape as IsAnyOf", () => {
57
+ const jsonShape: { _type: ObjectType; value: Array<string> } = {
58
+ _type: ObjectType.Includes,
59
+ value: ["system", "user"],
60
+ };
61
+
62
+ const detected: {
63
+ operator: DictionaryFilterOperator;
64
+ rawValues?: Array<string> | undefined;
65
+ } = detectOperatorFromValue(jsonShape);
66
+
67
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsAnyOf);
68
+ expect(detected.rawValues).toEqual(["system", "user"]);
69
+ });
70
+
71
+ it("round-trips build -> detect without losing values", () => {
72
+ const built: DictionaryEntryValue = buildDictionaryValue({
73
+ operator: DictionaryFilterOperator.IsAnyOf,
74
+ rawValue: "",
75
+ rawValues: ["nice", "softirq", "steal"],
76
+ });
77
+
78
+ const detected: {
79
+ operator: DictionaryFilterOperator;
80
+ rawValues?: Array<string> | undefined;
81
+ } = detectOperatorFromValue(built);
82
+
83
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsAnyOf);
84
+ expect(detected.rawValues).toEqual(["nice", "softirq", "steal"]);
85
+ });
86
+
87
+ it("detects an empty Includes as IsAnyOf with an empty raw value", () => {
88
+ const detected: {
89
+ operator: DictionaryFilterOperator;
90
+ rawValue: string;
91
+ rawValues?: Array<string> | undefined;
92
+ } = detectOperatorFromValue(new Includes([]));
93
+
94
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsAnyOf);
95
+ expect(detected.rawValue).toBe("");
96
+ expect(detected.rawValues).toEqual([]);
97
+ });
98
+
99
+ it("leaves a bare string mapping to EqualTo (existing single-value filters keep working)", () => {
100
+ const detected: {
101
+ operator: DictionaryFilterOperator;
102
+ rawValue: string;
103
+ } = detectOperatorFromValue("system");
104
+
105
+ expect(detected.operator).toBe(DictionaryFilterOperator.EqualTo);
106
+ expect(detected.rawValue).toBe("system");
107
+ });
108
+ });
109
+
110
+ describe("DictionaryFilterOperator - IsNoneOf (multi-value exclusion)", () => {
111
+ it("builds an IncludesNone wrapper from a multi-value selection", () => {
112
+ const built: DictionaryEntryValue = buildDictionaryValue({
113
+ operator: DictionaryFilterOperator.IsNoneOf,
114
+ rawValue: "",
115
+ rawValues: ["system", "user"],
116
+ });
117
+
118
+ expect(built).toBeInstanceOf(IncludesNone);
119
+ expect((built as IncludesNone).values).toEqual(["system", "user"]);
120
+ });
121
+
122
+ it("drops empty-string entries", () => {
123
+ const built: DictionaryEntryValue = buildDictionaryValue({
124
+ operator: DictionaryFilterOperator.IsNoneOf,
125
+ rawValue: "",
126
+ rawValues: ["system", "", "user"],
127
+ });
128
+
129
+ expect((built as IncludesNone).values).toEqual(["system", "user"]);
130
+ });
131
+
132
+ it("detects a hydrated IncludesNone instance as IsNoneOf with a joined display value", () => {
133
+ const detected: {
134
+ operator: DictionaryFilterOperator;
135
+ rawValue: string;
136
+ rawValues?: Array<string> | undefined;
137
+ } = detectOperatorFromValue(new IncludesNone(["system", "user"]));
138
+
139
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsNoneOf);
140
+ expect(detected.rawValue).toBe("system, user");
141
+ expect(detected.rawValues).toEqual(["system", "user"]);
142
+ });
143
+
144
+ it("detects the round-tripped `{_type, value}` JSON shape as IsNoneOf", () => {
145
+ const jsonShape: { _type: ObjectType; value: Array<string> } = {
146
+ _type: ObjectType.IncludesNone,
147
+ value: ["system", "user"],
148
+ };
149
+
150
+ const detected: {
151
+ operator: DictionaryFilterOperator;
152
+ rawValues?: Array<string> | undefined;
153
+ } = detectOperatorFromValue(jsonShape);
154
+
155
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsNoneOf);
156
+ expect(detected.rawValues).toEqual(["system", "user"]);
157
+ });
158
+
159
+ it("round-trips build -> detect without losing values", () => {
160
+ const built: DictionaryEntryValue = buildDictionaryValue({
161
+ operator: DictionaryFilterOperator.IsNoneOf,
162
+ rawValue: "",
163
+ rawValues: ["nice", "softirq"],
164
+ });
165
+
166
+ const detected: {
167
+ operator: DictionaryFilterOperator;
168
+ rawValues?: Array<string> | undefined;
169
+ } = detectOperatorFromValue(built);
170
+
171
+ expect(detected.operator).toBe(DictionaryFilterOperator.IsNoneOf);
172
+ expect(detected.rawValues).toEqual(["nice", "softirq"]);
173
+ });
174
+ });
@@ -6,7 +6,7 @@ import React from "react";
6
6
 
7
7
  describe("EmptyState", () => {
8
8
  test("renders correctly with all props", () => {
9
- render(
9
+ const { container } = render(
10
10
  <EmptyState
11
11
  id="empty-state"
12
12
  title="Empty State Title"
@@ -18,13 +18,14 @@ describe("EmptyState", () => {
18
18
  const titleElement: HTMLElement = screen.getByText("Empty State Title");
19
19
  const descriptionElement: HTMLElement =
20
20
  screen.getByText("Empty State Title");
21
- const iconElement: HTMLElement = screen.getByRole("icon");
21
+ // The icon renders an inline <svg> (no invalid role="icon"; WCAG 4.1.2).
22
+ const iconElement: Element | null = container.querySelector("svg");
22
23
  const footerElement: HTMLElement = screen.getByText(
23
24
  "This is a footer element",
24
25
  );
25
26
  expect(titleElement).toBeInTheDocument();
26
27
  expect(descriptionElement).toBeInTheDocument();
27
- expect(iconElement).toBeInTheDocument();
28
+ expect(iconElement).not.toBeNull();
28
29
  expect(footerElement).toBeInTheDocument();
29
30
  });
30
31
  test("renders without an icon", () => {
@@ -281,9 +281,10 @@ describe("Input", () => {
281
281
  test("displays error icon", () => {
282
282
  const error: string = "error";
283
283
 
284
- render(<Input {...{ error }} />);
284
+ const { container } = render(<Input {...{ error }} />);
285
285
 
286
- expect(screen.getByRole("icon", { hidden: true })).toBeInTheDocument();
286
+ // The error icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
287
+ expect(container.querySelector("svg")).not.toBeNull();
287
288
  });
288
289
 
289
290
  test("sets error style if error exists", () => {
@@ -52,8 +52,10 @@ describe("<Pill />", () => {
52
52
  const { container } = render(
53
53
  <Pill text="Love" color={color} icon={IconProp.Label} />,
54
54
  );
55
- // The icon renders an inline <svg>. (It no longer uses the invalid
56
- // role="icon" ARIA role, which was removed for WCAG 4.1.2 compliance.)
55
+ /*
56
+ * The icon renders an inline <svg>. (It no longer uses the invalid
57
+ * role="icon" ARIA role, which was removed for WCAG 4.1.2 compliance.)
58
+ */
57
59
  expect(container.querySelector("svg")).not.toBeNull();
58
60
  });
59
61
  });
@@ -60,9 +60,12 @@ describe("Side Menu Item", () => {
60
60
  });
61
61
 
62
62
  it("Should render icon if provided", () => {
63
- render(<SideMenuItem {...defaultProps} icon={IconProp.Home} />);
63
+ const { container } = render(
64
+ <SideMenuItem {...defaultProps} icon={IconProp.Home} />,
65
+ );
64
66
 
65
- expect(screen.getByRole("icon")).toBeInTheDocument();
67
+ // The icon renders an inline <svg> (no invalid role="icon"; WCAG 4.1.2).
68
+ expect(container.querySelector("svg")).not.toBeNull();
66
69
  });
67
70
 
68
71
  it("Should render the sub item link with given title and Icon", () => {
@@ -73,7 +76,7 @@ describe("Side Menu Item", () => {
73
76
  title: "Sub Page",
74
77
  to: new Route("/sub-page"),
75
78
  };
76
- render(
79
+ const { container } = render(
77
80
  <SideMenuItem
78
81
  {...defaultProps}
79
82
  subItemLink={subLink}
@@ -87,7 +90,8 @@ describe("Side Menu Item", () => {
87
90
  .closest("a");
88
91
 
89
92
  expect(subLinkElement).toBeInTheDocument();
90
- expect(screen.getAllByRole("icon")).toHaveLength(2);
93
+ // Main and sub-item icons each render an inline <svg> (no role="icon").
94
+ expect(container.querySelectorAll("svg")).toHaveLength(2);
91
95
  });
92
96
 
93
97
  it("Should render link badge if provided", () => {
@@ -104,15 +108,21 @@ describe("Side Menu Item", () => {
104
108
  });
105
109
 
106
110
  it("Should show alert", () => {
107
- render(<SideMenuItem {...defaultProps} showAlert={true} />);
111
+ const { container } = render(
112
+ <SideMenuItem {...defaultProps} showAlert={true} />,
113
+ );
108
114
 
109
- expect(screen.getByRole("icon")).toBeInTheDocument();
115
+ // The alert icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
116
+ expect(container.querySelector("svg")).not.toBeNull();
110
117
  });
111
118
 
112
119
  it("Should show warning", () => {
113
- render(<SideMenuItem {...defaultProps} showWarning={true} />);
120
+ const { container } = render(
121
+ <SideMenuItem {...defaultProps} showWarning={true} />,
122
+ );
114
123
 
115
- expect(screen.getByRole("icon")).toBeInTheDocument();
124
+ // The warning icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
125
+ expect(container.querySelector("svg")).not.toBeNull();
116
126
  });
117
127
 
118
128
  it("Should highlights the main link when on the same page", () => {
@@ -117,13 +117,15 @@ describe("TextArea", () => {
117
117
  });
118
118
 
119
119
  test("displays error icon", () => {
120
- const { getByRole } = render(<TextArea error="error" initialValue="" />);
121
- expect(getByRole("icon", { hidden: true })).toBeInTheDocument();
120
+ const { container } = render(<TextArea error="error" initialValue="" />);
121
+ // The error icon renders an inline <svg> (no role="icon"; WCAG 4.1.2).
122
+ expect(container.querySelector("svg")).not.toBeNull();
122
123
  });
123
124
 
124
125
  test("does not display error icon without error", () => {
125
- const { queryByRole } = render(<TextArea initialValue="" />);
126
- expect(queryByRole("icon", { hidden: true })).not.toBeInTheDocument();
126
+ const { container } = render(<TextArea initialValue="" />);
127
+ // Without an error there is no icon, so no <svg> is rendered.
128
+ expect(container.querySelector("svg")).toBeNull();
127
129
  });
128
130
 
129
131
  test("applies error styles", () => {
@@ -0,0 +1,74 @@
1
+ import Zod, { ZodSchema } from "../../../Utils/Schema/Zod";
2
+
3
+ /**
4
+ * Per-criteria configuration that lets a single Incoming Request /
5
+ * webhook monitor open MULTIPLE concurrent incidents — one per distinct
6
+ * value extracted from the incoming payload — instead of the default
7
+ * one-active-incident-per-criteria behaviour.
8
+ *
9
+ * This is what makes a single Grafana/Alertmanager webhook endpoint fan
10
+ * out into separate incidents ("High RAM Usage", "High CPU Usage", ...)
11
+ * that can all be active at the same time. Under the hood the extracted
12
+ * value is hashed into the same `seriesFingerprint` dedupe key that
13
+ * metric monitors already use for per-series incidents, so the existing
14
+ * create + dedupe machinery handles the rest.
15
+ *
16
+ * When this is absent on a criteria, the monitor behaves exactly as
17
+ * before (one active incident per criteria) — the feature is strictly
18
+ * opt-in.
19
+ */
20
+ export default interface IncidentGroupingConfig {
21
+ /**
22
+ * Path into the incoming request body used to derive the grouping /
23
+ * dedupe key. Uses the same `requestBody`-rooted convention as incident
24
+ * templates and JavaScript-expression filters; both of these resolve
25
+ * identically (the `{{ … }}` template wrapper is optional):
26
+ *
27
+ * `{{requestBody.alerts[*].labels.alertname}}`
28
+ * `requestBody.alerts[*].labels.alertname`
29
+ *
30
+ * The `requestBody` root is required. Supports dotted paths and array
31
+ * indexing (`[0]`, `[last]`) per the
32
+ * shared `VMUtil.deepFind` syntax, plus a single `[*]` wildcard to fan
33
+ * out over an array — one incident per element. For Grafana's
34
+ * Alertmanager-shaped webhook this is typically
35
+ * `requestBody.alerts[*].labels.alertname` or
36
+ * `requestBody.commonLabels.alertname`.
37
+ */
38
+ groupByJSONPath: string;
39
+
40
+ /**
41
+ * Optional explicit "resolved" classifier for event-driven sources.
42
+ * Webhooks are event-driven: a single POST describes only the alerts in
43
+ * that payload, not the full set of everything currently firing, so an
44
+ * incident must NOT be auto-resolved merely because its key is absent
45
+ * from a later payload (that's the snapshot model metric monitors use).
46
+ * Instead, when the value at `resolvedWhenJSONPath` equals
47
+ * `resolvedWhenValue`, the event resolves the incident for that key.
48
+ *
49
+ * Uses the same `requestBody`-rooted convention as `groupByJSONPath`.
50
+ * For Grafana this is `requestBody.status` == `resolved` (payload-level),
51
+ * or `requestBody.alerts[*].status` == `resolved` when `groupByJSONPath`
52
+ * also fans out over `alerts[*]` (the resolve check is then evaluated per
53
+ * element).
54
+ *
55
+ * When unset, every matching event is treated as firing and grouped
56
+ * incidents are only ever resolved manually.
57
+ */
58
+ resolvedWhenJSONPath?: string | undefined;
59
+ resolvedWhenValue?: string | undefined;
60
+
61
+ /**
62
+ * Safety cap on the number of distinct keys processed from a single
63
+ * payload. Protects against a high-cardinality field (or a key that
64
+ * changes every fire) spawning unbounded incidents. Defaults to 100.
65
+ */
66
+ maxKeysPerPayload?: number | undefined;
67
+ }
68
+
69
+ export const IncidentGroupingConfigSchema: ZodSchema = Zod.object({
70
+ groupByJSONPath: Zod.string(),
71
+ resolvedWhenJSONPath: Zod.string().optional(),
72
+ resolvedWhenValue: Zod.string().optional(),
73
+ maxKeysPerPayload: Zod.number().optional(),
74
+ });
@@ -15,6 +15,9 @@ import {
15
15
  CriteriaFilterSchema,
16
16
  } from "./CriteriaFilter";
17
17
  import { CriteriaIncident, CriteriaIncidentSchema } from "./CriteriaIncident";
18
+ import IncidentGroupingConfig, {
19
+ IncidentGroupingConfigSchema,
20
+ } from "./IncomingMonitor/IncidentGroupingConfig";
18
21
  import MonitorType from "./MonitorType";
19
22
  import { FindOperator } from "typeorm";
20
23
  import Zod, { ZodSchema } from "../../Utils/Schema/Zod";
@@ -31,6 +34,12 @@ export interface MonitorCriteriaInstanceType {
31
34
  createIncidents?: boolean | undefined;
32
35
  createAlerts?: boolean | undefined;
33
36
  isEnabled?: boolean | undefined;
37
+ /**
38
+ * Incoming Request monitors only: opt-in config to open one incident
39
+ * per distinct value extracted from the webhook payload (e.g. one per
40
+ * Grafana alert name) instead of a single active incident per criteria.
41
+ */
42
+ incidentGrouping?: IncidentGroupingConfig | undefined;
34
43
  id: string;
35
44
  }
36
45
 
@@ -1482,6 +1491,16 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1482
1491
  return this;
1483
1492
  }
1484
1493
 
1494
+ public setIncidentGrouping(
1495
+ incidentGrouping: IncidentGroupingConfig | undefined,
1496
+ ): MonitorCriteriaInstance {
1497
+ if (this.data) {
1498
+ this.data.incidentGrouping = incidentGrouping;
1499
+ }
1500
+
1501
+ return this;
1502
+ }
1503
+
1485
1504
  public override toJSON(): JSONObject {
1486
1505
  if (!this.data) {
1487
1506
  return MonitorCriteriaInstance.getNewMonitorCriteriaInstanceAsJSON();
@@ -1500,6 +1519,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1500
1519
  changeMonitorStatus: this.data.changeMonitorStatus,
1501
1520
  createIncidents: this.data.createIncidents,
1502
1521
  isEnabled: this.data.isEnabled,
1522
+ incidentGrouping: this.data.incidentGrouping,
1503
1523
  name: this.data.name,
1504
1524
  description: this.data.description,
1505
1525
  } as any,
@@ -1609,6 +1629,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1609
1629
  createAlerts: (json["createAlerts"] as boolean) || false,
1610
1630
  isEnabled:
1611
1631
  json["isEnabled"] === undefined ? true : (json["isEnabled"] as boolean),
1632
+ incidentGrouping: (json["incidentGrouping"] as any) || undefined,
1612
1633
  filters: filters as any,
1613
1634
  incidents: incidents as any,
1614
1635
  alerts: alerts as any,
@@ -1635,6 +1656,7 @@ export default class MonitorCriteriaInstance extends DatabaseProperty {
1635
1656
  createIncidents: Zod.boolean().optional(),
1636
1657
  createAlerts: Zod.boolean().optional(),
1637
1658
  isEnabled: Zod.boolean().optional(),
1659
+ incidentGrouping: IncidentGroupingConfigSchema.optional(),
1638
1660
  }).openapi({
1639
1661
  type: "object",
1640
1662
  example: {