@oneuptime/common 11.7.0 → 11.7.2
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/DashboardDomain.ts +1 -0
- package/Models/DatabaseModels/Incident.ts +2 -0
- package/Models/DatabaseModels/IncidentEpisode.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodePublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentEpisodeStateTimeline.ts +1 -0
- package/Models/DatabaseModels/IncidentPublicNote.ts +1 -0
- package/Models/DatabaseModels/IncidentStateTimeline.ts +1 -0
- package/Models/DatabaseModels/Monitor.ts +1 -0
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/NetworkSite.ts +1 -0
- package/Models/DatabaseModels/NetworkSiteStatusTimeline.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.ts +1 -0
- package/Models/DatabaseModels/OnCallDutyPolicySchedule.ts +2 -0
- package/Models/DatabaseModels/Probe.ts +13 -13
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenancePublicNote.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.ts +1 -0
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -0
- package/Models/DatabaseModels/StatusPage.ts +1 -0
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +1 -0
- package/Models/DatabaseModels/StatusPageDomain.ts +1 -0
- package/Models/DatabaseModels/WorkspaceNotificationSummary.ts +1 -0
- package/Models/DatabaseModels/WorkspaceProjectAuthToken.ts +11 -0
- package/Server/API/BaseAPI.ts +17 -1
- package/Server/API/CommonAPI.ts +32 -0
- package/Server/API/MicrosoftTeamsAPI.ts +99 -2
- package/Server/API/ProbeAPI.ts +2 -0
- package/Server/API/SlackAPI.ts +62 -0
- package/Server/API/StatusPageAPI.ts +157 -192
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.ts +89 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.ts +61 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.ts +280 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +6 -0
- package/Server/Services/DatabaseService.ts +11 -3
- package/Server/Services/MonitorGroupService.ts +145 -0
- package/Server/Services/MonitorService.ts +231 -30
- package/Server/Services/ProbeService.ts +319 -16
- package/Server/Services/StatusPageService.ts +72 -42
- package/Server/Services/WorkspaceNotificationRuleService.ts +232 -2
- package/Server/Utils/Database/PostgresErrorTranslator.ts +131 -0
- package/Server/Utils/Monitor/MonitorResource.ts +88 -35
- package/Server/Utils/Monitor/MonitorStatusTimeline.ts +21 -1
- package/Server/Utils/Monitor/MonitorStepsProjectValidator.ts +158 -0
- package/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.ts +706 -6
- package/Tests/Models/DatabaseModels/ProbeColumnAccessControl.test.ts +103 -0
- package/Tests/Server/API/BaseAPIUpdateNoOp.test.ts +135 -0
- package/Tests/Server/API/CommonAPIAuthGuard.test.ts +317 -0
- package/Tests/Server/API/MicrosoftTeamsManifest.test.ts +228 -0
- package/Tests/Server/API/ProbeAPI.test.ts +6 -0
- package/Tests/Server/API/ZZVerifyChatsAuth.test.ts +167 -0
- package/Tests/Server/Services/AddHotQueryIndexesMigration.test.ts +255 -0
- package/Tests/Server/Services/AddHotQueryIndexesSecondPassMigration.test.ts +258 -0
- package/Tests/Server/Services/DatabaseServiceSanitizeUpdateData.test.ts +1 -1
- package/Tests/Server/Services/DiscoveryScanClaimHookFreeSafety.test.ts +121 -0
- package/Tests/Server/Services/HeartbeatWriteFastPathSafety.test.ts +200 -0
- package/Tests/Server/Services/HookFreeWriteSafetySecondPass.test.ts +181 -0
- package/Tests/Server/Services/MonitorGroupServiceBatchedStatuses.test.ts +513 -0
- package/Tests/Server/Services/MonitorResourceProbeAgreementReuse.test.ts +492 -0
- package/Tests/Server/Services/MonitorServiceProbeSelection.test.ts +401 -0
- package/Tests/Server/Services/MonitorStatusTimelineFastPath.test.ts +453 -0
- package/Tests/Server/Services/ProbeAuthKeyCacheAndHeartbeat.test.ts +679 -0
- package/Tests/Server/Services/ServiceLevelObjectiveService.test.ts +3 -3
- package/Tests/Server/Services/StatusPageDomainResolutionCache.test.ts +245 -0
- package/Tests/Server/Services/StatusPageOverviewTimelineWindow.test.ts +283 -0
- package/Tests/Server/Services/StatusPageServiceMcp.test.ts +17 -1
- package/Tests/Server/Services/StatusPageServiceMonitorGroupBatching.test.ts +331 -0
- package/Tests/Server/Services/UserOnCallLogClaimNotificationRule.test.ts +12 -12
- package/Tests/Server/Services/WorkspaceNotificationRuleChats.test.ts +1390 -0
- package/Tests/Server/Utils/AI/Insights/InsightFixRouting.test.ts +4 -4
- package/Tests/Server/Utils/AI/Insights/InsightScanner.test.ts +2 -2
- package/Tests/Server/Utils/AI/Insights/InsightStore.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightStoreHardening.test.ts +1 -3
- package/Tests/Server/Utils/AI/Insights/InsightTriage.test.ts +1 -1
- package/Tests/Server/Utils/AI/Insights/InsightTriageRunner.test.ts +5 -5
- package/Tests/Server/Utils/Alert/AlertPrivacyFilter.test.ts +187 -0
- package/Tests/Server/Utils/AlertEpisode/AlertEpisodePrivacyFilter.test.ts +170 -0
- package/Tests/Server/Utils/Database/PostgresErrorTranslator.test.ts +125 -0
- package/Tests/Server/Utils/Incident/IncidentPrivacyFilter.test.ts +264 -0
- package/Tests/Server/Utils/IncidentEpisode/IncidentEpisodePrivacyFilter.test.ts +175 -0
- package/Tests/Server/Utils/Monitor/MonitorStepsProjectValidator.test.ts +181 -0
- package/Tests/Server/Utils/Monitor/NetworkDeviceWalkUtil.test.ts +1 -1
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +2 -2
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChannelsList.test.ts +341 -0
- package/Tests/Server/Utils/Workspace/MicrosoftTeamsChats.test.ts +2175 -0
- package/Tests/Server/Utils/Workspace/SlackChannelsList.test.ts +585 -0
- package/Tests/Types/Billing/MeteredPlan.test.ts +46 -0
- package/Tests/Types/DateUserTimezone.test.ts +205 -0
- package/Tests/Types/DateUserTimezoneOverridesBrowser.test.ts +73 -0
- package/Tests/Types/Workspace/NotificationRules/NotificationRuleCondition.test.ts +219 -1
- package/Tests/UI/Components/BulkUpdateForm.test.tsx +174 -0
- package/Tests/UI/Components/CardModelDetailEdit.test.tsx +315 -0
- package/Tests/UI/Components/ModelDetailSelect.test.tsx +229 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkSelectAll.test.tsx +1258 -0
- package/Tests/UI/Components/ModelTableExportFromColumns.test.ts +153 -0
- package/Tests/UI/Components/MonacoLoader.test.ts +67 -0
- package/Tests/UI/Components/MonacoRuntime.test.ts +121 -0
- package/Tests/UI/Components/TableBulkCsvExport.test.tsx +212 -3
- package/Tests/UI/Components/TimePicker/TimePicker.test.tsx +11 -0
- package/Tests/UI/Utils/Project.test.ts +809 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +360 -0
- package/Tests/Utils/Alerts/AlertMetricType.test.ts +143 -0
- package/Tests/Utils/Dashboard/VariableUrlState.test.ts +219 -0
- package/Tests/Utils/Incident/IncidentMetricType.test.ts +174 -0
- package/Tests/Utils/Monitor/MonitorProbeSelectionUtil.test.ts +145 -0
- package/Tests/Utils/StatusPage/ResourceUptime.test.ts +263 -0
- package/Types/Date.ts +119 -50
- package/Types/Workspace/NotificationRules/BaseNotificationRule.ts +4 -0
- package/Types/Workspace/NotificationRules/NotificationRuleCondition.ts +25 -10
- package/Types/Workspace/WorkspaceMessagePayload.ts +1 -0
- package/UI/Components/BulkUpdate/BulkUpdateForm.tsx +63 -8
- package/UI/Components/CodeEditor/CodeEditor.tsx +3 -0
- package/UI/Components/CodeEditor/MonacoLoader.ts +36 -0
- package/UI/Components/Forms/Fields/FormField.tsx +13 -3
- package/UI/Components/Forms/ModelForm.tsx +41 -18
- package/UI/Components/Input/Input.tsx +6 -1
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +300 -30
- package/UI/Components/ModelTable/Column.ts +12 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/Table/Table.tsx +52 -13
- package/UI/Components/Table/Types/Column.ts +18 -0
- package/UI/Components/TimePicker/TimePicker.tsx +8 -4
- package/UI/Utils/Project.ts +214 -18
- package/UI/Utils/TableColumnsToCsv.ts +86 -6
- package/UI/Utils/User.ts +19 -0
- package/UI/esbuild-config.js +34 -0
- package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/DashboardDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +2 -0
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisode.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentEpisodeStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentPublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/IncidentStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Monitor.js +1 -0
- package/build/dist/Models/DatabaseModels/Monitor.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSite.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSite.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/NetworkSiteStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js +1 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicyExecutionLog.js.map +1 -1
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js +2 -0
- package/build/dist/Models/DatabaseModels/OnCallDutyPolicySchedule.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Probe.js +13 -13
- package/build/dist/Models/DatabaseModels/Probe.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenancePublicNote.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceStateTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPage.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPage.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js +1 -0
- package/build/dist/Models/DatabaseModels/StatusPageDomain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js +1 -0
- package/build/dist/Models/DatabaseModels/WorkspaceNotificationSummary.js.map +1 -1
- package/build/dist/Models/DatabaseModels/WorkspaceProjectAuthToken.js.map +1 -1
- package/build/dist/Server/API/BaseAPI.js +12 -1
- package/build/dist/Server/API/BaseAPI.js.map +1 -1
- package/build/dist/Server/API/CommonAPI.js +21 -0
- package/build/dist/Server/API/CommonAPI.js.map +1 -1
- package/build/dist/Server/API/MicrosoftTeamsAPI.js +72 -2
- package/build/dist/Server/API/MicrosoftTeamsAPI.js.map +1 -1
- package/build/dist/Server/API/ProbeAPI.js +2 -0
- package/build/dist/Server/API/ProbeAPI.js.map +1 -1
- package/build/dist/Server/API/SlackAPI.js +44 -0
- package/build/dist/Server/API/SlackAPI.js.map +1 -1
- package/build/dist/Server/API/StatusPageAPI.js +98 -152
- package/build/dist/Server/API/StatusPageAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js +36 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785140242697-AddHotQueryIndexes.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785148065137-AddHotQueryIndexesSecondPass.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js +254 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785240000000-RepairCrossProjectMonitorStatusReferences.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +10 -2
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/MonitorGroupService.js +118 -0
- package/build/dist/Server/Services/MonitorGroupService.js.map +1 -1
- package/build/dist/Server/Services/MonitorService.js +198 -24
- package/build/dist/Server/Services/MonitorService.js.map +1 -1
- package/build/dist/Server/Services/ProbeService.js +238 -9
- package/build/dist/Server/Services/ProbeService.js.map +1 -1
- package/build/dist/Server/Services/StatusPageService.js +46 -39
- package/build/dist/Server/Services/StatusPageService.js.map +1 -1
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js +178 -3
- package/build/dist/Server/Services/WorkspaceNotificationRuleService.js.map +1 -1
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js +80 -0
- package/build/dist/Server/Utils/Database/PostgresErrorTranslator.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/MonitorResource.js +67 -29
- package/build/dist/Server/Utils/Monitor/MonitorResource.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js +31 -14
- package/build/dist/Server/Utils/Monitor/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js +117 -0
- package/build/dist/Server/Utils/Monitor/MonitorStepsProjectValidator.js.map +1 -0
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js +549 -7
- package/build/dist/Server/Utils/Workspace/MicrosoftTeams/MicrosoftTeams.js.map +1 -1
- package/build/dist/Types/Date.js +106 -35
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js +17 -7
- package/build/dist/Types/Workspace/NotificationRules/NotificationRuleCondition.js.map +1 -1
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js +37 -11
- package/build/dist/UI/Components/BulkUpdate/BulkUpdateForm.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js +2 -0
- package/build/dist/UI/Components/CodeEditor/CodeEditor.js.map +1 -1
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js +34 -0
- package/build/dist/UI/Components/CodeEditor/MonacoLoader.js.map +1 -0
- package/build/dist/UI/Components/Forms/Fields/FormField.js +11 -3
- package/build/dist/UI/Components/Forms/Fields/FormField.js.map +1 -1
- package/build/dist/UI/Components/Forms/ModelForm.js +37 -17
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/Input/Input.js +6 -1
- package/build/dist/UI/Components/Input/Input.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js +22 -1
- package/build/dist/UI/Components/ModelDetail/CardModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js +31 -1
- package/build/dist/UI/Components/ModelDetail/ModelDetail.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +234 -21
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js +30 -0
- package/build/dist/UI/Components/ModelTable/ExportFromColumns.js.map +1 -0
- package/build/dist/UI/Components/Navbar/NavBar.js +39 -1
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +31 -11
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/TimePicker/TimePicker.js +4 -4
- package/build/dist/UI/Components/TimePicker/TimePicker.js.map +1 -1
- package/build/dist/UI/Utils/Project.js +161 -15
- package/build/dist/UI/Utils/Project.js.map +1 -1
- package/build/dist/UI/Utils/TableColumnsToCsv.js +67 -6
- package/build/dist/UI/Utils/TableColumnsToCsv.js.map +1 -1
- package/build/dist/UI/Utils/User.js +19 -0
- package/build/dist/UI/Utils/User.js.map +1 -1
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
- package/package.json +4 -3
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +0 -79
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +0 -41
- package/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.ts +0 -17
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +0 -34
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +0 -22
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +0 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js +0 -12
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1776886248361-MigrationName.js.map +0 -1
|
@@ -0,0 +1,809 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "@jest/globals";
|
|
2
|
+
import ProjectUtil, {
|
|
3
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
4
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
5
|
+
} from "../../../UI/Utils/Project";
|
|
6
|
+
import Project from "../../../Models/DatabaseModels/Project";
|
|
7
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
8
|
+
|
|
9
|
+
/*
|
|
10
|
+
* A user who belongs to more than one project expects the dashboard to reopen
|
|
11
|
+
* on the project they were last working in. Before this was fixed, the only
|
|
12
|
+
* record of "the project I'm on" lived in SESSION storage, which the browser
|
|
13
|
+
* throws away when the tab closes - so every fresh visit to /dashboard silently
|
|
14
|
+
* dropped the user into whichever project happened to sort first in the list.
|
|
15
|
+
*
|
|
16
|
+
* These tests pin down the resolution order that fixes that:
|
|
17
|
+
*
|
|
18
|
+
* URL > this tab's session > last project opened on this browser > first
|
|
19
|
+
*
|
|
20
|
+
* and the invariant that makes it safe: a remembered project is only honoured
|
|
21
|
+
* while the user still has access to it.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const PROJECT_X_ID: string = "11111111-1111-4111-8111-111111111111";
|
|
25
|
+
const PROJECT_Y_ID: string = "22222222-2222-4222-8222-222222222222";
|
|
26
|
+
const PROJECT_Z_ID: string = "33333333-3333-4333-8333-333333333333";
|
|
27
|
+
|
|
28
|
+
type BuildProjectFunction = (id: string, name: string) => Project;
|
|
29
|
+
|
|
30
|
+
const buildProject: BuildProjectFunction = (
|
|
31
|
+
id: string,
|
|
32
|
+
name: string,
|
|
33
|
+
): Project => {
|
|
34
|
+
const project: Project = new Project();
|
|
35
|
+
project._id = id;
|
|
36
|
+
project.name = name;
|
|
37
|
+
return project;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SetUrlFunction = (url: string) => void;
|
|
41
|
+
|
|
42
|
+
const setUrl: SetUrlFunction = (url: string): void => {
|
|
43
|
+
window.history.replaceState(window.history.state, "", url);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* What the browser does when the user closes the tab (or opens the dashboard in
|
|
48
|
+
* a brand new one): session storage is gone, local storage survives.
|
|
49
|
+
*/
|
|
50
|
+
type CloseTheTabFunction = () => void;
|
|
51
|
+
|
|
52
|
+
const closeTheTab: CloseTheTabFunction = (): void => {
|
|
53
|
+
window.sessionStorage.clear();
|
|
54
|
+
setUrl("/dashboard");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
describe("ProjectUtil", () => {
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
window.localStorage.clear();
|
|
60
|
+
window.sessionStorage.clear();
|
|
61
|
+
setUrl("/dashboard");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
window.localStorage.clear();
|
|
66
|
+
window.sessionStorage.clear();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("the reported bug: reopening the dashboard", () => {
|
|
70
|
+
test("reopens the project the user was last in, not the first project", () => {
|
|
71
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
72
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
73
|
+
const projectZ: Project = buildProject(PROJECT_Z_ID, "Project Z");
|
|
74
|
+
|
|
75
|
+
// The user is working in Project Y.
|
|
76
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
77
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
78
|
+
|
|
79
|
+
// ... and closes the dashboard, then comes back to /dashboard.
|
|
80
|
+
closeTheTab();
|
|
81
|
+
|
|
82
|
+
const projectToLoad: Project | null = ProjectUtil.getProjectToLoad([
|
|
83
|
+
projectX,
|
|
84
|
+
projectY,
|
|
85
|
+
projectZ,
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
expect(projectToLoad?._id).toBe(PROJECT_Y_ID);
|
|
89
|
+
expect(projectToLoad?.name).toBe("Project Y");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("keeps reopening the same project across repeated visits", () => {
|
|
93
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
94
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
95
|
+
const projects: Array<Project> = [projectX, projectY];
|
|
96
|
+
|
|
97
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
98
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
99
|
+
|
|
100
|
+
for (let visit: number = 0; visit < 3; visit++) {
|
|
101
|
+
closeTheTab();
|
|
102
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_Y_ID);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("switching projects updates what gets reopened", () => {
|
|
107
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
108
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
109
|
+
const projects: Array<Project> = [projectX, projectY];
|
|
110
|
+
|
|
111
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
112
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
113
|
+
|
|
114
|
+
// The user now switches to Project X.
|
|
115
|
+
setUrl(`/dashboard/${PROJECT_X_ID}/home`);
|
|
116
|
+
ProjectUtil.setCurrentProject(projectX);
|
|
117
|
+
|
|
118
|
+
closeTheTab();
|
|
119
|
+
|
|
120
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_X_ID);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("falls back to the first project when the remembered one is gone", () => {
|
|
124
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
125
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
126
|
+
|
|
127
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
128
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
129
|
+
|
|
130
|
+
closeTheTab();
|
|
131
|
+
|
|
132
|
+
// The user has since been removed from Project Y.
|
|
133
|
+
expect(ProjectUtil.getProjectToLoad([projectX])?._id).toBe(PROJECT_X_ID);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a user with a single project is unaffected", () => {
|
|
137
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
138
|
+
|
|
139
|
+
closeTheTab();
|
|
140
|
+
|
|
141
|
+
expect(ProjectUtil.getProjectToLoad([projectX])?._id).toBe(PROJECT_X_ID);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe("getProjectIdFromUrl", () => {
|
|
146
|
+
test("reads the project id off a project scoped route", () => {
|
|
147
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
148
|
+
|
|
149
|
+
expect(ProjectUtil.getProjectIdFromUrl()?.toString()).toBe(PROJECT_Y_ID);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("reads the project id off a deeply nested route", () => {
|
|
153
|
+
setUrl(
|
|
154
|
+
`/dashboard/${PROJECT_Y_ID}/monitors/44444444-4444-4444-4444-444444444444/criteria`,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
expect(ProjectUtil.getProjectIdFromUrl()?.toString()).toBe(PROJECT_Y_ID);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("survives a query string and a hash", () => {
|
|
161
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/logs?query=error#tail`);
|
|
162
|
+
|
|
163
|
+
expect(ProjectUtil.getProjectIdFromUrl()?.toString()).toBe(PROJECT_Y_ID);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("returns null on routes that carry no project id", () => {
|
|
167
|
+
setUrl("/dashboard");
|
|
168
|
+
expect(ProjectUtil.getProjectIdFromUrl()).toBeNull();
|
|
169
|
+
|
|
170
|
+
setUrl("/dashboard/");
|
|
171
|
+
expect(ProjectUtil.getProjectIdFromUrl()).toBeNull();
|
|
172
|
+
|
|
173
|
+
setUrl("/dashboard/welcome");
|
|
174
|
+
expect(ProjectUtil.getProjectIdFromUrl()).toBeNull();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("returns null for an unpopulated route template", () => {
|
|
178
|
+
setUrl("/dashboard/:projectId/home");
|
|
179
|
+
|
|
180
|
+
expect(ProjectUtil.getProjectIdFromUrl()).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("returns null when the segment is not a uuid", () => {
|
|
184
|
+
setUrl("/dashboard/not-a-uuid/home");
|
|
185
|
+
|
|
186
|
+
expect(ProjectUtil.getProjectIdFromUrl()).toBeNull();
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe("getCurrentProjectId precedence", () => {
|
|
191
|
+
test("the URL wins over everything else", () => {
|
|
192
|
+
window.sessionStorage.setItem(
|
|
193
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
194
|
+
PROJECT_X_ID,
|
|
195
|
+
);
|
|
196
|
+
window.localStorage.setItem(
|
|
197
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
198
|
+
PROJECT_Z_ID,
|
|
199
|
+
);
|
|
200
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
201
|
+
|
|
202
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_Y_ID);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("this tab's session wins when the route has no project id", () => {
|
|
206
|
+
window.sessionStorage.setItem(
|
|
207
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
208
|
+
PROJECT_X_ID,
|
|
209
|
+
);
|
|
210
|
+
window.localStorage.setItem(
|
|
211
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
212
|
+
PROJECT_Z_ID,
|
|
213
|
+
);
|
|
214
|
+
setUrl("/dashboard");
|
|
215
|
+
|
|
216
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_X_ID);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test("the last accessed project is used once the session is gone", () => {
|
|
220
|
+
window.localStorage.setItem(
|
|
221
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
222
|
+
PROJECT_Z_ID,
|
|
223
|
+
);
|
|
224
|
+
setUrl("/dashboard");
|
|
225
|
+
|
|
226
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_Z_ID);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("returns null when nothing is known", () => {
|
|
230
|
+
expect(ProjectUtil.getCurrentProjectId()).toBeNull();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("ignores a corrupt session value and falls through", () => {
|
|
234
|
+
window.sessionStorage.setItem(
|
|
235
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
236
|
+
"not-a-uuid",
|
|
237
|
+
);
|
|
238
|
+
window.localStorage.setItem(
|
|
239
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
240
|
+
PROJECT_Z_ID,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_Z_ID);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("ignores a corrupt last accessed value", () => {
|
|
247
|
+
window.localStorage.setItem(
|
|
248
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
249
|
+
"not-a-uuid",
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
expect(ProjectUtil.getCurrentProjectId()).toBeNull();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("a second tab on another project does not disturb the first", () => {
|
|
256
|
+
/*
|
|
257
|
+
* Session storage is per tab, so tab 1 stays on X even though the last
|
|
258
|
+
* project opened on this browser is Z.
|
|
259
|
+
*/
|
|
260
|
+
window.localStorage.setItem(
|
|
261
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
262
|
+
PROJECT_Z_ID,
|
|
263
|
+
);
|
|
264
|
+
window.sessionStorage.setItem(
|
|
265
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
266
|
+
PROJECT_X_ID,
|
|
267
|
+
);
|
|
268
|
+
|
|
269
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_X_ID);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("a deep link into another project is not hijacked by a stale session", () => {
|
|
273
|
+
// The classic case: an alert email opened in a tab already on Project X.
|
|
274
|
+
window.sessionStorage.setItem(
|
|
275
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
276
|
+
PROJECT_X_ID,
|
|
277
|
+
);
|
|
278
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/alerts`);
|
|
279
|
+
|
|
280
|
+
expect(ProjectUtil.getCurrentProjectId()?.toString()).toBe(PROJECT_Y_ID);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe("last accessed project id", () => {
|
|
285
|
+
test("round trips an ObjectID", () => {
|
|
286
|
+
ProjectUtil.setLastAccessedProjectId(new ObjectID(PROJECT_Y_ID));
|
|
287
|
+
|
|
288
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
289
|
+
PROJECT_Y_ID,
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("round trips a string", () => {
|
|
294
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
295
|
+
|
|
296
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
297
|
+
PROJECT_Y_ID,
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("is persisted in local storage so it outlives the tab", () => {
|
|
302
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
303
|
+
|
|
304
|
+
expect(
|
|
305
|
+
window.localStorage.getItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY),
|
|
306
|
+
).toBe(PROJECT_Y_ID);
|
|
307
|
+
|
|
308
|
+
window.sessionStorage.clear();
|
|
309
|
+
|
|
310
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
311
|
+
PROJECT_Y_ID,
|
|
312
|
+
);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("returns null when nothing was ever stored", () => {
|
|
316
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test.each([
|
|
320
|
+
["null", null],
|
|
321
|
+
["undefined", undefined],
|
|
322
|
+
["an empty string", ""],
|
|
323
|
+
["a non-uuid", "not-a-uuid"],
|
|
324
|
+
["an unpopulated route param", ":projectId"],
|
|
325
|
+
])("clears rather than persisting %s", (_label: string, value: unknown) => {
|
|
326
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
327
|
+
|
|
328
|
+
ProjectUtil.setLastAccessedProjectId(
|
|
329
|
+
value as ObjectID | string | null | undefined,
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
333
|
+
expect(
|
|
334
|
+
window.localStorage.getItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY),
|
|
335
|
+
).toBeNull();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("clearLastAccessedProjectId removes the key", () => {
|
|
339
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
340
|
+
|
|
341
|
+
ProjectUtil.clearLastAccessedProjectId();
|
|
342
|
+
|
|
343
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
344
|
+
expect(
|
|
345
|
+
window.localStorage.getItem(LAST_ACCESSED_PROJECT_ID_STORAGE_KEY),
|
|
346
|
+
).toBeNull();
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("clearing it is safe when nothing was stored", () => {
|
|
350
|
+
expect(() => {
|
|
351
|
+
return ProjectUtil.clearLastAccessedProjectId();
|
|
352
|
+
}).not.toThrow();
|
|
353
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
describe("setCurrentProject", () => {
|
|
358
|
+
test("records the project as both current and last accessed", () => {
|
|
359
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
360
|
+
|
|
361
|
+
expect(
|
|
362
|
+
window.sessionStorage.getItem(CURRENT_PROJECT_ID_STORAGE_KEY),
|
|
363
|
+
).toBe(PROJECT_Y_ID);
|
|
364
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
365
|
+
PROJECT_Y_ID,
|
|
366
|
+
);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
test("accepts a plain JSON project", () => {
|
|
370
|
+
ProjectUtil.setCurrentProject({
|
|
371
|
+
_id: PROJECT_Y_ID,
|
|
372
|
+
name: "Project Y",
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
376
|
+
PROJECT_Y_ID,
|
|
377
|
+
);
|
|
378
|
+
expect(ProjectUtil.getCurrentProject()?._id).toBe(PROJECT_Y_ID);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("caches the project so it can be read back", () => {
|
|
382
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
383
|
+
|
|
384
|
+
const currentProject: Project | null = ProjectUtil.getCurrentProject();
|
|
385
|
+
|
|
386
|
+
expect(currentProject).toBeInstanceOf(Project);
|
|
387
|
+
expect(currentProject?._id).toBe(PROJECT_Y_ID);
|
|
388
|
+
expect(currentProject?.name).toBe("Project Y");
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("the cached project is still readable after the tab is closed", () => {
|
|
392
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
393
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
394
|
+
|
|
395
|
+
closeTheTab();
|
|
396
|
+
|
|
397
|
+
expect(ProjectUtil.getCurrentProject()?._id).toBe(PROJECT_Y_ID);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("does not clobber the remembered project when the payload has no id", () => {
|
|
401
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
402
|
+
|
|
403
|
+
ProjectUtil.setCurrentProject({ name: "Project Y" });
|
|
404
|
+
|
|
405
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
406
|
+
PROJECT_Y_ID,
|
|
407
|
+
);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("switching projects overwrites the previous one", () => {
|
|
411
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
412
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_X_ID, "Project X"));
|
|
413
|
+
|
|
414
|
+
expect(ProjectUtil.getLastAccessedProjectId()?.toString()).toBe(
|
|
415
|
+
PROJECT_X_ID,
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
describe("clearCurrentProject", () => {
|
|
421
|
+
test("forgets the project everywhere", () => {
|
|
422
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
423
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
424
|
+
|
|
425
|
+
setUrl("/dashboard");
|
|
426
|
+
ProjectUtil.clearCurrentProject();
|
|
427
|
+
|
|
428
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
429
|
+
expect(ProjectUtil.getCurrentProjectId()).toBeNull();
|
|
430
|
+
expect(ProjectUtil.getCurrentProject()).toBeNull();
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("a deleted project is not reopened on the next visit", () => {
|
|
434
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
435
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
436
|
+
|
|
437
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/settings/danger-zone`);
|
|
438
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
439
|
+
|
|
440
|
+
// The user deletes Project Y from the danger zone.
|
|
441
|
+
ProjectUtil.clearCurrentProject();
|
|
442
|
+
closeTheTab();
|
|
443
|
+
|
|
444
|
+
expect(ProjectUtil.getProjectToLoad([projectX])?._id).toBe(PROJECT_X_ID);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("is safe to call when nothing is stored", () => {
|
|
448
|
+
expect(() => {
|
|
449
|
+
return ProjectUtil.clearCurrentProject();
|
|
450
|
+
}).not.toThrow();
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
describe("getCurrentProject", () => {
|
|
455
|
+
test("returns null when no project is known", () => {
|
|
456
|
+
expect(ProjectUtil.getCurrentProject()).toBeNull();
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
test("returns null when the id is known but nothing was cached", () => {
|
|
460
|
+
window.localStorage.setItem(
|
|
461
|
+
LAST_ACCESSED_PROJECT_ID_STORAGE_KEY,
|
|
462
|
+
PROJECT_Y_ID,
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(ProjectUtil.getCurrentProject()).toBeNull();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("resolves the cached project through the last accessed id", () => {
|
|
469
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
470
|
+
ProjectUtil.setCurrentProject(buildProject(PROJECT_Y_ID, "Project Y"));
|
|
471
|
+
|
|
472
|
+
closeTheTab();
|
|
473
|
+
|
|
474
|
+
const currentProject: Project | null = ProjectUtil.getCurrentProject();
|
|
475
|
+
|
|
476
|
+
expect(currentProject?._id).toBe(PROJECT_Y_ID);
|
|
477
|
+
expect(currentProject?.name).toBe("Project Y");
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
describe("getProjectToSelect", () => {
|
|
482
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
483
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
484
|
+
const projectZ: Project = buildProject(PROJECT_Z_ID, "Project Z");
|
|
485
|
+
const projects: Array<Project> = [projectX, projectY, projectZ];
|
|
486
|
+
|
|
487
|
+
test("prefers the current project", () => {
|
|
488
|
+
expect(
|
|
489
|
+
ProjectUtil.getProjectToSelect({
|
|
490
|
+
projects: projects,
|
|
491
|
+
currentProjectId: new ObjectID(PROJECT_Z_ID),
|
|
492
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
493
|
+
})?._id,
|
|
494
|
+
).toBe(PROJECT_Z_ID);
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
test("falls back to the last accessed project", () => {
|
|
498
|
+
expect(
|
|
499
|
+
ProjectUtil.getProjectToSelect({
|
|
500
|
+
projects: projects,
|
|
501
|
+
currentProjectId: null,
|
|
502
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
503
|
+
})?._id,
|
|
504
|
+
).toBe(PROJECT_Y_ID);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("skips a current project the user no longer has access to", () => {
|
|
508
|
+
expect(
|
|
509
|
+
ProjectUtil.getProjectToSelect({
|
|
510
|
+
projects: [projectX, projectY],
|
|
511
|
+
currentProjectId: new ObjectID(PROJECT_Z_ID),
|
|
512
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
513
|
+
})?._id,
|
|
514
|
+
).toBe(PROJECT_Y_ID);
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
test("falls back to the first project when no candidate is accessible", () => {
|
|
518
|
+
expect(
|
|
519
|
+
ProjectUtil.getProjectToSelect({
|
|
520
|
+
projects: [projectX],
|
|
521
|
+
currentProjectId: new ObjectID(PROJECT_Z_ID),
|
|
522
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
523
|
+
})?._id,
|
|
524
|
+
).toBe(PROJECT_X_ID);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
test("falls back to the first project when there are no candidates", () => {
|
|
528
|
+
expect(
|
|
529
|
+
ProjectUtil.getProjectToSelect({
|
|
530
|
+
projects: projects,
|
|
531
|
+
})?._id,
|
|
532
|
+
).toBe(PROJECT_X_ID);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("returns null when the user has no projects", () => {
|
|
536
|
+
expect(
|
|
537
|
+
ProjectUtil.getProjectToSelect({
|
|
538
|
+
projects: [],
|
|
539
|
+
currentProjectId: new ObjectID(PROJECT_Y_ID),
|
|
540
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Z_ID),
|
|
541
|
+
}),
|
|
542
|
+
).toBeNull();
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
test("returns the project object from the list, not a stale copy", () => {
|
|
546
|
+
const freshProjectY: Project = buildProject(
|
|
547
|
+
PROJECT_Y_ID,
|
|
548
|
+
"Project Y (renamed)",
|
|
549
|
+
);
|
|
550
|
+
|
|
551
|
+
expect(
|
|
552
|
+
ProjectUtil.getProjectToSelect({
|
|
553
|
+
projects: [projectX, freshProjectY],
|
|
554
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
555
|
+
})?.name,
|
|
556
|
+
).toBe("Project Y (renamed)");
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test("tolerates holes in the project list", () => {
|
|
560
|
+
expect(
|
|
561
|
+
ProjectUtil.getProjectToSelect({
|
|
562
|
+
projects: [
|
|
563
|
+
null as unknown as Project,
|
|
564
|
+
undefined as unknown as Project,
|
|
565
|
+
projectY,
|
|
566
|
+
],
|
|
567
|
+
currentProjectId: null,
|
|
568
|
+
})?._id,
|
|
569
|
+
).toBe(PROJECT_Y_ID);
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
test("tolerates a project list that is entirely holes", () => {
|
|
573
|
+
expect(
|
|
574
|
+
ProjectUtil.getProjectToSelect({
|
|
575
|
+
projects: [null as unknown as Project],
|
|
576
|
+
}),
|
|
577
|
+
).toBeNull();
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
test("tolerates a missing project list", () => {
|
|
581
|
+
expect(
|
|
582
|
+
ProjectUtil.getProjectToSelect({
|
|
583
|
+
projects: undefined as unknown as Array<Project>,
|
|
584
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
585
|
+
}),
|
|
586
|
+
).toBeNull();
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
test("ignores projects without an id", () => {
|
|
590
|
+
const projectWithoutId: Project = new Project();
|
|
591
|
+
projectWithoutId.name = "Broken";
|
|
592
|
+
|
|
593
|
+
expect(
|
|
594
|
+
ProjectUtil.getProjectToSelect({
|
|
595
|
+
projects: [projectWithoutId, projectY],
|
|
596
|
+
lastAccessedProjectId: new ObjectID(PROJECT_Y_ID),
|
|
597
|
+
})?._id,
|
|
598
|
+
).toBe(PROJECT_Y_ID);
|
|
599
|
+
});
|
|
600
|
+
|
|
601
|
+
test("never falls back to a project without an id", () => {
|
|
602
|
+
/*
|
|
603
|
+
* The dashboard routes by project id, so an id-less project would send
|
|
604
|
+
* the user to /dashboard/undefined.
|
|
605
|
+
*/
|
|
606
|
+
const projectWithoutId: Project = new Project();
|
|
607
|
+
projectWithoutId.name = "Broken";
|
|
608
|
+
|
|
609
|
+
expect(
|
|
610
|
+
ProjectUtil.getProjectToSelect({
|
|
611
|
+
projects: [projectWithoutId, projectY],
|
|
612
|
+
})?._id,
|
|
613
|
+
).toBe(PROJECT_Y_ID);
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("returns null when no project has an id", () => {
|
|
617
|
+
expect(
|
|
618
|
+
ProjectUtil.getProjectToSelect({
|
|
619
|
+
projects: [new Project(), new Project()],
|
|
620
|
+
}),
|
|
621
|
+
).toBeNull();
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
|
|
625
|
+
/*
|
|
626
|
+
* This is the decision the dashboard's project picker makes every time the
|
|
627
|
+
* list of projects the user has access to lands.
|
|
628
|
+
*/
|
|
629
|
+
describe("getProjectToSelectOnProjectsLoaded", () => {
|
|
630
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
631
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
632
|
+
const projects: Array<Project> = [projectX, projectY];
|
|
633
|
+
|
|
634
|
+
test("opens the last accessed project when nothing is selected yet", () => {
|
|
635
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
636
|
+
|
|
637
|
+
expect(
|
|
638
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
639
|
+
projects: projects,
|
|
640
|
+
selectedProject: null,
|
|
641
|
+
})?._id,
|
|
642
|
+
).toBe(PROJECT_Y_ID);
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
test("opens the first project when nothing is selected and nothing is remembered", () => {
|
|
646
|
+
expect(
|
|
647
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
648
|
+
projects: projects,
|
|
649
|
+
selectedProject: null,
|
|
650
|
+
})?._id,
|
|
651
|
+
).toBe(PROJECT_X_ID);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
test("leaves an already selected project alone", () => {
|
|
655
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
656
|
+
|
|
657
|
+
expect(
|
|
658
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
659
|
+
projects: projects,
|
|
660
|
+
selectedProject: projectX,
|
|
661
|
+
}),
|
|
662
|
+
).toBeNull();
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
test("does not overrule a project the user just created", () => {
|
|
666
|
+
/*
|
|
667
|
+
* Creating a project selects it and appends it to the list, which
|
|
668
|
+
* re-triggers this decision. The remembered project must not win.
|
|
669
|
+
*/
|
|
670
|
+
const newProject: Project = buildProject(PROJECT_Z_ID, "Brand New");
|
|
671
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_X_ID);
|
|
672
|
+
|
|
673
|
+
expect(
|
|
674
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
675
|
+
projects: [...projects, newProject],
|
|
676
|
+
selectedProject: newProject,
|
|
677
|
+
}),
|
|
678
|
+
).toBeNull();
|
|
679
|
+
});
|
|
680
|
+
|
|
681
|
+
test("corrects a selection the user no longer has access to", () => {
|
|
682
|
+
const removedProject: Project = buildProject(PROJECT_Z_ID, "Project Z");
|
|
683
|
+
|
|
684
|
+
expect(
|
|
685
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
686
|
+
projects: projects,
|
|
687
|
+
selectedProject: removedProject,
|
|
688
|
+
})?._id,
|
|
689
|
+
).toBe(PROJECT_X_ID);
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
test("matches the selection by id, not by object identity", () => {
|
|
693
|
+
const sameProjectDifferentInstance: Project = buildProject(
|
|
694
|
+
PROJECT_Y_ID,
|
|
695
|
+
"Project Y",
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
expect(
|
|
699
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
700
|
+
projects: projects,
|
|
701
|
+
selectedProject: sameProjectDifferentInstance,
|
|
702
|
+
}),
|
|
703
|
+
).toBeNull();
|
|
704
|
+
});
|
|
705
|
+
|
|
706
|
+
test("waits while the project list is still loading", () => {
|
|
707
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
708
|
+
|
|
709
|
+
expect(
|
|
710
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
711
|
+
projects: [],
|
|
712
|
+
selectedProject: null,
|
|
713
|
+
}),
|
|
714
|
+
).toBeNull();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("does nothing for a user with no projects", () => {
|
|
718
|
+
expect(
|
|
719
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
720
|
+
projects: [],
|
|
721
|
+
selectedProject: null,
|
|
722
|
+
}),
|
|
723
|
+
).toBeNull();
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
test("does not treat an id-less selection as a match", () => {
|
|
727
|
+
const projectWithoutId: Project = new Project();
|
|
728
|
+
projectWithoutId.name = "Half loaded";
|
|
729
|
+
|
|
730
|
+
expect(
|
|
731
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
732
|
+
projects: [new Project(), projectY],
|
|
733
|
+
selectedProject: projectWithoutId,
|
|
734
|
+
})?._id,
|
|
735
|
+
).toBe(PROJECT_Y_ID);
|
|
736
|
+
});
|
|
737
|
+
|
|
738
|
+
test("settles after one pass - the picked project is then left alone", () => {
|
|
739
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
740
|
+
|
|
741
|
+
const firstPass: Project | null =
|
|
742
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
743
|
+
projects: projects,
|
|
744
|
+
selectedProject: null,
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
expect(firstPass?._id).toBe(PROJECT_Y_ID);
|
|
748
|
+
|
|
749
|
+
expect(
|
|
750
|
+
ProjectUtil.getProjectToSelectOnProjectsLoaded({
|
|
751
|
+
projects: projects,
|
|
752
|
+
selectedProject: firstPass,
|
|
753
|
+
}),
|
|
754
|
+
).toBeNull();
|
|
755
|
+
});
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
describe("getProjectToLoad", () => {
|
|
759
|
+
const projectX: Project = buildProject(PROJECT_X_ID, "Project X");
|
|
760
|
+
const projectY: Project = buildProject(PROJECT_Y_ID, "Project Y");
|
|
761
|
+
const projects: Array<Project> = [projectX, projectY];
|
|
762
|
+
|
|
763
|
+
test("loads the project in the URL", () => {
|
|
764
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_X_ID);
|
|
765
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
766
|
+
|
|
767
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_Y_ID);
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("loads this tab's project when the route has no project id", () => {
|
|
771
|
+
window.sessionStorage.setItem(
|
|
772
|
+
CURRENT_PROJECT_ID_STORAGE_KEY,
|
|
773
|
+
PROJECT_Y_ID,
|
|
774
|
+
);
|
|
775
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_X_ID);
|
|
776
|
+
|
|
777
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_Y_ID);
|
|
778
|
+
});
|
|
779
|
+
|
|
780
|
+
test("loads the last accessed project on a fresh session", () => {
|
|
781
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
782
|
+
|
|
783
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_Y_ID);
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
test("loads the first project for a brand new user", () => {
|
|
787
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_X_ID);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
test("returns null while the project list is still loading", () => {
|
|
791
|
+
ProjectUtil.setLastAccessedProjectId(PROJECT_Y_ID);
|
|
792
|
+
|
|
793
|
+
expect(ProjectUtil.getProjectToLoad([])).toBeNull();
|
|
794
|
+
});
|
|
795
|
+
|
|
796
|
+
test("does not resurrect another user's project after a logout", () => {
|
|
797
|
+
setUrl(`/dashboard/${PROJECT_Y_ID}/home`);
|
|
798
|
+
ProjectUtil.setCurrentProject(projectY);
|
|
799
|
+
|
|
800
|
+
// Logout clears all of local and session storage.
|
|
801
|
+
window.localStorage.clear();
|
|
802
|
+
window.sessionStorage.clear();
|
|
803
|
+
setUrl("/dashboard");
|
|
804
|
+
|
|
805
|
+
expect(ProjectUtil.getLastAccessedProjectId()).toBeNull();
|
|
806
|
+
expect(ProjectUtil.getProjectToLoad(projects)?._id).toBe(PROJECT_X_ID);
|
|
807
|
+
});
|
|
808
|
+
});
|
|
809
|
+
});
|