@oneuptime/common 11.3.4 → 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.
- package/Server/Infrastructure/Queue.ts +103 -16
- package/Server/Services/AnalyticsDatabaseService.ts +9 -5
- package/Server/Services/TeamMemberService.ts +38 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +31 -0
- package/Server/Utils/Monitor/IncomingRequestIncidentGrouping.ts +428 -0
- package/Server/Utils/Monitor/MonitorAlert.ts +158 -8
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +26 -0
- package/Server/Utils/Monitor/MonitorIncident.ts +147 -2
- package/Server/Utils/Monitor/MonitorResource.ts +74 -0
- package/Tests/Server/Utils/AnalyticsDatabase/ClusterAwareSchema.test.ts +2 -1
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +32 -0
- package/Tests/Server/Utils/Monitor/IncomingRequestIncidentGrouping.test.ts +389 -0
- package/Tests/Server/Utils/Monitor/SeriesAbsenceResolutionGuard.test.ts +177 -0
- package/Tests/Types/Events/Recurring.test.ts +475 -0
- package/Tests/Types/Monitor/CriteriaFilter.test.ts +348 -0
- package/Tests/Types/OnCallDutyPolicy/RestrictionTimes.test.ts +331 -0
- package/Tests/UI/Components/Alert.test.tsx +23 -15
- package/Tests/UI/Components/Breadcrumbs.test.tsx +18 -6
- package/Tests/UI/Components/Button.test.tsx +3 -3
- package/Tests/UI/Components/DictionaryFilterOperator.test.ts +174 -0
- package/Tests/UI/Components/EmptyState/EmptyState.test.tsx +4 -3
- package/Tests/UI/Components/Input.test.tsx +3 -2
- package/Tests/UI/Components/Pill.test.tsx +4 -2
- package/Tests/UI/Components/SideMenuItem.test.tsx +18 -8
- package/Tests/UI/Components/TextArea.test.tsx +6 -4
- package/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.ts +74 -0
- package/Types/Monitor/MonitorCriteriaInstance.ts +22 -0
- package/UI/Components/Dictionary/Dictionary.tsx +123 -50
- package/UI/Components/Dictionary/DictionaryFilterOperator.ts +96 -1
- package/UI/Components/MasterPage/MasterPage.tsx +6 -1
- package/build/dist/Server/Infrastructure/Queue.js +83 -15
- package/build/dist/Server/Infrastructure/Queue.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +9 -5
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/TeamMemberService.js +37 -1
- package/build/dist/Server/Services/TeamMemberService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +26 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js +288 -0
- package/build/dist/Server/Utils/Monitor/IncomingRequestIncidentGrouping.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js +119 -8
- package/build/dist/Server/Utils/Monitor/MonitorAlert.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +24 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js +103 -2
- package/build/dist/Server/Utils/Monitor/MonitorIncident.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +62 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js +8 -0
- package/build/dist/Types/Monitor/IncomingMonitor/IncidentGroupingConfig.js.map +1 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js +10 -0
- package/build/dist/Types/Monitor/MonitorCriteriaInstance.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/Dictionary.js +60 -9
- package/build/dist/UI/Components/Dictionary/Dictionary.js.map +1 -1
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js +69 -0
- package/build/dist/UI/Components/Dictionary/DictionaryFilterOperator.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +7 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- 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
|
-
|
|
22
|
-
expect(
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
32
|
-
expect(
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
-
|
|
63
|
+
const { container } = render(
|
|
64
|
+
<SideMenuItem {...defaultProps} icon={IconProp.Home} />,
|
|
65
|
+
);
|
|
64
66
|
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
+
const { container } = render(
|
|
112
|
+
<SideMenuItem {...defaultProps} showAlert={true} />,
|
|
113
|
+
);
|
|
108
114
|
|
|
109
|
-
|
|
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
|
-
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<SideMenuItem {...defaultProps} showWarning={true} />,
|
|
122
|
+
);
|
|
114
123
|
|
|
115
|
-
|
|
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 {
|
|
121
|
-
|
|
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 {
|
|
126
|
-
|
|
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: {
|