@oneuptime/common 14.0.0 → 14.0.1
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/Index.ts +4 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveFeed.ts +2 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.ts +522 -0
- package/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.ts +38 -4
- package/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.ts +603 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.ts +248 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.ts +19 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/DatabaseService.ts +10 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/MonitorService.ts +3 -2
- package/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.ts +385 -0
- package/Server/Services/ServiceLevelObjectiveLabelRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.ts +92 -16
- package/Server/Services/ServiceLevelObjectiveMonitorRuleService.ts +36 -1
- package/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.ts +521 -0
- package/Server/Services/ServiceLevelObjectiveOwnerRuleService.ts +51 -0
- package/Server/Services/ServiceLevelObjectiveService.ts +28 -0
- package/Server/Types/Workflow/Components/Telegram/SendMessageToChat.ts +27 -1
- package/Server/Utils/AI/Toolbox/SloTools.ts +1 -0
- package/Server/Utils/Monitor/AffectedResourceList.ts +31 -73
- package/Server/Utils/Monitor/Criteria/CompareCriteria.ts +1 -1
- package/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.ts +1 -1
- package/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts +181 -136
- package/Server/Utils/Monitor/RootCauseList.ts +139 -0
- package/Server/Utils/Rules/RuleRun/RuleRunRegistry.ts +28 -0
- package/Server/Utils/Slo/SloRulePatternValidator.ts +42 -0
- package/Tests/App/Dashboard/FeedFilterSortIntegration.test.tsx +1632 -0
- package/Tests/App/Dashboard/FeedOptionsLocales.test.ts +250 -0
- package/Tests/App/Dashboard/FeedOrdering.test.ts +279 -9
- package/Tests/App/Dashboard/InvestigationFeedRefresh.test.tsx +60 -1
- package/Tests/App/Dashboard/InvestigationPanel.test.tsx +594 -3
- package/Tests/App/Dashboard/ProductOrientation.test.tsx +646 -0
- package/Tests/App/Dashboard/ResourceFeed.test.tsx +612 -2
- package/Tests/App/Dashboard/ResourceFeedPageWiring.test.ts +30 -0
- package/Tests/App/Dashboard/ResourceFeedPagesFilterSort.test.tsx +1202 -0
- package/Tests/App/Dashboard/ScheduledMaintenanceFeedRefresh.test.tsx +52 -1
- package/Tests/App/Dashboard/SloFeed.test.tsx +106 -1
- package/Tests/App/Dashboard/SloMonitorRuleCriteria.test.tsx +566 -0
- package/Tests/App/Dashboard/SloNavigationWiring.test.tsx +6 -4
- package/Tests/App/Dashboard/SloNoticeBanner.test.tsx +186 -0
- package/Tests/App/Dashboard/SloOverviewKpiStrip.test.tsx +388 -44
- package/Tests/App/Dashboard/SloOverviewLayout.test.tsx +613 -0
- package/Tests/Models/DatabaseModels/ResourceFeedModels.test.ts +0 -8
- package/Tests/Models/DatabaseModels/SloLabelAndOwnerRuleModels.test.ts +404 -0
- package/Tests/Models/DatabaseModels/SloOverhaulModels.test.ts +17 -1
- package/Tests/Models/DatabaseModels/StaticRuleCriteriaInheritance.test.ts +4 -2
- package/Tests/Server/Infrastructure/Postgres/AddConfigurableRuleCriteriaMigration.test.ts +3 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloLabelAndOwnerRulesMigration.test.ts +407 -0
- package/Tests/Server/Infrastructure/Postgres/AddSloMonitorRuleMonitorTypeMigration.test.ts +171 -0
- package/Tests/Server/Infrastructure/Postgres/SloProductOverhaulMigration.test.ts +88 -4
- package/Tests/Server/Services/DatabaseServiceRuleCriteriaValidation.test.ts +104 -0
- package/Tests/Server/Services/RuleRun/LabelRuleEnginesGroupBRun.test.ts +48 -1
- package/Tests/Server/Services/RuleRun/OwnerRuleEnginesGroupBRun.test.ts +30 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleEngines.test.ts +1170 -0
- package/Tests/Server/Services/ServiceLevelObjectiveLabelAndOwnerRuleServices.test.ts +172 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.test.ts +323 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleService.test.ts +115 -0
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorRuleTriggers.test.ts +21 -2
- package/Tests/Server/Services/ServiceLevelObjectiveMonitorTypePostgres.test.ts +480 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +162 -0
- package/Tests/Server/Services/StaticRuleCriteriaRuntimeCoverage.test.ts +45 -6
- package/Tests/Server/Types/Database/FeedEventTypeFilterQuery.test.ts +745 -0
- package/Tests/Server/Types/Markdown.test.ts +2 -1
- package/Tests/Server/Types/Workflow/Components/ApiComponentSsrf.test.ts +49 -0
- package/Tests/Server/Types/Workflow/Components/TelegramSendMessageToChat.test.ts +331 -0
- package/Tests/Server/Utils/AI/SloTools.test.ts +10 -0
- package/Tests/Server/Utils/Monitor/Criteria/MetricMonitorCriteriaAnomalyUnits.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluator.test.ts +405 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorBreachingSamples.test.ts +1101 -0
- package/Tests/Server/Utils/Monitor/MonitorCriteriaEvaluatorMetricUnits.test.ts +106 -80
- package/Tests/Server/Utils/Monitor/RootCauseList.test.ts +252 -0
- package/Tests/Server/Utils/Rules/RuleRun/RuleRunPermission.test.ts +154 -0
- package/Tests/Server/Utils/SSRFProtection.test.ts +111 -0
- package/Tests/Server/Utils/Slo/SloRulePatternValidator.test.ts +67 -0
- package/Tests/Server/Utils/VM/VMRunnerSsrf.test.ts +213 -0
- package/Tests/Types/API/Route.test.ts +56 -0
- package/Tests/Types/API/URLSchemeLikePath.test.ts +304 -0
- package/Tests/UI/Components/FeedOptions.test.ts +1404 -0
- package/Tests/UI/Components/FeedOptionsButton.test.tsx +2603 -0
- package/Tests/UI/Components/Icon.test.tsx +26 -0
- package/Tests/UI/Components/RuleCriteria/RuleCriteriaModelTable.test.tsx +3 -3
- package/Tests/UI/Components/useFeedItemsViewKey.test.tsx +1017 -0
- package/Tests/UI/Components/useFeedOptions.test.tsx +1107 -0
- package/Tests/Utils/LabelRuleImportExport.test.ts +102 -0
- package/Tests/Utils/Slo/SloDuration.test.ts +115 -24
- package/Tests/Utils/Slo/SloHealth.test.ts +93 -10
- package/Tests/Utils/Slo/SloMonitorRuleCriteria.test.ts +109 -0
- package/Tests/Utils/Slo/SloOverviewText.test.ts +64 -0
- package/Types/API/Route.ts +19 -2
- package/Types/API/URL.ts +26 -2
- package/Types/Monitor/MetricMonitor/MetricCriteriaContext.ts +6 -5
- package/Types/Monitor/MetricMonitor/MetricMonitorResponse.ts +9 -0
- package/Types/Permission.ts +90 -0
- package/Types/Rules/RuleCriteriaFieldRegistry.ts +11 -0
- package/Types/Rules/RuleRun.ts +4 -0
- package/UI/Components/Feed/FeedOptions.ts +427 -0
- package/UI/Components/Feed/FeedOptionsButton.tsx +692 -0
- package/UI/Components/Feed/useFeedItems.ts +28 -9
- package/UI/Components/Feed/useFeedOptions.ts +225 -0
- package/UI/Components/Icon/Icon.tsx +1 -1
- package/Utils/LabelRuleImportExport.ts +27 -9
- package/Utils/Monitor/MetricValueFormatter.ts +2 -2
- package/Utils/Rules/MonitorTypeRuleCriteria.ts +31 -0
- package/Utils/Slo/SloDuration.ts +22 -6
- package/Utils/Slo/SloHealth.ts +8 -7
- package/Utils/Slo/SloMonitorRuleCriteria.ts +33 -1
- package/Utils/Slo/SloOverviewText.ts +33 -0
- package/Utils/Slo/SloProjection.ts +4 -3
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js +2 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveFeed.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js +530 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveLabelRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js +39 -4
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveMonitorRule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js +612 -0
- package/build/dist/Models/DatabaseModels/ServiceLevelObjectiveOwnerRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js +93 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794100000000-AddSloLabelAndOwnerRules.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1794200000000-AddSloMonitorRuleMonitorType.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/Services/DatabaseService.js +7 -0
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +3 -2
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js +295 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveLabelRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js +59 -14
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleEngineService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js +22 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveMonitorRuleService.js.map +1 -1
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js +400 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js +56 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveOwnerRuleService.js.map +1 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js +21 -0
- package/build/dist/Server/Services/ServiceLevelObjectiveService.js.map +1 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js +20 -1
- package/build/dist/Server/Types/Workflow/Components/Telegram/SendMessageToChat.js.map +1 -1
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js +1 -0
- package/build/dist/Server/Utils/AI/Toolbox/SloTools.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js +22 -51
- package/build/dist/Server/Utils/Monitor/AffectedResourceList.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/CompareCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/Criteria/MetricMonitorCriteria.js +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js +139 -109
- package/build/dist/Server/Utils/Monitor/MonitorCriteriaEvaluator.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/RootCauseList.js +101 -0
- package/build/dist/Server/Utils/Monitor/RootCauseList.js.map +1 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js +28 -0
- package/build/dist/Server/Utils/Rules/RuleRun/RuleRunRegistry.js.map +1 -1
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js +28 -0
- package/build/dist/Server/Utils/Slo/SloRulePatternValidator.js.map +1 -0
- package/build/dist/Types/API/Route.js +18 -2
- package/build/dist/Types/API/Route.js.map +1 -1
- package/build/dist/Types/API/URL.js +23 -2
- package/build/dist/Types/API/URL.js.map +1 -1
- package/build/dist/Types/Permission.js +84 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js +11 -0
- package/build/dist/Types/Rules/RuleCriteriaFieldRegistry.js.map +1 -1
- package/build/dist/Types/Rules/RuleRun.js +4 -0
- package/build/dist/Types/Rules/RuleRun.js.map +1 -1
- package/build/dist/UI/Components/Feed/FeedOptions.js +235 -0
- package/build/dist/UI/Components/Feed/FeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js +346 -0
- package/build/dist/UI/Components/Feed/FeedOptionsButton.js.map +1 -0
- package/build/dist/UI/Components/Feed/useFeedItems.js +17 -7
- package/build/dist/UI/Components/Feed/useFeedItems.js.map +1 -1
- package/build/dist/UI/Components/Feed/useFeedOptions.js +130 -0
- package/build/dist/UI/Components/Feed/useFeedOptions.js.map +1 -0
- package/build/dist/UI/Components/Icon/Icon.js +1 -1
- package/build/dist/UI/Components/Icon/Icon.js.map +1 -1
- package/build/dist/Utils/LabelRuleImportExport.js +22 -6
- package/build/dist/Utils/LabelRuleImportExport.js.map +1 -1
- package/build/dist/Utils/Monitor/MetricValueFormatter.js +2 -2
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js +18 -0
- package/build/dist/Utils/Rules/MonitorTypeRuleCriteria.js.map +1 -0
- package/build/dist/Utils/Slo/SloDuration.js +13 -5
- package/build/dist/Utils/Slo/SloDuration.js.map +1 -1
- package/build/dist/Utils/Slo/SloHealth.js +8 -7
- package/build/dist/Utils/Slo/SloHealth.js.map +1 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js +16 -1
- package/build/dist/Utils/Slo/SloMonitorRuleCriteria.js.map +1 -1
- package/build/dist/Utils/Slo/SloOverviewText.js +16 -0
- package/build/dist/Utils/Slo/SloOverviewText.js.map +1 -1
- package/build/dist/Utils/Slo/SloProjection.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FEED_OPTIONS_TEXT,
|
|
3
|
+
FEED_SORT_ORDER_OPTIONS,
|
|
4
|
+
FILTERED_FEED_NO_ITEMS_MESSAGE,
|
|
5
|
+
FeedSortOrderOption,
|
|
6
|
+
getFeedEventTypeLabel,
|
|
7
|
+
} from "../../../UI/Components/Feed/FeedOptions";
|
|
8
|
+
import {
|
|
9
|
+
DEFAULT_DASHBOARD_LANGUAGE,
|
|
10
|
+
SUPPORTED_DASHBOARD_LANGUAGE_CODES,
|
|
11
|
+
} from "../../../Types/Dashboard/DashboardLanguage";
|
|
12
|
+
import { describe, expect, test } from "@jest/globals";
|
|
13
|
+
import fs from "fs";
|
|
14
|
+
import path from "path";
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
* The "Filter & Sort" control on every dashboard activity feed looks each of
|
|
18
|
+
* its strings up in the Dashboard locale files by its English text - its own
|
|
19
|
+
* sentences, the two sort choices, the filtered empty state, and the label of
|
|
20
|
+
* every event type in the checklist. A string with no entry silently stays
|
|
21
|
+
* English, and a translation that drops or renames a {{placeholder}} shows a
|
|
22
|
+
* raw brace or a missing number. This pins the English keys to what the code
|
|
23
|
+
* renders and every other language to having a usable translation of each.
|
|
24
|
+
*
|
|
25
|
+
* The event types are discovered from the feed models on disk, not listed
|
|
26
|
+
* here, so a feed model or event type added later needs its label translated
|
|
27
|
+
* before this passes.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const COMMON_ROOT: string = path.resolve(__dirname, "..", "..", "..");
|
|
31
|
+
|
|
32
|
+
const MODELS_DIRECTORY: string = path.join(
|
|
33
|
+
COMMON_ROOT,
|
|
34
|
+
"Models",
|
|
35
|
+
"DatabaseModels",
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const DASHBOARD_LOCALES: string = path.join(
|
|
39
|
+
COMMON_ROOT,
|
|
40
|
+
"..",
|
|
41
|
+
"App",
|
|
42
|
+
"FeatureSet",
|
|
43
|
+
"Dashboard",
|
|
44
|
+
"src",
|
|
45
|
+
"Locales",
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// The sentence the trigger used as its accessible name before it was retired.
|
|
49
|
+
const RETIRED_KEY: string = "Filter and sort feed: {{summary}}";
|
|
50
|
+
|
|
51
|
+
const FEED_EVENT_TYPE_ENUM_PATTERN: RegExp =
|
|
52
|
+
/export enum (\w+FeedEventType)\b/g;
|
|
53
|
+
|
|
54
|
+
const PLACEHOLDER_PATTERN: RegExp = /\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g;
|
|
55
|
+
|
|
56
|
+
interface FeedEventTypeEnum {
|
|
57
|
+
model: string;
|
|
58
|
+
enumName: string;
|
|
59
|
+
values: Array<string>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type FindFeedEventTypeEnums = () => Array<FeedEventTypeEnum>;
|
|
63
|
+
|
|
64
|
+
/*
|
|
65
|
+
* Every *FeedEventType enum under Models/DatabaseModels, with the values the
|
|
66
|
+
* model module actually exports (loaded, not parsed, so a value written in an
|
|
67
|
+
* unusual way cannot be misread).
|
|
68
|
+
*/
|
|
69
|
+
const findFeedEventTypeEnums: FindFeedEventTypeEnums =
|
|
70
|
+
(): Array<FeedEventTypeEnum> => {
|
|
71
|
+
const found: Array<FeedEventTypeEnum> = [];
|
|
72
|
+
|
|
73
|
+
for (const entry of fs.readdirSync(MODELS_DIRECTORY, {
|
|
74
|
+
withFileTypes: true,
|
|
75
|
+
})) {
|
|
76
|
+
if (!entry.isFile() || !entry.name.endsWith(".ts")) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const source: string = fs.readFileSync(
|
|
81
|
+
path.join(MODELS_DIRECTORY, entry.name),
|
|
82
|
+
"utf8",
|
|
83
|
+
);
|
|
84
|
+
const enumNames: Array<string> = Array.from(
|
|
85
|
+
source.matchAll(FEED_EVENT_TYPE_ENUM_PATTERN),
|
|
86
|
+
(match: RegExpMatchArray): string => {
|
|
87
|
+
return match[1]!;
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (enumNames.length === 0) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const model: string = entry.name.replace(/\.ts$/, "");
|
|
96
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
97
|
+
const exported: Record<string, unknown> = require(
|
|
98
|
+
path.join(MODELS_DIRECTORY, model),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
for (const enumName of enumNames) {
|
|
102
|
+
const eventTypeEnum: unknown = exported[enumName];
|
|
103
|
+
|
|
104
|
+
found.push({
|
|
105
|
+
model,
|
|
106
|
+
enumName,
|
|
107
|
+
values:
|
|
108
|
+
eventTypeEnum && typeof eventTypeEnum === "object"
|
|
109
|
+
? Object.values(eventTypeEnum as Record<string, string>)
|
|
110
|
+
: [],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return found;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type ReadLocale = (fileName: string) => Record<string, unknown>;
|
|
119
|
+
|
|
120
|
+
const readLocale: ReadLocale = (fileName: string): Record<string, unknown> => {
|
|
121
|
+
return JSON.parse(
|
|
122
|
+
fs.readFileSync(path.join(DASHBOARD_LOCALES, fileName), "utf8"),
|
|
123
|
+
) as Record<string, unknown>;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
type GetPlaceholders = (text: string) => Array<string>;
|
|
127
|
+
|
|
128
|
+
// The placeholder names in a string, repeats kept, in a stable order.
|
|
129
|
+
const getPlaceholders: GetPlaceholders = (text: string): Array<string> => {
|
|
130
|
+
return Array.from(
|
|
131
|
+
text.matchAll(PLACEHOLDER_PATTERN),
|
|
132
|
+
(match: RegExpMatchArray): string => {
|
|
133
|
+
return match[1]!;
|
|
134
|
+
},
|
|
135
|
+
).sort();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
type Unique = (values: Array<string>) => Array<string>;
|
|
139
|
+
|
|
140
|
+
const unique: Unique = (values: Array<string>): Array<string> => {
|
|
141
|
+
return Array.from(new Set<string>(values));
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const FEED_EVENT_TYPE_ENUMS: Array<FeedEventTypeEnum> =
|
|
145
|
+
findFeedEventTypeEnums();
|
|
146
|
+
|
|
147
|
+
// The control's own words, exactly as it passes them to the translator.
|
|
148
|
+
const CONTROL_KEYS: Array<string> = unique([
|
|
149
|
+
...Object.values(FEED_OPTIONS_TEXT),
|
|
150
|
+
...FEED_SORT_ORDER_OPTIONS.map((option: FeedSortOrderOption): string => {
|
|
151
|
+
return option.label;
|
|
152
|
+
}),
|
|
153
|
+
FILTERED_FEED_NO_ITEMS_MESSAGE,
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
// The checklist's labels, which are translated by their English text too.
|
|
157
|
+
const EVENT_TYPE_LABEL_KEYS: Array<string> = unique(
|
|
158
|
+
FEED_EVENT_TYPE_ENUMS.flatMap((eventTypeEnum: FeedEventTypeEnum) => {
|
|
159
|
+
return eventTypeEnum.values.map((value: string): string => {
|
|
160
|
+
return getFeedEventTypeLabel(value);
|
|
161
|
+
});
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const ALL_KEYS: Array<string> = unique([
|
|
166
|
+
...CONTROL_KEYS,
|
|
167
|
+
...EVENT_TYPE_LABEL_KEYS,
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const OTHER_LANGUAGES: Array<string> =
|
|
171
|
+
SUPPORTED_DASHBOARD_LANGUAGE_CODES.filter((code: string): boolean => {
|
|
172
|
+
return code !== DEFAULT_DASHBOARD_LANGUAGE;
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("Filter & Sort translations in the Dashboard locales", () => {
|
|
176
|
+
test("the feed models on disk were found and loaded", () => {
|
|
177
|
+
// An empty or unloaded scan would make every check below vacuous.
|
|
178
|
+
expect(FEED_EVENT_TYPE_ENUMS.length).toBeGreaterThan(0);
|
|
179
|
+
|
|
180
|
+
for (const eventTypeEnum of FEED_EVENT_TYPE_ENUMS) {
|
|
181
|
+
expect({
|
|
182
|
+
enumName: eventTypeEnum.enumName,
|
|
183
|
+
loaded: eventTypeEnum.values.length > 0,
|
|
184
|
+
}).toEqual({ enumName: eventTypeEnum.enumName, loaded: true });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
expect(OTHER_LANGUAGES.length).toBeGreaterThan(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("English maps every string the control shows to itself", () => {
|
|
191
|
+
const english: Record<string, unknown> = readLocale(
|
|
192
|
+
`${DEFAULT_DASHBOARD_LANGUAGE}.json`,
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
const missing: Array<string> = ALL_KEYS.filter((key: string): boolean => {
|
|
196
|
+
return english[key] !== key;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(missing).toEqual([]);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test.each(OTHER_LANGUAGES)(
|
|
203
|
+
"%s translates every string the control shows, keeping its placeholders",
|
|
204
|
+
(language: string) => {
|
|
205
|
+
const locale: Record<string, unknown> = readLocale(`${language}.json`);
|
|
206
|
+
const problems: Array<string> = [];
|
|
207
|
+
|
|
208
|
+
for (const key of ALL_KEYS) {
|
|
209
|
+
const value: unknown = locale[key];
|
|
210
|
+
|
|
211
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
212
|
+
problems.push(`missing: ${key}`);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const expected: string = getPlaceholders(key).join(",");
|
|
217
|
+
const actual: string = getPlaceholders(value).join(",");
|
|
218
|
+
|
|
219
|
+
if (actual !== expected) {
|
|
220
|
+
problems.push(
|
|
221
|
+
`placeholders {${actual}} instead of {${expected}}: ${key}`,
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
expect(problems).toEqual([]);
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
test("the retired accessible-name sentence is in no locale file", () => {
|
|
231
|
+
const localeFiles: Array<string> = fs
|
|
232
|
+
.readdirSync(DASHBOARD_LOCALES)
|
|
233
|
+
.filter((fileName: string): boolean => {
|
|
234
|
+
return fileName.endsWith(".json");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
expect(localeFiles.length).toBeGreaterThan(1);
|
|
238
|
+
|
|
239
|
+
const stillThere: Array<string> = localeFiles.filter(
|
|
240
|
+
(fileName: string): boolean => {
|
|
241
|
+
return Object.prototype.hasOwnProperty.call(
|
|
242
|
+
readLocale(fileName),
|
|
243
|
+
RETIRED_KEY,
|
|
244
|
+
);
|
|
245
|
+
},
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
expect(stillThere).toEqual([]);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -7,6 +7,22 @@ interface FeedImplementation {
|
|
|
7
7
|
relativePath: string;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
+
/*
|
|
11
|
+
* How one feed connects its Filter & Sort state to the request it makes. The
|
|
12
|
+
* checks below compare against the name the feed itself gave the
|
|
13
|
+
* useFeedOptions() result, and look inside the two hook calls rather than
|
|
14
|
+
* the whole file, so they survive renames and reformatting but not a wire
|
|
15
|
+
* that goes to the wrong place.
|
|
16
|
+
*/
|
|
17
|
+
interface FeedOptionsWiring {
|
|
18
|
+
// What the feed called its useFeedOptions() result.
|
|
19
|
+
hook: string;
|
|
20
|
+
// The arguments of the useFeedOptions() call.
|
|
21
|
+
optionsCall: string;
|
|
22
|
+
// The arguments of the useFeedItems() call: the request the feed makes.
|
|
23
|
+
itemsCall: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
10
26
|
const DASHBOARD_COMPONENTS: string = path.join(
|
|
11
27
|
__dirname,
|
|
12
28
|
"..",
|
|
@@ -62,14 +78,30 @@ const FEED_IMPLEMENTATIONS: Array<FeedImplementation> = [
|
|
|
62
78
|
},
|
|
63
79
|
];
|
|
64
80
|
|
|
65
|
-
|
|
81
|
+
/*
|
|
82
|
+
* Ends at the call's opening parenthesis, so the same pattern both names the
|
|
83
|
+
* result and locates the call's arguments.
|
|
84
|
+
*/
|
|
85
|
+
const FEED_OPTIONS_HOOK_CALL: RegExp =
|
|
86
|
+
/const\s+(\w+)\s*:\s*UseFeedOptionsResult\s*=\s*useFeedOptions\s*\(/;
|
|
87
|
+
|
|
88
|
+
const FEED_ITEMS_HOOK_CALL: RegExp = /\buseFeedItems\s*(?:<[^<>()]*>)?\s*\(/;
|
|
89
|
+
|
|
90
|
+
// A storage key written as a string literal ("incident"), not an expression.
|
|
91
|
+
const QUOTED_STRING_LITERAL: RegExp = /^(["'`])[^"'`]*\1$/;
|
|
92
|
+
|
|
93
|
+
const readSource: (implementation: FeedImplementation) => string = (
|
|
94
|
+
implementation: FeedImplementation,
|
|
95
|
+
): string => {
|
|
66
96
|
return fs.readFileSync(
|
|
67
97
|
path.join(DASHBOARD_COMPONENTS, implementation.relativePath),
|
|
68
98
|
"utf8",
|
|
69
99
|
);
|
|
70
|
-
}
|
|
100
|
+
};
|
|
71
101
|
|
|
72
|
-
|
|
102
|
+
const findFeedImplementationPaths: (directory: string) => Array<string> = (
|
|
103
|
+
directory: string,
|
|
104
|
+
): Array<string> => {
|
|
73
105
|
return fs
|
|
74
106
|
.readdirSync(directory, { withFileTypes: true })
|
|
75
107
|
.flatMap((entry: fs.Dirent): Array<string> => {
|
|
@@ -95,7 +127,104 @@ function findFeedImplementationPaths(directory: string): Array<string> {
|
|
|
95
127
|
.join("/"),
|
|
96
128
|
];
|
|
97
129
|
});
|
|
98
|
-
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* The text between the parentheses of the first call that `callee` finds (a
|
|
134
|
+
* pattern ending at the opening parenthesis). Strings and comments are
|
|
135
|
+
* skipped, so a bracket inside one cannot end the call early.
|
|
136
|
+
*/
|
|
137
|
+
const getCallArguments: (source: string, callee: RegExp) => string = (
|
|
138
|
+
source: string,
|
|
139
|
+
callee: RegExp,
|
|
140
|
+
): string => {
|
|
141
|
+
const match: RegExpExecArray | null = callee.exec(source);
|
|
142
|
+
|
|
143
|
+
if (!match) {
|
|
144
|
+
throw new Error(`No call matching ${callee.toString()}`);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const openIndex: number = match.index + match[0].length - 1;
|
|
148
|
+
let depth: number = 0;
|
|
149
|
+
let index: number = openIndex;
|
|
150
|
+
|
|
151
|
+
while (index < source.length) {
|
|
152
|
+
const character: string = source[index]!;
|
|
153
|
+
const nextCharacter: string | undefined = source[index + 1];
|
|
154
|
+
|
|
155
|
+
if (character === "/" && nextCharacter === "*") {
|
|
156
|
+
index = source.indexOf("*/", index + 2) + 2;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (character === "/" && nextCharacter === "/") {
|
|
161
|
+
const lineEnd: number = source.indexOf("\n", index);
|
|
162
|
+
index = lineEnd === -1 ? source.length : lineEnd;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (character === '"' || character === "'" || character === "`") {
|
|
167
|
+
index++;
|
|
168
|
+
|
|
169
|
+
while (index < source.length && source[index] !== character) {
|
|
170
|
+
index += source[index] === "\\" ? 2 : 1;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
index++;
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if ("([{".includes(character)) {
|
|
178
|
+
depth++;
|
|
179
|
+
} else if (")]}".includes(character)) {
|
|
180
|
+
depth--;
|
|
181
|
+
|
|
182
|
+
if (depth === 0) {
|
|
183
|
+
return source.slice(openIndex + 1, index);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
index++;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new Error(`Unbalanced call matching ${callee.toString()}`);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/*
|
|
194
|
+
* A property's value expression inside a call's arguments, with whitespace
|
|
195
|
+
* removed so that line breaks never decide whether two expressions match.
|
|
196
|
+
*/
|
|
197
|
+
const getPropertyExpression: (
|
|
198
|
+
callArguments: string,
|
|
199
|
+
property: string,
|
|
200
|
+
) => string | undefined = (
|
|
201
|
+
callArguments: string,
|
|
202
|
+
property: string,
|
|
203
|
+
): string | undefined => {
|
|
204
|
+
const match: RegExpMatchArray | null = callArguments.match(
|
|
205
|
+
new RegExp(String.raw`\b${property}\s*:\s*([^,}]+?)\s*[,}]`),
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return match ? match[1]!.replace(/\s+/g, "") : undefined;
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const getFeedOptionsWiring: (source: string) => FeedOptionsWiring = (
|
|
212
|
+
source: string,
|
|
213
|
+
): FeedOptionsWiring => {
|
|
214
|
+
const hookMatch: RegExpMatchArray | null = source.match(
|
|
215
|
+
FEED_OPTIONS_HOOK_CALL,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
if (!hookMatch) {
|
|
219
|
+
throw new Error("The feed does not keep a UseFeedOptionsResult");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
hook: hookMatch[1]!,
|
|
224
|
+
optionsCall: getCallArguments(source, FEED_OPTIONS_HOOK_CALL),
|
|
225
|
+
itemsCall: getCallArguments(source, FEED_ITEMS_HOOK_CALL),
|
|
226
|
+
};
|
|
227
|
+
};
|
|
99
228
|
|
|
100
229
|
describe("dashboard feed ordering parity", () => {
|
|
101
230
|
test("tracks every feed implementation", () => {
|
|
@@ -110,20 +239,161 @@ describe("dashboard feed ordering parity", () => {
|
|
|
110
239
|
);
|
|
111
240
|
});
|
|
112
241
|
|
|
242
|
+
/*
|
|
243
|
+
* The order is the reader's choice from the Filter & Sort button, and it
|
|
244
|
+
* is applied by the API. Reversing or re-sorting the loaded window in the
|
|
245
|
+
* browser would show the newest ten upside down instead of the oldest ten.
|
|
246
|
+
*/
|
|
113
247
|
test.each(FEED_IMPLEMENTATIONS)(
|
|
114
|
-
"$name requests the
|
|
248
|
+
"$name requests activity in the reader's chosen order",
|
|
115
249
|
(implementation: FeedImplementation) => {
|
|
116
250
|
const source: string = readSource(implementation);
|
|
251
|
+
const wiring: FeedOptionsWiring = getFeedOptionsWiring(source);
|
|
117
252
|
|
|
118
|
-
expect(
|
|
119
|
-
|
|
253
|
+
expect(wiring.itemsCall).toMatch(
|
|
254
|
+
new RegExp(
|
|
255
|
+
String.raw`\bpostedAt\s*:\s*${wiring.hook}\.options\.sortOrder\b`,
|
|
256
|
+
),
|
|
257
|
+
);
|
|
258
|
+
expect(source).not.toMatch(/\bpostedAt\s*:\s*SortOrder\./);
|
|
120
259
|
expect(source).not.toContain(".reverse()");
|
|
121
|
-
expect(
|
|
122
|
-
expect(
|
|
260
|
+
expect(wiring.itemsCall).toMatch(/\bskip\s*:\s*0\b/);
|
|
261
|
+
expect(wiring.itemsCall).toMatch(/\blimit\s*,/);
|
|
123
262
|
expect(source).not.toContain("LIMIT_PER_PROJECT");
|
|
124
263
|
},
|
|
125
264
|
);
|
|
126
265
|
|
|
266
|
+
test.each(FEED_IMPLEMENTATIONS)(
|
|
267
|
+
"$name offers Filter & Sort as the first header control and filters through the API",
|
|
268
|
+
(implementation: FeedImplementation) => {
|
|
269
|
+
const source: string = readSource(implementation);
|
|
270
|
+
const wiring: FeedOptionsWiring = getFeedOptionsWiring(source);
|
|
271
|
+
const hook: string = wiring.hook;
|
|
272
|
+
|
|
273
|
+
expect(source).toMatch(
|
|
274
|
+
/import\s+useFeedOptions\b[^;]*from\s+"Common\/UI\/Components\/Feed\/useFeedOptions"/,
|
|
275
|
+
);
|
|
276
|
+
expect(source).toMatch(
|
|
277
|
+
/import\s+FeedOptionsButton\s+from\s+"Common\/UI\/Components\/Feed\/FeedOptionsButton"/,
|
|
278
|
+
);
|
|
279
|
+
expect(source).toMatch(/buttons=\{\[\s*<FeedOptionsButton\b/);
|
|
280
|
+
|
|
281
|
+
/*
|
|
282
|
+
* A change of options must both re-read the feed (viewKey) and change
|
|
283
|
+
* what is read (the event type query). Either one alone shows stale or
|
|
284
|
+
* mislabelled items.
|
|
285
|
+
*/
|
|
286
|
+
expect(wiring.itemsCall).toMatch(
|
|
287
|
+
new RegExp(String.raw`\bviewKey\s*:\s*${hook}\.optionsKey\b`),
|
|
288
|
+
);
|
|
289
|
+
/*
|
|
290
|
+
* The first argument may carry a generic cast with a comma of its own:
|
|
291
|
+
* ResourceFeed names its column `as Extract<keyof TFeedModel, string>`.
|
|
292
|
+
*/
|
|
293
|
+
expect(wiring.itemsCall).toMatch(
|
|
294
|
+
new RegExp(
|
|
295
|
+
String.raw`\.\.\.getFeedEventTypeQuery<\w+>\(\s*(?:[^,()<>]|<[^<>]*>)+,\s*${hook}\.options\s*,?\s*\)`,
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
|
|
299
|
+
const button: RegExpMatchArray | null = source.match(
|
|
300
|
+
/<FeedOptionsButton\b[\s\S]*?\/>/,
|
|
301
|
+
);
|
|
302
|
+
|
|
303
|
+
expect(button).not.toBeNull();
|
|
304
|
+
expect(button![0]).toMatch(
|
|
305
|
+
new RegExp(String.raw`\bvalue=\{\s*${hook}\.options\s*\}`),
|
|
306
|
+
);
|
|
307
|
+
expect(button![0]).toMatch(
|
|
308
|
+
new RegExp(
|
|
309
|
+
String.raw`\beventTypeOptions=\{\s*${hook}\.eventTypeOptions\s*\}`,
|
|
310
|
+
),
|
|
311
|
+
);
|
|
312
|
+
expect(button![0]).toMatch(
|
|
313
|
+
new RegExp(String.raw`\bonChange=\{\s*${hook}\.setOptions\s*\}`),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
/*
|
|
317
|
+
* A filtered feed with no matches must say it is filtered, not claim
|
|
318
|
+
* the resource has no activity at all.
|
|
319
|
+
*/
|
|
320
|
+
expect(
|
|
321
|
+
getCallArguments(
|
|
322
|
+
source,
|
|
323
|
+
/noItemsMessage=\{\s*getFeedNoItemsMessage\s*\(/,
|
|
324
|
+
),
|
|
325
|
+
).toMatch(new RegExp(String.raw`\boptions\s*:\s*${hook}\.options\b`));
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
/*
|
|
330
|
+
* The dashboard moves from one incident (cluster, monitor, ...) to the next
|
|
331
|
+
* without remounting the feed. useFeedItems starts over when its
|
|
332
|
+
* resourceKey changes, and useFeedOptions drops the event type filter when
|
|
333
|
+
* its resetKey changes. Only when both are the same expression do the two
|
|
334
|
+
* happen in the same render, so the first request for the new resource is
|
|
335
|
+
* unfiltered instead of carrying the filter chosen on the previous one.
|
|
336
|
+
*/
|
|
337
|
+
test.each(FEED_IMPLEMENTATIONS)(
|
|
338
|
+
"$name drops its event type filter on the render that moves it to another resource",
|
|
339
|
+
(implementation: FeedImplementation) => {
|
|
340
|
+
const wiring: FeedOptionsWiring = getFeedOptionsWiring(
|
|
341
|
+
readSource(implementation),
|
|
342
|
+
);
|
|
343
|
+
const resourceKey: string | undefined = getPropertyExpression(
|
|
344
|
+
wiring.itemsCall,
|
|
345
|
+
"resourceKey",
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
expect(resourceKey).toBeTruthy();
|
|
349
|
+
expect(getPropertyExpression(wiring.optionsCall, "resetKey")).toBe(
|
|
350
|
+
resourceKey,
|
|
351
|
+
);
|
|
352
|
+
},
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
/*
|
|
356
|
+
* Only literal keys can be compared here. ResourceFeed keys its order by
|
|
357
|
+
* props.eventTypeColumn, one per product, and
|
|
358
|
+
* ResourceFeedPagesFilterSort.test.tsx checks that those are distinct.
|
|
359
|
+
*/
|
|
360
|
+
test("each feed remembers its sort order under its own storage key", () => {
|
|
361
|
+
const storageKeys: Array<[string, string | undefined]> =
|
|
362
|
+
FEED_IMPLEMENTATIONS.map(
|
|
363
|
+
(implementation: FeedImplementation): [string, string | undefined] => {
|
|
364
|
+
return [
|
|
365
|
+
implementation.name,
|
|
366
|
+
getPropertyExpression(
|
|
367
|
+
getFeedOptionsWiring(readSource(implementation)).optionsCall,
|
|
368
|
+
"storageKey",
|
|
369
|
+
),
|
|
370
|
+
];
|
|
371
|
+
},
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
// Without a key the order is forgotten on every page load.
|
|
375
|
+
expect(
|
|
376
|
+
storageKeys
|
|
377
|
+
.filter((entry: [string, string | undefined]): boolean => {
|
|
378
|
+
return !entry[1];
|
|
379
|
+
})
|
|
380
|
+
.map((entry: [string, string | undefined]): string => {
|
|
381
|
+
return entry[0];
|
|
382
|
+
}),
|
|
383
|
+
).toEqual([]);
|
|
384
|
+
|
|
385
|
+
const literalKeys: Array<string> = storageKeys
|
|
386
|
+
.map((entry: [string, string | undefined]): string => {
|
|
387
|
+
return entry[1] || "";
|
|
388
|
+
})
|
|
389
|
+
.filter((key: string): boolean => {
|
|
390
|
+
return QUOTED_STRING_LITERAL.test(key);
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
expect(literalKeys.length).toBeGreaterThan(1);
|
|
394
|
+
expect(new Set(literalKeys).size).toBe(literalKeys.length);
|
|
395
|
+
});
|
|
396
|
+
|
|
127
397
|
test.each(FEED_IMPLEMENTATIONS)(
|
|
128
398
|
"$name uses the shared progressive feed UI",
|
|
129
399
|
(implementation: FeedImplementation) => {
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
afterEach,
|
|
3
|
+
beforeEach,
|
|
4
|
+
describe,
|
|
5
|
+
expect,
|
|
6
|
+
jest,
|
|
7
|
+
test,
|
|
8
|
+
} from "@jest/globals";
|
|
2
9
|
import "@testing-library/jest-dom";
|
|
3
10
|
import { act, cleanup, render, screen, waitFor } from "@testing-library/react";
|
|
4
11
|
import * as React from "react";
|
|
@@ -59,6 +66,7 @@ import IconProp from "../../../Types/Icon/IconProp";
|
|
|
59
66
|
import ObjectID from "../../../Types/ObjectID";
|
|
60
67
|
import SortOrder from "../../../Types/BaseDatabase/SortOrder";
|
|
61
68
|
import { DEFAULT_LIMIT } from "../../../Types/Database/LimitMax";
|
|
69
|
+
import { getSortOrderStorageKey } from "../../../UI/Components/Feed/useFeedOptions";
|
|
62
70
|
|
|
63
71
|
interface RenderedFeedItem {
|
|
64
72
|
key: string;
|
|
@@ -322,6 +330,15 @@ function lastRenderedFeedProps(): RenderedFeedProps {
|
|
|
322
330
|
return calls[calls.length - 1]![0]!;
|
|
323
331
|
}
|
|
324
332
|
|
|
333
|
+
/*
|
|
334
|
+
* Both feeds open in the sort order the reader last chose, read from
|
|
335
|
+
* localStorage on mount, so one test's choice must not reorder the next
|
|
336
|
+
* test's requests.
|
|
337
|
+
*/
|
|
338
|
+
beforeEach(() => {
|
|
339
|
+
window.localStorage.clear();
|
|
340
|
+
});
|
|
341
|
+
|
|
325
342
|
afterEach(() => {
|
|
326
343
|
cleanup();
|
|
327
344
|
getListMock.mockReset();
|
|
@@ -439,6 +456,48 @@ describe("investigation reports in incident and alert feeds", () => {
|
|
|
439
456
|
expect(getListMock).toHaveBeenCalledTimes(2);
|
|
440
457
|
});
|
|
441
458
|
|
|
459
|
+
/*
|
|
460
|
+
* The refresh after an investigation re-reads the same view the reader is
|
|
461
|
+
* looking at. A reader who keeps these feeds oldest first must get the new
|
|
462
|
+
* report at the end of that order, not a feed flipped back to newest first.
|
|
463
|
+
*/
|
|
464
|
+
test("a refresh re-reads the incident and alert feeds in the reader's remembered order", async () => {
|
|
465
|
+
window.localStorage.setItem(
|
|
466
|
+
getSortOrderStorageKey("incident"),
|
|
467
|
+
SortOrder.Ascending,
|
|
468
|
+
);
|
|
469
|
+
window.localStorage.setItem(
|
|
470
|
+
getSortOrderStorageKey("alert"),
|
|
471
|
+
SortOrder.Ascending,
|
|
472
|
+
);
|
|
473
|
+
getListMock.mockResolvedValue(listResult<IncidentFeed>([]) as never);
|
|
474
|
+
|
|
475
|
+
const incidentView: ReturnType<typeof render> = render(incidentElement(0));
|
|
476
|
+
await flush();
|
|
477
|
+
incidentView.rerender(incidentElement(1));
|
|
478
|
+
await flush();
|
|
479
|
+
|
|
480
|
+
const alertView: ReturnType<typeof render> = render(alertElement(0));
|
|
481
|
+
await flush();
|
|
482
|
+
alertView.rerender(alertElement(1));
|
|
483
|
+
await flush();
|
|
484
|
+
|
|
485
|
+
const requests: Array<FeedListRequest> = getListMock.mock.calls.map(
|
|
486
|
+
(call: Array<unknown>): FeedListRequest => {
|
|
487
|
+
return call[0] as FeedListRequest;
|
|
488
|
+
},
|
|
489
|
+
);
|
|
490
|
+
|
|
491
|
+
expect(
|
|
492
|
+
requests.map((request: FeedListRequest): unknown => {
|
|
493
|
+
return request.modelType;
|
|
494
|
+
}),
|
|
495
|
+
).toEqual([IncidentFeed, IncidentFeed, AlertFeed, AlertFeed]);
|
|
496
|
+
for (const request of requests) {
|
|
497
|
+
expect(request.sort).toEqual({ postedAt: SortOrder.Ascending });
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
|
|
442
501
|
test("a late mount-time incident response cannot erase the refreshed report", async () => {
|
|
443
502
|
const initialRequest: Deferred<ListResult<IncidentFeed>> =
|
|
444
503
|
createDeferred<ListResult<IncidentFeed>>();
|