@oneuptime/common 12.0.10 → 12.0.11
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/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceLink.ts +116 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.ts +52 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/NetworkDeviceLinkService.ts +338 -2
- package/Server/Types/Billing/MeteredPlan/AllMeteredPlans.ts +19 -5
- package/Server/Types/Database/JSONColumnQuery.ts +42 -4
- package/Server/Utils/Telemetry/InventoryEntityRegistry.ts +186 -38
- package/Tests/App/Dashboard/AdminNotificationRulesPage.test.tsx +420 -112
- package/Tests/App/Dashboard/PayAsYouGoConsentGate.test.tsx +543 -0
- package/Tests/App/Dashboard/PayAsYouGoFreePlan.test.ts +125 -0
- package/Tests/App/Dashboard/PayAsYouGoNotices.test.tsx +612 -0
- package/Tests/App/Dashboard/SloWidgetChartRendering.test.tsx +536 -0
- package/Tests/Models/AnalyticsModels/CommonModel.test.ts +312 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +250 -0
- package/Tests/Server/Infrastructure/Postgres/EpisodeMemberNotifyIndexesMigration.test.ts +46 -18
- package/Tests/Server/Services/NetworkDeviceLinkParentValidation.test.ts +1043 -0
- package/Tests/Server/Types/Billing/PayAsYouGoMeteredPricing.test.ts +125 -0
- package/Tests/Server/Types/Database/JSONColumnQuery.test.ts +508 -0
- package/Tests/Server/Utils/Telemetry/InventoryEntityRegistry.test.ts +668 -0
- package/Tests/Types/Billing/PayAsYouGoPricing.test.ts +64 -0
- package/Tests/Types/CustomField/CustomFieldTypeCorrespondence.test.ts +107 -0
- package/Tests/Types/Monitor/MonitorType.test.ts +62 -0
- package/Tests/UI/Components/Charts/ChartXAxisDataKey.test.tsx +256 -0
- package/Tests/UI/Components/Charts/DataPointUtil.test.ts +323 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +349 -1
- package/Tests/Utils/Monitor/NetworkDeviceRoleUtil.test.ts +502 -0
- package/Tests/Utils/Monitor/NetworkTopologyDeclaredHierarchy.test.ts +759 -0
- package/Tests/Utils/Schema/BaseSchema.test.ts +651 -0
- package/Types/Billing/PayAsYouGoPricing.ts +47 -0
- package/Types/CustomField/CustomFieldType.ts +28 -0
- package/Types/Monitor/MonitorType.ts +11 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +13 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +4 -2
- package/UI/Components/Charts/Bar/BarChart.tsx +4 -2
- package/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +4 -2
- package/UI/Components/Charts/Types/XAxis/XAxis.ts +6 -0
- package/UI/Components/Charts/Utils/DataPoint.ts +15 -12
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +55 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +45 -0
- package/Utils/Monitor/NetworkTopologyUtil.ts +42 -2
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js +115 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLink.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.js +33 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787400000000-AddDeviceRoleAndDeclaredLinkParent.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/NetworkDeviceLinkService.js +253 -1
- package/build/dist/Server/Services/NetworkDeviceLinkService.js.map +1 -1
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js +12 -5
- package/build/dist/Server/Types/Billing/MeteredPlan/AllMeteredPlans.js.map +1 -1
- package/build/dist/Server/Types/Database/JSONColumnQuery.js +40 -4
- package/build/dist/Server/Types/Database/JSONColumnQuery.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js +128 -35
- package/build/dist/Server/Utils/Telemetry/InventoryEntityRegistry.js.map +1 -1
- package/build/dist/Types/Billing/PayAsYouGoPricing.js +41 -0
- package/build/dist/Types/Billing/PayAsYouGoPricing.js.map +1 -0
- package/build/dist/Types/CustomField/CustomFieldType.js +26 -0
- package/build/dist/Types/CustomField/CustomFieldType.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +10 -0
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +2 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +2 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js +12 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Types/ChartDataPoint.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +2 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js +17 -10
- package/build/dist/UI/Components/Charts/Utils/DataPoint.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +42 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +37 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +25 -3
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,9 +1,37 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The member VALUES are load-bearing beyond this enum.
|
|
3
|
+
*
|
|
4
|
+
* CustomFieldsDetail passes a definition's `customFieldType` straight through
|
|
5
|
+
* as the `fieldType` of a Detail field and of a form field, so every member
|
|
6
|
+
* here has to name a member of BOTH `FieldType` (Detail) and
|
|
7
|
+
* `FormFieldSchemaType` (Forms) by value. `Boolean` is the standing example:
|
|
8
|
+
* it reaches the form as FormFieldSchemaType.Toggle, which is spelled
|
|
9
|
+
* `Toggle = "Boolean"` for exactly this reason.
|
|
10
|
+
*
|
|
11
|
+
* Adding a member without that correspondence compiles fine and then renders
|
|
12
|
+
* an empty cell and an input the user cannot type into.
|
|
13
|
+
*/
|
|
1
14
|
enum CustomFieldType {
|
|
2
15
|
Text = "Text",
|
|
3
16
|
Number = "Number",
|
|
4
17
|
Boolean = "Boolean",
|
|
5
18
|
Dropdown = "Dropdown",
|
|
6
19
|
MultiSelectDropdown = "MultiSelectDropdown",
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A calendar date with no time of day — purchase date, warranty expiry,
|
|
23
|
+
* end-of-life. Stored in the `customFields` jsonb bag as an ISO-8601 string,
|
|
24
|
+
* which is what makes it comparable: ISO-8601 sorts identically as text and
|
|
25
|
+
* as an instant, so the range filters work over a jsonb key without a cast
|
|
26
|
+
* (see JSONColumnQuery.compareText).
|
|
27
|
+
*/
|
|
28
|
+
Date = "Date",
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* As `Date`, but the time of day is part of the answer — last audited at,
|
|
32
|
+
* decommissioned at.
|
|
33
|
+
*/
|
|
34
|
+
DateTime = "DateTime",
|
|
7
35
|
}
|
|
8
36
|
|
|
9
37
|
export default CustomFieldType;
|
|
@@ -170,6 +170,17 @@ export class MonitorTypeHelper {
|
|
|
170
170
|
return monitorType === MonitorType.Manual;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
+
/*
|
|
174
|
+
* Whether a monitor of this type adds to the project's Active Monitoring
|
|
175
|
+
* bill. Mirrors what the server actually meters -
|
|
176
|
+
* ActiveMonitoringMeteredPlan counts every monitor whose type is not
|
|
177
|
+
* Manual - so the dashboard can warn a Free plan user about a charge using
|
|
178
|
+
* the same rule that produces it. Manual monitors are free and unlimited.
|
|
179
|
+
*/
|
|
180
|
+
public static isBilledAsActiveMonitor(monitorType: MonitorType): boolean {
|
|
181
|
+
return !this.isManualMonitor(monitorType);
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
public static getAllMonitorTypeProps(): Array<MonitorTypeProps> {
|
|
174
185
|
const monitorTypeProps: Array<MonitorTypeProps> = [
|
|
175
186
|
{
|
|
@@ -156,6 +156,19 @@ export interface NetworkTopologyEdge {
|
|
|
156
156
|
monitorState?: "up" | "down" | undefined;
|
|
157
157
|
// Operator-supplied label, set only on manual links.
|
|
158
158
|
name?: string | undefined;
|
|
159
|
+
/*
|
|
160
|
+
* Which end of this edge is the parent, when somebody has SAID so —
|
|
161
|
+
* either on the link itself or through a link rule, which is stated in
|
|
162
|
+
* child/parent labels and so knows the answer by construction.
|
|
163
|
+
*
|
|
164
|
+
* Always one of `fromNodeId`/`toNodeId`. Absent on every discovered
|
|
165
|
+
* edge: LLDP and CDP report a cable, not a hierarchy, and the direction
|
|
166
|
+
* a neighbour entry happens to be read from says nothing about which
|
|
167
|
+
* box is upstream. Layouts that draw a tree must therefore treat this as
|
|
168
|
+
* a constraint where it is present and keep inferring where it is not —
|
|
169
|
+
* absent is "not stated", never "these are peers".
|
|
170
|
+
*/
|
|
171
|
+
parentNodeId?: string | undefined;
|
|
159
172
|
}
|
|
160
173
|
|
|
161
174
|
export default interface NetworkTopology {
|
|
@@ -13,7 +13,9 @@ import SeriesPoint from "../Types/SeriesPoints";
|
|
|
13
13
|
import { XAxis } from "../Types/XAxis/XAxis";
|
|
14
14
|
import YAxis from "../Types/YAxis/YAxis";
|
|
15
15
|
import ChartCurve from "../Types/ChartCurve";
|
|
16
|
-
import ChartDataPoint
|
|
16
|
+
import ChartDataPoint, {
|
|
17
|
+
CHART_DATA_POINT_X_AXIS_KEY,
|
|
18
|
+
} from "../ChartLibrary/Types/ChartDataPoint";
|
|
17
19
|
import FormattedExemplarPoint from "../ChartLibrary/Types/FormattedExemplarPoint";
|
|
18
20
|
import FormattedReferenceRegion from "../ChartLibrary/Types/FormattedReferenceRegion";
|
|
19
21
|
import FormattedTimeReferenceLine from "../ChartLibrary/Types/FormattedTimeReferenceLine";
|
|
@@ -185,7 +187,7 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
|
|
|
185
187
|
<AreaChart
|
|
186
188
|
data={records}
|
|
187
189
|
tickGap={30}
|
|
188
|
-
index={
|
|
190
|
+
index={CHART_DATA_POINT_X_AXIS_KEY}
|
|
189
191
|
categories={categories}
|
|
190
192
|
colors={
|
|
191
193
|
props.colors && props.colors.length > 0
|
|
@@ -12,7 +12,9 @@ import React, {
|
|
|
12
12
|
import SeriesPoint from "../Types/SeriesPoints";
|
|
13
13
|
import { XAxis } from "../Types/XAxis/XAxis";
|
|
14
14
|
import YAxis from "../Types/YAxis/YAxis";
|
|
15
|
-
import ChartDataPoint
|
|
15
|
+
import ChartDataPoint, {
|
|
16
|
+
CHART_DATA_POINT_X_AXIS_KEY,
|
|
17
|
+
} from "../ChartLibrary/Types/ChartDataPoint";
|
|
16
18
|
import FormattedReferenceRegion from "../ChartLibrary/Types/FormattedReferenceRegion";
|
|
17
19
|
import FormattedTimeReferenceLine from "../ChartLibrary/Types/FormattedTimeReferenceLine";
|
|
18
20
|
import DataPointUtil from "../Utils/DataPoint";
|
|
@@ -119,7 +121,7 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
|
|
|
119
121
|
<BarChart
|
|
120
122
|
data={records}
|
|
121
123
|
tickGap={30}
|
|
122
|
-
index={
|
|
124
|
+
index={CHART_DATA_POINT_X_AXIS_KEY}
|
|
123
125
|
categories={categories}
|
|
124
126
|
colors={
|
|
125
127
|
props.colors && props.colors.length > 0
|
|
@@ -5,6 +5,19 @@
|
|
|
5
5
|
*/
|
|
6
6
|
export const CHART_DATA_POINT_DATE_KEY: string = "__date";
|
|
7
7
|
|
|
8
|
+
/**
|
|
9
|
+
* Per-row field carrying the FORMATTED x-axis label.
|
|
10
|
+
*
|
|
11
|
+
* This is the recharts `XAxis` dataKey — the Line / Bar / Area wrappers all
|
|
12
|
+
* pass it down as their `index` prop. `DataPointUtil` must therefore write
|
|
13
|
+
* the label under exactly this key: a row keyed anything else leaves every
|
|
14
|
+
* point with an undefined category, and recharts then draws no line, bar or
|
|
15
|
+
* area at all (the axes and grid still render, so the chart looks alive but
|
|
16
|
+
* empty). Keeping the single constant on both sides is what stops that from
|
|
17
|
+
* being an invisible coupling between two distant files.
|
|
18
|
+
*/
|
|
19
|
+
export const CHART_DATA_POINT_X_AXIS_KEY: string = "Time";
|
|
20
|
+
|
|
8
21
|
export default interface ChartDataPoint {
|
|
9
22
|
[x: string]: number | string;
|
|
10
23
|
}
|
|
@@ -13,7 +13,9 @@ import SeriesPoint from "../Types/SeriesPoints";
|
|
|
13
13
|
import { XAxis } from "../Types/XAxis/XAxis";
|
|
14
14
|
import YAxis from "../Types/YAxis/YAxis";
|
|
15
15
|
import ChartCurve from "../Types/ChartCurve";
|
|
16
|
-
import ChartDataPoint
|
|
16
|
+
import ChartDataPoint, {
|
|
17
|
+
CHART_DATA_POINT_X_AXIS_KEY,
|
|
18
|
+
} from "../ChartLibrary/Types/ChartDataPoint";
|
|
17
19
|
import FormattedExemplarPoint from "../ChartLibrary/Types/FormattedExemplarPoint";
|
|
18
20
|
import FormattedReferenceRegion from "../ChartLibrary/Types/FormattedReferenceRegion";
|
|
19
21
|
import FormattedTimeReferenceLine from "../ChartLibrary/Types/FormattedTimeReferenceLine";
|
|
@@ -165,7 +167,7 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
|
|
165
167
|
<LineChart
|
|
166
168
|
data={records}
|
|
167
169
|
tickGap={30}
|
|
168
|
-
index={
|
|
170
|
+
index={CHART_DATA_POINT_X_AXIS_KEY}
|
|
169
171
|
categories={categories}
|
|
170
172
|
colors={
|
|
171
173
|
props.colors && props.colors.length > 0
|
|
@@ -16,6 +16,12 @@ export interface XAxisOptions {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export interface XAxis {
|
|
19
|
+
/*
|
|
20
|
+
* Human-readable name for the axis. Purely descriptive: the per-row key
|
|
21
|
+
* the charts read is CHART_DATA_POINT_X_AXIS_KEY, never this string. It
|
|
22
|
+
* used to double as that key, which meant a caller passing anything but
|
|
23
|
+
* "Time" got a chart with axes and no series drawn on them.
|
|
24
|
+
*/
|
|
19
25
|
legend: string;
|
|
20
26
|
options: XAxisOptions;
|
|
21
27
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import BadDataException from "../../../../Types/Exception/BadDataException";
|
|
11
11
|
import ChartDataPoint, {
|
|
12
12
|
CHART_DATA_POINT_DATE_KEY,
|
|
13
|
+
CHART_DATA_POINT_X_AXIS_KEY,
|
|
13
14
|
} from "../ChartLibrary/Types/ChartDataPoint";
|
|
14
15
|
import SeriesPoints from "../Types/SeriesPoints";
|
|
15
16
|
import { XAxis, XAxisAggregateType } from "../Types/XAxis/XAxis";
|
|
@@ -30,18 +31,14 @@ export default class DataPointUtil {
|
|
|
30
31
|
xAxis: XAxis;
|
|
31
32
|
yAxis: YAxis;
|
|
32
33
|
}): Array<ChartDataPoint> {
|
|
33
|
-
const {
|
|
34
|
-
data.xAxis,
|
|
35
|
-
);
|
|
34
|
+
const { intervals, formatter } = this.initializeXAxisData(data.xAxis);
|
|
36
35
|
const arrayOfData: ChartDataPoint[] = this.initializeArrayOfData(
|
|
37
36
|
intervals,
|
|
38
|
-
xAxisLegend,
|
|
39
37
|
formatter,
|
|
40
38
|
);
|
|
41
39
|
this.processSeriesData(
|
|
42
40
|
data.seriesPoints,
|
|
43
41
|
arrayOfData,
|
|
44
|
-
xAxisLegend,
|
|
45
42
|
formatter,
|
|
46
43
|
data.xAxis.options.aggregateType,
|
|
47
44
|
);
|
|
@@ -51,13 +48,11 @@ export default class DataPointUtil {
|
|
|
51
48
|
private static initializeXAxisData(xAxis: XAxis): {
|
|
52
49
|
xAxisMax: XAxisMaxMin;
|
|
53
50
|
xAxisMin: XAxisMaxMin;
|
|
54
|
-
xAxisLegend: string;
|
|
55
51
|
intervals: Array<Date>;
|
|
56
52
|
formatter: (value: Date) => string;
|
|
57
53
|
} {
|
|
58
54
|
const xAxisMax: XAxisMaxMin = xAxis.options.max;
|
|
59
55
|
const xAxisMin: XAxisMaxMin = xAxis.options.min;
|
|
60
|
-
const xAxisLegend: string = xAxis.legend;
|
|
61
56
|
const intervals: Array<Date> = XAxisUtil.getPrecisionIntervals({
|
|
62
57
|
xAxisMax,
|
|
63
58
|
xAxisMin,
|
|
@@ -66,18 +61,25 @@ export default class DataPointUtil {
|
|
|
66
61
|
xAxisMax,
|
|
67
62
|
xAxisMin,
|
|
68
63
|
});
|
|
69
|
-
return { xAxisMax, xAxisMin,
|
|
64
|
+
return { xAxisMax, xAxisMin, intervals, formatter };
|
|
70
65
|
}
|
|
71
66
|
|
|
72
67
|
private static initializeArrayOfData(
|
|
73
68
|
intervals: Array<Date>,
|
|
74
|
-
xAxisLegend: string,
|
|
75
69
|
formatter: (value: Date) => string,
|
|
76
70
|
): Array<ChartDataPoint> {
|
|
77
71
|
const arrayOfData: Array<ChartDataPoint> = [];
|
|
78
72
|
for (const interval of intervals) {
|
|
79
73
|
const dataPoint: ChartDataPoint = {};
|
|
80
|
-
|
|
74
|
+
/*
|
|
75
|
+
* Always the canonical key, NEVER `xAxis.legend`. The wrappers hand
|
|
76
|
+
* recharts this exact key as the XAxis dataKey, so keying rows off a
|
|
77
|
+
* caller-supplied string meant one caller passing anything else
|
|
78
|
+
* (the SLO dashboard widget passed "") produced rows recharts could
|
|
79
|
+
* not read — every point landed on an undefined category and the
|
|
80
|
+
* line silently vanished while the axes still drew.
|
|
81
|
+
*/
|
|
82
|
+
dataPoint[CHART_DATA_POINT_X_AXIS_KEY] = formatter(interval);
|
|
81
83
|
/*
|
|
82
84
|
* The formatted label is not round-trippable back to a Date, so
|
|
83
85
|
* keep the raw bucket start on the row — charts use it to recover
|
|
@@ -92,7 +94,6 @@ export default class DataPointUtil {
|
|
|
92
94
|
private static processSeriesData(
|
|
93
95
|
seriesPoints: Array<SeriesPoints>,
|
|
94
96
|
arrayOfData: Array<ChartDataPoint>,
|
|
95
|
-
xAxisLegend: string,
|
|
96
97
|
formatter: (value: Date) => string,
|
|
97
98
|
aggregateType: XAxisAggregateType,
|
|
98
99
|
): { [key: string]: SeriesData } {
|
|
@@ -123,7 +124,9 @@ export default class DataPointUtil {
|
|
|
123
124
|
|
|
124
125
|
const target: ChartDataPoint | undefined = arrayOfData.find(
|
|
125
126
|
(chartDataPoint: ChartDataPoint) => {
|
|
126
|
-
return
|
|
127
|
+
return (
|
|
128
|
+
chartDataPoint[CHART_DATA_POINT_X_AXIS_KEY] === formattedDate
|
|
129
|
+
);
|
|
127
130
|
},
|
|
128
131
|
);
|
|
129
132
|
if (!target) {
|
|
@@ -5,6 +5,7 @@ import DropdownValueBadge from "../Dropdown/DropdownValueBadge";
|
|
|
5
5
|
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
6
6
|
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
7
7
|
import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
|
|
8
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
8
9
|
import { CustomFieldDefinition } from "../../../Types/CustomField/CustomFieldDefinition";
|
|
9
10
|
import {
|
|
10
11
|
CustomFieldDropdownOption,
|
|
@@ -200,6 +201,60 @@ export const renderCustomFieldValue: RenderCustomFieldValueFunction = (data: {
|
|
|
200
201
|
);
|
|
201
202
|
}
|
|
202
203
|
|
|
204
|
+
if (
|
|
205
|
+
definition.customFieldType === CustomFieldType.Date ||
|
|
206
|
+
definition.customFieldType === CustomFieldType.DateTime
|
|
207
|
+
) {
|
|
208
|
+
/*
|
|
209
|
+
* Stored as ISO-8601 (the shared date Input normalises every entry through
|
|
210
|
+
* OneUptimeDate.toString()), which is unreadable in a cell, so it is shown
|
|
211
|
+
* in the viewer's own timezone — the same rendering Detail gives a date
|
|
212
|
+
* field, so the list and the item page agree.
|
|
213
|
+
*/
|
|
214
|
+
const isDateOnly: boolean =
|
|
215
|
+
definition.customFieldType === CustomFieldType.Date;
|
|
216
|
+
const raw: string = String(value);
|
|
217
|
+
|
|
218
|
+
/*
|
|
219
|
+
* Validity is decided here rather than by inspecting what the formatter
|
|
220
|
+
* returned, because the formatter never reports failure: OneUptimeDate
|
|
221
|
+
* .fromString swallows moment's parse error and falls back to `new
|
|
222
|
+
* Date(str)`, so junk becomes an Invalid Date and moment renders it as the
|
|
223
|
+
* literal, truthy string "Invalid date" (and "Invalid date UTC" for the
|
|
224
|
+
* DateTime variant). A `formatted || raw` fallback therefore never fires.
|
|
225
|
+
*
|
|
226
|
+
* Showing the raw text instead matters because the value is not
|
|
227
|
+
* necessarily one this UI wrote — a project can PUT anything into the
|
|
228
|
+
* `customFields` bag over the API, and may well have been storing dates as
|
|
229
|
+
* free text before this field became a date. "Invalid date" hides which
|
|
230
|
+
* value is wrong; the value itself is the thing that lets someone fix it.
|
|
231
|
+
*/
|
|
232
|
+
let parsed: Date | null = null;
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
parsed = OneUptimeDate.fromString(raw);
|
|
236
|
+
} catch {
|
|
237
|
+
parsed = null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!parsed || isNaN(parsed.getTime())) {
|
|
241
|
+
return <span>{raw}</span>;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
return (
|
|
246
|
+
<span>
|
|
247
|
+
{OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
248
|
+
parsed,
|
|
249
|
+
isDateOnly,
|
|
250
|
+
)}
|
|
251
|
+
</span>
|
|
252
|
+
);
|
|
253
|
+
} catch {
|
|
254
|
+
return <span>{raw}</span>;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
203
258
|
if (definition.customFieldType === CustomFieldType.MultiSelectDropdown) {
|
|
204
259
|
/*
|
|
205
260
|
* Tolerant on the way out: an older single-select value that was later
|
|
@@ -531,3 +531,48 @@ export function labelForDeviceRole(
|
|
|
531
531
|
}
|
|
532
532
|
return DEVICE_ROLE_LABELS[role] || DEVICE_ROLE_LABELS.unknown;
|
|
533
533
|
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Read an operator's stored role override off a NetworkDevice row.
|
|
537
|
+
*
|
|
538
|
+
* The classifier above is evidence-driven, and its evidence is SNMP: a
|
|
539
|
+
* device nothing walks — a ping-only device imported by discovery, say —
|
|
540
|
+
* offers it a hostname and nothing else, so it comes back "unknown" and
|
|
541
|
+
* stays there forever. `NetworkDevice.deviceRole` is the operator saying
|
|
542
|
+
* what the box actually is, and it is the only statement about a role that
|
|
543
|
+
* is not an inference, so it outranks everything the classifier can find.
|
|
544
|
+
*
|
|
545
|
+
* Undefined means "no override" — NOT "unknown". The two are different
|
|
546
|
+
* answers and the difference decides whether the classifier gets to run at
|
|
547
|
+
* all, which is why an unrecognised or empty column reads as undefined
|
|
548
|
+
* rather than falling back to a role. "unknown" is itself refused as an
|
|
549
|
+
* override for the same reason: storing it would silently disable the
|
|
550
|
+
* classifier on a device the operator was only declining to classify.
|
|
551
|
+
*/
|
|
552
|
+
export function parseDeviceRoleOverride(
|
|
553
|
+
value: string | undefined | null,
|
|
554
|
+
): NetworkTopologyDeviceRole | undefined {
|
|
555
|
+
const normalized: string = (value || "").trim().toLowerCase();
|
|
556
|
+
if (!normalized) {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
return DEVICE_ROLES_IN_LEGEND_ORDER.find(
|
|
560
|
+
(role: NetworkTopologyDeviceRole) => {
|
|
561
|
+
return role.toLowerCase() === normalized;
|
|
562
|
+
},
|
|
563
|
+
);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* The role to draw a managed device with: the operator's override when
|
|
568
|
+
* there is one, otherwise whatever the evidence supports.
|
|
569
|
+
*
|
|
570
|
+
* One function so the topology builder, the forms and the tests cannot
|
|
571
|
+
* drift about which of the two wins.
|
|
572
|
+
*/
|
|
573
|
+
export function resolveDeviceRole(
|
|
574
|
+
roleOverride: string | undefined | null,
|
|
575
|
+
signals: DeviceRoleSignals,
|
|
576
|
+
): NetworkTopologyDeviceRole {
|
|
577
|
+
return parseDeviceRoleOverride(roleOverride) ?? classifyDeviceRole(signals);
|
|
578
|
+
}
|
|
@@ -11,6 +11,7 @@ import { normalizeMac } from "./EndpointAttachmentUtil";
|
|
|
11
11
|
import {
|
|
12
12
|
classifyDeviceRole,
|
|
13
13
|
classifyEndpointRole,
|
|
14
|
+
resolveDeviceRole,
|
|
14
15
|
} from "./NetworkDeviceRoleUtil";
|
|
15
16
|
|
|
16
17
|
export interface TopologyDeviceInput {
|
|
@@ -29,6 +30,14 @@ export interface TopologyDeviceInput {
|
|
|
29
30
|
*/
|
|
30
31
|
sysDescr?: string | undefined;
|
|
31
32
|
sysObjectId?: string | undefined;
|
|
33
|
+
/*
|
|
34
|
+
* The operator's own answer to what this device is, when they gave one.
|
|
35
|
+
* Outranks the classifier below, because it is the only statement about
|
|
36
|
+
* the role that is not an inference — and on a device nothing walks it
|
|
37
|
+
* is the ONLY statement available at all, the classifier having nothing
|
|
38
|
+
* but a hostname to go on.
|
|
39
|
+
*/
|
|
40
|
+
deviceRole?: string | undefined;
|
|
32
41
|
/*
|
|
33
42
|
* ENTITY-MIB chassis serial. Not rendered — it is here because LLDP
|
|
34
43
|
* chassis-id subtype 7 ("locally assigned") is very often exactly this
|
|
@@ -109,6 +118,13 @@ export interface TopologyManualLinkInput {
|
|
|
109
118
|
toPortName?: string | undefined;
|
|
110
119
|
// Health from a bound Monitor, already reduced by the caller.
|
|
111
120
|
monitorStatus?: "up" | "down" | undefined;
|
|
121
|
+
/*
|
|
122
|
+
* Whichever end the operator declared the parent, when they declared
|
|
123
|
+
* one. Must be `fromDeviceId` or `toDeviceId`; anything else is dropped
|
|
124
|
+
* rather than drawn, because a parent that is not on the link cannot be
|
|
125
|
+
* rendered as one and a wrong hierarchy is worse than an inferred one.
|
|
126
|
+
*/
|
|
127
|
+
parentDeviceId?: string | undefined;
|
|
112
128
|
}
|
|
113
129
|
|
|
114
130
|
/*
|
|
@@ -232,7 +248,10 @@ export default class NetworkTopologyUtil {
|
|
|
232
248
|
* alias are one unmanaged node, however many switches saw it. Edges are
|
|
233
249
|
* undirected and deduplicated — a link reported from both ends, or by
|
|
234
250
|
* both protocols, appears once with the union of what each report knew
|
|
235
|
-
* (ports, protocols, per-end interface state).
|
|
251
|
+
* (ports, protocols, per-end interface state). Undirected in the sense
|
|
252
|
+
* that neither end is privileged by how the edge was discovered: an edge
|
|
253
|
+
* carries a direction only where an operator declared one, as
|
|
254
|
+
* `parentNodeId`. When `interfaces` rows are
|
|
236
255
|
* provided, each edge end whose interface is identifiable carries its
|
|
237
256
|
* operational state (up/down, utilization, rates). When `endpoints` rows
|
|
238
257
|
* are provided, each one that attaches to a device in the graph becomes an
|
|
@@ -306,7 +325,7 @@ export default class NetworkTopologyUtil {
|
|
|
306
325
|
name: device.name,
|
|
307
326
|
isManaged: true,
|
|
308
327
|
kind: "device",
|
|
309
|
-
role:
|
|
328
|
+
role: resolveDeviceRole(device.deviceRole, {
|
|
310
329
|
sysDescr: device.sysDescr,
|
|
311
330
|
sysObjectId: device.sysObjectId,
|
|
312
331
|
vendor: device.vendor,
|
|
@@ -520,6 +539,18 @@ export default class NetworkTopologyUtil {
|
|
|
520
539
|
continue;
|
|
521
540
|
}
|
|
522
541
|
|
|
542
|
+
/*
|
|
543
|
+
* A declared parent is only meaningful if it is one of the two ends.
|
|
544
|
+
* The service refuses anything else at write time, so this is a
|
|
545
|
+
* second line rather than the only one — it also covers rule-derived
|
|
546
|
+
* links, which never pass through that service.
|
|
547
|
+
*/
|
|
548
|
+
const declaredParentId: string | undefined =
|
|
549
|
+
link.parentDeviceId === link.fromDeviceId ||
|
|
550
|
+
link.parentDeviceId === link.toDeviceId
|
|
551
|
+
? link.parentDeviceId
|
|
552
|
+
: undefined;
|
|
553
|
+
|
|
523
554
|
const edgeKey: string = [link.fromDeviceId, link.toDeviceId]
|
|
524
555
|
.sort()
|
|
525
556
|
.join("::");
|
|
@@ -534,6 +565,7 @@ export default class NetworkTopologyUtil {
|
|
|
534
565
|
protocols: ["manual"],
|
|
535
566
|
monitorState: link.monitorStatus,
|
|
536
567
|
name: link.name,
|
|
568
|
+
parentNodeId: declaredParentId,
|
|
537
569
|
});
|
|
538
570
|
continue;
|
|
539
571
|
}
|
|
@@ -557,6 +589,14 @@ export default class NetworkTopologyUtil {
|
|
|
557
589
|
}
|
|
558
590
|
existing.name = existing.name || link.name;
|
|
559
591
|
existing.monitorState = existing.monitorState ?? link.monitorStatus;
|
|
592
|
+
/*
|
|
593
|
+
* First declaration wins, matching the ordering the caller already
|
|
594
|
+
* relies on: explicit links are merged before rule-derived ones, so
|
|
595
|
+
* a hierarchy somebody set on the link itself beats a rule that
|
|
596
|
+
* happens to cover the same pair. The specific statement beats the
|
|
597
|
+
* general one — the same precedence the ports and the name use.
|
|
598
|
+
*/
|
|
599
|
+
existing.parentNodeId = existing.parentNodeId || declaredParentId;
|
|
560
600
|
}
|
|
561
601
|
|
|
562
602
|
const edges: Array<NetworkTopologyEdge> = Array.from(edgeByKey.values());
|
|
@@ -50,6 +50,7 @@ let NetworkDevice = class NetworkDevice extends BaseModel {
|
|
|
50
50
|
this.currentMonitorStatus = undefined;
|
|
51
51
|
this.currentMonitorStatusId = undefined;
|
|
52
52
|
this.monitoringMethod = undefined;
|
|
53
|
+
this.deviceRole = undefined;
|
|
53
54
|
this.monitor = undefined;
|
|
54
55
|
this.monitorId = undefined;
|
|
55
56
|
this.snmpVersion = undefined;
|
|
@@ -641,6 +642,50 @@ __decorate([
|
|
|
641
642
|
}),
|
|
642
643
|
__metadata("design:type", String)
|
|
643
644
|
], NetworkDevice.prototype, "monitoringMethod", void 0);
|
|
645
|
+
__decorate([
|
|
646
|
+
ColumnAccessControl({
|
|
647
|
+
create: [
|
|
648
|
+
Permission.ProjectOwner,
|
|
649
|
+
Permission.ProjectAdmin,
|
|
650
|
+
Permission.ProjectMember,
|
|
651
|
+
Permission.SettingsAdmin,
|
|
652
|
+
Permission.SettingsMember,
|
|
653
|
+
Permission.CreateNetworkDevice,
|
|
654
|
+
],
|
|
655
|
+
read: [
|
|
656
|
+
Permission.ProjectOwner,
|
|
657
|
+
Permission.ProjectAdmin,
|
|
658
|
+
Permission.ProjectMember,
|
|
659
|
+
Permission.Viewer,
|
|
660
|
+
Permission.SettingsAdmin,
|
|
661
|
+
Permission.SettingsMember,
|
|
662
|
+
Permission.SettingsViewer,
|
|
663
|
+
Permission.ReadNetworkDevice,
|
|
664
|
+
],
|
|
665
|
+
update: [
|
|
666
|
+
Permission.ProjectOwner,
|
|
667
|
+
Permission.ProjectAdmin,
|
|
668
|
+
Permission.ProjectMember,
|
|
669
|
+
Permission.SettingsAdmin,
|
|
670
|
+
Permission.SettingsMember,
|
|
671
|
+
Permission.EditNetworkDevice,
|
|
672
|
+
],
|
|
673
|
+
}),
|
|
674
|
+
TableColumn({
|
|
675
|
+
required: false,
|
|
676
|
+
type: TableColumnType.ShortText,
|
|
677
|
+
canReadOnRelationQuery: true,
|
|
678
|
+
title: "Device Role",
|
|
679
|
+
description: "What this device does on the network — router, switch, access point and so on. Left empty, the role is worked out from the device's own SNMP identity. Set it when there is no SNMP to read: a ping-only device has no identity to classify, and the role decides both the shape it is drawn with and where it sits in the topology hierarchy.",
|
|
680
|
+
example: "switch",
|
|
681
|
+
}),
|
|
682
|
+
Column({
|
|
683
|
+
nullable: true,
|
|
684
|
+
type: ColumnType.ShortText,
|
|
685
|
+
length: ColumnLength.ShortText,
|
|
686
|
+
}),
|
|
687
|
+
__metadata("design:type", String)
|
|
688
|
+
], NetworkDevice.prototype, "deviceRole", void 0);
|
|
644
689
|
__decorate([
|
|
645
690
|
ColumnAccessControl({
|
|
646
691
|
create: [
|