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