@oneuptime/common 12.0.12 → 12.0.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.ts +32 -5
- package/Models/DatabaseModels/NetworkDevice.ts +64 -0
- package/Models/DatabaseModels/StatusPage.ts +59 -0
- package/Models/DatabaseModels/StatusPageOidc.ts +2 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +19 -2
- package/Server/API/AIChatAPI.ts +55 -56
- package/Server/API/CommonAPI.ts +89 -0
- package/Server/API/MicrosoftTeamsAPI.ts +20 -9
- package/Server/API/SlackAPI.ts +13 -10
- package/Server/API/StatusPageAPI.ts +2197 -2128
- package/Server/API/TelemetryAPI.ts +72 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.ts +30 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.ts +47 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Middleware/UserAuthorization.ts +11 -2
- package/Server/Services/ApiKeyPermissionService.ts +116 -2
- package/Server/Services/DatabaseService.ts +17 -6
- package/Server/Services/LogAggregationService.ts +34 -0
- package/Server/Services/LogService.ts +21 -0
- package/Server/Services/NetworkSiteService.ts +12 -0
- package/Server/Services/SpanService.ts +21 -0
- package/Server/Services/TraceAggregationService.ts +15 -0
- package/Server/Services/WorkspaceNotificationRuleService.ts +172 -220
- package/Server/Types/AnalyticsDatabase/ModelPermission.ts +9 -0
- package/Server/Types/Database/Permissions/AccessControlPermission.ts +47 -16
- package/Server/Utils/APIKey/AccessPermission.ts +16 -40
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +144 -0
- package/Server/Utils/LogRedaction.ts +576 -0
- package/Server/Utils/Logger.ts +123 -46
- package/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.ts +94 -23
- package/Server/Utils/Monitor/MonitorResource.ts +29 -9
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +28 -7
- package/Server/Utils/SessionReplay/SessionReplayGateCacheStore.ts +56 -10
- package/Server/Utils/StartServer.ts +30 -9
- package/Server/Utils/StatusPageSearchEngineIndexing.ts +33 -0
- package/Server/Utils/Telemetry/ResourceEntityFilter.ts +441 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +488 -46
- package/Tests/App/Dashboard/MonitorTypePicker.test.tsx +300 -0
- package/Tests/App/StatusPage/StatusPageIndexPageRobotsMeta.test.ts +130 -0
- package/Tests/Models/DatabaseModels/DatabaseBaseModelToJSONObjectCache.test.ts +180 -0
- package/Tests/Models/StatusPageEnableSearchEngineIndexing.test.ts +157 -0
- package/Tests/Server/API/AIChatCancelAndFeedback.test.ts +23 -1
- package/Tests/Server/API/AIChatRouteAuthorization.test.ts +785 -0
- package/Tests/Server/API/BaseAPIApiKeyAuth.test.ts +21 -8
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +290 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +7 -1
- package/Tests/Server/API/StatusPageOverviewCache.test.ts +474 -0
- package/Tests/Server/API/StatusPageSeoSearchEngineIndexing.test.ts +189 -0
- package/Tests/Server/Infrastructure/InMemoryTTLCache.test.ts +158 -0
- package/Tests/Server/Infrastructure/Postgres/DeviceRoleAndDeclaredLinkParentMigration.test.ts +9 -60
- package/Tests/Server/Middleware/ProjectAuthorizationApiKeyMiddleware.test.ts +24 -15
- package/Tests/Server/Middleware/UserAuthorization.test.ts +79 -0
- package/Tests/Server/Services/AddNetworkDeviceReachabilityColumnsMigration.test.ts +332 -0
- package/Tests/Server/Services/ApiKeyPermissionService.test.ts +347 -0
- package/Tests/Server/Services/DatabaseServiceUpdateDebugLogging.test.ts +282 -0
- package/Tests/Server/Services/NetworkSiteService.test.ts +56 -2
- package/Tests/Server/Services/TelemetryResourceFacetFilters.test.ts +267 -0
- package/Tests/Server/Services/WorkspaceNotificationRuleTestRuleChannels.test.ts +702 -0
- package/Tests/Server/Types/Database/Permissions/AccessControlPermission.test.ts +165 -0
- package/Tests/Server/Utils/APIKey/AccessPermission.test.ts +48 -36
- package/Tests/Server/Utils/AnalyticsDatabase/StatementGenerator.test.ts +213 -0
- package/Tests/Server/Utils/LogRedaction.test.ts +415 -0
- package/Tests/Server/Utils/LoggerCredentialLeak.test.ts +245 -0
- package/Tests/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.test.ts +374 -4
- package/Tests/Server/Utils/Monitor/MonitorResourceIngestedStepSelection.test.ts +455 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceReachabilityRoundTrip.test.ts +369 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +86 -8
- package/Tests/Server/Utils/PrivacyFilterUtil.test.ts +64 -0
- package/Tests/Server/Utils/SessionReplay/SessionReplayGateCacheStore.test.ts +207 -0
- package/Tests/Server/Utils/StartServerBodyVerify.test.ts +335 -0
- package/Tests/Server/Utils/StatusPageSearchEngineIndexing.test.ts +60 -0
- package/Tests/Server/Utils/Telemetry/ResourceEntityFilter.test.ts +435 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelSend.test.ts +607 -62
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsInstalledTeamIdSpace.test.ts +778 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsTeamInstalls.test.ts +44 -10
- package/Tests/Types/API/HostnameFromAuthority.test.ts +164 -0
- package/Tests/Types/Database/AccessControl/ColumnAccessControlCache.test.ts +344 -0
- package/Tests/Types/Database/DatabasePropertyFindOperator.test.ts +518 -0
- package/Tests/Types/Database/TableColumnMetadataCache.test.ts +286 -0
- package/Tests/Types/Monitor/MonitorStep.test.ts +100 -0
- package/Tests/Types/Monitor/MonitorTypeKeywords.test.ts +360 -0
- package/Tests/Types/StatusPage/SearchEngineIndexing.test.ts +86 -0
- package/Tests/Types/Telemetry/ResourceEntityFacet.test.ts +254 -0
- package/Tests/UI/Components/CardSelect.test.tsx +1021 -0
- package/Tests/UI/Components/MasterPage.test.tsx +36 -1
- package/Tests/Utils/Monitor/MonitorMetricType.test.ts +66 -1
- package/Tests/Utils/Monitor/NetworkTopologyUtil.test.ts +115 -6
- package/Tests/Utils/NetworkDevice/DeviceReachabilityUtil.test.ts +610 -0
- package/Tests/Utils/NetworkSite/SiteStatusRollupUtil.test.ts +114 -45
- package/Types/API/Hostname.ts +79 -0
- package/Types/Database/AccessControl/ColumnAccessControl.ts +33 -11
- package/Types/Database/AccessControl/ColumnBillingAccessControl.ts +33 -11
- package/Types/Database/DatabaseProperty.ts +38 -0
- package/Types/Database/TableColumn.ts +34 -7
- package/Types/Events/Recurring.ts +16 -1
- package/Types/Monitor/MonitorStep.ts +13 -1
- package/Types/Monitor/MonitorType.ts +345 -40
- package/Types/Monitor/SSLMonitor/SslMonitorResponse.ts +43 -0
- package/Types/StatusPage/SearchEngineIndexing.ts +61 -0
- package/Types/Telemetry/ResourceEntityFacet.ts +211 -0
- package/UI/Components/CardSelect/CardSelect.tsx +714 -105
- package/UI/Components/EmptyState/EmptyState.tsx +8 -1
- package/UI/Components/Footer/Footer.tsx +32 -6
- package/UI/Components/Forms/Fields/FormField.tsx +3 -0
- package/UI/Components/Forms/Types/Field.ts +8 -0
- package/UI/Components/Loader/PageLoader.tsx +29 -8
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +26 -4
- package/UI/Components/Markdown.tsx/LazyMarkdownViewer.tsx +17 -1
- package/UI/Components/MasterPage/MasterPage.tsx +24 -11
- package/Utils/Monitor/MonitorMetricType.ts +16 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +60 -8
- package/Utils/NetworkDevice/DeviceReachabilityUtil.ts +262 -0
- package/Utils/NetworkSite/SiteStatusRollupUtil.ts +41 -24
- package/Utils/Telemetry/CrossSignalScope.ts +75 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js +20 -4
- package/build/dist/Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +66 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +60 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js +2 -0
- package/build/dist/Models/DatabaseModels/StatusPageOidc.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/AIChatAPI.js +48 -35
- package/build/dist/Server/API/AIChatAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +59 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +20 -6
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +13 -10
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +745 -673
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +34 -3
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js +23 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787500000000-AddEnableSearchEngineIndexingToStatusPage.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1787600000000-AddNetworkDeviceReachabilityColumns.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/UserAuthorization.js +6 -2
- package/build/dist/Server/Middleware/UserAuthorization.js.map +1 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js +90 -1
- package/build/dist/Server/Services/ApiKeyPermissionService.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +15 -6
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +2 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogService.js +16 -0
- package/build/dist/Server/Services/LogService.js.map +1 -1
- package/build/dist/Server/Services/NetworkSiteService.js +12 -0
- package/build/dist/Server/Services/NetworkSiteService.js.map +1 -1
- package/build/dist/Server/Services/SpanService.js +16 -0
- package/build/dist/Server/Services/SpanService.js.map +1 -1
- package/build/dist/Server/Services/TraceAggregationService.js +2 -0
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +137 -164
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +9 -0
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js +21 -13
- package/build/dist/Server/Types/Database/Permissions/AccessControlPermission.js.map +1 -1
- package/build/dist/Server/Utils/APIKey/AccessPermission.js +9 -32
- package/build/dist/Server/Utils/APIKey/AccessPermission.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +87 -0
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Server/Utils/LogRedaction.js +449 -0
- package/build/dist/Server/Utils/LogRedaction.js.map +1 -0
- package/build/dist/Server/Utils/Logger.js +98 -45
- package/build/dist/Server/Utils/Logger.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js +65 -13
- package/build/dist/Server/Utils/Monitor/Criteria/SSLMonitorCriteria.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +20 -3
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +27 -7
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js +29 -10
- package/build/dist/Server/Utils/SessionReplay/SessionReplayGateCacheStore.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +10 -7
- package/build/dist/Server/Utils/StartServer.js.map +1 -1
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js +24 -0
- package/build/dist/Server/Utils/StatusPageSearchEngineIndexing.js.map +1 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js +311 -0
- package/build/dist/Server/Utils/Telemetry/ResourceEntityFilter.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +366 -37
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/API/Hostname.js +63 -0
- package/build/dist/Types/API/Hostname.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnAccessControl.js.map +1 -1
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js +20 -6
- package/build/dist/Types/Database/AccessControl/ColumnBillingAccessControl.js.map +1 -1
- package/build/dist/Types/Database/DatabaseProperty.js +38 -0
- package/build/dist/Types/Database/DatabaseProperty.js.map +1 -1
- package/build/dist/Types/Database/TableColumn.js +24 -6
- package/build/dist/Types/Database/TableColumn.js.map +1 -1
- package/build/dist/Types/Events/Recurring.js +13 -1
- package/build/dist/Types/Events/Recurring.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorStep.js +9 -1
- package/build/dist/Types/Monitor/MonitorStep.js.map +1 -1
- package/build/dist/Types/Monitor/MonitorType.js +330 -31
- package/build/dist/Types/Monitor/MonitorType.js.map +1 -1
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js +55 -0
- package/build/dist/Types/StatusPage/SearchEngineIndexing.js.map +1 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js +156 -0
- package/build/dist/Types/Telemetry/ResourceEntityFacet.js.map +1 -0
- package/build/dist/UI/Components/CardSelect/CardSelect.js +360 -40
- package/build/dist/UI/Components/CardSelect/CardSelect.js.map +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js +1 -1
- package/build/dist/UI/Components/EmptyState/EmptyState.js.map +1 -1
- package/build/dist/UI/Components/Footer/Footer.js +7 -4
- package/build/dist/UI/Components/Footer/Footer.js.map +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js +1 -1
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Loader/PageLoader.js +15 -5
- package/build/dist/UI/Components/Loader/PageLoader.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +16 -3
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js +13 -1
- package/build/dist/UI/Components/Markdown.tsx/LazyMarkdownViewer.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +10 -2
- package/build/dist/UI/Components/MasterPage/MasterPage.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js +13 -1
- package/build/dist/Utils/Monitor/MonitorMetricType.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +34 -7
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js +177 -0
- package/build/dist/Utils/NetworkDevice/DeviceReachabilityUtil.js.map +1 -0
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js +23 -27
- package/build/dist/Utils/NetworkSite/SiteStatusRollupUtil.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +44 -4
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import BadDataException from "../../../Types/Exception/BadDataException";
|
|
2
|
+
import MonitorType, {
|
|
3
|
+
MonitorTypeCategory,
|
|
4
|
+
MonitorTypeHelper,
|
|
5
|
+
MonitorTypeProps,
|
|
6
|
+
} from "../../../Types/Monitor/MonitorType";
|
|
7
|
+
import {
|
|
8
|
+
CardSelectOption,
|
|
9
|
+
cardSelectOptionMatchesSearch,
|
|
10
|
+
getCardSelectOptionSearchScore,
|
|
11
|
+
getCardSelectSearchTokens,
|
|
12
|
+
} from "../../../UI/Components/CardSelect/CardSelect";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* The monitor type picker offers 29 types. Whether someone finds the right one
|
|
16
|
+
* comes down to whether the words they already use - "k8s", "postgres",
|
|
17
|
+
* "heartbeat", "tls" - are attached to the card, because none of them appear
|
|
18
|
+
* in its title or its copy. These are the searches this catalog promises to
|
|
19
|
+
* answer.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const allProps: Array<MonitorTypeProps> =
|
|
23
|
+
MonitorTypeHelper.getAllMonitorTypeProps();
|
|
24
|
+
|
|
25
|
+
type PropsForFunction = (monitorType: MonitorType) => MonitorTypeProps;
|
|
26
|
+
|
|
27
|
+
const propsFor: PropsForFunction = (
|
|
28
|
+
monitorType: MonitorType,
|
|
29
|
+
): MonitorTypeProps => {
|
|
30
|
+
const found: MonitorTypeProps | undefined = allProps.find(
|
|
31
|
+
(item: MonitorTypeProps) => {
|
|
32
|
+
return item.monitorType === monitorType;
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (!found) {
|
|
37
|
+
throw new Error(`No props for ${monitorType}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return found;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ToCardOptionFunction = (monitorType: MonitorType) => CardSelectOption;
|
|
44
|
+
|
|
45
|
+
const toCardOption: ToCardOptionFunction = (
|
|
46
|
+
monitorType: MonitorType,
|
|
47
|
+
): CardSelectOption => {
|
|
48
|
+
const props: MonitorTypeProps = propsFor(monitorType);
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
value: props.monitorType,
|
|
52
|
+
title: props.title,
|
|
53
|
+
description: props.description,
|
|
54
|
+
icon: props.icon,
|
|
55
|
+
keywords: props.keywords,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* Runs a search over the whole catalog exactly the way the picker does, and
|
|
61
|
+
* returns the monitor types in the order they would be shown.
|
|
62
|
+
*/
|
|
63
|
+
type SearchCatalogFunction = (search: string) => Array<MonitorType>;
|
|
64
|
+
|
|
65
|
+
const searchCatalog: SearchCatalogFunction = (
|
|
66
|
+
search: string,
|
|
67
|
+
): Array<MonitorType> => {
|
|
68
|
+
const tokens: Array<string> = getCardSelectSearchTokens(search);
|
|
69
|
+
const categories: Array<MonitorTypeCategory> =
|
|
70
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
71
|
+
|
|
72
|
+
const results: Array<{ monitorType: MonitorType; score: number }> = [];
|
|
73
|
+
|
|
74
|
+
for (const category of categories) {
|
|
75
|
+
for (const monitorType of category.monitorTypes) {
|
|
76
|
+
const option: CardSelectOption = toCardOption(monitorType);
|
|
77
|
+
|
|
78
|
+
if (!cardSelectOptionMatchesSearch(option, tokens, category.label)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
results.push({
|
|
83
|
+
monitorType: monitorType,
|
|
84
|
+
score: getCardSelectOptionSearchScore(option, tokens, category.label),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return results
|
|
90
|
+
.sort(
|
|
91
|
+
(
|
|
92
|
+
a: { monitorType: MonitorType; score: number },
|
|
93
|
+
b: { monitorType: MonitorType; score: number },
|
|
94
|
+
) => {
|
|
95
|
+
const scoreDifference: number = b.score - a.score;
|
|
96
|
+
|
|
97
|
+
if (scoreDifference !== 0) {
|
|
98
|
+
return scoreDifference;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return propsFor(a.monitorType).title.localeCompare(
|
|
102
|
+
propsFor(b.monitorType).title,
|
|
103
|
+
);
|
|
104
|
+
},
|
|
105
|
+
)
|
|
106
|
+
.map((result: { monitorType: MonitorType; score: number }) => {
|
|
107
|
+
return result.monitorType;
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
describe("MonitorType keywords", () => {
|
|
112
|
+
describe("every monitor type carries usable keywords", () => {
|
|
113
|
+
test("no monitor type is left without any", () => {
|
|
114
|
+
for (const props of allProps) {
|
|
115
|
+
expect(Array.isArray(props.keywords)).toBe(true);
|
|
116
|
+
expect(props.keywords.length).toBeGreaterThan(0);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every enum member has props, so nothing can be searched for and missed", () => {
|
|
121
|
+
const covered: Array<MonitorType> = allProps.map(
|
|
122
|
+
(props: MonitorTypeProps) => {
|
|
123
|
+
return props.monitorType;
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
for (const monitorType of Object.values(MonitorType)) {
|
|
128
|
+
expect(covered).toContain(monitorType);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("keywords are lower case, so a search never misses on case alone", () => {
|
|
133
|
+
for (const props of allProps) {
|
|
134
|
+
for (const keyword of props.keywords) {
|
|
135
|
+
expect(keyword).toBe(keyword.toLowerCase());
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("keywords are trimmed and non-empty", () => {
|
|
141
|
+
for (const props of allProps) {
|
|
142
|
+
for (const keyword of props.keywords) {
|
|
143
|
+
expect(keyword).toBe(keyword.trim());
|
|
144
|
+
expect(keyword.length).toBeGreaterThan(0);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("a monitor type does not repeat the same keyword", () => {
|
|
150
|
+
for (const props of allProps) {
|
|
151
|
+
expect(new Set(props.keywords).size).toBe(props.keywords.length);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
/*
|
|
156
|
+
* A keyword that only repeats the title earns nothing - the title is
|
|
157
|
+
* already searched. Every type must add at least one word the card does
|
|
158
|
+
* not already say.
|
|
159
|
+
*/
|
|
160
|
+
test("each type adds at least one word its title does not already contain", () => {
|
|
161
|
+
for (const props of allProps) {
|
|
162
|
+
const title: string = props.title.toLowerCase();
|
|
163
|
+
|
|
164
|
+
const addsSomething: boolean = props.keywords.some(
|
|
165
|
+
(keyword: string) => {
|
|
166
|
+
return !title.includes(keyword);
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
expect({
|
|
171
|
+
monitorType: props.monitorType,
|
|
172
|
+
addsSomething: addsSomething,
|
|
173
|
+
}).toEqual({ monitorType: props.monitorType, addsSomething: true });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("getKeywords", () => {
|
|
179
|
+
test("returns the configured keywords", () => {
|
|
180
|
+
expect(MonitorTypeHelper.getKeywords(MonitorType.Kubernetes)).toContain(
|
|
181
|
+
"k8s",
|
|
182
|
+
);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("throws BadDataException for a type without props", () => {
|
|
186
|
+
expect(() => {
|
|
187
|
+
MonitorTypeHelper.getKeywords("NonExistent" as MonitorType);
|
|
188
|
+
}).toThrowError(BadDataException);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("card copy is scannable", () => {
|
|
193
|
+
/*
|
|
194
|
+
* Every description used to open with "This monitor type lets you
|
|
195
|
+
* monitor", so the first five words of all 29 cards were identical and
|
|
196
|
+
* carried nothing. Reading the grid meant skipping them 29 times.
|
|
197
|
+
*/
|
|
198
|
+
test("no description opens with the old boilerplate", () => {
|
|
199
|
+
for (const props of allProps) {
|
|
200
|
+
expect({
|
|
201
|
+
monitorType: props.monitorType,
|
|
202
|
+
startsWithBoilerplate: props.description
|
|
203
|
+
.toLowerCase()
|
|
204
|
+
.startsWith("this monitor type"),
|
|
205
|
+
}).toEqual({
|
|
206
|
+
monitorType: props.monitorType,
|
|
207
|
+
startsWithBoilerplate: false,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("descriptions stay short enough to keep the cards an even height", () => {
|
|
213
|
+
for (const props of allProps) {
|
|
214
|
+
expect({
|
|
215
|
+
monitorType: props.monitorType,
|
|
216
|
+
tooLong: props.description.length > 100,
|
|
217
|
+
}).toEqual({ monitorType: props.monitorType, tooLong: false });
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("descriptions are still there and still say something", () => {
|
|
222
|
+
for (const props of allProps) {
|
|
223
|
+
expect(props.description.trim().length).toBeGreaterThan(20);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe("the searches the catalog promises to answer", () => {
|
|
229
|
+
test.each([
|
|
230
|
+
["k8s", MonitorType.Kubernetes],
|
|
231
|
+
["kubernetes", MonitorType.Kubernetes],
|
|
232
|
+
["eks", MonitorType.Kubernetes],
|
|
233
|
+
["postgres", MonitorType.SQLQuery],
|
|
234
|
+
["postgresql", MonitorType.SQLQuery],
|
|
235
|
+
["mysql", MonitorType.SQLQuery],
|
|
236
|
+
["database", MonitorType.SQLQuery],
|
|
237
|
+
["heartbeat", MonitorType.IncomingRequest],
|
|
238
|
+
["cron", MonitorType.IncomingRequest],
|
|
239
|
+
["webhook", MonitorType.IncomingRequest],
|
|
240
|
+
["tls", MonitorType.SSLCertificate],
|
|
241
|
+
["cert", MonitorType.SSLCertificate],
|
|
242
|
+
["x509", MonitorType.SSLCertificate],
|
|
243
|
+
["url", MonitorType.Website],
|
|
244
|
+
["icmp", MonitorType.Ping],
|
|
245
|
+
["tcp", MonitorType.Port],
|
|
246
|
+
["udp", MonitorType.Port],
|
|
247
|
+
["ipv6", MonitorType.IP],
|
|
248
|
+
["whois", MonitorType.Domain],
|
|
249
|
+
["aws", MonitorType.ExternalStatusPage],
|
|
250
|
+
["cloudflare", MonitorType.ExternalStatusPage],
|
|
251
|
+
["cname", MonitorType.DNS],
|
|
252
|
+
["dnskey", MonitorType.DNSSEC],
|
|
253
|
+
["snmp", MonitorType.NetworkDevice],
|
|
254
|
+
["router", MonitorType.NetworkDevice],
|
|
255
|
+
["prometheus", MonitorType.Metrics],
|
|
256
|
+
["apm", MonitorType.Traces],
|
|
257
|
+
["stack trace", MonitorType.Exceptions],
|
|
258
|
+
["playwright", MonitorType.SyntheticMonitor],
|
|
259
|
+
["javascript", MonitorType.CustomJavaScriptCode],
|
|
260
|
+
["rootless", MonitorType.Podman],
|
|
261
|
+
["lxc", MonitorType.Proxmox],
|
|
262
|
+
["osd", MonitorType.Ceph],
|
|
263
|
+
["sensor", MonitorType.IoTDevice],
|
|
264
|
+
["imap", MonitorType.IncomingEmail],
|
|
265
|
+
])(
|
|
266
|
+
"ranks %s first, and it is %s",
|
|
267
|
+
(search: string, expected: MonitorType) => {
|
|
268
|
+
const results: Array<MonitorType> = searchCatalog(search);
|
|
269
|
+
|
|
270
|
+
expect(results.length).toBeGreaterThan(0);
|
|
271
|
+
expect(results[0]).toBe(expected);
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
test("a plain title search still wins over anything that merely mentions it", () => {
|
|
276
|
+
expect(searchCatalog("ping")[0]).toBe(MonitorType.Ping);
|
|
277
|
+
expect(searchCatalog("domain")[0]).toBe(MonitorType.Domain);
|
|
278
|
+
expect(searchCatalog("logs")[0]).toBe(MonitorType.Logs);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("words describing the job, in any order, find the type", () => {
|
|
282
|
+
expect(searchCatalog("expiry certificate")).toContain(
|
|
283
|
+
MonitorType.SSLCertificate,
|
|
284
|
+
);
|
|
285
|
+
expect(searchCatalog("certificate expiry")).toContain(
|
|
286
|
+
MonitorType.SSLCertificate,
|
|
287
|
+
);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("a category name gathers everything under it", () => {
|
|
291
|
+
const results: Array<MonitorType> = searchCatalog("infrastructure");
|
|
292
|
+
|
|
293
|
+
expect(results).toContain(MonitorType.Kubernetes);
|
|
294
|
+
expect(results).toContain(MonitorType.Docker);
|
|
295
|
+
expect(results).toContain(MonitorType.Ceph);
|
|
296
|
+
expect(results).not.toContain(MonitorType.Website);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
test("a word nothing uses returns nothing rather than everything", () => {
|
|
300
|
+
expect(searchCatalog("mainframe")).toEqual([]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("an empty search returns the whole catalog", () => {
|
|
304
|
+
const categories: Array<MonitorTypeCategory> =
|
|
305
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
306
|
+
|
|
307
|
+
const total: number = categories.reduce(
|
|
308
|
+
(count: number, category: MonitorTypeCategory) => {
|
|
309
|
+
return count + category.monitorTypes.length;
|
|
310
|
+
},
|
|
311
|
+
0,
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
expect(searchCatalog("").length).toBe(total);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe("the catalog the picker actually offers", () => {
|
|
319
|
+
test("every categorised type has props to render", () => {
|
|
320
|
+
const categories: Array<MonitorTypeCategory> =
|
|
321
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
322
|
+
|
|
323
|
+
for (const category of categories) {
|
|
324
|
+
for (const monitorType of category.monitorTypes) {
|
|
325
|
+
expect(() => {
|
|
326
|
+
return propsFor(monitorType);
|
|
327
|
+
}).not.toThrow();
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("no monitor type is listed under two categories, which would render it twice", () => {
|
|
333
|
+
const categories: Array<MonitorTypeCategory> =
|
|
334
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
335
|
+
|
|
336
|
+
const seen: Array<MonitorType> = [];
|
|
337
|
+
|
|
338
|
+
for (const category of categories) {
|
|
339
|
+
for (const monitorType of category.monitorTypes) {
|
|
340
|
+
expect(seen).not.toContain(monitorType);
|
|
341
|
+
seen.push(monitorType);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
/*
|
|
347
|
+
* The first category is the one the picker leaves open, so it has to be
|
|
348
|
+
* the one most people want. Everything else starts closed behind a count.
|
|
349
|
+
*/
|
|
350
|
+
test("the category shown open by default is the everyday one", () => {
|
|
351
|
+
const categories: Array<MonitorTypeCategory> =
|
|
352
|
+
MonitorTypeHelper.getMonitorTypeCategories();
|
|
353
|
+
|
|
354
|
+
expect(categories[0]?.label).toBe("Basic Monitoring");
|
|
355
|
+
expect(categories[0]?.monitorTypes).toContain(MonitorType.Website);
|
|
356
|
+
expect(categories[0]?.monitorTypes).toContain(MonitorType.API);
|
|
357
|
+
expect(categories[0]?.monitorTypes).toContain(MonitorType.Ping);
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place that decides "is this status page indexable?".
|
|
3
|
+
*
|
|
4
|
+
* Every renderer feeds this the raw flag off the /seo API response, and the
|
|
5
|
+
* answer decides whether a <meta name="robots"> and an X-Robots-Tag go out.
|
|
6
|
+
* The interesting half is what it does with values that are NOT an explicit
|
|
7
|
+
* off - a missing field, a null, an API that did not answer - because that is
|
|
8
|
+
* the state the internal HTTP call lands in whenever anything goes wrong, and
|
|
9
|
+
* it must resolve to "indexable" rather than silently de-indexing pages whose
|
|
10
|
+
* owners never asked for it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
NOINDEX_ROBOTS_DIRECTIVE,
|
|
15
|
+
SEARCH_ENGINE_INDEXING_FLAG_NAME,
|
|
16
|
+
X_ROBOTS_TAG_HEADER_NAME,
|
|
17
|
+
isSearchEngineIndexingEnabled,
|
|
18
|
+
} from "../../../Types/StatusPage/SearchEngineIndexing";
|
|
19
|
+
import { describe, expect, test } from "@jest/globals";
|
|
20
|
+
|
|
21
|
+
describe("isSearchEngineIndexingEnabled", () => {
|
|
22
|
+
test("the owner turning the toggle off is the only thing that blocks indexing", () => {
|
|
23
|
+
expect(isSearchEngineIndexingEnabled(false)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("the toggle left on indexes", () => {
|
|
27
|
+
expect(isSearchEngineIndexingEnabled(true)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("a JSON round-trip that stringified the boolean still reads as off", () => {
|
|
31
|
+
/*
|
|
32
|
+
* The flag crosses an HTTP boundary between the API and the two frontend
|
|
33
|
+
* servers. A serializer that renders booleans as strings must not turn an
|
|
34
|
+
* opt-out back into an opt-in.
|
|
35
|
+
*/
|
|
36
|
+
expect(isSearchEngineIndexingEnabled("false")).toBe(false);
|
|
37
|
+
expect(isSearchEngineIndexingEnabled("true")).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe("anything that is not an explicit off leaves the page indexable", () => {
|
|
41
|
+
/*
|
|
42
|
+
* These are the shapes an unreachable or older /seo API produces. Reading
|
|
43
|
+
* any of them as "noindex" would take a page out of Google over a blip on
|
|
44
|
+
* an internal HTTP call - a failure the owner never asked for and would
|
|
45
|
+
* have no way to see.
|
|
46
|
+
*/
|
|
47
|
+
const notAnExplicitOff: Array<[string, unknown]> = [
|
|
48
|
+
["field absent from the response", undefined],
|
|
49
|
+
["field present but null", null],
|
|
50
|
+
["empty string", ""],
|
|
51
|
+
["zero", 0],
|
|
52
|
+
["the string 'no'", "no"],
|
|
53
|
+
["the string 'FALSE' in the wrong case", "FALSE"],
|
|
54
|
+
["an object", {}],
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
test.each(notAnExplicitOff)("%s", (_label: string, value: unknown) => {
|
|
58
|
+
expect(isSearchEngineIndexingEnabled(value)).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("the directives that go on the wire", () => {
|
|
64
|
+
/*
|
|
65
|
+
* Pinned strings. Crawlers parse these literally, so a typo here is a
|
|
66
|
+
* feature that silently does nothing - the page keeps being indexed and
|
|
67
|
+
* nothing errors.
|
|
68
|
+
*/
|
|
69
|
+
test("the robots directive is noindex, nofollow", () => {
|
|
70
|
+
expect(NOINDEX_ROBOTS_DIRECTIVE).toBe("noindex, nofollow");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("the response header is X-Robots-Tag", () => {
|
|
74
|
+
expect(X_ROBOTS_TAG_HEADER_NAME).toBe("X-Robots-Tag");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("the API/template flag name matches the model column name", () => {
|
|
78
|
+
/*
|
|
79
|
+
* The /seo API keys its response off this constant, and index.ejs reads a
|
|
80
|
+
* variable of the same name. Renaming the column without renaming this
|
|
81
|
+
* would leave the template reading undefined - which reads as indexable,
|
|
82
|
+
* so the toggle would just stop working, quietly.
|
|
83
|
+
*/
|
|
84
|
+
expect(SEARCH_ENGINE_INDEXING_FLAG_NAME).toBe("enableSearchEngineIndexing");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { describe, expect, test } from "@jest/globals";
|
|
2
|
+
import {
|
|
3
|
+
ALL_RESOURCE_FACET_KEYS,
|
|
4
|
+
MAX_RESOURCE_IDS_PER_FACET,
|
|
5
|
+
RESOURCE_ENTITY_FACET_KEYS,
|
|
6
|
+
ResourceEntityFacetSelections,
|
|
7
|
+
SERVICE_FACET_KEYS,
|
|
8
|
+
collectResourceEntityFacetSelections,
|
|
9
|
+
collectServiceFacetSelections,
|
|
10
|
+
hasResourceEntityFacetSelections,
|
|
11
|
+
isResourceEntityFacetKey,
|
|
12
|
+
isResourceFacetKey,
|
|
13
|
+
isServiceFacetKey,
|
|
14
|
+
parseResourceEntityFacetSelections,
|
|
15
|
+
} from "../../../Types/Telemetry/ResourceEntityFacet";
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* The split this module encodes is the whole fix for issue #3216: the
|
|
19
|
+
* Services facet reads out of `primaryEntityId`, every other resource facet
|
|
20
|
+
* has to be resolved through the resource's entity key. Getting a key into
|
|
21
|
+
* the wrong bucket silently reintroduces the bug (a cluster id compared
|
|
22
|
+
* against a column that only holds Service ids), so the classification is
|
|
23
|
+
* pinned here rather than left to the call sites.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const CLUSTER_ID: string = "8c0f2f1e-2e4f-4a8c-9a1a-2f5b6c7d8e9f";
|
|
27
|
+
const OTHER_CLUSTER_ID: string = "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9";
|
|
28
|
+
const HOST_ID: string = "5f4e3d2c-1b0a-4998-8776-655443322110";
|
|
29
|
+
const SERVICE_ID: string = "0d1c2b3a-4958-4867-9776-8574a3b2c1d0";
|
|
30
|
+
|
|
31
|
+
function facetMap(
|
|
32
|
+
entries: Record<string, Array<string>>,
|
|
33
|
+
): Map<string, Set<string>> {
|
|
34
|
+
const map: Map<string, Set<string>> = new Map();
|
|
35
|
+
|
|
36
|
+
for (const [key, values] of Object.entries(entries)) {
|
|
37
|
+
map.set(key, new Set(values));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return map;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe("ResourceEntityFacet", () => {
|
|
44
|
+
describe("facet classification", () => {
|
|
45
|
+
test("the Services facet and its pre-rename alias read out of primaryEntityId", () => {
|
|
46
|
+
expect(isServiceFacetKey("primaryEntityId")).toBe(true);
|
|
47
|
+
expect(isServiceFacetKey("serviceId")).toBe(true);
|
|
48
|
+
expect(isResourceEntityFacetKey("primaryEntityId")).toBe(false);
|
|
49
|
+
expect(isResourceEntityFacetKey("serviceId")).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("every non-Service resource facet needs entity-key resolution", () => {
|
|
53
|
+
for (const facetKey of [
|
|
54
|
+
"hostId",
|
|
55
|
+
"dockerHostId",
|
|
56
|
+
"podmanHostId",
|
|
57
|
+
"kubernetesClusterId",
|
|
58
|
+
]) {
|
|
59
|
+
expect(isResourceEntityFacetKey(facetKey)).toBe(true);
|
|
60
|
+
expect(isServiceFacetKey(facetKey)).toBe(false);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("non-resource facets belong to neither group", () => {
|
|
65
|
+
for (const facetKey of [
|
|
66
|
+
"severityText",
|
|
67
|
+
"traceId",
|
|
68
|
+
"spanId",
|
|
69
|
+
"attributes.k8s.cluster.name",
|
|
70
|
+
"",
|
|
71
|
+
]) {
|
|
72
|
+
expect(isResourceFacetKey(facetKey)).toBe(false);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("the combined list is exactly the two groups, with no overlap", () => {
|
|
77
|
+
expect(ALL_RESOURCE_FACET_KEYS).toEqual([
|
|
78
|
+
...SERVICE_FACET_KEYS,
|
|
79
|
+
...RESOURCE_ENTITY_FACET_KEYS,
|
|
80
|
+
]);
|
|
81
|
+
expect(new Set(ALL_RESOURCE_FACET_KEYS).size).toBe(
|
|
82
|
+
ALL_RESOURCE_FACET_KEYS.length,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
describe("collectResourceEntityFacetSelections", () => {
|
|
88
|
+
test("keeps each facet's ids under its own key", () => {
|
|
89
|
+
const selections: ResourceEntityFacetSelections =
|
|
90
|
+
collectResourceEntityFacetSelections(
|
|
91
|
+
facetMap({
|
|
92
|
+
kubernetesClusterId: [CLUSTER_ID],
|
|
93
|
+
hostId: [HOST_ID],
|
|
94
|
+
}).entries(),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
expect(selections).toEqual({
|
|
98
|
+
kubernetesClusterId: [CLUSTER_ID],
|
|
99
|
+
hostId: [HOST_ID],
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("leaves Service selections out — they are not entity-key resolved", () => {
|
|
104
|
+
const selections: ResourceEntityFacetSelections =
|
|
105
|
+
collectResourceEntityFacetSelections(
|
|
106
|
+
facetMap({
|
|
107
|
+
primaryEntityId: [SERVICE_ID],
|
|
108
|
+
serviceId: [SERVICE_ID],
|
|
109
|
+
severityText: ["Error"],
|
|
110
|
+
}).entries(),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
expect(selections).toEqual({});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("accepts a plain record via Object.entries (the traces explorer's shape)", () => {
|
|
117
|
+
const selections: ResourceEntityFacetSelections =
|
|
118
|
+
collectResourceEntityFacetSelections(
|
|
119
|
+
Object.entries({
|
|
120
|
+
kubernetesClusterId: [CLUSTER_ID, OTHER_CLUSTER_ID],
|
|
121
|
+
}),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
expect(selections["kubernetesClusterId"]).toEqual([
|
|
125
|
+
CLUSTER_ID,
|
|
126
|
+
OTHER_CLUSTER_ID,
|
|
127
|
+
]);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("de-duplicates repeated ids within one facet", () => {
|
|
131
|
+
const selections: ResourceEntityFacetSelections =
|
|
132
|
+
collectResourceEntityFacetSelections(
|
|
133
|
+
Object.entries({
|
|
134
|
+
kubernetesClusterId: [CLUSTER_ID, CLUSTER_ID],
|
|
135
|
+
}),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
expect(selections["kubernetesClusterId"]).toEqual([CLUSTER_ID]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("drops a facet whose last chip was just removed rather than emitting []", () => {
|
|
142
|
+
const selections: ResourceEntityFacetSelections =
|
|
143
|
+
collectResourceEntityFacetSelections(
|
|
144
|
+
facetMap({ kubernetesClusterId: [] }).entries(),
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
expect(selections).toEqual({});
|
|
148
|
+
expect(hasResourceEntityFacetSelections(selections)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("skips blank values", () => {
|
|
152
|
+
const selections: ResourceEntityFacetSelections =
|
|
153
|
+
collectResourceEntityFacetSelections(
|
|
154
|
+
Object.entries({ hostId: ["", HOST_ID] }),
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(selections["hostId"]).toEqual([HOST_ID]);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
describe("collectServiceFacetSelections", () => {
|
|
162
|
+
test("unions the canonical key and its alias", () => {
|
|
163
|
+
expect(
|
|
164
|
+
collectServiceFacetSelections(
|
|
165
|
+
facetMap({
|
|
166
|
+
primaryEntityId: [SERVICE_ID],
|
|
167
|
+
serviceId: [SERVICE_ID, HOST_ID],
|
|
168
|
+
}).entries(),
|
|
169
|
+
),
|
|
170
|
+
).toEqual([SERVICE_ID, HOST_ID]);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("ignores non-Service resource facets", () => {
|
|
174
|
+
expect(
|
|
175
|
+
collectServiceFacetSelections(
|
|
176
|
+
facetMap({ kubernetesClusterId: [CLUSTER_ID] }).entries(),
|
|
177
|
+
),
|
|
178
|
+
).toEqual([]);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
describe("parseResourceEntityFacetSelections", () => {
|
|
183
|
+
test("accepts a well-formed selection map", () => {
|
|
184
|
+
expect(
|
|
185
|
+
parseResourceEntityFacetSelections({
|
|
186
|
+
kubernetesClusterId: [CLUSTER_ID],
|
|
187
|
+
}),
|
|
188
|
+
).toEqual({ kubernetesClusterId: [CLUSTER_ID] });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("drops unknown facet keys", () => {
|
|
192
|
+
expect(
|
|
193
|
+
parseResourceEntityFacetSelections({
|
|
194
|
+
kubernetesClusterId: [CLUSTER_ID],
|
|
195
|
+
primaryEntityId: [SERVICE_ID],
|
|
196
|
+
somethingElse: [CLUSTER_ID],
|
|
197
|
+
}),
|
|
198
|
+
).toEqual({ kubernetesClusterId: [CLUSTER_ID] });
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("drops ids that are not ObjectID strings", () => {
|
|
202
|
+
expect(
|
|
203
|
+
parseResourceEntityFacetSelections({
|
|
204
|
+
kubernetesClusterId: [CLUSTER_ID, "not-an-id", "'; DROP TABLE --"],
|
|
205
|
+
}),
|
|
206
|
+
).toEqual({ kubernetesClusterId: [CLUSTER_ID] });
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("a facet left with no valid id is dropped entirely", () => {
|
|
210
|
+
expect(parseResourceEntityFacetSelections({ hostId: ["nope"] })).toEqual(
|
|
211
|
+
{},
|
|
212
|
+
);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("non-array values carry no filter", () => {
|
|
216
|
+
expect(
|
|
217
|
+
parseResourceEntityFacetSelections({
|
|
218
|
+
kubernetesClusterId: CLUSTER_ID,
|
|
219
|
+
}),
|
|
220
|
+
).toEqual({});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("junk input parses to no constraint rather than throwing", () => {
|
|
224
|
+
for (const junk of [undefined, null, "", 0, [], "string", true]) {
|
|
225
|
+
expect(parseResourceEntityFacetSelections(junk)).toEqual({});
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("caps the id list so a crafted request cannot send an unbounded IN", () => {
|
|
230
|
+
const many: Array<string> = Array.from(
|
|
231
|
+
{ length: MAX_RESOURCE_IDS_PER_FACET + 25 },
|
|
232
|
+
(_unused: unknown, index: number): string => {
|
|
233
|
+
return `00000000-0000-4000-8000-${String(index).padStart(12, "0")}`;
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const parsed: ResourceEntityFacetSelections =
|
|
238
|
+
parseResourceEntityFacetSelections({ kubernetesClusterId: many });
|
|
239
|
+
|
|
240
|
+
expect(parsed["kubernetesClusterId"]).toHaveLength(
|
|
241
|
+
MAX_RESOURCE_IDS_PER_FACET,
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("de-duplicates before capping", () => {
|
|
246
|
+
const parsed: ResourceEntityFacetSelections =
|
|
247
|
+
parseResourceEntityFacetSelections({
|
|
248
|
+
hostId: [HOST_ID, HOST_ID, HOST_ID],
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(parsed["hostId"]).toEqual([HOST_ID]);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
});
|