@oneuptime/common 12.0.2 → 12.0.3

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 (228) hide show
  1. package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
  2. package/Models/DatabaseModels/Index.ts +2 -0
  3. package/Models/DatabaseModels/LogDropFilter.ts +9 -20
  4. package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
  5. package/Models/DatabaseModels/StatusPageResource.ts +73 -0
  6. package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
  7. package/Server/API/BaseAPI.ts +13 -2
  8. package/Server/API/DashboardAPI.ts +39 -0
  9. package/Server/EnvironmentConfig.ts +31 -1
  10. package/Server/Infrastructure/GlobalCache.ts +115 -0
  11. package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
  14. package/Server/Services/AlertStateTimelineService.ts +20 -0
  15. package/Server/Services/CephClusterService.ts +26 -86
  16. package/Server/Services/CloudResourceService.ts +23 -83
  17. package/Server/Services/DatabaseService.ts +111 -0
  18. package/Server/Services/DockerHostService.ts +19 -79
  19. package/Server/Services/DockerSwarmClusterService.ts +27 -88
  20. package/Server/Services/HostService.ts +46 -99
  21. package/Server/Services/IncidentStateTimelineService.ts +20 -0
  22. package/Server/Services/Index.ts +2 -0
  23. package/Server/Services/IoTFleetService.ts +19 -79
  24. package/Server/Services/KubernetesClusterService.ts +17 -78
  25. package/Server/Services/LabelService.ts +57 -10
  26. package/Server/Services/MetricTypeService.ts +84 -0
  27. package/Server/Services/MonitorService.ts +89 -14
  28. package/Server/Services/MonitorStatusTimelineService.ts +29 -0
  29. package/Server/Services/MonitorTemplateService.ts +65 -0
  30. package/Server/Services/PodmanHostService.ts +19 -79
  31. package/Server/Services/ProxmoxClusterService.ts +23 -83
  32. package/Server/Services/RumApplicationService.ts +19 -80
  33. package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
  34. package/Server/Services/ServerlessFunctionService.ts +24 -85
  35. package/Server/Services/ServiceService.ts +299 -73
  36. package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
  37. package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
  38. package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
  39. package/Server/Services/TelemetryEntityService.ts +1 -0
  40. package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
  41. package/Server/Utils/Monitor/MonitorResource.ts +63 -36
  42. package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
  43. package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
  44. package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
  45. package/Server/Utils/SingleFlight.ts +83 -0
  46. package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
  47. package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
  48. package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
  49. package/Server/Utils/Telemetry/Telemetry.ts +351 -122
  50. package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
  51. package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
  52. package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
  53. package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
  54. package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
  55. package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
  56. package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
  57. package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
  58. package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
  59. package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
  60. package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
  61. package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
  62. package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
  63. package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
  64. package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
  65. package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
  66. package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
  67. package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
  68. package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
  69. package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
  70. package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
  71. package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
  72. package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
  73. package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
  74. package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
  75. package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
  76. package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
  77. package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
  78. package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
  79. package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
  80. package/Tests/Types/Decimal.test.ts +37 -0
  81. package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
  82. package/Tests/Types/Port.test.ts +38 -0
  83. package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
  84. package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
  85. package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
  86. package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
  87. package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
  88. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  89. package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
  90. package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
  91. package/Types/Dashboard/DashboardTemplates.ts +397 -0
  92. package/Types/Database/BigIntColumnTransformer.ts +74 -0
  93. package/Types/Database/DatabaseProperty.ts +26 -0
  94. package/Types/Database/NumericColumnValue.ts +128 -0
  95. package/Types/Decimal.ts +16 -4
  96. package/Types/Permission.ts +48 -0
  97. package/Types/Port.ts +16 -4
  98. package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
  99. package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
  100. package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
  101. package/Utils/Dashboard/Components/Index.ts +7 -0
  102. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
  103. package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
  104. package/build/dist/Models/DatabaseModels/Index.js +2 -0
  105. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  106. package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
  107. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
  108. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
  109. package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
  110. package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
  111. package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
  112. package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
  113. package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
  114. package/build/dist/Server/API/BaseAPI.js +11 -1
  115. package/build/dist/Server/API/BaseAPI.js.map +1 -1
  116. package/build/dist/Server/API/DashboardAPI.js +39 -0
  117. package/build/dist/Server/API/DashboardAPI.js.map +1 -1
  118. package/build/dist/Server/EnvironmentConfig.js +27 -1
  119. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  120. package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
  121. package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
  122. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
  123. package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
  125. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
  126. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
  127. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  128. package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
  129. package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
  130. package/build/dist/Server/Services/CephClusterService.js +26 -76
  131. package/build/dist/Server/Services/CephClusterService.js.map +1 -1
  132. package/build/dist/Server/Services/CloudResourceService.js +23 -73
  133. package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
  134. package/build/dist/Server/Services/DatabaseService.js +86 -0
  135. package/build/dist/Server/Services/DatabaseService.js.map +1 -1
  136. package/build/dist/Server/Services/DockerHostService.js +19 -69
  137. package/build/dist/Server/Services/DockerHostService.js.map +1 -1
  138. package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
  139. package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
  140. package/build/dist/Server/Services/HostService.js +58 -102
  141. package/build/dist/Server/Services/HostService.js.map +1 -1
  142. package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
  143. package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
  144. package/build/dist/Server/Services/Index.js +2 -0
  145. package/build/dist/Server/Services/Index.js.map +1 -1
  146. package/build/dist/Server/Services/IoTFleetService.js +19 -69
  147. package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
  148. package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
  149. package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
  150. package/build/dist/Server/Services/LabelService.js +42 -7
  151. package/build/dist/Server/Services/LabelService.js.map +1 -1
  152. package/build/dist/Server/Services/MetricTypeService.js +74 -0
  153. package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
  154. package/build/dist/Server/Services/MonitorService.js +81 -9
  155. package/build/dist/Server/Services/MonitorService.js.map +1 -1
  156. package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
  157. package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
  158. package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
  159. package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
  160. package/build/dist/Server/Services/PodmanHostService.js +19 -69
  161. package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
  162. package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
  163. package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
  164. package/build/dist/Server/Services/RumApplicationService.js +19 -70
  165. package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
  166. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
  167. package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
  168. package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
  169. package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
  170. package/build/dist/Server/Services/ServiceService.js +236 -61
  171. package/build/dist/Server/Services/ServiceService.js.map +1 -1
  172. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
  173. package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
  174. package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
  175. package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
  176. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
  177. package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
  178. package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
  179. package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
  180. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
  181. package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
  182. package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
  183. package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
  184. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
  185. package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
  186. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
  187. package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
  188. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
  189. package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
  190. package/build/dist/Server/Utils/SingleFlight.js +76 -0
  191. package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
  192. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
  193. package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
  194. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
  195. package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
  196. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
  197. package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
  198. package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
  199. package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
  200. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  201. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  202. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
  203. package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
  204. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
  205. package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
  206. package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
  207. package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
  208. package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
  209. package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
  210. package/build/dist/Types/Database/DatabaseProperty.js +25 -0
  211. package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
  212. package/build/dist/Types/Database/NumericColumnValue.js +97 -0
  213. package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
  214. package/build/dist/Types/Decimal.js +14 -3
  215. package/build/dist/Types/Decimal.js.map +1 -1
  216. package/build/dist/Types/Permission.js +42 -0
  217. package/build/dist/Types/Permission.js.map +1 -1
  218. package/build/dist/Types/Port.js +15 -5
  219. package/build/dist/Types/Port.js.map +1 -1
  220. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
  221. package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
  222. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
  223. package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
  224. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
  225. package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
  226. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  227. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  228. package/package.json +1 -1
@@ -0,0 +1,793 @@
1
+ import DashboardChartType from "../../../Types/Dashboard/Chart/ChartType";
2
+ import DashboardBaseComponent from "../../../Types/Dashboard/DashboardComponents/DashboardBaseComponent";
3
+ import { DashboardValueTrendDirection } from "../../../Types/Dashboard/DashboardComponents/DashboardValueComponent";
4
+ import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
5
+ import DashboardVariable, {
6
+ DashboardVariableType,
7
+ } from "../../../Types/Dashboard/DashboardVariable";
8
+ import DashboardViewConfig from "../../../Types/Dashboard/DashboardViewConfig";
9
+ import {
10
+ DashboardTemplate,
11
+ DashboardTemplateCategory,
12
+ DashboardTemplates,
13
+ DashboardTemplateType,
14
+ getDashboardTemplatesByCategory,
15
+ getTemplateConfig,
16
+ } from "../../../Types/Dashboard/DashboardTemplates";
17
+ import IconProp from "../../../Types/Icon/IconProp";
18
+ import MonitorType from "../../../Types/Monitor/MonitorType";
19
+
20
+ /*
21
+ * Editorial invariants for the Network template specifically. The
22
+ * structural rules every template must satisfy (grid bounds, overlap,
23
+ * unique ids, aggregation spelling) live in DashboardTemplateInvariants —
24
+ * this file pins the things that make THIS template the Network one:
25
+ * which metrics it queries, which attributes it groups and varies by, and
26
+ * that the map is on it at all.
27
+ */
28
+
29
+ /*
30
+ * The metric names are the wire contract: they are persisted into the
31
+ * saved dashboard as raw strings and matched against what the probe
32
+ * emitted. Pinned as literals rather than as MonitorMetricType members so
33
+ * that renaming a member's VALUE — which would silently empty every
34
+ * widget on every dashboard already created from this template — fails
35
+ * here instead of shipping.
36
+ *
37
+ * All five are emitted by exactly one code path,
38
+ * Common/Server/Utils/Monitor/NetworkDeviceMetricUtil.saveWalkMetrics, off
39
+ * a network device walk. That is the property the template depends on and
40
+ * the one the tests below defend.
41
+ */
42
+ const INTERFACE_IN_METRIC: string =
43
+ "oneuptime.monitor.snmp.interface.in.bits.per.second";
44
+ const INTERFACE_OUT_METRIC: string =
45
+ "oneuptime.monitor.snmp.interface.out.bits.per.second";
46
+ const INTERFACE_UTILIZATION_METRIC: string =
47
+ "oneuptime.monitor.snmp.interface.utilization.percent";
48
+ const INTERFACE_ERRORS_METRIC: string =
49
+ "oneuptime.monitor.snmp.interface.errors.per.second";
50
+ const INTERFACE_OPER_STATUS_METRIC: string =
51
+ "oneuptime.monitor.snmp.interface.oper.status";
52
+
53
+ /*
54
+ * Metrics that every probeable monitor in the project emits, NOT just
55
+ * network devices. A tile on a network dashboard built from one of these
56
+ * silently averages every website check in the project, so the template
57
+ * must not carry any.
58
+ */
59
+ const PROJECT_WIDE_METRICS: Array<string> = [
60
+ "oneuptime.monitor.online",
61
+ "oneuptime.monitor.response.time",
62
+ "oneuptime.monitor.ping.packet.loss.percent",
63
+ "oneuptime.monitor.ping.jitter",
64
+ ];
65
+
66
+ /*
67
+ * The two bare attribute keys NetworkDeviceMetricUtil stamps onto its
68
+ * rows: `deviceName` on every metric, `interfaceName` on the per-interface
69
+ * ones. They carry no `resource.` prefix, so a template that guessed
70
+ * "resource.deviceName" would compile and group by nothing.
71
+ */
72
+ const DEVICE_ATTRIBUTE: string = "deviceName";
73
+ const INTERFACE_ATTRIBUTE: string = "interfaceName";
74
+
75
+ const AVG_AGGREGATION: string = "Avg";
76
+ const MAX_AGGREGATION: string = "Max";
77
+ const SUM_AGGREGATION: string = "Sum";
78
+
79
+ const GRID_WIDTH_IN_UNITS: number = 12;
80
+
81
+ // -- Helpers ---------------------------------------------------------------
82
+
83
+ type WidgetArguments = Record<string, unknown>;
84
+
85
+ function getConfig(): DashboardViewConfig {
86
+ const config: DashboardViewConfig | null = getTemplateConfig(
87
+ DashboardTemplateType.Network,
88
+ );
89
+ expect(config).not.toBeNull();
90
+ return config as DashboardViewConfig;
91
+ }
92
+
93
+ function argumentsOf(component: DashboardBaseComponent): WidgetArguments {
94
+ return (component.arguments as WidgetArguments | undefined) || {};
95
+ }
96
+
97
+ /*
98
+ * Every widget family stores its title under a different argument key, and
99
+ * titles are the only stable handle: ids are freshly generated on every
100
+ * call and array positions move whenever a row is inserted.
101
+ */
102
+ const TITLE_ARGUMENT_KEYS: Array<string> = [
103
+ "title",
104
+ "chartTitle",
105
+ "gaugeTitle",
106
+ "tableTitle",
107
+ "text",
108
+ ];
109
+
110
+ function titleOf(component: DashboardBaseComponent): string {
111
+ const args: WidgetArguments = argumentsOf(component);
112
+
113
+ for (const key of TITLE_ARGUMENT_KEYS) {
114
+ const value: unknown = args[key];
115
+
116
+ if (typeof value === "string") {
117
+ return value;
118
+ }
119
+ }
120
+
121
+ return "";
122
+ }
123
+
124
+ // Fails loudly (rather than returning undefined) when the widget is gone.
125
+ function findWidget(
126
+ config: DashboardViewConfig,
127
+ title: string,
128
+ ): DashboardBaseComponent {
129
+ const matches: Array<DashboardBaseComponent> = config.components.filter(
130
+ (component: DashboardBaseComponent): boolean => {
131
+ return titleOf(component) === title;
132
+ },
133
+ );
134
+
135
+ expect(matches).toHaveLength(1);
136
+ return matches[0] as DashboardBaseComponent;
137
+ }
138
+
139
+ function componentsOfType(
140
+ config: DashboardViewConfig,
141
+ componentType: DashboardComponentType,
142
+ ): Array<DashboardBaseComponent> {
143
+ return config.components.filter(
144
+ (component: DashboardBaseComponent): boolean => {
145
+ return component.componentType === componentType;
146
+ },
147
+ );
148
+ }
149
+
150
+ function metricQueryDataOf(component: DashboardBaseComponent): WidgetArguments {
151
+ const queryConfig: WidgetArguments =
152
+ (argumentsOf(component)["metricQueryConfig"] as
153
+ | WidgetArguments
154
+ | undefined) || {};
155
+
156
+ return (queryConfig["metricQueryData"] as WidgetArguments | undefined) || {};
157
+ }
158
+
159
+ function filterDataOf(component: DashboardBaseComponent): WidgetArguments {
160
+ return (
161
+ (metricQueryDataOf(component)["filterData"] as
162
+ | WidgetArguments
163
+ | undefined) || {}
164
+ );
165
+ }
166
+
167
+ function metricNameOf(component: DashboardBaseComponent): string | undefined {
168
+ const value: unknown = filterDataOf(component)["metricName"];
169
+ return typeof value === "string" ? value : undefined;
170
+ }
171
+
172
+ /*
173
+ * The aggregation is stored under the MISSPELLED key `aggegationType`.
174
+ * That misspelling is what the fetch layer reads, so it is the contract.
175
+ */
176
+ function aggregationOf(component: DashboardBaseComponent): unknown {
177
+ return filterDataOf(component)["aggegationType"];
178
+ }
179
+
180
+ function groupByAttributeKeysOf(component: DashboardBaseComponent): unknown {
181
+ return metricQueryDataOf(component)["groupByAttributeKeys"];
182
+ }
183
+
184
+ // -- Tests -----------------------------------------------------------------
185
+
186
+ describe("Network dashboard template", () => {
187
+ describe("catalog entry", () => {
188
+ it("is registered exactly once", () => {
189
+ const entries: Array<DashboardTemplate> = DashboardTemplates.filter(
190
+ (template: DashboardTemplate): boolean => {
191
+ return template.type === DashboardTemplateType.Network;
192
+ },
193
+ );
194
+
195
+ expect(entries).toHaveLength(1);
196
+ });
197
+
198
+ it("is filed under Infrastructure with a map icon and a real description", () => {
199
+ const entry: DashboardTemplate = DashboardTemplates.find(
200
+ (template: DashboardTemplate): boolean => {
201
+ return template.type === DashboardTemplateType.Network;
202
+ },
203
+ ) as DashboardTemplate;
204
+
205
+ expect(entry.name).toBe("Network Dashboard");
206
+ expect(entry.category).toBe(DashboardTemplateCategory.Infrastructure);
207
+ expect(entry.icon).toBe(IconProp.Map);
208
+ expect(entry.description.trim().length).toBeGreaterThan(0);
209
+ });
210
+
211
+ it("is listed by getDashboardTemplatesByCategory for Infrastructure", () => {
212
+ const types: Array<DashboardTemplateType> =
213
+ getDashboardTemplatesByCategory(
214
+ DashboardTemplateCategory.Infrastructure,
215
+ ).map((template: DashboardTemplate): DashboardTemplateType => {
216
+ return template.type;
217
+ });
218
+
219
+ expect(types).toContain(DashboardTemplateType.Network);
220
+ });
221
+
222
+ it("resolves to a config rather than to null the way Blank does", () => {
223
+ expect(getTemplateConfig(DashboardTemplateType.Network)).not.toBeNull();
224
+ });
225
+ });
226
+
227
+ describe("the map", () => {
228
+ /*
229
+ * This is the whole reason the template exists: a network dashboard
230
+ * that does not show WHERE the network is, is just a metrics
231
+ * dashboard with SNMP metric names on it.
232
+ */
233
+ it("puts exactly one Network Map on the dashboard", () => {
234
+ expect(
235
+ componentsOfType(getConfig(), DashboardComponentType.NetworkMap),
236
+ ).toHaveLength(1);
237
+ });
238
+
239
+ it("opens the map in map mode with names on, not as a table", () => {
240
+ const args: WidgetArguments = argumentsOf(
241
+ findWidget(getConfig(), "Sites"),
242
+ );
243
+
244
+ expect(args["viewMode"]).toBe("map");
245
+ expect(args["showLabels"]).toBe(true);
246
+ });
247
+
248
+ it("caps the map's fetch at a positive whole number of sites", () => {
249
+ const maxSites: unknown = argumentsOf(findWidget(getConfig(), "Sites"))[
250
+ "maxSites"
251
+ ];
252
+
253
+ expect(typeof maxSites).toBe("number");
254
+ expect(Number.isInteger(maxSites as number)).toBe(true);
255
+ expect(maxSites as number).toBeGreaterThan(0);
256
+ });
257
+
258
+ it("does not pre-filter the map to a status, so it opens on the whole estate", () => {
259
+ expect(
260
+ argumentsOf(findWidget(getConfig(), "Sites"))["statusFilter"],
261
+ ).toBeUndefined();
262
+ });
263
+
264
+ /*
265
+ * The world is roughly 2:1. The map is the largest thing on the
266
+ * dashboard and it has to be tall as well as wide, or it renders as a
267
+ * letterboxed strip with every marker on top of every other.
268
+ */
269
+ it("gives the map a tile that is big in both axes", () => {
270
+ const map: DashboardBaseComponent = findWidget(getConfig(), "Sites");
271
+
272
+ expect(map.widthInDashboardUnits).toBeGreaterThanOrEqual(6);
273
+ expect(map.heightInDashboardUnits).toBeGreaterThanOrEqual(4);
274
+ expect(map.minWidthInDashboardUnits).toBe(4);
275
+ expect(map.minHeightInDashboardUnits).toBe(4);
276
+ });
277
+
278
+ it("places the map in the top band, above every section header below it", () => {
279
+ const config: DashboardViewConfig = getConfig();
280
+ const map: DashboardBaseComponent = findWidget(config, "Sites");
281
+
282
+ expect(map.topInDashboardUnits).toBe(1);
283
+ expect(map.leftInDashboardUnits).toBe(0);
284
+ });
285
+ });
286
+
287
+ describe("metric queries", () => {
288
+ it("queries only SNMP metrics the device walk actually emits", () => {
289
+ const config: DashboardViewConfig = getConfig();
290
+
291
+ expect(metricNameOf(findWidget(config, "Interfaces Up (avg)"))).toBe(
292
+ INTERFACE_OPER_STATUS_METRIC,
293
+ );
294
+ expect(
295
+ metricNameOf(findWidget(config, "Avg Interface Utilization")),
296
+ ).toBe(INTERFACE_UTILIZATION_METRIC);
297
+ expect(
298
+ metricNameOf(findWidget(config, "Worst Interface Errors/sec")),
299
+ ).toBe(INTERFACE_ERRORS_METRIC);
300
+ expect(metricNameOf(findWidget(config, "Peak Inbound"))).toBe(
301
+ INTERFACE_IN_METRIC,
302
+ );
303
+ expect(metricNameOf(findWidget(config, "Peak Outbound"))).toBe(
304
+ INTERFACE_OUT_METRIC,
305
+ );
306
+ expect(metricNameOf(findWidget(config, "Inbound by Interface"))).toBe(
307
+ INTERFACE_IN_METRIC,
308
+ );
309
+ expect(metricNameOf(findWidget(config, "Outbound by Interface"))).toBe(
310
+ INTERFACE_OUT_METRIC,
311
+ );
312
+ expect(
313
+ metricNameOf(findWidget(config, "Peak Interface Utilization")),
314
+ ).toBe(INTERFACE_UTILIZATION_METRIC);
315
+ expect(metricNameOf(findWidget(config, "Interface Error Rate"))).toBe(
316
+ INTERFACE_ERRORS_METRIC,
317
+ );
318
+ expect(
319
+ metricNameOf(findWidget(config, "Interface Errors by Interface")),
320
+ ).toBe(INTERFACE_ERRORS_METRIC);
321
+ expect(metricNameOf(findWidget(config, "Utilization by Interface"))).toBe(
322
+ INTERFACE_UTILIZATION_METRIC,
323
+ );
324
+ });
325
+
326
+ /*
327
+ * THE invariant of this template. `oneuptime.monitor.online`,
328
+ * `.response.time`, `.ping.packet.loss.percent` and `.ping.jitter` are
329
+ * emitted by MonitorMetricUtil for EVERY probeable monitor in the
330
+ * project, not just network devices — so a tile built on one of them
331
+ * would average every website check in the project while wearing a
332
+ * network label. Device reachability lives on this dashboard as the
333
+ * Network Device monitor list, which is correctly scoped.
334
+ */
335
+ it("carries no metric that monitors outside the network estate also emit", () => {
336
+ const offenders: Array<string> = [];
337
+
338
+ for (const component of getConfig().components) {
339
+ const metric: string | undefined = metricNameOf(component);
340
+
341
+ if (metric && PROJECT_WIDE_METRICS.includes(metric)) {
342
+ offenders.push(`${titleOf(component)} -> ${metric}`);
343
+ }
344
+ }
345
+
346
+ expect(offenders).toEqual([]);
347
+ });
348
+
349
+ it("queries an snmp metric namespace on every metric-bearing widget", () => {
350
+ const metrics: Array<string> = [];
351
+
352
+ for (const component of getConfig().components) {
353
+ const metric: string | undefined = metricNameOf(component);
354
+
355
+ if (metric) {
356
+ metrics.push(metric);
357
+ }
358
+ }
359
+
360
+ expect(metrics.length).toBeGreaterThan(0);
361
+
362
+ for (const metric of metrics) {
363
+ expect(metric.startsWith("oneuptime.monitor.snmp.")).toBe(true);
364
+ }
365
+ });
366
+
367
+ /*
368
+ * The aggregation is not decoration, and Sum is never right here. The
369
+ * interface series are already per-second RATES, and a Value tile
370
+ * reduces across time buckets — so a summed rate doubles when somebody
371
+ * widens the time picker, describing the dashboard rather than the
372
+ * network. Max answers "how bad did it get", Avg answers "what is
373
+ * normal"; both are range-independent.
374
+ */
375
+ it("never aggregates a per-second rate with Sum", () => {
376
+ const summed: Array<string> = [];
377
+
378
+ for (const component of getConfig().components) {
379
+ if (
380
+ metricNameOf(component) &&
381
+ aggregationOf(component) === SUM_AGGREGATION
382
+ ) {
383
+ summed.push(titleOf(component));
384
+ }
385
+ }
386
+
387
+ expect(summed).toEqual([]);
388
+ });
389
+
390
+ it("picks the aggregation that makes each number mean what its title says", () => {
391
+ const config: DashboardViewConfig = getConfig();
392
+
393
+ for (const title of [
394
+ "Peak Interface Utilization",
395
+ "Worst Interface Errors/sec",
396
+ "Peak Inbound",
397
+ "Peak Outbound",
398
+ ]) {
399
+ expect(aggregationOf(findWidget(config, title))).toBe(MAX_AGGREGATION);
400
+ }
401
+
402
+ for (const title of [
403
+ "Interfaces Up (avg)",
404
+ "Avg Interface Utilization",
405
+ "Interface Error Rate",
406
+ "Inbound by Interface",
407
+ "Outbound by Interface",
408
+ "Interface Errors by Interface",
409
+ "Utilization by Interface",
410
+ ]) {
411
+ expect(aggregationOf(findWidget(config, title))).toBe(AVG_AGGREGATION);
412
+ }
413
+ });
414
+
415
+ /*
416
+ * "Is a link saturated" is only answerable per link. A throughput
417
+ * chart that does not split by interface draws one flat line that is
418
+ * the average of a saturated uplink and nine idle access ports.
419
+ */
420
+ it("splits the per-interface charts by interfaceName", () => {
421
+ const config: DashboardViewConfig = getConfig();
422
+
423
+ for (const title of [
424
+ "Inbound by Interface",
425
+ "Outbound by Interface",
426
+ "Interface Errors by Interface",
427
+ "Utilization by Interface",
428
+ ]) {
429
+ expect(groupByAttributeKeysOf(findWidget(config, title))).toEqual([
430
+ INTERFACE_ATTRIBUTE,
431
+ ]);
432
+ }
433
+ });
434
+
435
+ /*
436
+ * These two metrics are already RATES computed by the probe from the
437
+ * SNMP counter deltas, unlike the Ceph template's cumulative
438
+ * ceph_pool_*_bytes counters. Rate-transforming them would plot the
439
+ * rate of change of a rate — a chart that reads as noise around zero.
440
+ */
441
+ it("does not rate-transform the interface bit-rate charts", () => {
442
+ const config: DashboardViewConfig = getConfig();
443
+
444
+ for (const title of ["Inbound by Interface", "Outbound by Interface"]) {
445
+ const queryConfig: WidgetArguments =
446
+ (argumentsOf(findWidget(config, title))["metricQueryConfig"] as
447
+ | WidgetArguments
448
+ | undefined) || {};
449
+
450
+ expect(queryConfig["transformAsRate"]).toBeUndefined();
451
+ }
452
+ });
453
+
454
+ it("marks every 'more is worse' number so its trend arrow is colored right", () => {
455
+ const config: DashboardViewConfig = getConfig();
456
+
457
+ for (const title of [
458
+ "Avg Interface Utilization",
459
+ "Worst Interface Errors/sec",
460
+ ]) {
461
+ expect(argumentsOf(findWidget(config, title))["trendDirection"]).toBe(
462
+ DashboardValueTrendDirection.HigherIsWorse,
463
+ );
464
+ }
465
+
466
+ expect(
467
+ argumentsOf(findWidget(config, "Interfaces Up (avg)"))[
468
+ "trendDirection"
469
+ ],
470
+ ).toBe(DashboardValueTrendDirection.HigherIsBetter);
471
+
472
+ /*
473
+ * Throughput is deliberately unmarked: more traffic through an uplink
474
+ * is neither good nor bad on its own, and a colored arrow would claim
475
+ * otherwise. The renderer falls back to its neutral heuristic.
476
+ */
477
+ for (const title of ["Peak Inbound", "Peak Outbound"]) {
478
+ expect(
479
+ argumentsOf(findWidget(config, title))["trendDirection"],
480
+ ).toBeUndefined();
481
+ }
482
+ });
483
+
484
+ /*
485
+ * A dashboard that prints the same figure under two names is a smaller
486
+ * dashboard pretending to be a bigger one. A number paired with its own
487
+ * trend chart is fine — that is a different AGGREGATION shape, not a
488
+ * repeat — so the check is on (metric, aggregation, widget family).
489
+ */
490
+ it("never prints the same metric and aggregation twice in the same widget family", () => {
491
+ const seen: Map<string, string> = new Map<string, string>();
492
+ const duplicates: Array<string> = [];
493
+
494
+ for (const component of getConfig().components) {
495
+ const metric: string | undefined = metricNameOf(component);
496
+
497
+ if (!metric) {
498
+ continue;
499
+ }
500
+
501
+ const key: string = `${component.componentType}|${metric}|${String(
502
+ aggregationOf(component),
503
+ )}`;
504
+ const existing: string | undefined = seen.get(key);
505
+
506
+ if (existing) {
507
+ duplicates.push(
508
+ `${key} on "${existing}" and "${titleOf(component)}"`,
509
+ );
510
+ }
511
+
512
+ seen.set(key, titleOf(component));
513
+ }
514
+
515
+ expect(duplicates).toEqual([]);
516
+ });
517
+
518
+ it("gives every widget on the dashboard a distinct title", () => {
519
+ const titles: Array<string> = getConfig().components.map(titleOf);
520
+
521
+ expect(
522
+ titles.every((title: string): boolean => {
523
+ return title.trim().length > 0;
524
+ }),
525
+ ).toBe(true);
526
+ expect(new Set(titles).size).toBe(titles.length);
527
+ });
528
+ });
529
+
530
+ describe("gauges", () => {
531
+ /*
532
+ * A healthy Ethernet port errors essentially never, while a link at 60%
533
+ * utilization is simply busy. Putting the error gauge on the same
534
+ * 0-100 scale as the utilization gauge beside it would render every
535
+ * real fault as a flat line on the floor.
536
+ */
537
+ it("scales the error gauge for a metric that is normally zero", () => {
538
+ const config: DashboardViewConfig = getConfig();
539
+ const errors: WidgetArguments = argumentsOf(
540
+ findWidget(config, "Interface Error Rate"),
541
+ );
542
+ const utilization: WidgetArguments = argumentsOf(
543
+ findWidget(config, "Peak Interface Utilization"),
544
+ );
545
+
546
+ expect(errors["minValue"]).toBe(0);
547
+ expect(errors["maxValue"]).toBe(10);
548
+ expect(errors["warningThreshold"]).toBe(1);
549
+ expect(errors["criticalThreshold"]).toBe(5);
550
+
551
+ expect(utilization["minValue"]).toBe(0);
552
+ expect(utilization["maxValue"]).toBe(100);
553
+ expect(utilization["warningThreshold"]).toBe(70);
554
+ expect(utilization["criticalThreshold"]).toBe(90);
555
+
556
+ expect(errors["maxValue"] as number).toBeLessThan(
557
+ utilization["maxValue"] as number,
558
+ );
559
+ });
560
+
561
+ it("orders every gauge min <= warning < critical <= max", () => {
562
+ for (const title of [
563
+ "Peak Interface Utilization",
564
+ "Interface Error Rate",
565
+ ]) {
566
+ const args: WidgetArguments = argumentsOf(
567
+ findWidget(getConfig(), title),
568
+ );
569
+
570
+ expect(args["minValue"] as number).toBeLessThanOrEqual(
571
+ args["warningThreshold"] as number,
572
+ );
573
+ expect(args["warningThreshold"] as number).toBeLessThan(
574
+ args["criticalThreshold"] as number,
575
+ );
576
+ expect(args["criticalThreshold"] as number).toBeLessThanOrEqual(
577
+ args["maxValue"] as number,
578
+ );
579
+ }
580
+ });
581
+ });
582
+
583
+ describe("the monitor list", () => {
584
+ /*
585
+ * An unfiltered monitor list on a network dashboard is the Monitors
586
+ * page with fewer columns. Pinning it to Network Device monitors is
587
+ * what makes it worth the tile.
588
+ */
589
+ it("is pinned to Network Device monitors", () => {
590
+ const args: WidgetArguments = argumentsOf(
591
+ findWidget(getConfig(), "Network Device Monitors"),
592
+ );
593
+
594
+ expect(args["monitorTypes"]).toEqual([MonitorType.NetworkDevice]);
595
+ });
596
+
597
+ it("renders as a list and caps its rows", () => {
598
+ const args: WidgetArguments = argumentsOf(
599
+ findWidget(getConfig(), "Network Device Monitors"),
600
+ );
601
+
602
+ expect(args["viewMode"]).toBe("list");
603
+ expect(args["maxRows"] as number).toBeGreaterThan(0);
604
+ });
605
+ });
606
+
607
+ describe("variables", () => {
608
+ it("ships a Device and an Interface picker, both multi-select", () => {
609
+ const variables: Array<DashboardVariable> = getConfig().variables || [];
610
+
611
+ expect(
612
+ variables.map((variable: DashboardVariable): string => {
613
+ return variable.name;
614
+ }),
615
+ ).toEqual(["device", "interface"]);
616
+
617
+ for (const variable of variables) {
618
+ expect(variable.type).toBe(DashboardVariableType.TelemetryAttribute);
619
+ /*
620
+ * Comparing two uplinks, or two branch routers, is the normal use
621
+ * of this dashboard rather than the exception.
622
+ */
623
+ expect(variable.isMultiSelect).toBe(true);
624
+ expect((variable.label || "").trim().length).toBeGreaterThan(0);
625
+ }
626
+ });
627
+
628
+ /*
629
+ * Network device metrics carry BARE attribute keys — see
630
+ * NetworkDeviceMetricUtil.buildDeviceMetricRow. A variable bound to
631
+ * `resource.deviceName` would offer an empty picker.
632
+ */
633
+ it("binds to the bare attribute keys, not the resource-prefixed ones", () => {
634
+ const variables: Array<DashboardVariable> = getConfig().variables || [];
635
+
636
+ expect(
637
+ variables.map((variable: DashboardVariable): string | undefined => {
638
+ return variable.attributeKey;
639
+ }),
640
+ ).toEqual([DEVICE_ATTRIBUTE, INTERFACE_ATTRIBUTE]);
641
+ });
642
+ });
643
+
644
+ describe("layout", () => {
645
+ it("lays every widget out inside the 12-unit grid", () => {
646
+ for (const component of getConfig().components) {
647
+ expect(component.leftInDashboardUnits).toBeGreaterThanOrEqual(0);
648
+ expect(component.topInDashboardUnits).toBeGreaterThanOrEqual(0);
649
+ expect(
650
+ component.leftInDashboardUnits + component.widthInDashboardUnits,
651
+ ).toBeLessThanOrEqual(GRID_WIDTH_IN_UNITS);
652
+ }
653
+ });
654
+
655
+ /*
656
+ * A band of empty rows in the middle of a template reads as a widget
657
+ * that failed to render rather than as whitespace. Every row from the
658
+ * title to the last widget must be occupied by something.
659
+ */
660
+ it("leaves no empty row between the title and the last widget", () => {
661
+ const config: DashboardViewConfig = getConfig();
662
+ const lowestEdge: number = Math.max(
663
+ ...config.components.map(
664
+ (component: DashboardBaseComponent): number => {
665
+ return (
666
+ component.topInDashboardUnits + component.heightInDashboardUnits
667
+ );
668
+ },
669
+ ),
670
+ );
671
+
672
+ const occupiedRows: Set<number> = new Set<number>();
673
+ for (const component of config.components) {
674
+ for (
675
+ let row: number = component.topInDashboardUnits;
676
+ row <
677
+ component.topInDashboardUnits + component.heightInDashboardUnits;
678
+ row++
679
+ ) {
680
+ occupiedRows.add(row);
681
+ }
682
+ }
683
+
684
+ const emptyRows: Array<number> = [];
685
+ for (let row: number = 0; row < lowestEdge; row++) {
686
+ if (!occupiedRows.has(row)) {
687
+ emptyRows.push(row);
688
+ }
689
+ }
690
+
691
+ expect(emptyRows).toEqual([]);
692
+ });
693
+
694
+ it("is tall enough to contain everything on it", () => {
695
+ const config: DashboardViewConfig = getConfig();
696
+ const lowestEdge: number = Math.max(
697
+ ...config.components.map(
698
+ (component: DashboardBaseComponent): number => {
699
+ return (
700
+ component.topInDashboardUnits + component.heightInDashboardUnits
701
+ );
702
+ },
703
+ ),
704
+ );
705
+
706
+ expect(config.heightInDashboardUnits).toBeGreaterThanOrEqual(lowestEdge);
707
+ });
708
+
709
+ it("opens with a bold title row across the full width", () => {
710
+ const title: DashboardBaseComponent = findWidget(
711
+ getConfig(),
712
+ "Network Dashboard",
713
+ );
714
+
715
+ expect(title.componentType).toBe(DashboardComponentType.Text);
716
+ expect(title.topInDashboardUnits).toBe(0);
717
+ expect(title.widthInDashboardUnits).toBe(GRID_WIDTH_IN_UNITS);
718
+ expect(argumentsOf(title)["isBold"]).toBe(true);
719
+ });
720
+
721
+ it("labels each band with its own bold section header", () => {
722
+ const config: DashboardViewConfig = getConfig();
723
+
724
+ for (const heading of [
725
+ "Interface Throughput",
726
+ "Utilization & Errors",
727
+ "Devices & Monitors",
728
+ ]) {
729
+ const header: DashboardBaseComponent = findWidget(config, heading);
730
+
731
+ expect(header.componentType).toBe(DashboardComponentType.Text);
732
+ expect(header.widthInDashboardUnits).toBe(GRID_WIDTH_IN_UNITS);
733
+ expect(argumentsOf(header)["isBold"]).toBe(true);
734
+ }
735
+ });
736
+
737
+ it("picks a chart shape that suits each series", () => {
738
+ const config: DashboardViewConfig = getConfig();
739
+
740
+ // Filled areas for volume...
741
+ expect(
742
+ argumentsOf(findWidget(config, "Inbound by Interface"))["chartType"],
743
+ ).toBe(DashboardChartType.Area);
744
+ expect(
745
+ argumentsOf(findWidget(config, "Outbound by Interface"))["chartType"],
746
+ ).toBe(DashboardChartType.Area);
747
+ // ...a line for a percentage that should be read against a threshold...
748
+ expect(
749
+ argumentsOf(findWidget(config, "Utilization by Interface"))[
750
+ "chartType"
751
+ ],
752
+ ).toBe(DashboardChartType.Line);
753
+ // ...and bars for a mostly-zero series where each spike is an event.
754
+ expect(
755
+ argumentsOf(findWidget(config, "Interface Errors by Interface"))[
756
+ "chartType"
757
+ ],
758
+ ).toBe(DashboardChartType.Bar);
759
+ });
760
+ });
761
+
762
+ describe("freshness", () => {
763
+ /*
764
+ * Two dashboards created from this template must not share component
765
+ * or variable ids — they are independent saved rows from the moment
766
+ * they are created.
767
+ */
768
+ it("generates fresh component and variable ids on every call", () => {
769
+ const first: DashboardViewConfig = getConfig();
770
+ const second: DashboardViewConfig = getConfig();
771
+
772
+ const firstComponentIds: Set<string> = new Set<string>(
773
+ first.components.map((component: DashboardBaseComponent): string => {
774
+ return component.componentId.toString();
775
+ }),
776
+ );
777
+ for (const component of second.components) {
778
+ expect(firstComponentIds.has(component.componentId.toString())).toBe(
779
+ false,
780
+ );
781
+ }
782
+
783
+ const firstVariableIds: Set<string> = new Set<string>(
784
+ (first.variables || []).map((variable: DashboardVariable): string => {
785
+ return variable.id;
786
+ }),
787
+ );
788
+ for (const variable of second.variables || []) {
789
+ expect(firstVariableIds.has(variable.id)).toBe(false);
790
+ }
791
+ });
792
+ });
793
+ });