@oneuptime/common 12.0.31 → 12.0.32

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 (188) hide show
  1. package/Models/DatabaseModels/AlertState.ts +0 -2
  2. package/Models/DatabaseModels/Domain.ts +1 -1
  3. package/Models/DatabaseModels/Incident.ts +1 -1
  4. package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
  5. package/Models/DatabaseModels/Index.ts +2 -0
  6. package/Models/DatabaseModels/MetricType.ts +0 -2
  7. package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
  8. package/Models/DatabaseModels/MonitorTest.ts +0 -2
  9. package/Models/DatabaseModels/NetworkDevice.ts +121 -2
  10. package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
  11. package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
  12. package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
  13. package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
  14. package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
  15. package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
  16. package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
  17. package/Models/DatabaseModels/StatusPageResource.ts +0 -2
  18. package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
  19. package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
  20. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  21. package/Server/Services/DatabaseService.ts +39 -9
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/NetworkDeviceRoleService.ts +90 -0
  24. package/Server/Services/ProjectService.ts +85 -0
  25. package/Server/Services/UserNotificationSettingService.ts +0 -6
  26. package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
  27. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
  28. package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
  29. package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
  30. package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
  31. package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
  32. package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
  33. package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
  34. package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
  35. package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
  36. package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
  37. package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
  38. package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
  39. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
  40. package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
  41. package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
  42. package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
  43. package/Tests/Server/Utils/BasicCron.test.ts +283 -0
  44. package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
  45. package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
  46. package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
  47. package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
  48. package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
  49. package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
  50. package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
  51. package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
  52. package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
  53. package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
  54. package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
  55. package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
  56. package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
  57. package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
  58. package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
  59. package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
  60. package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
  61. package/Tests/Utils/Slug.test.ts +61 -1
  62. package/Tests/jest.setup.ts +19 -0
  63. package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
  64. package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
  65. package/Types/Permission.ts +45 -0
  66. package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
  67. package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
  68. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
  69. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
  70. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
  71. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
  72. package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
  73. package/UI/Components/Charts/Line/LineChart.tsx +7 -0
  74. package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
  75. package/UI/Components/Navbar/NavBar.tsx +62 -13
  76. package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
  77. package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
  78. package/UI/Components/SideMenu/SideMenu.tsx +12 -2
  79. package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
  80. package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
  81. package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
  82. package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
  83. package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
  84. package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
  85. package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
  86. package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
  87. package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
  88. package/Utils/Slug.ts +34 -1
  89. package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
  90. package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
  91. package/build/dist/Models/DatabaseModels/Domain.js +1 -1
  92. package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
  93. package/build/dist/Models/DatabaseModels/Incident.js +1 -1
  94. package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
  95. package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
  96. package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
  97. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  98. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  99. package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
  100. package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
  101. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
  102. package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
  103. package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
  104. package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
  105. package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
  106. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  107. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
  108. package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
  109. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
  110. package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
  111. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
  112. package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
  113. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
  114. package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
  115. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
  116. package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
  117. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
  118. package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
  119. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
  120. package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
  121. package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
  122. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  123. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
  124. package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  128. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  129. package/build/dist/Server/Services/DatabaseService.js +31 -5
  130. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  131. package/build/dist/Server/Services/Index.js +2 -0
  132. package/build/dist/Server/Services/Index.js.map +1 -1
  133. package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
  134. package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
  135. package/build/dist/Server/Services/ProjectService.js +65 -0
  136. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  137. package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
  138. package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
  139. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
  140. package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
  141. package/build/dist/Types/Permission.js +40 -0
  142. package/build/dist/Types/Permission.js.map +1 -1
  143. package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
  144. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  145. package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
  146. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  147. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
  148. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  149. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
  150. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  151. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
  152. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  153. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
  154. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  155. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
  156. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
  157. package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
  158. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  159. package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
  160. package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
  161. package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
  162. package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
  163. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
  164. package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
  165. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
  166. package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
  167. package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
  168. package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
  169. package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
  170. package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
  171. package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
  172. package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
  173. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
  174. package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
  175. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
  176. package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
  177. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
  178. package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
  179. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
  180. package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
  181. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
  182. package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
  183. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
  184. package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
  185. package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
  186. package/build/dist/Utils/Slug.js +31 -1
  187. package/build/dist/Utils/Slug.js.map +1 -1
  188. package/package.json +1 -1
@@ -0,0 +1,171 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { describe, expect, test } from "@jest/globals";
4
+ import {
5
+ SLO_CURRENT_BURN_RATE_WINDOW_MINUTES,
6
+ SLO_EVALUATION_CADENCE_MINUTES,
7
+ } from "../../../Utils/Slo/SloEvaluation";
8
+
9
+ /*
10
+ * The SLO cadence constants.
11
+ *
12
+ * These two numbers were prose before they were code — the worker owned them
13
+ * and the dashboard described them in words ("evaluated every few minutes",
14
+ * "1x spends the budget exactly over the window") without naming the lookback
15
+ * the headline burn rate is measured over. Naming them once was the fix, and
16
+ * each carries a claim about a number that lives somewhere else:
17
+ *
18
+ * - the burn-rate window is "deliberately equal to the default fast-burn
19
+ * rule's long window, so the tile and that rule move together", and
20
+ * - the cadence is how often a due SLO is re-evaluated.
21
+ *
22
+ * Nothing makes either claim fail to compile. The seeded rules are written by
23
+ * ServiceLevelObjectiveService as plain literals, so someone retuning the
24
+ * fast-burn default there would leave an overview tile silently measuring a
25
+ * different span from the rule it is meant to agree with — the two would
26
+ * disagree on screen with no error anywhere.
27
+ *
28
+ * The defaults are read as text because seeding them is a database write
29
+ * behind a full SLO create, which is not what is under test here.
30
+ */
31
+
32
+ const SLO_SERVICE_SOURCE: string = fs.readFileSync(
33
+ path.join(
34
+ __dirname,
35
+ "../../../Server/Services/ServiceLevelObjectiveService.ts",
36
+ ),
37
+ "utf8",
38
+ );
39
+
40
+ /*
41
+ * The literal block for one seeded rule, so "Fast burn" and "Slow burn"
42
+ * cannot be confused for one another — they differ only in their numbers.
43
+ */
44
+ function seededRuleBlock(ruleName: string): string {
45
+ const start: number = SLO_SERVICE_SOURCE.indexOf(`name: "${ruleName}"`);
46
+
47
+ expect(start).toBeGreaterThan(-1);
48
+
49
+ const end: number = SLO_SERVICE_SOURCE.indexOf("}", start);
50
+ expect(end).toBeGreaterThan(start);
51
+
52
+ return SLO_SERVICE_SOURCE.slice(start, end);
53
+ }
54
+
55
+ function minutesFieldIn(block: string, field: string): number {
56
+ const match: RegExpMatchArray | null = block.match(
57
+ new RegExp(`${field}:\\s*(\\d+)`),
58
+ );
59
+
60
+ expect(match).not.toBeNull();
61
+
62
+ return Number(match![1]);
63
+ }
64
+
65
+ describe("SLO evaluation cadence constants", () => {
66
+ describe("the values themselves", () => {
67
+ /*
68
+ * Both are used as minutes and multiplied out to seconds by their
69
+ * callers, so a zero would ask for a zero-length lookback and a
70
+ * fractional value would land the query on a boundary no bucket starts
71
+ * on.
72
+ */
73
+ test("both are positive whole numbers of minutes", () => {
74
+ for (const value of [
75
+ SLO_EVALUATION_CADENCE_MINUTES,
76
+ SLO_CURRENT_BURN_RATE_WINDOW_MINUTES,
77
+ ]) {
78
+ expect(Number.isInteger(value)).toBe(true);
79
+ expect(value).toBeGreaterThan(0);
80
+ }
81
+ });
82
+
83
+ /*
84
+ * A burn rate measured over a span shorter than the gap between
85
+ * evaluations would be recomputed from data the worker has not refreshed,
86
+ * so the tile would repeat itself and call it a new reading.
87
+ */
88
+ test("the burn-rate lookback is at least one evaluation apart", () => {
89
+ expect(SLO_CURRENT_BURN_RATE_WINDOW_MINUTES).toBeGreaterThanOrEqual(
90
+ SLO_EVALUATION_CADENCE_MINUTES,
91
+ );
92
+ });
93
+ });
94
+
95
+ /*
96
+ * The claim in the doc comment, checked against the thing it is a claim
97
+ * about. If the fast-burn default is ever retuned, this fails here rather
98
+ * than as two numbers quietly disagreeing on the SLO overview.
99
+ */
100
+ describe("the tile and the default fast-burn rule move together", () => {
101
+ test("the burn-rate window equals the fast-burn rule's long window", () => {
102
+ expect(
103
+ minutesFieldIn(seededRuleBlock("Fast burn"), "longWindowInMinutes"),
104
+ ).toBe(SLO_CURRENT_BURN_RATE_WINDOW_MINUTES);
105
+ });
106
+
107
+ /*
108
+ * The fast-burn short window is the tightest span the worker is ever
109
+ * asked to answer for. Evaluating less often than that would leave it
110
+ * reading a window that has already closed.
111
+ */
112
+ test("the evaluation cadence keeps up with the fast-burn short window", () => {
113
+ expect(
114
+ minutesFieldIn(seededRuleBlock("Fast burn"), "shortWindowInMinutes"),
115
+ ).toBeGreaterThanOrEqual(SLO_EVALUATION_CADENCE_MINUTES);
116
+ });
117
+
118
+ /*
119
+ * Slow burn is the deliberately longer pair. Pinned only as an ordering
120
+ * against fast burn: if the two ever crossed over, the names would be
121
+ * backwards and the rule an operator reaches for in an incident would be
122
+ * the slower of the two.
123
+ */
124
+ test("slow burn is genuinely slower than fast burn", () => {
125
+ const fast: string = seededRuleBlock("Fast burn");
126
+ const slow: string = seededRuleBlock("Slow burn");
127
+
128
+ expect(minutesFieldIn(slow, "longWindowInMinutes")).toBeGreaterThan(
129
+ minutesFieldIn(fast, "longWindowInMinutes"),
130
+ );
131
+ expect(minutesFieldIn(slow, "shortWindowInMinutes")).toBeGreaterThan(
132
+ minutesFieldIn(fast, "shortWindowInMinutes"),
133
+ );
134
+ });
135
+ });
136
+
137
+ /*
138
+ * Both constants exist so the worker and the UI read the same number. A
139
+ * caller that inlined the value instead would drift the moment the constant
140
+ * changed, which is the exact failure the constants were introduced to end.
141
+ */
142
+ describe("everything reads the shared constants rather than its own copy", () => {
143
+ const WORKER_SOURCE: string = fs.readFileSync(
144
+ path.join(
145
+ __dirname,
146
+ "../../../../App/FeatureSet/Workers/Jobs/Slo/EvaluateSlos.ts",
147
+ ),
148
+ "utf8",
149
+ );
150
+
151
+ test("the evaluation worker imports both from here", () => {
152
+ expect(WORKER_SOURCE).toContain("SLO_CURRENT_BURN_RATE_WINDOW_MINUTES");
153
+ expect(WORKER_SOURCE).toContain("SLO_EVALUATION_CADENCE_MINUTES");
154
+ expect(WORKER_SOURCE).toContain('from "Common/Utils/Slo/SloEvaluation"');
155
+ });
156
+
157
+ test("the SLO overview reads the window from here too", () => {
158
+ const overview: string = fs.readFileSync(
159
+ path.join(
160
+ __dirname,
161
+ "../../../../App/FeatureSet/Dashboard/src/Pages/Slo/View/Index.tsx",
162
+ ),
163
+ "utf8",
164
+ );
165
+
166
+ expect(overview).toContain(
167
+ 'import { SLO_CURRENT_BURN_RATE_WINDOW_MINUTES } from "Common/Utils/Slo/SloEvaluation"',
168
+ );
169
+ });
170
+ });
171
+ });
@@ -1,4 +1,5 @@
1
- import Slug from "../../Utils/Slug";
1
+ import Slug, { SLUG_SUFFIX_LENGTH } from "../../Utils/Slug";
2
+ import ColumnLength from "../../Types/Database/ColumnLength";
2
3
 
3
4
  describe("Slug.getSlug()", () => {
4
5
  test("should return empty string, if name is empty ", () => {
@@ -21,3 +22,62 @@ describe("Slug.getSlug()", () => {
21
22
  );
22
23
  });
23
24
  });
25
+
26
+ /*
27
+ * The ceiling exists because the create path THROWS on an oversized value
28
+ * rather than truncating it (DatabaseService.checkMaxLengthOfFields), and slug
29
+ * sources are routinely wider than the slug column they feed: Incident.title
30
+ * is varchar(500) against a varchar(100) slug. DatabaseService.generateSlug
31
+ * passes the destination column's declared width in.
32
+ */
33
+ describe("Slug.getSlug() — clamped to a destination column", () => {
34
+ const LONG_NAME: string = "a very long incident title indeed ".repeat(10);
35
+
36
+ test("leaves a name that already fits alone", () => {
37
+ expect(Slug.getSlug("short name", ColumnLength.Slug)).toMatch(
38
+ /^short-name-[\d]{10}$/,
39
+ );
40
+ });
41
+
42
+ test("fits the result inside the ceiling", () => {
43
+ expect(
44
+ Slug.getSlug(LONG_NAME, ColumnLength.Slug).length,
45
+ ).toBeLessThanOrEqual(ColumnLength.Slug);
46
+ });
47
+
48
+ test("cuts the readable half and keeps the unique tail whole", () => {
49
+ const slug: string = Slug.getSlug(LONG_NAME, ColumnLength.Slug);
50
+
51
+ expect(slug.substring(slug.length - SLUG_SUFFIX_LENGTH)).toMatch(
52
+ /^-[\d]{10}$/,
53
+ );
54
+ expect(slug.startsWith("a-very-long-incident-title-indeed")).toBe(true);
55
+ });
56
+
57
+ test("leaves no dash dangling where the cut fell", () => {
58
+ /*
59
+ * 44 puts the ceiling exactly on the dash after the second "indeed",
60
+ * which is the case that would otherwise read `...indeed--0123456789`.
61
+ */
62
+ expect(Slug.getSlug(LONG_NAME, 45)).not.toMatch(/--[\d]{10}$/);
63
+
64
+ /* Every nearby ceiling, so the case above is not the only one covered. */
65
+ for (let ceiling: number = 20; ceiling <= 100; ceiling++) {
66
+ const slug: string = Slug.getSlug(LONG_NAME, ceiling);
67
+
68
+ expect(slug.length).toBeLessThanOrEqual(ceiling);
69
+ expect(slug).not.toMatch(/--[\d]{10}$/);
70
+ }
71
+ });
72
+
73
+ test("still answers with a random name for a null source", () => {
74
+ const slug: string = Slug.getSlug(null, ColumnLength.Slug);
75
+
76
+ expect(slug).toMatch(/^[a-z0-9-]+-[\d]{10}$/);
77
+ expect(slug.length).toBeLessThanOrEqual(ColumnLength.Slug);
78
+ });
79
+
80
+ test("still answers with an empty string for an empty name", () => {
81
+ expect(Slug.getSlug("", ColumnLength.Slug)).toEqual("");
82
+ });
83
+ });
@@ -1,4 +1,5 @@
1
1
  import "@testing-library/jest-dom";
2
+ import { configure } from "@testing-library/react";
2
3
  import { jest } from "@jest/globals";
3
4
  import { TextEncoder, TextDecoder } from "util";
4
5
  import { webcrypto, randomUUID } from "crypto";
@@ -31,3 +32,21 @@ Object.defineProperty(window, "scrollTo", {
31
32
  value: jest.fn(),
32
33
  writable: true,
33
34
  });
35
+
36
+ /*
37
+ * Testing Library waits a default of one second for findBy/waitFor. That is a
38
+ * generous budget for a state update and a hopeless one for a React.lazy
39
+ * boundary: the first component to render LazyMarkdownViewer has to resolve
40
+ * the dynamic import and put MarkdownViewer (plus its mermaid mock) through
41
+ * ts-jest before any assertion on the markdown text can pass. Locally that is
42
+ * ~400 ms; on a CI runner with the other shard workers competing for the same
43
+ * cores it goes past a second, and the test fails having rendered the Suspense
44
+ * fallback -- a timeout wearing the costume of a missing element.
45
+ *
46
+ * ~20 components reach the viewer through that wrapper (FeedItem, EventItem,
47
+ * Detail, BaseModelTable and the rest), so this is a whole class of flake
48
+ * rather than one test, and it belongs here rather than at one call site.
49
+ * Five seconds is still short enough that a genuinely broken assertion fails
50
+ * promptly instead of sitting out the 60 s testTimeout.
51
+ */
52
+ configure({ asyncUtilTimeout: 5000 });
@@ -57,6 +57,25 @@ export type NetworkTopologyDeviceRole =
57
57
  | "host"
58
58
  | "unknown";
59
59
 
60
+ /*
61
+ * The silhouette a node is drawn with on the topology map.
62
+ *
63
+ * It lives here, next to the roles, rather than in the renderer that owns
64
+ * the geometry, because a project's device roles are configurable
65
+ * (NetworkDeviceRole) and each one stores the shape it is drawn with. The
66
+ * renderer re-exports this type, so every existing import of it still
67
+ * resolves to exactly this union.
68
+ */
69
+ export type NetworkTopologyNodeShape =
70
+ | "circle"
71
+ | "rounded-square"
72
+ | "diamond"
73
+ | "triangle"
74
+ | "hexagon"
75
+ | "tower"
76
+ | "cylinder"
77
+ | "rect";
78
+
60
79
  /*
61
80
  * Why a device is drawn with no links.
62
81
  *
@@ -100,6 +119,48 @@ export interface NetworkTopologyNode {
100
119
  * name one — readers must treat both the same way.
101
120
  */
102
121
  role?: NetworkTopologyDeviceRole | undefined;
122
+ /*
123
+ * The project's OWN role for this node, from the configurable
124
+ * NetworkDeviceRole table (Network > Settings > Device Roles).
125
+ *
126
+ * `role` above is the built-in vocabulary the classifier speaks and can
127
+ * only ever be one of the twelve values in NetworkTopologyDeviceRole.
128
+ * A project may rename those roles or add roles of its own ("PoS
129
+ * Terminal", "SD-WAN Edge"), and neither survives that union — so the
130
+ * four fields here carry the configured answer alongside it.
131
+ *
132
+ * They are additive and every one of them is optional: a payload built
133
+ * before this existed, or for a project with no roles configured, simply
134
+ * omits them and every reader falls back to `role` exactly as before.
135
+ * When present they WIN, because they are what the operator configured.
136
+ *
137
+ * `roleKey` is the role's stable key — the built-in key ("switch") for a
138
+ * built-in role however it has been renamed, or a derived key for a
139
+ * custom one. It is what the legend groups by and what search matches.
140
+ */
141
+ roleKey?: string | undefined;
142
+ // The configured display name, e.g. "Edge Router" for a renamed "router".
143
+ roleLabel?: string | undefined;
144
+ // The configured silhouette. Falls back to the built-in shape for `role`.
145
+ roleShape?: NetworkTopologyNodeShape | undefined;
146
+ /*
147
+ * Whether devices of this role sit at the top of the network. Load-bearing:
148
+ * the tiered and radial layouts band by it, so a custom role can be placed
149
+ * at core level without the layout having to recognise its name.
150
+ */
151
+ isCoreLayerRole?: boolean | undefined;
152
+ /*
153
+ * Whether devices of this role are worth walking with SNMP. Read by the
154
+ * adopt-a-neighbour flow to decide whether it opens on SNMP polling or on
155
+ * a monitor.
156
+ */
157
+ isSnmpWalkableRole?: boolean | undefined;
158
+ /*
159
+ * The NetworkDeviceRole row's id, so a form can preselect the role the map
160
+ * already shows without resolving the key itself. Absent when the project
161
+ * has no row for this role.
162
+ */
163
+ roleId?: string | undefined;
103
164
  interfacesUp?: number | undefined;
104
165
  interfacesDown?: number | undefined;
105
166
  // Extra device identity for search and the detail panel (managed nodes).
@@ -0,0 +1,145 @@
1
+ import {
2
+ NetworkTopologyDeviceRole,
3
+ NetworkTopologyNodeShape,
4
+ } from "../Monitor/SnmpMonitor/NetworkTopology";
5
+
6
+ /*
7
+ * The device roles seeded into every project.
8
+ *
9
+ * Device roles used to be a fixed union (NetworkTopologyDeviceRole) with the
10
+ * label, the silhouette and the "is this a core device?" flag each hardcoded
11
+ * in a different module. They are now the per-project NetworkDeviceRole
12
+ * lookup table, so a project can rename "Wireless AP" to "Access Point",
13
+ * change what a firewall is drawn as, or add a role of its own.
14
+ *
15
+ * This file is the single source of truth for the DEFAULTS, shared by
16
+ * ProjectService (which seeds new projects) and the BackfillNetworkDeviceRoles
17
+ * data migration (which seeds projects that predate the table), so the two can
18
+ * never disagree about what a fresh project starts with.
19
+ *
20
+ * `key` is deliberately the built-in NetworkTopologyDeviceRole value. The SNMP
21
+ * classifier speaks that vocabulary and always will — it is evidence-driven
22
+ * and cannot invent a project's custom role — so matching its answer back to a
23
+ * configured row is a lookup by this key. That is also why the key is stable
24
+ * and the name is not: renaming a role must not stop the classifier finding it.
25
+ */
26
+ export interface DefaultNetworkDeviceRole {
27
+ key: NetworkTopologyDeviceRole;
28
+ name: string;
29
+ description: string;
30
+ topologyShape: NetworkTopologyNodeShape;
31
+ isCoreLayer: boolean;
32
+ isSnmpWalkable: boolean;
33
+ }
34
+
35
+ /*
36
+ * Ordered exactly as the map legend lists them, because the seeded `order`
37
+ * column is this array's index — a fresh project's settings page and its map
38
+ * legend therefore read in the same sequence.
39
+ *
40
+ * The shapes reproduce the renderer's historical SHAPE_BY_ROLE map and the
41
+ * isCoreLayer flags reproduce its CORE_DEVICE_ROLES set, so a project seeded
42
+ * from this list draws exactly the map it drew before roles were configurable.
43
+ *
44
+ * "unknown" is not here on purpose. It is not a role an operator assigns; it
45
+ * is the classifier saying it has no answer, and offering it as a choice would
46
+ * mean "stop classifying this device forever" rather than "I don't know".
47
+ */
48
+ export const DEFAULT_NETWORK_DEVICE_ROLES: ReadonlyArray<DefaultNetworkDeviceRole> =
49
+ [
50
+ {
51
+ key: "router",
52
+ name: "Router",
53
+ description:
54
+ "Moves traffic between networks. Sits at the top of the topology with the other core devices.",
55
+ topologyShape: "circle",
56
+ isCoreLayer: true,
57
+ isSnmpWalkable: true,
58
+ },
59
+ {
60
+ key: "switch",
61
+ name: "Switch",
62
+ description:
63
+ "Access or distribution switching. Endpoints hang off it, and it hangs off a core device.",
64
+ topologyShape: "rounded-square",
65
+ isCoreLayer: false,
66
+ isSnmpWalkable: true,
67
+ },
68
+ {
69
+ key: "firewall",
70
+ name: "Firewall",
71
+ description:
72
+ "Security appliance at a network boundary. Drawn at core level.",
73
+ topologyShape: "diamond",
74
+ isCoreLayer: true,
75
+ isSnmpWalkable: true,
76
+ },
77
+ {
78
+ key: "wirelessAccessPoint",
79
+ name: "Wireless AP",
80
+ description: "Wireless access point serving client devices.",
81
+ topologyShape: "triangle",
82
+ isCoreLayer: false,
83
+ isSnmpWalkable: true,
84
+ },
85
+ {
86
+ key: "loadBalancer",
87
+ name: "Load balancer",
88
+ description:
89
+ "Distributes traffic across a pool of servers. Drawn at core level.",
90
+ topologyShape: "hexagon",
91
+ isCoreLayer: true,
92
+ isSnmpWalkable: true,
93
+ },
94
+ {
95
+ key: "server",
96
+ name: "Server",
97
+ description: "A general purpose server or compute node.",
98
+ topologyShape: "tower",
99
+ isCoreLayer: false,
100
+ isSnmpWalkable: true,
101
+ },
102
+ {
103
+ key: "storage",
104
+ name: "Storage",
105
+ description: "A NAS, SAN or other storage appliance.",
106
+ topologyShape: "cylinder",
107
+ isCoreLayer: false,
108
+ isSnmpWalkable: true,
109
+ },
110
+ {
111
+ key: "printer",
112
+ name: "Printer",
113
+ description: "A network printer or multifunction device.",
114
+ topologyShape: "rect",
115
+ isCoreLayer: false,
116
+ isSnmpWalkable: false,
117
+ },
118
+ {
119
+ key: "camera",
120
+ name: "Camera",
121
+ description: "An IP camera or other video endpoint.",
122
+ topologyShape: "rect",
123
+ isCoreLayer: false,
124
+ isSnmpWalkable: false,
125
+ },
126
+ {
127
+ key: "phone",
128
+ name: "IP phone",
129
+ description: "A VoIP handset or conference phone.",
130
+ topologyShape: "rect",
131
+ isCoreLayer: false,
132
+ isSnmpWalkable: false,
133
+ },
134
+ {
135
+ key: "host",
136
+ name: "Host",
137
+ description:
138
+ "Anything plugged into an access port that is not one of the roles above.",
139
+ topologyShape: "rect",
140
+ isCoreLayer: false,
141
+ isSnmpWalkable: false,
142
+ },
143
+ ];
144
+
145
+ export default DEFAULT_NETWORK_DEVICE_ROLES;
@@ -1393,6 +1393,11 @@ enum Permission {
1393
1393
  EditNetworkSiteType = "EditNetworkSiteType",
1394
1394
  ReadNetworkSiteType = "ReadNetworkSiteType",
1395
1395
 
1396
+ CreateNetworkDeviceRole = "CreateNetworkDeviceRole",
1397
+ DeleteNetworkDeviceRole = "DeleteNetworkDeviceRole",
1398
+ EditNetworkDeviceRole = "EditNetworkDeviceRole",
1399
+ ReadNetworkDeviceRole = "ReadNetworkDeviceRole",
1400
+
1396
1401
  CreateRecommendationDismissal = "CreateRecommendationDismissal",
1397
1402
  DeleteRecommendationDismissal = "DeleteRecommendationDismissal",
1398
1403
  EditRecommendationDismissal = "EditRecommendationDismissal",
@@ -8572,6 +8577,46 @@ export class PermissionHelper {
8572
8577
  isRolePermission: false,
8573
8578
  group: PermissionGroup.Monitor,
8574
8579
  },
8580
+ {
8581
+ permission: Permission.CreateNetworkDeviceRole,
8582
+ title: "Create Network Device Role",
8583
+ description:
8584
+ "This permission can create Network Device Role in this project.",
8585
+ isAssignableToTenant: true,
8586
+ isAccessControlPermission: false,
8587
+ isRolePermission: false,
8588
+ group: PermissionGroup.Monitor,
8589
+ },
8590
+ {
8591
+ permission: Permission.DeleteNetworkDeviceRole,
8592
+ title: "Delete Network Device Role",
8593
+ description:
8594
+ "This permission can delete Network Device Role of this project.",
8595
+ isAssignableToTenant: true,
8596
+ isAccessControlPermission: true,
8597
+ isRolePermission: false,
8598
+ group: PermissionGroup.Monitor,
8599
+ },
8600
+ {
8601
+ permission: Permission.EditNetworkDeviceRole,
8602
+ title: "Edit Network Device Role",
8603
+ description:
8604
+ "This permission can edit Network Device Role of this project.",
8605
+ isAssignableToTenant: true,
8606
+ isAccessControlPermission: true,
8607
+ isRolePermission: false,
8608
+ group: PermissionGroup.Monitor,
8609
+ },
8610
+ {
8611
+ permission: Permission.ReadNetworkDeviceRole,
8612
+ title: "Read Network Device Role",
8613
+ description:
8614
+ "This permission can read Network Device Role of this project.",
8615
+ isAssignableToTenant: true,
8616
+ isAccessControlPermission: true,
8617
+ isRolePermission: false,
8618
+ group: PermissionGroup.Monitor,
8619
+ },
8575
8620
  {
8576
8621
  permission: Permission.CreateRecommendationDismissal,
8577
8622
  title: "Create Recommendation Dismissal",
@@ -63,6 +63,12 @@ export interface ComponentProps {
63
63
  * buckets calls back with the [start, end) of the selected time range.
64
64
  */
65
65
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
66
+ /*
67
+ * Double-click on the plot: undoes whatever drag-to-select produced.
68
+ * Supply it only when a reset is actually possible — see ChartLibrary
69
+ * onTimeRangeReset for why an idle handler costs click latency.
70
+ */
71
+ onTimeRangeReset?: (() => void) | undefined;
66
72
  // Plain click on a bucket — see ChartLibrary onBucketClick.
67
73
  onBucketClick?:
68
74
  | ((
@@ -241,6 +247,7 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
241
247
  }
242
248
  onExemplarClick={props.onExemplarClick}
243
249
  onTimeRangeSelect={props.onTimeRangeSelect}
250
+ onTimeRangeReset={props.onTimeRangeReset}
244
251
  onBucketClick={props.onBucketClick}
245
252
  ghostCategories={props.ghostSeriesNames}
246
253
  anomalyBandLowerKey={bandLower}
@@ -58,6 +58,11 @@ export interface ComponentProps {
58
58
  * indexed by series position (index % length), matching the default palette.
59
59
  */
60
60
  colors?: Array<ChartColorValue> | undefined;
61
+ /*
62
+ * Double-click on the plot: undoes a board-wide zoom. Bar panels have no
63
+ * drag-to-select of their own but share the dashboard's time range.
64
+ */
65
+ onTimeRangeReset?: (() => void) | undefined;
61
66
  }
62
67
 
63
68
  export interface BarInternalProps extends ComponentProps {
@@ -152,6 +157,7 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
152
157
  ? formattedReferenceRegions
153
158
  : undefined
154
159
  }
160
+ onTimeRangeReset={props.onTimeRangeReset}
155
161
  />
156
162
  {hasNoData && <NoDataMessage />}
157
163
  </div>
@@ -147,9 +147,17 @@ const ChartGroup: FunctionComponent<ComponentProps> = (
147
147
  return <></>;
148
148
  }
149
149
 
150
+ /*
151
+ * The way back out is only worth naming while there is something to
152
+ * reset — the host supplies onTimeRangeReset exactly then.
153
+ */
154
+ const canReset: boolean = Boolean(
155
+ (chart.props as LineChartProps | AreaChartProps).onTimeRangeReset,
156
+ );
157
+
150
158
  return (
151
159
  <span className="ml-auto shrink-0 whitespace-nowrap text-[10px] text-gray-400">
152
- Drag to zoom
160
+ {canReset ? "Drag to zoom · double-click to reset" : "Drag to zoom"}
153
161
  </span>
154
162
  );
155
163
  };