@oneuptime/common 12.0.14 → 12.0.15
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/GlobalOidc.ts +20 -0
- package/Models/DatabaseModels/GlobalSso.ts +20 -0
- package/Models/DatabaseModels/MarketingConversion.ts +19 -9
- package/Models/DatabaseModels/NetworkDeviceLinkRule.ts +46 -0
- package/Models/DatabaseModels/StatusPage.ts +7 -4
- package/Server/API/StatusPageAPI.ts +29 -4
- package/Server/EnvironmentConfig.ts +7 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.ts +40 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/SlackAuthorization.ts +43 -6
- package/Server/Middleware/UserAuthorization.ts +236 -26
- package/Server/Middleware/WhatsAppAuthorization.ts +43 -3
- package/Server/Services/GlobalOidcProjectService.ts +126 -1
- package/Server/Services/GlobalOidcService.ts +132 -0
- package/Server/Services/GlobalSsoProjectService.ts +126 -1
- package/Server/Services/GlobalSsoService.ts +132 -0
- package/Server/Services/UserEmailService.ts +4 -4
- package/Server/Utils/CronTab.ts +73 -0
- package/Server/Utils/GlobalSsoAuthorization.ts +175 -0
- package/Server/Utils/Marketing/ConversionUploadProvider.ts +35 -5
- package/Server/Utils/Marketing/Providers/GoogleAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/LinkedIn.ts +1 -1
- package/Server/Utils/Marketing/Providers/Meta.ts +1 -1
- package/Server/Utils/Marketing/Providers/MicrosoftAds.ts +1 -1
- package/Server/Utils/Marketing/Providers/Reddit.ts +1 -1
- package/Server/Utils/Monitor/Criteria/APIRequestCriteria.ts +48 -33
- package/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/EvaluateOverTime.ts +475 -47
- package/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.ts +35 -30
- package/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.ts +34 -28
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +33 -28
- package/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.ts +69 -40
- package/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.ts +35 -29
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +8 -0
- package/Server/Utils/Monitor/MonitorResource.ts +161 -21
- package/Server/Utils/StatusPageContentSecurityPolicy.ts +56 -0
- package/Server/Utils/StatusPageCustomizationAccess.ts +117 -0
- package/Tests/Server/API/StatusPageCustomizationOrigin.test.ts +348 -0
- package/Tests/Server/Infrastructure/Postgres/FixTotpOtpUrlAlgorithmMigration.test.ts +9 -59
- package/Tests/Server/Infrastructure/Postgres/SchemaMigrationsOrdering.test.ts +227 -0
- package/Tests/Server/Middleware/SlackAuthorization.test.ts +149 -0
- package/Tests/Server/Middleware/UserAuthorization.test.ts +27 -13
- package/Tests/Server/Middleware/UserAuthorizationGlobalSsoGovernance.test.ts +1455 -0
- package/Tests/Server/Middleware/UserAuthorizationSSOProvider.test.ts +91 -2
- package/Tests/Server/Middleware/WhatsAppAuthorization.test.ts +233 -0
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +21 -8
- package/Tests/Server/Services/GlobalSsoProviderAuthorization.test.ts +1242 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementHydration.test.ts +459 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +10 -0
- package/Tests/Server/Types/AnalyticsDatabase/AggregateUtilBucketTimestamp.test.ts +416 -0
- package/Tests/Server/Types/Database/QueryHelperManyToManyAndEmptyValues.test.ts +551 -0
- package/Tests/Server/Types/Database/SelectUtil.test.ts +348 -0
- package/Tests/Server/Utils/CronTab.test.ts +116 -0
- package/Tests/Server/Utils/GlobalSSOToken.test.ts +581 -0
- package/Tests/Server/Utils/GlobalSsoAuthorization.test.ts +1401 -0
- package/Tests/Server/Utils/Marketing/AdUploadableConversionTypes.test.ts +254 -0
- package/Tests/Server/Utils/Marketing/ConversionUploadProvider.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaEvaluateOverTime.test.ts +508 -0
- package/Tests/Server/Utils/Monitor/Criteria/APIRequestCriteriaPortTimings.test.ts +30 -9
- package/Tests/Server/Utils/Monitor/Criteria/EvaluateOverTime.test.ts +987 -0
- package/Tests/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.test.ts +916 -0
- package/Tests/Server/Utils/Monitor/Criteria/ProfileMonitorCriteria.test.ts +382 -0
- package/Tests/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.test.ts +1219 -0
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +86 -0
- package/Tests/Server/Utils/StatusPageContentSecurityPolicy.test.ts +136 -0
- package/Tests/Server/Utils/StatusPageCustomizationAccess.test.ts +223 -0
- package/Tests/Types/Monitor/MonitorStepConfigHelpers.test.ts +165 -0
- package/Tests/Types/NetworkDevice/NetworkDeviceLinkRuleScope.test.ts +215 -0
- package/Tests/UI/Components/Forms/FormSubmitAnalytics.test.tsx +206 -0
- package/Tests/UI/Utils/AIChatExport/ConversationMarkdown.test.ts +774 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownBlocks.test.ts +791 -0
- package/Tests/UI/Utils/AIChatExport/MarkdownSafety.test.ts +544 -0
- package/Tests/Utils/Analytics.test.ts +187 -0
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +80 -0
- package/Tests/Utils/Monitor/NetworkDeviceLinkRuleUtil.test.ts +1291 -0
- package/Tests/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.test.ts +214 -0
- package/Types/Marketing/MarketingConversion.ts +12 -0
- package/Types/Monitor/CriteriaFilter.ts +9 -0
- package/Types/NetworkDevice/NetworkDeviceLinkRuleScope.ts +51 -0
- package/UI/Components/FormModal/BasicFormModal.tsx +12 -0
- package/UI/Components/Forms/BasicForm.tsx +13 -1
- package/UI/Components/Forms/BasicModelForm.tsx +2 -1
- package/UI/Components/Forms/ModelForm.tsx +10 -1
- package/UI/Components/Forms/Utils/FormAnalyticsName.ts +39 -0
- package/UI/Components/ModelFormModal/ModelFormModal.tsx +6 -1
- package/Utils/Analytics.ts +55 -2
- package/Utils/Monitor/MonitorMetricType.ts +40 -0
- package/Utils/Monitor/NetworkDeviceLinkRuleUtil.ts +587 -37
- package/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.ts +115 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/GlobalSso.js +21 -0
- package/build/dist/Models/DatabaseModels/GlobalSso.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MarketingConversion.js +20 -10
- package/build/dist/Models/DatabaseModels/MarketingConversion.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js +47 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceLinkRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +4 -4
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +25 -4
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +6 -0
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787800000000-AddScopeToNetworkDeviceLinkRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js +29 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787900000000-AddRestrictToAttachedProjectsToGlobalSso.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Middleware/SlackAuthorization.js +28 -4
- package/build/dist/Server/Middleware/SlackAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/UserAuthorization.js +197 -22
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js +32 -2
- package/build/dist/Server/Middleware/WhatsAppAuthorization.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalOidcProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalOidcService.js +127 -0
- package/build/dist/Server/Services/GlobalOidcService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoProjectService.js +105 -0
- package/build/dist/Server/Services/GlobalSsoProjectService.js.map +1 -1
- package/build/dist/Server/Services/GlobalSsoService.js +127 -0
- package/build/dist/Server/Services/GlobalSsoService.js.map +1 -1
- package/build/dist/Server/Services/UserEmailService.js +4 -4
- package/build/dist/Server/Utils/CronTab.js +66 -0
- package/build/dist/Server/Utils/CronTab.js.map +1 -1
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js +112 -0
- package/build/dist/Server/Utils/GlobalSsoAuthorization.js.map +1 -0
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js +17 -1
- package/build/dist/Server/Utils/Marketing/ConversionUploadProvider.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/GoogleAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/LinkedIn.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Meta.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/MicrosoftAds.js.map +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js +1 -1
- package/build/dist/Server/Utils/Marketing/Providers/Reddit.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js +37 -35
- package/build/dist/Server/Utils/Monitor/Criteria/APIRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/DnsMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js +20 -22
- package/build/dist/Server/Utils/Monitor/Criteria/DomainMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js +370 -42
- package/build/dist/Server/Utils/Monitor/Criteria/EvaluateOverTime.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/ExternalStatusPageMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/IncomingRequestCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +21 -25
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js +64 -52
- package/build/dist/Server/Utils/Monitor/Criteria/ServerMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js +25 -29
- package/build/dist/Server/Utils/Monitor/Criteria/SnmpMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +8 -0
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +123 -23
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js +39 -0
- package/build/dist/Server/Utils/StatusPageContentSecurityPolicy.js.map +1 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js +85 -0
- package/build/dist/Server/Utils/StatusPageCustomizationAccess.js.map +1 -0
- package/build/dist/Types/Marketing/MarketingConversion.js +10 -0
- package/build/dist/Types/Marketing/MarketingConversion.js.map +1 -1
- package/build/dist/Types/Monitor/CriteriaFilter.js +1 -0
- package/build/dist/Types/Monitor/CriteriaFilter.js.map +1 -1
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js +46 -0
- package/build/dist/Types/NetworkDevice/NetworkDeviceLinkRuleScope.js.map +1 -0
- package/build/dist/UI/Components/FormModal/BasicFormModal.js +2 -1
- package/build/dist/UI/Components/FormModal/BasicFormModal.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicForm.js +10 -1
- package/build/dist/UI/Components/Forms/BasicForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js +2 -1
- package/build/dist/UI/Components/Forms/BasicModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +4 -1
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js +33 -0
- package/build/dist/UI/Components/Forms/Utils/FormAnalyticsName.js.map +1 -0
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js +2 -1
- package/build/dist/UI/Components/ModelFormModal/ModelFormModal.js.map +1 -1
- package/build/dist/Utils/Analytics.js +50 -2
- package/build/dist/Utils/Analytics.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +37 -0
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js +318 -37
- package/build/dist/Utils/Monitor/NetworkDeviceLinkRuleUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js +87 -0
- package/build/dist/Utils/NetworkDiscovery/UnclaimedScanDiagnosis.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { NetworkDeviceLinkRuleScopeUtil } from "../../Types/NetworkDevice/NetworkDeviceLinkRuleScope";
|
|
2
|
+
|
|
1
3
|
/*
|
|
2
4
|
* Turning label rules into uplinks.
|
|
3
5
|
*
|
|
@@ -9,12 +11,35 @@
|
|
|
9
11
|
* edge to the single device carrying all the parent labels. Matching "devices
|
|
10
12
|
* that share a label" symmetrically, which is what was originally asked for,
|
|
11
13
|
* would turn one forty-device site label into 780 edges.
|
|
14
|
+
*
|
|
15
|
+
* "The single device" needs a universe to be single IN, and the rule's scope
|
|
16
|
+
* is what names it. Project scope asks across everything the caller passed;
|
|
17
|
+
* Site scope asks once per site, which is what lets one rule draw the same
|
|
18
|
+
* router-to-switch star in fourteen buildings instead of reporting fourteen
|
|
19
|
+
* candidate routers and drawing nothing anywhere.
|
|
12
20
|
*/
|
|
13
21
|
|
|
14
22
|
export interface LinkRuleDeviceInput {
|
|
15
23
|
id: string;
|
|
16
24
|
// Normalised label ids the device carries.
|
|
17
25
|
labelIds: Array<string>;
|
|
26
|
+
/*
|
|
27
|
+
* Which site the device belongs to. ALWAYS a string, never an ObjectID —
|
|
28
|
+
* grouping keys a Map on it, and an ObjectID instance would compare by
|
|
29
|
+
* identity and put every device in a group of its own. null, undefined, ""
|
|
30
|
+
* and whitespace all normalise to the same "no site" key.
|
|
31
|
+
*
|
|
32
|
+
* Read only when the rule is site-scoped, so a caller that never populates
|
|
33
|
+
* it leaves project-scoped rules resolving exactly as before.
|
|
34
|
+
*/
|
|
35
|
+
siteId?: string | null | undefined;
|
|
36
|
+
/*
|
|
37
|
+
* Display name of that site, for the warning text. Carried on the device
|
|
38
|
+
* rather than looked up by the caller so the name comes from the same
|
|
39
|
+
* permission-filtered rows the map itself is drawn from — a viewer can
|
|
40
|
+
* never be told the name of a site none of whose devices they can read.
|
|
41
|
+
*/
|
|
42
|
+
siteName?: string | undefined;
|
|
18
43
|
}
|
|
19
44
|
|
|
20
45
|
export interface LinkRuleInput {
|
|
@@ -23,6 +48,19 @@ export interface LinkRuleInput {
|
|
|
23
48
|
isEnabled?: boolean | undefined;
|
|
24
49
|
childLabelIds: Array<string>;
|
|
25
50
|
parentLabelIds: Array<string>;
|
|
51
|
+
/*
|
|
52
|
+
* Raw column value. Always read through NetworkDeviceLinkRuleScopeUtil.parse
|
|
53
|
+
* so undefined and NULL read as Project — every rule written before the
|
|
54
|
+
* column existed means "one parent for the whole project", and defaulting
|
|
55
|
+
* the other way would silently redraw somebody's map.
|
|
56
|
+
*/
|
|
57
|
+
scope?: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Structurally identical to the inline type it replaces.
|
|
61
|
+
export interface LinkRuleLink {
|
|
62
|
+
fromDeviceId: string;
|
|
63
|
+
toDeviceId: string;
|
|
26
64
|
}
|
|
27
65
|
|
|
28
66
|
/*
|
|
@@ -36,36 +74,233 @@ export type LinkRuleSkipReason =
|
|
|
36
74
|
| "noParentLabels"
|
|
37
75
|
| "noParentMatched"
|
|
38
76
|
| "ambiguousParent"
|
|
39
|
-
| "noChildrenMatched"
|
|
77
|
+
| "noChildrenMatched"
|
|
78
|
+
/*
|
|
79
|
+
* Site scope only: sites were in play and not one of them resolved. Its own
|
|
80
|
+
* reason because the per-site failures can be MIXED — one site with no
|
|
81
|
+
* parent, another with two — which none of the reasons above states.
|
|
82
|
+
*/
|
|
83
|
+
| "noSiteResolved";
|
|
84
|
+
|
|
85
|
+
/*
|
|
86
|
+
* The only two ways ONE site can fail in a way worth reporting. Deliberately
|
|
87
|
+
* narrower than LinkRuleSkipReason: `noChildrenMatched` is unreachable here
|
|
88
|
+
* (a site with nothing to place is never considered at all), and the three
|
|
89
|
+
* rule-level reasons are settled before any device is looked at.
|
|
90
|
+
*/
|
|
91
|
+
export type LinkRuleGroupSkipReason = "noParentMatched" | "ambiguousParent";
|
|
92
|
+
|
|
93
|
+
/*
|
|
94
|
+
* Warnings have a slightly wider vocabulary than skips: "some matching devices
|
|
95
|
+
* have no site" is not a reason the RULE drew nothing, so it has no business
|
|
96
|
+
* in LinkRuleSkipReason, which describeOutcome switches over.
|
|
97
|
+
*/
|
|
98
|
+
export type LinkRuleWarningReason = LinkRuleSkipReason | "devicesWithoutSite";
|
|
99
|
+
|
|
100
|
+
/*
|
|
101
|
+
* One site this rule was clearly meant to cover and could not. Independent of
|
|
102
|
+
* outcome.links.length — thirteen sites can draw while the fourteenth is
|
|
103
|
+
* listed here.
|
|
104
|
+
*/
|
|
105
|
+
export interface LinkRuleGroupFailure {
|
|
106
|
+
siteId: string;
|
|
107
|
+
siteName?: string | undefined;
|
|
108
|
+
reason: LinkRuleGroupSkipReason;
|
|
109
|
+
// Devices in THIS site carrying all the parent labels. 0 or >= 2.
|
|
110
|
+
matchedParentCount: number;
|
|
111
|
+
/*
|
|
112
|
+
* Devices in THIS site carrying all the child labels — the damage count,
|
|
113
|
+
* "how many nodes are floating on the map right now". No parent exclusion
|
|
114
|
+
* is applied because there is no resolved parent to exclude.
|
|
115
|
+
*/
|
|
116
|
+
matchedChildCount: number;
|
|
117
|
+
}
|
|
40
118
|
|
|
41
119
|
export interface LinkRuleOutcome {
|
|
42
120
|
ruleId: string;
|
|
43
121
|
ruleName?: string | undefined;
|
|
44
122
|
// Device pairs this rule contributes, parent second.
|
|
45
|
-
links: Array<
|
|
46
|
-
|
|
123
|
+
links: Array<LinkRuleLink>;
|
|
124
|
+
/*
|
|
125
|
+
* Set when links is empty and the rule had something to say about why.
|
|
126
|
+
* INVARIANT, both scopes: links.length === 0 <=> skipReason !== undefined.
|
|
127
|
+
* A site-scoped rule that drew in thirteen of fourteen sites has NOT been
|
|
128
|
+
* skipped; the fourteenth lives in groupFailures.
|
|
129
|
+
*/
|
|
47
130
|
skipReason?: LinkRuleSkipReason | undefined;
|
|
48
|
-
|
|
131
|
+
/*
|
|
132
|
+
* How many devices the parent labels matched — the ambiguity evidence.
|
|
133
|
+
* Project scope: unchanged. Site scope: summed over the sites the rule
|
|
134
|
+
* actually applies to.
|
|
135
|
+
*/
|
|
49
136
|
matchedParentCount: number;
|
|
137
|
+
// INVARIANT, both scopes: matchedChildCount === links.length.
|
|
50
138
|
matchedChildCount: number;
|
|
139
|
+
|
|
140
|
+
/*
|
|
141
|
+
* The three fields below are SITE SCOPE ONLY. On the project path the keys
|
|
142
|
+
* are not written at all, so a project-scoped outcome is the exact object
|
|
143
|
+
* it has always been, key for key.
|
|
144
|
+
*/
|
|
145
|
+
|
|
146
|
+
// Sites the rule was meant to cover and could not. Omitted when empty.
|
|
147
|
+
groupFailures?: Array<LinkRuleGroupFailure> | undefined;
|
|
148
|
+
/*
|
|
149
|
+
* Sites this rule applies to: the ones holding at least one device it wanted
|
|
150
|
+
* to place. The coverage denominator, and deliberately not "every site in
|
|
151
|
+
* the project" — a healthy rule reading "drawing in 12 of 74 sites" would
|
|
152
|
+
* make the coverage sentence itself the noise.
|
|
153
|
+
*/
|
|
154
|
+
applicableSiteCount?: number | undefined;
|
|
155
|
+
// Devices carrying ALL the child labels that have no site. Omitted when 0.
|
|
156
|
+
unsitedChildDeviceCount?: number | undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** One bullet in the topology map's warning banner. At most one per rule. */
|
|
160
|
+
export interface LinkRuleWarning {
|
|
161
|
+
ruleId: string;
|
|
162
|
+
ruleName?: string | undefined;
|
|
163
|
+
reason: LinkRuleWarningReason;
|
|
164
|
+
message: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/*
|
|
168
|
+
* Devices are normalised once per resolveRule call: the label Set is built
|
|
169
|
+
* here rather than allocated afresh inside every match, and the group key is
|
|
170
|
+
* stringified and trimmed here so null, undefined, "" and " " cannot become
|
|
171
|
+
* four different sites. Nothing here mutates the caller's array, which matters
|
|
172
|
+
* because resolveRules hands the same array to every rule in turn.
|
|
173
|
+
*/
|
|
174
|
+
interface NormalizedDevice {
|
|
175
|
+
device: LinkRuleDeviceInput;
|
|
176
|
+
labelSet: Set<string>;
|
|
177
|
+
// "" means the device has no site.
|
|
178
|
+
siteKey: string;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/*
|
|
182
|
+
* One site's verdict — the same decision matrix as always, asked inside a
|
|
183
|
+
* narrower device set. Used by both scopes, so the parent/child matching
|
|
184
|
+
* exists exactly once.
|
|
185
|
+
*/
|
|
186
|
+
interface LinkRuleGroupResult {
|
|
187
|
+
// Empty on every failure path.
|
|
188
|
+
links: Array<LinkRuleLink>;
|
|
189
|
+
// 0, 1, or >= 2.
|
|
190
|
+
parentMatchCount: number;
|
|
191
|
+
// Devices carrying all the child labels, BEFORE the unique-parent exclusion.
|
|
192
|
+
childMatchCount: number;
|
|
193
|
+
// null when links is non-empty.
|
|
194
|
+
failure: "noParentMatched" | "ambiguousParent" | "noChildrenMatched" | null;
|
|
51
195
|
}
|
|
52
196
|
|
|
53
197
|
export default class NetworkDeviceLinkRuleUtil {
|
|
198
|
+
/*
|
|
199
|
+
* Appended by the caller when the device list hit its row cap. Lives here so
|
|
200
|
+
* every operator-facing string this feature produces stays in one file.
|
|
201
|
+
*/
|
|
202
|
+
public static readonly TRUNCATED_DEVICE_LIST_NOTE: string =
|
|
203
|
+
"The device list was truncated, so some of these may be false alarms.";
|
|
204
|
+
|
|
205
|
+
// Three names is enough to recognise a pattern; fourteen is a wall of text.
|
|
206
|
+
private static readonly MAX_NAMED_SITES: number = 3;
|
|
207
|
+
// Site names are ShortText and can run to 100 characters.
|
|
208
|
+
private static readonly MAX_SITE_NAME_LENGTH: number = 40;
|
|
209
|
+
private static readonly UNNAMED_SITE: string = "an unnamed site";
|
|
210
|
+
|
|
54
211
|
/*
|
|
55
212
|
* ALL of the rule's labels must be present on the device, not any. "Devices
|
|
56
213
|
* that are both an access point AND on floor 1" is the question an operator
|
|
57
214
|
* is asking; any-of would sweep in every access point in the building.
|
|
58
215
|
*/
|
|
59
|
-
private static
|
|
60
|
-
|
|
216
|
+
private static matches(
|
|
217
|
+
labelSet: Set<string>,
|
|
61
218
|
requiredLabelIds: Array<string>,
|
|
62
219
|
): boolean {
|
|
63
|
-
const owned: Set<string> = new Set<string>(device.labelIds);
|
|
64
220
|
return requiredLabelIds.every((labelId: string) => {
|
|
65
|
-
return
|
|
221
|
+
return labelSet.has(labelId);
|
|
66
222
|
});
|
|
67
223
|
}
|
|
68
224
|
|
|
225
|
+
private static resolveGroup(
|
|
226
|
+
rule: LinkRuleInput,
|
|
227
|
+
group: Array<NormalizedDevice>,
|
|
228
|
+
): LinkRuleGroupResult {
|
|
229
|
+
const parents: Array<NormalizedDevice> = group.filter(
|
|
230
|
+
(entry: NormalizedDevice) => {
|
|
231
|
+
return NetworkDeviceLinkRuleUtil.matches(
|
|
232
|
+
entry.labelSet,
|
|
233
|
+
rule.parentLabelIds,
|
|
234
|
+
);
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
/*
|
|
239
|
+
* Counted before the parent branch, not after: a site that failed to
|
|
240
|
+
* resolve still has to report how many devices are floating there, and
|
|
241
|
+
* that number is the operator's damage assessment.
|
|
242
|
+
*/
|
|
243
|
+
const childCandidates: Array<NormalizedDevice> = group.filter(
|
|
244
|
+
(entry: NormalizedDevice) => {
|
|
245
|
+
return NetworkDeviceLinkRuleUtil.matches(
|
|
246
|
+
entry.labelSet,
|
|
247
|
+
rule.childLabelIds,
|
|
248
|
+
);
|
|
249
|
+
},
|
|
250
|
+
);
|
|
251
|
+
const childMatchCount: number = childCandidates.length;
|
|
252
|
+
|
|
253
|
+
if (parents.length === 0) {
|
|
254
|
+
return {
|
|
255
|
+
links: [],
|
|
256
|
+
parentMatchCount: 0,
|
|
257
|
+
childMatchCount: childMatchCount,
|
|
258
|
+
failure: "noParentMatched",
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/*
|
|
263
|
+
* Two candidate parents is not a tie to break — it is a question the
|
|
264
|
+
* labels do not answer. Drawing to either one would assert a cabling
|
|
265
|
+
* fact nobody stated.
|
|
266
|
+
*/
|
|
267
|
+
if (parents.length > 1) {
|
|
268
|
+
return {
|
|
269
|
+
links: [],
|
|
270
|
+
parentMatchCount: parents.length,
|
|
271
|
+
childMatchCount: childMatchCount,
|
|
272
|
+
failure: "ambiguousParent",
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const parent: NormalizedDevice = parents[0]!;
|
|
277
|
+
|
|
278
|
+
// The parent is excluded even when it matches both sets: no self-links.
|
|
279
|
+
const children: Array<NormalizedDevice> = childCandidates.filter(
|
|
280
|
+
(entry: NormalizedDevice) => {
|
|
281
|
+
return entry.device.id !== parent.device.id;
|
|
282
|
+
},
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
if (children.length === 0) {
|
|
286
|
+
return {
|
|
287
|
+
links: [],
|
|
288
|
+
parentMatchCount: 1,
|
|
289
|
+
childMatchCount: childMatchCount,
|
|
290
|
+
failure: "noChildrenMatched",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
links: children.map((child: NormalizedDevice) => {
|
|
296
|
+
return { fromDeviceId: child.device.id, toDeviceId: parent.device.id };
|
|
297
|
+
}),
|
|
298
|
+
parentMatchCount: 1,
|
|
299
|
+
childMatchCount: childMatchCount,
|
|
300
|
+
failure: null,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
69
304
|
public static resolveRule(
|
|
70
305
|
rule: LinkRuleInput,
|
|
71
306
|
devices: Array<LinkRuleDeviceInput>,
|
|
@@ -89,6 +324,13 @@ export default class NetworkDeviceLinkRuleUtil {
|
|
|
89
324
|
};
|
|
90
325
|
};
|
|
91
326
|
|
|
327
|
+
/*
|
|
328
|
+
* The three checks below are properties of the rule's TEXT, true in every
|
|
329
|
+
* site, so they are settled before scope is even read. A disabled rule is
|
|
330
|
+
* not "disabled in Unit 7", and partitioning a rule with an empty parent
|
|
331
|
+
* label set would make every site in the project ambiguous at once — the
|
|
332
|
+
* worst noise outcome available.
|
|
333
|
+
*/
|
|
92
334
|
if (rule.isEnabled === false) {
|
|
93
335
|
return empty("disabled");
|
|
94
336
|
}
|
|
@@ -105,53 +347,233 @@ export default class NetworkDeviceLinkRuleUtil {
|
|
|
105
347
|
return empty("noParentLabels");
|
|
106
348
|
}
|
|
107
349
|
|
|
108
|
-
const
|
|
350
|
+
const normalized: Array<NormalizedDevice> = devices.map(
|
|
109
351
|
(device: LinkRuleDeviceInput) => {
|
|
110
|
-
return
|
|
111
|
-
device,
|
|
112
|
-
|
|
113
|
-
|
|
352
|
+
return {
|
|
353
|
+
device: device,
|
|
354
|
+
labelSet: new Set<string>(device.labelIds),
|
|
355
|
+
siteKey: (device.siteId || "").toString().trim(),
|
|
356
|
+
};
|
|
114
357
|
},
|
|
115
358
|
);
|
|
116
359
|
|
|
117
|
-
if (
|
|
360
|
+
if (NetworkDeviceLinkRuleScopeUtil.isSiteScoped(rule.scope)) {
|
|
361
|
+
return NetworkDeviceLinkRuleUtil.resolvePerSite(rule, normalized);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
return NetworkDeviceLinkRuleUtil.resolveProject(rule, normalized, empty);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private static resolveProject(
|
|
368
|
+
rule: LinkRuleInput,
|
|
369
|
+
normalized: Array<NormalizedDevice>,
|
|
370
|
+
empty: (
|
|
371
|
+
reason: LinkRuleSkipReason,
|
|
372
|
+
parents?: number,
|
|
373
|
+
children?: number,
|
|
374
|
+
) => LinkRuleOutcome,
|
|
375
|
+
): LinkRuleOutcome {
|
|
376
|
+
const result: LinkRuleGroupResult = NetworkDeviceLinkRuleUtil.resolveGroup(
|
|
377
|
+
rule,
|
|
378
|
+
normalized,
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
if (result.failure === "noParentMatched") {
|
|
118
382
|
return empty("noParentMatched", 0);
|
|
119
383
|
}
|
|
384
|
+
if (result.failure === "ambiguousParent") {
|
|
385
|
+
return empty("ambiguousParent", result.parentMatchCount);
|
|
386
|
+
}
|
|
387
|
+
if (result.failure === "noChildrenMatched") {
|
|
388
|
+
return empty("noChildrenMatched", 1);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
ruleId: rule.id,
|
|
393
|
+
ruleName: rule.name,
|
|
394
|
+
links: result.links,
|
|
395
|
+
matchedParentCount: 1,
|
|
396
|
+
matchedChildCount: result.links.length,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
120
399
|
|
|
400
|
+
private static resolvePerSite(
|
|
401
|
+
rule: LinkRuleInput,
|
|
402
|
+
normalized: Array<NormalizedDevice>,
|
|
403
|
+
): LinkRuleOutcome {
|
|
121
404
|
/*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
405
|
+
* Bucketed in ONE pass in first-appearance order. Never
|
|
406
|
+
* `devices.filter(...)` inside a loop over sites: that is O(devices x
|
|
407
|
+
* sites) per rule, and this endpoint runs at ten thousand devices.
|
|
125
408
|
*/
|
|
126
|
-
|
|
127
|
-
|
|
409
|
+
const buckets: Map<string, Array<NormalizedDevice>> = new Map<
|
|
410
|
+
string,
|
|
411
|
+
Array<NormalizedDevice>
|
|
412
|
+
>();
|
|
413
|
+
let unsitedChildDeviceCount: number = 0;
|
|
414
|
+
|
|
415
|
+
for (const entry of normalized) {
|
|
416
|
+
/*
|
|
417
|
+
* A device with no site is EXCLUDED, not pooled with the other unsited
|
|
418
|
+
* ones and not given a group of its own. Pooling would ask "exactly one
|
|
419
|
+
* parent among all the unsited devices", a question nobody posed, and
|
|
420
|
+
* would draw an edge between two boxes whose only relationship is that
|
|
421
|
+
* neither has been assigned a site yet. Counted instead, so the map can
|
|
422
|
+
* say how many the rule walked past.
|
|
423
|
+
*/
|
|
424
|
+
if (entry.siteKey === "") {
|
|
425
|
+
if (
|
|
426
|
+
NetworkDeviceLinkRuleUtil.matches(entry.labelSet, rule.childLabelIds)
|
|
427
|
+
) {
|
|
428
|
+
unsitedChildDeviceCount++;
|
|
429
|
+
}
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const bucket: Array<NormalizedDevice> | undefined = buckets.get(
|
|
434
|
+
entry.siteKey,
|
|
435
|
+
);
|
|
436
|
+
if (bucket) {
|
|
437
|
+
bucket.push(entry);
|
|
438
|
+
} else {
|
|
439
|
+
buckets.set(entry.siteKey, [entry]);
|
|
440
|
+
}
|
|
128
441
|
}
|
|
129
442
|
|
|
130
|
-
const
|
|
443
|
+
const links: Array<LinkRuleLink> = [];
|
|
444
|
+
const groupFailures: Array<LinkRuleGroupFailure> = [];
|
|
445
|
+
let applicableSiteCount: number = 0;
|
|
446
|
+
let matchedParentCount: number = 0;
|
|
131
447
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
return (
|
|
136
|
-
device.id !== parent.id &&
|
|
137
|
-
NetworkDeviceLinkRuleUtil.deviceMatches(device, rule.childLabelIds)
|
|
138
|
-
);
|
|
139
|
-
},
|
|
140
|
-
);
|
|
448
|
+
for (const [siteId, group] of buckets) {
|
|
449
|
+
const result: LinkRuleGroupResult =
|
|
450
|
+
NetworkDeviceLinkRuleUtil.resolveGroup(rule, group);
|
|
141
451
|
|
|
142
|
-
|
|
143
|
-
|
|
452
|
+
if (result.links.length > 0) {
|
|
453
|
+
applicableSiteCount++;
|
|
454
|
+
matchedParentCount += result.parentMatchCount;
|
|
455
|
+
links.push(...result.links);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/*
|
|
460
|
+
* A site speaks only when it holds devices this rule wanted to place.
|
|
461
|
+
* Sites with nothing to place are not misconfigured — the rule simply
|
|
462
|
+
* does not apply there, and a parent label like `SubCategory:Router` is
|
|
463
|
+
* present in nearly every site by construction. Without this gate, site
|
|
464
|
+
* scoping would trade one loud failure for two hundred quiet ones and
|
|
465
|
+
* the banner would be ignored inside a week.
|
|
466
|
+
*
|
|
467
|
+
* `noChildrenMatched` lands here too: the only child candidate was the
|
|
468
|
+
* parent itself, so after the self-link exclusion there is nothing to
|
|
469
|
+
* place and nothing to report.
|
|
470
|
+
*/
|
|
471
|
+
if (
|
|
472
|
+
result.childMatchCount === 0 ||
|
|
473
|
+
result.failure === null ||
|
|
474
|
+
result.failure === "noChildrenMatched"
|
|
475
|
+
) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
applicableSiteCount++;
|
|
480
|
+
matchedParentCount += result.parentMatchCount;
|
|
481
|
+
groupFailures.push({
|
|
482
|
+
siteId: siteId,
|
|
483
|
+
siteName: NetworkDeviceLinkRuleUtil.siteNameOf(group),
|
|
484
|
+
reason: result.failure,
|
|
485
|
+
matchedParentCount: result.parentMatchCount,
|
|
486
|
+
matchedChildCount: result.childMatchCount,
|
|
487
|
+
});
|
|
144
488
|
}
|
|
145
489
|
|
|
146
|
-
|
|
490
|
+
const outcome: LinkRuleOutcome = {
|
|
147
491
|
ruleId: rule.id,
|
|
148
492
|
ruleName: rule.name,
|
|
149
|
-
links:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
matchedChildCount: children.length,
|
|
493
|
+
links: links,
|
|
494
|
+
matchedParentCount: matchedParentCount,
|
|
495
|
+
matchedChildCount: links.length,
|
|
496
|
+
applicableSiteCount: applicableSiteCount,
|
|
154
497
|
};
|
|
498
|
+
|
|
499
|
+
/*
|
|
500
|
+
* Keeps `links.length === 0 <=> skipReason defined` true with no gaps, so
|
|
501
|
+
* describeOutcome can never fall through to "Draws 0 uplinks" on an empty
|
|
502
|
+
* outcome. The second arm covers both an empty device list and a rule
|
|
503
|
+
* whose labels match nothing anywhere.
|
|
504
|
+
*/
|
|
505
|
+
if (links.length === 0) {
|
|
506
|
+
outcome.skipReason =
|
|
507
|
+
groupFailures.length > 0 ? "noSiteResolved" : "noChildrenMatched";
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (groupFailures.length > 0) {
|
|
511
|
+
outcome.groupFailures = groupFailures;
|
|
512
|
+
}
|
|
513
|
+
if (unsitedChildDeviceCount > 0) {
|
|
514
|
+
outcome.unsitedChildDeviceCount = unsitedChildDeviceCount;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return outcome;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** First non-empty site name among a site's devices, if any of them carry one. */
|
|
521
|
+
private static siteNameOf(
|
|
522
|
+
group: Array<NormalizedDevice>,
|
|
523
|
+
): string | undefined {
|
|
524
|
+
for (const entry of group) {
|
|
525
|
+
if (entry.device.siteName) {
|
|
526
|
+
return entry.device.siteName;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
return undefined;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/*
|
|
533
|
+
* "Unit 4, Unit 9 and 2 more sites".
|
|
534
|
+
*
|
|
535
|
+
* Sorted before naming, and that is load-bearing rather than cosmetic: the
|
|
536
|
+
* live view refetches every sixty seconds and the device query has no ORDER
|
|
537
|
+
* BY, so an unsorted list would reshuffle the banner on every poll and read
|
|
538
|
+
* as a change that did not happen. The count stays exact even when the names
|
|
539
|
+
* are elided, so the operator always knows the true blast radius.
|
|
540
|
+
*/
|
|
541
|
+
private static describeSiteList(
|
|
542
|
+
failures: Array<LinkRuleGroupFailure>,
|
|
543
|
+
): string {
|
|
544
|
+
const sorted: Array<LinkRuleGroupFailure> = [...failures].sort(
|
|
545
|
+
(a: LinkRuleGroupFailure, b: LinkRuleGroupFailure) => {
|
|
546
|
+
return (
|
|
547
|
+
(a.siteName || "").localeCompare(b.siteName || "") ||
|
|
548
|
+
a.siteId.localeCompare(b.siteId)
|
|
549
|
+
);
|
|
550
|
+
},
|
|
551
|
+
);
|
|
552
|
+
|
|
553
|
+
const names: Array<string> = sorted
|
|
554
|
+
.slice(0, NetworkDeviceLinkRuleUtil.MAX_NAMED_SITES)
|
|
555
|
+
.map((failure: LinkRuleGroupFailure) => {
|
|
556
|
+
const name: string =
|
|
557
|
+
failure.siteName || NetworkDeviceLinkRuleUtil.UNNAMED_SITE;
|
|
558
|
+
return name.length > NetworkDeviceLinkRuleUtil.MAX_SITE_NAME_LENGTH
|
|
559
|
+
? `${name.slice(
|
|
560
|
+
0,
|
|
561
|
+
NetworkDeviceLinkRuleUtil.MAX_SITE_NAME_LENGTH - 1,
|
|
562
|
+
)}…`
|
|
563
|
+
: name;
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
const remaining: number = sorted.length - names.length;
|
|
567
|
+
|
|
568
|
+
if (remaining > 0) {
|
|
569
|
+
return `${names.join(", ")} and ${remaining} more site${
|
|
570
|
+
remaining === 1 ? "" : "s"
|
|
571
|
+
}`;
|
|
572
|
+
}
|
|
573
|
+
if (names.length === 1) {
|
|
574
|
+
return names[0]!;
|
|
575
|
+
}
|
|
576
|
+
return `${names.slice(0, -1).join(", ")} and ${names[names.length - 1]!}`;
|
|
155
577
|
}
|
|
156
578
|
|
|
157
579
|
public static resolveRules(
|
|
@@ -178,10 +600,138 @@ export default class NetworkDeviceLinkRuleUtil {
|
|
|
178
600
|
return `${outcome.matchedParentCount} devices carry the parent labels. Exactly one must, or there is no way to tell which is the uplink.`;
|
|
179
601
|
case "noChildrenMatched":
|
|
180
602
|
return "No device carries the child labels yet.";
|
|
603
|
+
case "noSiteResolved":
|
|
604
|
+
return "No site resolved this rule.";
|
|
181
605
|
default:
|
|
182
606
|
return `Draws ${outcome.matchedChildCount} uplink${
|
|
183
607
|
outcome.matchedChildCount === 1 ? "" : "s"
|
|
184
608
|
}.`;
|
|
185
609
|
}
|
|
186
610
|
}
|
|
611
|
+
|
|
612
|
+
/**
|
|
613
|
+
* The operator-facing warning, or null when the rule needs no explanation.
|
|
614
|
+
*
|
|
615
|
+
* Singular on purpose: ONE bullet per rule, never one per site. The banner's
|
|
616
|
+
* length is then bounded by the number of rules the operator wrote — a small
|
|
617
|
+
* number they control — rather than by the number of sites in their project.
|
|
618
|
+
*
|
|
619
|
+
* This is also the only sanctioned "did this rule fail" predicate. A caller
|
|
620
|
+
* must not go back to testing `outcome.links.length === 0`, which a rule
|
|
621
|
+
* drawing thirteen of fourteen sites passes while genuinely failing.
|
|
622
|
+
*/
|
|
623
|
+
public static getWarning(outcome: LinkRuleOutcome): LinkRuleWarning | null {
|
|
624
|
+
const failures: Array<LinkRuleGroupFailure> = outcome.groupFailures || [];
|
|
625
|
+
const unsited: number = outcome.unsitedChildDeviceCount || 0;
|
|
626
|
+
|
|
627
|
+
/*
|
|
628
|
+
* Project scope always lands here, and this branch is what the topology
|
|
629
|
+
* API used to compute inline: warn iff the rule drew nothing, using
|
|
630
|
+
* describeOutcome's own words. The `!skipReason` guard makes the old
|
|
631
|
+
* code's implicit assumption explicit rather than emitting a warning with
|
|
632
|
+
* an undefined reason.
|
|
633
|
+
*/
|
|
634
|
+
if (failures.length === 0 && unsited === 0) {
|
|
635
|
+
if (outcome.links.length > 0 || !outcome.skipReason) {
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
return {
|
|
639
|
+
ruleId: outcome.ruleId,
|
|
640
|
+
ruleName: outcome.ruleName,
|
|
641
|
+
reason: outcome.skipReason,
|
|
642
|
+
message: NetworkDeviceLinkRuleUtil.describeOutcome(outcome),
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const parts: Array<string> = [];
|
|
647
|
+
const applicableSiteCount: number = outcome.applicableSiteCount || 0;
|
|
648
|
+
|
|
649
|
+
if (outcome.links.length === 0) {
|
|
650
|
+
parts.push("No site resolved this rule.");
|
|
651
|
+
} else if (applicableSiteCount > 1) {
|
|
652
|
+
// A single applicable site needs no coverage fraction.
|
|
653
|
+
parts.push(
|
|
654
|
+
`Drawing in ${
|
|
655
|
+
applicableSiteCount - failures.length
|
|
656
|
+
} of ${applicableSiteCount} sites.`,
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const ambiguous: Array<LinkRuleGroupFailure> = failures.filter(
|
|
661
|
+
(failure: LinkRuleGroupFailure) => {
|
|
662
|
+
return failure.reason === "ambiguousParent";
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
const parentless: Array<LinkRuleGroupFailure> = failures.filter(
|
|
666
|
+
(failure: LinkRuleGroupFailure) => {
|
|
667
|
+
return failure.reason === "noParentMatched";
|
|
668
|
+
},
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
if (ambiguous.length === 1) {
|
|
672
|
+
/*
|
|
673
|
+
* The trailing sentence deliberately echoes the project-scope wording so
|
|
674
|
+
* an operator's vocabulary does not fork between the two scopes.
|
|
675
|
+
*/
|
|
676
|
+
parts.push(
|
|
677
|
+
`${
|
|
678
|
+
ambiguous[0]!.matchedParentCount
|
|
679
|
+
} devices carry the parent labels in ${NetworkDeviceLinkRuleUtil.describeSiteList(
|
|
680
|
+
ambiguous,
|
|
681
|
+
)}. Exactly one must, or there is no way to tell which is the uplink.`,
|
|
682
|
+
);
|
|
683
|
+
} else if (ambiguous.length > 1) {
|
|
684
|
+
parts.push(
|
|
685
|
+
`Several devices carry the parent labels in ${NetworkDeviceLinkRuleUtil.describeSiteList(
|
|
686
|
+
ambiguous,
|
|
687
|
+
)}. Exactly one per site must, or there is no way to tell which is the uplink.`,
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (parentless.length > 0) {
|
|
692
|
+
const stranded: number = parentless.reduce(
|
|
693
|
+
(sum: number, failure: LinkRuleGroupFailure) => {
|
|
694
|
+
return sum + failure.matchedChildCount;
|
|
695
|
+
},
|
|
696
|
+
0,
|
|
697
|
+
);
|
|
698
|
+
parts.push(
|
|
699
|
+
`No device carries the parent labels in ${NetworkDeviceLinkRuleUtil.describeSiteList(
|
|
700
|
+
parentless,
|
|
701
|
+
)}, so ${stranded} ${stranded === 1 ? "device" : "devices"} there ${
|
|
702
|
+
stranded === 1 ? "has" : "have"
|
|
703
|
+
} no uplink.`,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/*
|
|
708
|
+
* Always last and always its own sentence: the fix here is "assign these
|
|
709
|
+
* devices to a site", not "fix your labels", so they are never folded into
|
|
710
|
+
* a site-name list.
|
|
711
|
+
*/
|
|
712
|
+
if (unsited > 0) {
|
|
713
|
+
parts.push(
|
|
714
|
+
`${unsited} ${
|
|
715
|
+
unsited === 1 ? "device" : "devices"
|
|
716
|
+
} carrying the child labels ${
|
|
717
|
+
unsited === 1 ? "is" : "are"
|
|
718
|
+
} not assigned to a site, so this site-scoped rule skips ${
|
|
719
|
+
unsited === 1 ? "it" : "them"
|
|
720
|
+
}.`,
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return {
|
|
725
|
+
ruleId: outcome.ruleId,
|
|
726
|
+
ruleName: outcome.ruleName,
|
|
727
|
+
// Highest-ranked condition present. Ambiguity outranks a missing parent.
|
|
728
|
+
reason:
|
|
729
|
+
ambiguous.length > 0
|
|
730
|
+
? "ambiguousParent"
|
|
731
|
+
: parentless.length > 0
|
|
732
|
+
? "noParentMatched"
|
|
733
|
+
: "devicesWithoutSite",
|
|
734
|
+
message: parts.join(" "),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
187
737
|
}
|