@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.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +14 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/LogDropFilter.ts +9 -20
- package/Models/DatabaseModels/StatusPageMonitorRule.ts +911 -0
- package/Models/DatabaseModels/StatusPageResource.ts +73 -0
- package/Models/DatabaseModels/TraceDropFilter.ts +9 -20
- package/Server/API/BaseAPI.ts +13 -2
- package/Server/API/DashboardAPI.ts +39 -0
- package/Server/EnvironmentConfig.ts +31 -1
- package/Server/Infrastructure/GlobalCache.ts +115 -0
- package/Server/Infrastructure/Postgres/DataSourceOptions.ts +16 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.ts +123 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/AlertStateTimelineService.ts +20 -0
- package/Server/Services/CephClusterService.ts +26 -86
- package/Server/Services/CloudResourceService.ts +23 -83
- package/Server/Services/DatabaseService.ts +111 -0
- package/Server/Services/DockerHostService.ts +19 -79
- package/Server/Services/DockerSwarmClusterService.ts +27 -88
- package/Server/Services/HostService.ts +46 -99
- package/Server/Services/IncidentStateTimelineService.ts +20 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/IoTFleetService.ts +19 -79
- package/Server/Services/KubernetesClusterService.ts +17 -78
- package/Server/Services/LabelService.ts +57 -10
- package/Server/Services/MetricTypeService.ts +84 -0
- package/Server/Services/MonitorService.ts +89 -14
- package/Server/Services/MonitorStatusTimelineService.ts +29 -0
- package/Server/Services/MonitorTemplateService.ts +65 -0
- package/Server/Services/PodmanHostService.ts +19 -79
- package/Server/Services/ProxmoxClusterService.ts +23 -83
- package/Server/Services/RumApplicationService.ts +19 -80
- package/Server/Services/ScheduledMaintenanceStateTimelineService.ts +21 -0
- package/Server/Services/ServerlessFunctionService.ts +24 -85
- package/Server/Services/ServiceService.ts +299 -73
- package/Server/Services/StatusPageMonitorRuleEngineService.ts +752 -0
- package/Server/Services/StatusPageMonitorRuleService.ts +471 -0
- package/Server/Services/TelemetryEntityRelationshipService.ts +1 -0
- package/Server/Services/TelemetryEntityService.ts +1 -0
- package/Server/Utils/Database/ProjectScopedReferenceValidator.ts +170 -39
- package/Server/Utils/Monitor/MonitorResource.ts +63 -36
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +36 -0
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +148 -127
- package/Server/Utils/Monitor/MonitorStepsReferenceExtractor.ts +434 -0
- package/Server/Utils/SingleFlight.ts +83 -0
- package/Server/Utils/StatusPage/MonitorRulePatternValidator.ts +46 -0
- package/Server/Utils/Telemetry/EntityRegistry.ts +107 -26
- package/Server/Utils/Telemetry/ResourceHeartbeat.ts +314 -0
- package/Server/Utils/Telemetry/Telemetry.ts +351 -122
- package/Tests/App/Dashboard/WidgetCatalog.test.ts +2 -0
- package/Tests/Models/DatabaseModels/ColumnTransformerInvariants.test.ts +306 -0
- package/Tests/Models/DatabaseModels/NumberColumnDeserialization.test.ts +333 -0
- package/Tests/Models/DatabaseModels/StatusPageMonitorRuleModel.test.ts +190 -0
- package/Tests/Server/API/DashboardPublicResourceListAPI.test.ts +26 -0
- package/Tests/Server/Infrastructure/GlobalCache.test.ts +226 -0
- package/Tests/Server/Infrastructure/PostgresLockTimeoutOptions.test.ts +181 -0
- package/Tests/Server/Services/DatabaseServiceUpdateColumnsIfUnlocked.test.ts +342 -0
- package/Tests/Server/Services/DropFilterSaveValidation.test.ts +245 -0
- package/Tests/Server/Services/HostServiceUpdateLastSeen.test.ts +163 -15
- package/Tests/Server/Services/MetricTypeAttachServices.test.ts +222 -0
- package/Tests/Server/Services/MissingReferenceWriteGuard.test.ts +614 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +23 -0
- package/Tests/Server/Services/ResourceUpdateLastSeenLivenessFallback.test.ts +305 -56
- package/Tests/Server/Services/ServiceUpdateLastSeenSplitGates.test.ts +705 -0
- package/Tests/Server/Services/StatusPageMonitorRuleEngineService.test.ts +1719 -0
- package/Tests/Server/Services/StatusPageMonitorRuleHooks.test.ts +1212 -0
- package/Tests/Server/Services/StatusPageMonitorRuleMonitorCreateHook.test.ts +228 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +267 -0
- package/Tests/Server/Utils/Database/ProjectScopedReferenceValidator.test.ts +260 -13
- package/Tests/Server/Utils/EntityRegistryRowFence.test.ts +302 -0
- package/Tests/Server/Utils/MetricTypeIndexConvergence.test.ts +598 -0
- package/Tests/Server/Utils/Monitor/MonitorStatusTimelineUnusableStatus.test.ts +154 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +638 -92
- package/Tests/Server/Utils/Monitor/MonitorStepsReferenceExtractor.test.ts +724 -0
- package/Tests/Server/Utils/SingleFlight.test.ts +221 -0
- package/Tests/Types/Dashboard/NetworkDashboardTemplate.test.ts +793 -0
- package/Tests/Types/Database/BigIntColumnTransformer.test.ts +147 -0
- package/Tests/Types/Database/DatabasePropertyTransformer.test.ts +140 -0
- package/Tests/Types/Database/NumericColumnValue.test.ts +127 -0
- package/Tests/Types/Decimal.test.ts +37 -0
- package/Tests/Types/Monitor/SnmpVersionParsing.test.ts +152 -0
- package/Tests/Types/Port.test.ts +38 -0
- package/Tests/UI/Components/LogsHistogram.test.tsx +276 -0
- package/Tests/UI/Components/UseLiveLogsRefresh.test.tsx +489 -0
- package/Tests/UI/Components/UseLogsHistogram.test.tsx +500 -0
- package/Tests/Utils/Dashboard/Components/DashboardComponentsUtil.test.ts +120 -0
- package/Tests/Utils/Dashboard/DashboardNetworkMapComponent.test.ts +327 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/ComponentArgument.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.ts +40 -0
- package/Types/Dashboard/DashboardTemplates.ts +397 -0
- package/Types/Database/BigIntColumnTransformer.ts +74 -0
- package/Types/Database/DatabaseProperty.ts +26 -0
- package/Types/Database/NumericColumnValue.ts +128 -0
- package/Types/Decimal.ts +16 -4
- package/Types/Permission.ts +48 -0
- package/Types/Port.ts +16 -4
- package/UI/Components/LogsViewer/useLiveLogsRefresh.ts +86 -0
- package/UI/Components/LogsViewer/useLogsHistogram.ts +113 -0
- package/Utils/Dashboard/Components/DashboardNetworkMapComponent.ts +136 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +12 -0
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js +925 -0
- package/build/dist/Models/DatabaseModels/StatusPageMonitorRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +72 -0
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js +9 -20
- package/build/dist/Models/DatabaseModels/TraceDropFilter.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +11 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/DashboardAPI.js +39 -0
- package/build/dist/Server/API/DashboardAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +27 -1
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/GlobalCache.js +91 -0
- package/build/dist/Server/Infrastructure/GlobalCache.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js +5 -3
- package/build/dist/Server/Infrastructure/Postgres/DataSourceOptions.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js +48 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1786005052769-AddStatusPageMonitorRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AlertStateTimelineService.js +19 -0
- package/build/dist/Server/Services/AlertStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/CephClusterService.js +26 -76
- package/build/dist/Server/Services/CephClusterService.js.map +1 -1
- package/build/dist/Server/Services/CloudResourceService.js +23 -73
- package/build/dist/Server/Services/CloudResourceService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +86 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/DockerHostService.js +19 -69
- package/build/dist/Server/Services/DockerHostService.js.map +1 -1
- package/build/dist/Server/Services/DockerSwarmClusterService.js +27 -78
- package/build/dist/Server/Services/DockerSwarmClusterService.js.map +1 -1
- package/build/dist/Server/Services/HostService.js +58 -102
- package/build/dist/Server/Services/HostService.js.map +1 -1
- package/build/dist/Server/Services/IncidentStateTimelineService.js +19 -0
- package/build/dist/Server/Services/IncidentStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/IoTFleetService.js +19 -69
- package/build/dist/Server/Services/IoTFleetService.js.map +1 -1
- package/build/dist/Server/Services/KubernetesClusterService.js +17 -68
- package/build/dist/Server/Services/KubernetesClusterService.js.map +1 -1
- package/build/dist/Server/Services/LabelService.js +42 -7
- package/build/dist/Server/Services/LabelService.js.map +1 -1
- package/build/dist/Server/Services/MetricTypeService.js +74 -0
- package/build/dist/Server/Services/MetricTypeService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +81 -9
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/MonitorStatusTimelineService.js +28 -0
- package/build/dist/Server/Services/MonitorStatusTimelineService.js.map +1 -1
- package/build/dist/Server/Services/MonitorTemplateService.js +60 -0
- package/build/dist/Server/Services/MonitorTemplateService.js.map +1 -1
- package/build/dist/Server/Services/PodmanHostService.js +19 -69
- package/build/dist/Server/Services/PodmanHostService.js.map +1 -1
- package/build/dist/Server/Services/ProxmoxClusterService.js +23 -73
- package/build/dist/Server/Services/ProxmoxClusterService.js.map +1 -1
- package/build/dist/Server/Services/RumApplicationService.js +19 -70
- package/build/dist/Server/Services/RumApplicationService.js.map +1 -1
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js +20 -0
- package/build/dist/Server/Services/ScheduledMaintenanceStateTimelineService.js.map +1 -1
- package/build/dist/Server/Services/ServerlessFunctionService.js +24 -75
- package/build/dist/Server/Services/ServerlessFunctionService.js.map +1 -1
- package/build/dist/Server/Services/ServiceService.js +236 -61
- package/build/dist/Server/Services/ServiceService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js +603 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js +401 -0
- package/build/dist/Server/Services/StatusPageMonitorRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityRelationshipService.js.map +1 -1
- package/build/dist/Server/Services/TelemetryEntityService.js +1 -0
- package/build/dist/Server/Services/TelemetryEntityService.js.map +1 -1
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js +105 -29
- package/build/dist/Server/Utils/Database/ProjectScopedReferenceValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +56 -32
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +30 -3
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +110 -98
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js +312 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsReferenceExtractor.js.map +1 -0
- package/build/dist/Server/Utils/SingleFlight.js +76 -0
- package/build/dist/Server/Utils/SingleFlight.js.map +1 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js +29 -0
- package/build/dist/Server/Utils/StatusPage/MonitorRulePatternValidator.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js +78 -19
- package/build/dist/Server/Utils/Telemetry/EntityRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js +160 -0
- package/build/dist/Server/Utils/Telemetry/ResourceHeartbeat.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/Telemetry.js +279 -104
- package/build/dist/Server/Utils/Telemetry/Telemetry.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponents/ComponentArgument.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js +368 -0
- package/build/dist/Types/Dashboard/DashboardTemplates.js.map +1 -1
- package/build/dist/Types/Database/BigIntColumnTransformer.js +66 -0
- package/build/dist/Types/Database/BigIntColumnTransformer.js.map +1 -0
- package/build/dist/Types/Database/DatabaseProperty.js +25 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/NumericColumnValue.js +97 -0
- package/build/dist/Types/Database/NumericColumnValue.js.map +1 -0
- package/build/dist/Types/Decimal.js +14 -3
- package/build/dist/Types/Decimal.js.map +1 -1
- package/build/dist/Types/Permission.js +42 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Port.js +15 -5
- package/build/dist/Types/Port.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js +44 -0
- package/build/dist/UI/Components/LogsViewer/useLiveLogsRefresh.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js +75 -0
- package/build/dist/UI/Components/LogsViewer/useLogsHistogram.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js +114 -0
- package/build/dist/Utils/Dashboard/Components/DashboardNetworkMapComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- 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
|
+
});
|