@oneuptime/common 11.7.1 → 11.7.3
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/AnalyticsModels/KubernetesCostAllocation.ts +103 -0
- 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/TableView.ts +40 -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/1785241000000-AddColumnsToTableView.ts +13 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -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/AnalyticsModels/KubernetesCostAllocation.test.ts +16 -0
- 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/JSONFunctions.test.ts +103 -1
- 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/ModelTable/BaseModelTableColumnCustomization.test.tsx +679 -0
- package/Tests/UI/Components/ModelTable/ColumnCustomizationModal.test.tsx +572 -0
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +1678 -0
- package/Tests/UI/Components/ModelTable/CustomFieldColumns.test.tsx +1094 -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/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/Tests/Utils/UserPreferences.test.ts +563 -0
- package/Types/JSONFunctions.ts +7 -2
- package/Types/Kubernetes/KubernetesCostIngest.ts +37 -1
- 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/ModelForm.tsx +41 -18
- package/UI/Components/MasterPage/MasterPage.tsx +65 -0
- package/UI/Components/ModelDetail/CardModelDetail.tsx +23 -1
- package/UI/Components/ModelDetail/ModelDetail.tsx +46 -1
- package/UI/Components/ModelTable/BaseModelTable.tsx +687 -34
- package/UI/Components/ModelTable/Column.ts +29 -0
- package/UI/Components/ModelTable/ColumnCustomizationModal.tsx +420 -0
- package/UI/Components/ModelTable/ColumnPreference.ts +482 -0
- package/UI/Components/ModelTable/CustomFieldColumns.tsx +326 -0
- package/UI/Components/ModelTable/ExportFromColumns.ts +57 -0
- package/UI/Components/ModelTable/TableView.tsx +24 -2
- package/UI/Components/ModelTable/useCustomFieldColumns.ts +150 -0
- package/UI/Components/Navbar/NavBar.tsx +66 -1
- package/UI/Components/SideMenu/SideMenu.tsx +24 -4
- package/UI/Components/Table/Table.tsx +66 -14
- package/UI/Components/Table/TableRow.tsx +180 -175
- package/UI/Components/Table/Types/Column.ts +20 -0
- package/UI/Utils/Project.ts +214 -18
- package/UI/Utils/TableColumnsToCsv.ts +86 -6
- package/UI/esbuild-config.js +34 -0
- package/Utils/Monitor/MonitorProbeSelectionUtil.ts +72 -0
- package/Utils/UserPreferences.ts +53 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js +86 -0
- package/build/dist/Models/AnalyticsModels/KubernetesCostAllocation.js.map +1 -1
- 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/TableView.js +40 -0
- package/build/dist/Models/DatabaseModels/TableView.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/1785241000000-AddColumnsToTableView.js +12 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1785241000000-AddColumnsToTableView.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -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/JSONFunctions.js +8 -3
- package/build/dist/Types/JSONFunctions.js.map +1 -1
- package/build/dist/Types/Kubernetes/KubernetesCostIngest.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/ModelForm.js +37 -17
- package/build/dist/UI/Components/Forms/ModelForm.js.map +1 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.js +50 -1
- package/build/dist/UI/Components/MasterPage/MasterPage.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 +472 -27
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js +189 -0
- package/build/dist/UI/Components/ModelTable/ColumnCustomizationModal.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js +258 -0
- package/build/dist/UI/Components/ModelTable/ColumnPreference.js.map +1 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js +168 -0
- package/build/dist/UI/Components/ModelTable/CustomFieldColumns.js.map +1 -0
- 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/ModelTable/TableView.js +13 -2
- package/build/dist/UI/Components/ModelTable/TableView.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.js +84 -0
- package/build/dist/UI/Components/ModelTable/useCustomFieldColumns.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/SideMenu/SideMenu.js +15 -5
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/Table/Table.js +46 -13
- package/build/dist/UI/Components/Table/Table.js.map +1 -1
- package/build/dist/UI/Components/Table/TableRow.js +11 -6
- package/build/dist/UI/Components/Table/TableRow.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/Utils/Monitor/MonitorProbeSelectionUtil.js +52 -0
- package/build/dist/Utils/Monitor/MonitorProbeSelectionUtil.js.map +1 -0
- package/build/dist/Utils/UserPreferences.js +33 -0
- package/build/dist/Utils/UserPreferences.js.map +1 -1
- package/package.json +4 -3
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import Column from "./Column";
|
|
2
|
+
import Columns from "./Columns";
|
|
3
|
+
import FieldType from "../Types/FieldType";
|
|
4
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
5
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
6
|
+
import CustomFieldType from "../../../Types/CustomField/CustomFieldType";
|
|
7
|
+
import { JSONObject } from "../../../Types/JSON";
|
|
8
|
+
import React, { ReactElement } from "react";
|
|
9
|
+
|
|
10
|
+
/*
|
|
11
|
+
* ---------------------------------------------------------------------------
|
|
12
|
+
* One table column per custom field
|
|
13
|
+
* ---------------------------------------------------------------------------
|
|
14
|
+
*
|
|
15
|
+
* Custom field *definitions* live in a per-resource table (MonitorCustomField,
|
|
16
|
+
* IncidentCustomField, ...); the *values* live in a single `customFields`
|
|
17
|
+
* jsonb column on the resource itself, keyed by the definition's name. So a
|
|
18
|
+
* generated column reads the whole JSON column and picks one key out of it:
|
|
19
|
+
*
|
|
20
|
+
* field: { customFields: true } <- a real model column
|
|
21
|
+
* selectedProperty: "Severity" <- the key inside it
|
|
22
|
+
*
|
|
23
|
+
* which BaseModelTable turns into the column key "customFields.Severity".
|
|
24
|
+
*
|
|
25
|
+
* Two things about that shape are load-bearing:
|
|
26
|
+
*
|
|
27
|
+
* - the declared field has to be the real column. A synthetic
|
|
28
|
+
* `field: { "customFields.Severity": true }` would fail the model's column
|
|
29
|
+
* check in getSelectFromColumns and throw the whole table away.
|
|
30
|
+
*
|
|
31
|
+
* - sorting has to be off. Nothing downstream validates `sort` before it
|
|
32
|
+
* reaches TypeORM, so a clickable header here would send an unknown
|
|
33
|
+
* property into the query builder. Postgres can only order by a jsonb
|
|
34
|
+
* key through an explicit ->> expression, which the sort path has no
|
|
35
|
+
* concept of.
|
|
36
|
+
*
|
|
37
|
+
* Cells render through `getElement` for the same class of reason: the typed
|
|
38
|
+
* cell renderers index `item[column.key]` directly, so they would look for a
|
|
39
|
+
* literal "customFields.Severity" property and find nothing. Only the
|
|
40
|
+
* fall-through text branch understands a dotted key.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
export interface CustomFieldDefinition {
|
|
44
|
+
name: string;
|
|
45
|
+
description?: string | undefined;
|
|
46
|
+
customFieldType?: CustomFieldType | undefined;
|
|
47
|
+
dropdownOptions?: string | undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const CustomFieldsColumnKey: string = "customFields";
|
|
51
|
+
|
|
52
|
+
export type ParseDropdownOptionsFunction = (
|
|
53
|
+
value: string | undefined | null,
|
|
54
|
+
) => Array<string>;
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* Dropdown options are stored as one option per line, the same format the
|
|
58
|
+
* settings page writes.
|
|
59
|
+
*/
|
|
60
|
+
export const parseDropdownOptions: ParseDropdownOptionsFunction = (
|
|
61
|
+
value: string | undefined | null,
|
|
62
|
+
): Array<string> => {
|
|
63
|
+
if (typeof value !== "string" || !value) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return value
|
|
68
|
+
.split("\n")
|
|
69
|
+
.map((line: string) => {
|
|
70
|
+
return line.trim();
|
|
71
|
+
})
|
|
72
|
+
.filter((line: string) => {
|
|
73
|
+
return line.length > 0;
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export type GetCustomFieldDefinitionsFunction = (
|
|
78
|
+
items: Array<BaseModel | JSONObject>,
|
|
79
|
+
) => Array<CustomFieldDefinition>;
|
|
80
|
+
|
|
81
|
+
/*
|
|
82
|
+
* Normalise whatever the definition API returned into a plain, sorted list.
|
|
83
|
+
*
|
|
84
|
+
* Sorting matters: the definition models carry no ordering column and the
|
|
85
|
+
* list endpoint is queried with an empty sort, so Postgres is free to hand
|
|
86
|
+
* back a different order on every request. Without a deterministic order
|
|
87
|
+
* here, a viewer's saved column layout would appear to shuffle itself
|
|
88
|
+
* between page loads.
|
|
89
|
+
*/
|
|
90
|
+
export const getCustomFieldDefinitions: GetCustomFieldDefinitionsFunction = (
|
|
91
|
+
items: Array<BaseModel | JSONObject>,
|
|
92
|
+
): Array<CustomFieldDefinition> => {
|
|
93
|
+
const definitions: Array<CustomFieldDefinition> = [];
|
|
94
|
+
const seenNames: Set<string> = new Set();
|
|
95
|
+
|
|
96
|
+
for (const item of items || []) {
|
|
97
|
+
const name: unknown = (item as JSONObject)?.["name"];
|
|
98
|
+
|
|
99
|
+
if (typeof name !== "string" || name.trim().length === 0) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const trimmedName: string = name.trim();
|
|
104
|
+
|
|
105
|
+
if (seenNames.has(trimmedName)) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
seenNames.add(trimmedName);
|
|
110
|
+
|
|
111
|
+
const description: unknown = (item as JSONObject)["description"];
|
|
112
|
+
const customFieldType: unknown = (item as JSONObject)["customFieldType"];
|
|
113
|
+
const dropdownOptions: unknown = (item as JSONObject)["dropdownOptions"];
|
|
114
|
+
|
|
115
|
+
definitions.push({
|
|
116
|
+
name: trimmedName,
|
|
117
|
+
description: typeof description === "string" ? description : undefined,
|
|
118
|
+
customFieldType:
|
|
119
|
+
typeof customFieldType === "string"
|
|
120
|
+
? (customFieldType as CustomFieldType)
|
|
121
|
+
: undefined,
|
|
122
|
+
dropdownOptions:
|
|
123
|
+
typeof dropdownOptions === "string" ? dropdownOptions : undefined,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return definitions.sort(
|
|
128
|
+
(a: CustomFieldDefinition, b: CustomFieldDefinition) => {
|
|
129
|
+
return a.name.localeCompare(b.name);
|
|
130
|
+
},
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type GetCustomFieldValueFunction = (
|
|
135
|
+
item: unknown,
|
|
136
|
+
name: string,
|
|
137
|
+
) => unknown;
|
|
138
|
+
|
|
139
|
+
export const getCustomFieldValue: GetCustomFieldValueFunction = (
|
|
140
|
+
item: unknown,
|
|
141
|
+
name: string,
|
|
142
|
+
): unknown => {
|
|
143
|
+
const customFields: unknown = (item as JSONObject | undefined)?.[
|
|
144
|
+
CustomFieldsColumnKey
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
if (!customFields || typeof customFields !== "object") {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return (customFields as JSONObject)[name];
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
type BadgeFunction = (data: { label: string; key: string }) => ReactElement;
|
|
155
|
+
|
|
156
|
+
const badge: BadgeFunction = (data: {
|
|
157
|
+
label: string;
|
|
158
|
+
key: string;
|
|
159
|
+
}): ReactElement => {
|
|
160
|
+
return (
|
|
161
|
+
<span
|
|
162
|
+
key={data.key}
|
|
163
|
+
className="inline-flex items-center gap-x-1.5 px-2 py-1 rounded-md bg-indigo-50 text-indigo-700 text-xs font-medium ring-1 ring-inset ring-indigo-700/10"
|
|
164
|
+
>
|
|
165
|
+
<svg
|
|
166
|
+
className="h-1.5 w-1.5 fill-indigo-500"
|
|
167
|
+
viewBox="0 0 6 6"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
>
|
|
170
|
+
<circle cx={3} cy={3} r={3} />
|
|
171
|
+
</svg>
|
|
172
|
+
{data.label}
|
|
173
|
+
</span>
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export type RenderCustomFieldValueFunction = (data: {
|
|
178
|
+
value: unknown;
|
|
179
|
+
definition: CustomFieldDefinition;
|
|
180
|
+
noValueMessage?: string | undefined;
|
|
181
|
+
}) => ReactElement;
|
|
182
|
+
|
|
183
|
+
export const renderCustomFieldValue: RenderCustomFieldValueFunction = (data: {
|
|
184
|
+
value: unknown;
|
|
185
|
+
definition: CustomFieldDefinition;
|
|
186
|
+
noValueMessage?: string | undefined;
|
|
187
|
+
}): ReactElement => {
|
|
188
|
+
const { value, definition } = data;
|
|
189
|
+
const noValueMessage: string = data.noValueMessage ?? "-";
|
|
190
|
+
|
|
191
|
+
const empty: ReactElement = (
|
|
192
|
+
<span className="text-gray-400">{noValueMessage}</span>
|
|
193
|
+
);
|
|
194
|
+
|
|
195
|
+
if (value === undefined || value === null || value === "") {
|
|
196
|
+
return empty;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (definition.customFieldType === CustomFieldType.Boolean) {
|
|
200
|
+
/*
|
|
201
|
+
* A boolean custom field renders "No" rather than falling into the empty
|
|
202
|
+
* branch above - `false` is a real answer, not a missing one.
|
|
203
|
+
*/
|
|
204
|
+
const isTrue: boolean = value === true || value === "true";
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<span
|
|
208
|
+
className={
|
|
209
|
+
isTrue
|
|
210
|
+
? "inline-flex items-center px-2 py-1 rounded-md bg-green-50 text-green-700 text-xs font-medium ring-1 ring-inset ring-green-600/20"
|
|
211
|
+
: "inline-flex items-center px-2 py-1 rounded-md bg-gray-50 text-gray-600 text-xs font-medium ring-1 ring-inset ring-gray-500/10"
|
|
212
|
+
}
|
|
213
|
+
>
|
|
214
|
+
{isTrue ? "Yes" : "No"}
|
|
215
|
+
</span>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (definition.customFieldType === CustomFieldType.MultiSelectDropdown) {
|
|
220
|
+
/*
|
|
221
|
+
* Tolerant on the way out: an older single-select value that was later
|
|
222
|
+
* switched to multi-select is still a bare string in the JSON.
|
|
223
|
+
*/
|
|
224
|
+
const values: Array<unknown> = Array.isArray(value) ? value : [value];
|
|
225
|
+
|
|
226
|
+
const labels: Array<string> = values
|
|
227
|
+
.filter((entry: unknown) => {
|
|
228
|
+
return entry !== undefined && entry !== null && entry !== "";
|
|
229
|
+
})
|
|
230
|
+
.map((entry: unknown) => {
|
|
231
|
+
return String(entry);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (labels.length === 0) {
|
|
235
|
+
return empty;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<div className="flex flex-wrap gap-1.5">
|
|
240
|
+
{labels.map((label: string, index: number) => {
|
|
241
|
+
return badge({ label, key: `${label}-${index}` });
|
|
242
|
+
})}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (definition.customFieldType === CustomFieldType.Dropdown) {
|
|
248
|
+
return badge({ label: String(value), key: String(value) });
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (Array.isArray(value)) {
|
|
252
|
+
return <span>{value.join(", ")}</span>;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (typeof value === "object") {
|
|
256
|
+
return <span>{JSON.stringify(value)}</span>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return <span>{String(value)}</span>;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
export type GetCustomFieldColumnsFunction = <
|
|
263
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
264
|
+
>(data: {
|
|
265
|
+
definitions: Array<CustomFieldDefinition>;
|
|
266
|
+
isHiddenByDefault?: boolean | undefined;
|
|
267
|
+
}) => Columns<TBaseModel>;
|
|
268
|
+
|
|
269
|
+
/*
|
|
270
|
+
* Custom field columns default to hidden. A project is free to define a dozen
|
|
271
|
+
* of them, and turning every one on by default would push the columns the
|
|
272
|
+
* table was designed around off the side of the screen the first time anyone
|
|
273
|
+
* opens it. They are all listed in the column picker, one click away.
|
|
274
|
+
*/
|
|
275
|
+
export const getCustomFieldColumns: GetCustomFieldColumnsFunction = <
|
|
276
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
277
|
+
>(data: {
|
|
278
|
+
definitions: Array<CustomFieldDefinition>;
|
|
279
|
+
isHiddenByDefault?: boolean | undefined;
|
|
280
|
+
}): Columns<TBaseModel> => {
|
|
281
|
+
const isHidden: boolean = data.isHiddenByDefault !== false;
|
|
282
|
+
|
|
283
|
+
return (data.definitions || []).map(
|
|
284
|
+
(definition: CustomFieldDefinition): Column<TBaseModel> => {
|
|
285
|
+
return {
|
|
286
|
+
id: `${CustomFieldsColumnKey}.${definition.name}`,
|
|
287
|
+
field: {
|
|
288
|
+
[CustomFieldsColumnKey]: true,
|
|
289
|
+
} as Column<TBaseModel>["field"],
|
|
290
|
+
selectedProperty: definition.name,
|
|
291
|
+
title: definition.name,
|
|
292
|
+
description: definition.description,
|
|
293
|
+
type: FieldType.Element,
|
|
294
|
+
/*
|
|
295
|
+
* See the file header: sorting a jsonb key is not something the
|
|
296
|
+
* query path can express.
|
|
297
|
+
*/
|
|
298
|
+
disableSort: true,
|
|
299
|
+
isHiddenByDefault: isHidden,
|
|
300
|
+
getExportValue: (item: TBaseModel): string => {
|
|
301
|
+
const value: unknown = getCustomFieldValue(item, definition.name);
|
|
302
|
+
|
|
303
|
+
if (value === undefined || value === null) {
|
|
304
|
+
return "";
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (Array.isArray(value)) {
|
|
308
|
+
return value.join(", ");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (typeof value === "object") {
|
|
312
|
+
return JSON.stringify(value);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return String(value);
|
|
316
|
+
},
|
|
317
|
+
getElement: (item: TBaseModel): ReactElement => {
|
|
318
|
+
return renderCustomFieldValue({
|
|
319
|
+
value: getCustomFieldValue(item, definition.name),
|
|
320
|
+
definition: definition,
|
|
321
|
+
});
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
},
|
|
325
|
+
);
|
|
326
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import Column from "./Column";
|
|
2
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
3
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
* Derives the fields a ModelTable column contributes to a CSV export from its
|
|
7
|
+
* column definition, and mirrors getSelectFromColumns: a column may declare
|
|
8
|
+
* MORE THAN ONE field when its cell is composed from several of them, and all
|
|
9
|
+
* of them are fetched. The Table column's `key` only ever holds the first, so
|
|
10
|
+
* an exporter reading `key` alone drops the rest - the alert "Affected
|
|
11
|
+
* Resources" cell spans hosts / kubernetesClusters / dockerHosts /
|
|
12
|
+
* podmanHosts / services, and only the hosts made it into the file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export function getExportKeysFromColumn<
|
|
16
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
17
|
+
>(data: {
|
|
18
|
+
column: Column<TBaseModel>;
|
|
19
|
+
/*
|
|
20
|
+
* The key the Table column renders and sorts by. It is the first declared
|
|
21
|
+
* field, with `selectedProperty` appended when the column picks a single
|
|
22
|
+
* property off a relation ("label.name"), so the export reads the same path
|
|
23
|
+
* the cell does.
|
|
24
|
+
*/
|
|
25
|
+
columnKey: string | null;
|
|
26
|
+
/*
|
|
27
|
+
* Secondary fields are gated on this exactly as they are in the select: a
|
|
28
|
+
* field the caller cannot read was never fetched, so exporting it would
|
|
29
|
+
* only ever produce an empty cell.
|
|
30
|
+
*/
|
|
31
|
+
hasPermissionToReadField?: ((field: string) => boolean) | undefined;
|
|
32
|
+
}): Array<string> {
|
|
33
|
+
const exportKeys: Array<string> = [];
|
|
34
|
+
|
|
35
|
+
if (data.columnKey) {
|
|
36
|
+
exportKeys.push(data.columnKey);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!data.column.field) {
|
|
40
|
+
return exportKeys;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const declaredKeys: Array<string> = Object.keys(data.column.field);
|
|
44
|
+
|
|
45
|
+
// The primary field is already covered by columnKey.
|
|
46
|
+
for (const key of declaredKeys.slice(1)) {
|
|
47
|
+
if (data.hasPermissionToReadField && !data.hasPermissionToReadField(key)) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!exportKeys.includes(key)) {
|
|
52
|
+
exportKeys.push(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return exportKeys;
|
|
57
|
+
}
|
|
@@ -40,6 +40,13 @@ export interface ComponentProps<T extends GenericObject> {
|
|
|
40
40
|
* `TableView.facets`). Owner: the parent's filter hook.
|
|
41
41
|
*/
|
|
42
42
|
currentFacetState?: JSONObject | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* The viewer's current column layout ({ order, hidden }), persisted on
|
|
45
|
+
* `TableView.columns` so a saved view restores the columns it was saved
|
|
46
|
+
* with. `undefined` means "the table's default layout", which is stored as
|
|
47
|
+
* no value at all.
|
|
48
|
+
*/
|
|
49
|
+
currentColumns?: JSONObject | undefined;
|
|
43
50
|
tableView: TableView | null;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -90,6 +97,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
|
|
|
90
97
|
query: true,
|
|
91
98
|
name: true,
|
|
92
99
|
facets: true,
|
|
100
|
+
columns: true,
|
|
93
101
|
},
|
|
94
102
|
sort: {
|
|
95
103
|
name: SortOrder.Ascending,
|
|
@@ -223,6 +231,17 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
|
|
|
223
231
|
|
|
224
232
|
const hasActiveFilters: boolean = hasQueryFilters || hasFacetState;
|
|
225
233
|
|
|
234
|
+
/*
|
|
235
|
+
* A customized column layout is worth saving on its own - "the columns I
|
|
236
|
+
* care about, in my order" is a view even when nothing is filtered.
|
|
237
|
+
*/
|
|
238
|
+
const hasColumnLayout: boolean = Boolean(
|
|
239
|
+
props.currentColumns &&
|
|
240
|
+
Object.keys(props.currentColumns as JSONObject).length > 0,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
const hasSavableState: boolean = hasActiveFilters || hasColumnLayout;
|
|
244
|
+
|
|
226
245
|
const getMenuContents: GetReactElementArrayFunction =
|
|
227
246
|
(): Array<ReactElement> => {
|
|
228
247
|
if (isLoading) {
|
|
@@ -239,7 +258,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
|
|
|
239
258
|
);
|
|
240
259
|
}
|
|
241
260
|
|
|
242
|
-
if (
|
|
261
|
+
if (hasSavableState) {
|
|
243
262
|
elements.push(
|
|
244
263
|
<MoreMenuItem
|
|
245
264
|
key={"save-new-view"}
|
|
@@ -359,6 +378,9 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
|
|
|
359
378
|
if (props.currentFacetState) {
|
|
360
379
|
tableView.facets = props.currentFacetState;
|
|
361
380
|
}
|
|
381
|
+
if (props.currentColumns) {
|
|
382
|
+
tableView.columns = props.currentColumns;
|
|
383
|
+
}
|
|
362
384
|
return Promise.resolve(tableView);
|
|
363
385
|
}}
|
|
364
386
|
onSuccess={async (tableView: TableView) => {
|
|
@@ -476,7 +498,7 @@ const TableViewElement: <T extends DatabaseBaseModel | AnalyticsBaseModel>(
|
|
|
476
498
|
if (
|
|
477
499
|
!isLoading &&
|
|
478
500
|
allTableViews.length === 0 &&
|
|
479
|
-
!
|
|
501
|
+
!hasSavableState &&
|
|
480
502
|
!currentlySelectedView
|
|
481
503
|
) {
|
|
482
504
|
return <></>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import Columns from "./Columns";
|
|
2
|
+
import {
|
|
3
|
+
CustomFieldDefinition,
|
|
4
|
+
getCustomFieldColumns,
|
|
5
|
+
getCustomFieldDefinitions,
|
|
6
|
+
} from "./CustomFieldColumns";
|
|
7
|
+
import ModelAPI from "../../Utils/ModelAPI/ModelAPI";
|
|
8
|
+
import ProjectUtil from "../../Utils/Project";
|
|
9
|
+
import { Logger } from "../../Utils/Logger";
|
|
10
|
+
import AnalyticsBaseModel from "../../../Models/AnalyticsModels/AnalyticsBaseModel/AnalyticsBaseModel";
|
|
11
|
+
import BaseModel from "../../../Models/DatabaseModels/DatabaseBaseModel/DatabaseBaseModel";
|
|
12
|
+
import ListResult from "../../../Types/BaseDatabase/ListResult";
|
|
13
|
+
import { LIMIT_PER_PROJECT } from "../../../Types/Database/LimitMax";
|
|
14
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
15
|
+
import { useEffect, useMemo, useState } from "react";
|
|
16
|
+
|
|
17
|
+
export interface CustomFieldColumnsResult<
|
|
18
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
19
|
+
> {
|
|
20
|
+
columns: Columns<TBaseModel>;
|
|
21
|
+
definitions: Array<CustomFieldDefinition>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
* Loads a resource's custom field *definitions* and turns them into table
|
|
28
|
+
* columns.
|
|
29
|
+
*
|
|
30
|
+
* The definitions arrive after the table's first render, which is fine: they
|
|
31
|
+
* only add columns, and every one of them is hidden until the viewer switches
|
|
32
|
+
* it on. What must NOT wait on this request is the `customFields` entry in the
|
|
33
|
+
* API select - the table does not refetch when its column set changes, so the
|
|
34
|
+
* select has to be right from the first request. BaseModelTable handles that
|
|
35
|
+
* from the synchronous `customFieldsModelType` prop instead.
|
|
36
|
+
*
|
|
37
|
+
* Pass `undefined` for the model type to switch the whole thing off; the hook
|
|
38
|
+
* still runs (hooks cannot be conditional) but makes no request.
|
|
39
|
+
*/
|
|
40
|
+
export type UseCustomFieldColumnsFunction = <
|
|
41
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
42
|
+
>(data: {
|
|
43
|
+
customFieldsModelType?: { new (): BaseModel } | undefined;
|
|
44
|
+
projectId?: ObjectID | null | undefined;
|
|
45
|
+
}) => CustomFieldColumnsResult<TBaseModel>;
|
|
46
|
+
|
|
47
|
+
const useCustomFieldColumns: UseCustomFieldColumnsFunction = <
|
|
48
|
+
TBaseModel extends BaseModel | AnalyticsBaseModel,
|
|
49
|
+
>(data: {
|
|
50
|
+
customFieldsModelType?: { new (): BaseModel } | undefined;
|
|
51
|
+
projectId?: ObjectID | null | undefined;
|
|
52
|
+
}): CustomFieldColumnsResult<TBaseModel> => {
|
|
53
|
+
const { customFieldsModelType } = data;
|
|
54
|
+
|
|
55
|
+
const [definitions, setDefinitions] = useState<Array<CustomFieldDefinition>>(
|
|
56
|
+
[],
|
|
57
|
+
);
|
|
58
|
+
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
59
|
+
const [error, setError] = useState<string>("");
|
|
60
|
+
|
|
61
|
+
const projectId: ObjectID | null =
|
|
62
|
+
data.projectId ?? ProjectUtil.getCurrentProjectId();
|
|
63
|
+
|
|
64
|
+
const projectIdString: string = projectId?.toString() || "";
|
|
65
|
+
const modelName: string = customFieldsModelType?.name || "";
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (!customFieldsModelType || !projectId) {
|
|
69
|
+
setDefinitions([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let isCancelled: boolean = false;
|
|
74
|
+
|
|
75
|
+
const load: () => Promise<void> = async (): Promise<void> => {
|
|
76
|
+
setIsLoading(true);
|
|
77
|
+
setError("");
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const result: ListResult<BaseModel> = await ModelAPI.getList<BaseModel>(
|
|
81
|
+
{
|
|
82
|
+
modelType: customFieldsModelType,
|
|
83
|
+
query: {
|
|
84
|
+
projectId: projectId,
|
|
85
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
86
|
+
} as any,
|
|
87
|
+
limit: LIMIT_PER_PROJECT,
|
|
88
|
+
skip: 0,
|
|
89
|
+
select: {
|
|
90
|
+
name: true,
|
|
91
|
+
customFieldType: true,
|
|
92
|
+
description: true,
|
|
93
|
+
dropdownOptions: true,
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
} as any,
|
|
96
|
+
sort: {},
|
|
97
|
+
},
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (isCancelled) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setDefinitions(getCustomFieldDefinitions(result.data || []));
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (isCancelled) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/*
|
|
111
|
+
* Custom fields are a paid feature and the caller may not have read
|
|
112
|
+
* access to the definition table, so a failure here is expected in
|
|
113
|
+
* normal operation. It costs the viewer some optional columns; it must
|
|
114
|
+
* never take the table down with it.
|
|
115
|
+
*/
|
|
116
|
+
Logger.warn(
|
|
117
|
+
`Could not load custom field definitions for ${modelName}: ${
|
|
118
|
+
(err as Error)?.message || String(err)
|
|
119
|
+
}`,
|
|
120
|
+
);
|
|
121
|
+
setDefinitions([]);
|
|
122
|
+
setError((err as Error)?.message || "");
|
|
123
|
+
} finally {
|
|
124
|
+
if (!isCancelled) {
|
|
125
|
+
setIsLoading(false);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
load().catch(() => {
|
|
131
|
+
// load() already funnels every failure into the catch above.
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
isCancelled = true;
|
|
136
|
+
};
|
|
137
|
+
}, [modelName, projectIdString]);
|
|
138
|
+
|
|
139
|
+
const columns: Columns<TBaseModel> = useMemo(() => {
|
|
140
|
+
if (definitions.length === 0) {
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return getCustomFieldColumns<TBaseModel>({ definitions });
|
|
145
|
+
}, [definitions]);
|
|
146
|
+
|
|
147
|
+
return { columns, definitions, isLoading, error };
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
export default useCustomFieldColumns;
|
|
@@ -69,6 +69,13 @@ export interface ComponentProps {
|
|
|
69
69
|
children?: ReactElement | Array<ReactElement>;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
/*
|
|
73
|
+
* Breathing room kept between the bottom of the open mobile menu and the bottom
|
|
74
|
+
* of the viewport, and the smallest height we will ever shrink the menu to.
|
|
75
|
+
*/
|
|
76
|
+
const MOBILE_MENU_BOTTOM_GAP_IN_PX: number = 16;
|
|
77
|
+
const MOBILE_MENU_MIN_HEIGHT_IN_PX: number = 160;
|
|
78
|
+
|
|
72
79
|
const Navbar: FunctionComponent<ComponentProps> = (
|
|
73
80
|
props: ComponentProps,
|
|
74
81
|
): ReactElement => {
|
|
@@ -76,6 +83,9 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
76
83
|
const [isMobileMenuVisible, setIsMobileMenuVisible] =
|
|
77
84
|
useState<boolean>(false);
|
|
78
85
|
const [isMoreMenuVisible, setIsMoreMenuVisible] = useState<boolean>(false);
|
|
86
|
+
const [mobileMenuMaxHeight, setMobileMenuMaxHeight] = useState<
|
|
87
|
+
number | undefined
|
|
88
|
+
>(undefined);
|
|
79
89
|
|
|
80
90
|
// Use the existing outside click hook for mobile menu
|
|
81
91
|
const {
|
|
@@ -103,6 +113,54 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
103
113
|
};
|
|
104
114
|
}, []);
|
|
105
115
|
|
|
116
|
+
/*
|
|
117
|
+
* The mobile menu is absolutely positioned inside a `sticky top-0` header, so
|
|
118
|
+
* anything that overflows past the bottom of the viewport is unreachable —
|
|
119
|
+
* scrolling the page just drags the sticky header (and the menu pinned to it)
|
|
120
|
+
* along. Cap the menu to the space left below it so it scrolls on its own.
|
|
121
|
+
*/
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!isMobileMenuOpen) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const updateMobileMenuMaxHeight: () => void = (): void => {
|
|
128
|
+
const menuTop: number =
|
|
129
|
+
mobileMenuRef.current?.getBoundingClientRect().top ?? 0;
|
|
130
|
+
const viewportHeight: number =
|
|
131
|
+
window.visualViewport?.height ?? window.innerHeight;
|
|
132
|
+
|
|
133
|
+
setMobileMenuMaxHeight(
|
|
134
|
+
Math.max(
|
|
135
|
+
viewportHeight - menuTop - MOBILE_MENU_BOTTOM_GAP_IN_PX,
|
|
136
|
+
MOBILE_MENU_MIN_HEIGHT_IN_PX,
|
|
137
|
+
),
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
updateMobileMenuMaxHeight();
|
|
142
|
+
|
|
143
|
+
window.addEventListener("resize", updateMobileMenuMaxHeight);
|
|
144
|
+
window.addEventListener("orientationchange", updateMobileMenuMaxHeight);
|
|
145
|
+
// The address bar collapsing on mobile only shows up on the visual viewport.
|
|
146
|
+
window.visualViewport?.addEventListener(
|
|
147
|
+
"resize",
|
|
148
|
+
updateMobileMenuMaxHeight,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return () => {
|
|
152
|
+
window.removeEventListener("resize", updateMobileMenuMaxHeight);
|
|
153
|
+
window.removeEventListener(
|
|
154
|
+
"orientationchange",
|
|
155
|
+
updateMobileMenuMaxHeight,
|
|
156
|
+
);
|
|
157
|
+
window.visualViewport?.removeEventListener(
|
|
158
|
+
"resize",
|
|
159
|
+
updateMobileMenuMaxHeight,
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
}, [isMobileMenuOpen]);
|
|
163
|
+
|
|
106
164
|
// Open/close the products menu with Cmd/Ctrl + K from anywhere.
|
|
107
165
|
useEffect(() => {
|
|
108
166
|
const handleGlobalKeyDown: (event: KeyboardEvent) => void = (
|
|
@@ -235,7 +293,14 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
235
293
|
ref={mobileMenuRef}
|
|
236
294
|
className="absolute top-full left-0 right-0 z-50 mt-1 transition-all duration-200 ease-in-out"
|
|
237
295
|
>
|
|
238
|
-
<nav
|
|
296
|
+
<nav
|
|
297
|
+
className="bg-white rounded-lg shadow-lg px-3 py-3 space-y-1 border border-gray-200 overflow-y-auto overscroll-contain"
|
|
298
|
+
style={
|
|
299
|
+
mobileMenuMaxHeight
|
|
300
|
+
? { maxHeight: `${mobileMenuMaxHeight}px` }
|
|
301
|
+
: undefined
|
|
302
|
+
}
|
|
303
|
+
>
|
|
239
304
|
{allNavItems.map((item: any) => {
|
|
240
305
|
return (
|
|
241
306
|
<div key={item.id} className="block w-full">
|