@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
|
@@ -58,6 +58,7 @@ import TableColumn from "../Table/Types/Column";
|
|
|
58
58
|
import FieldType from "../Types/FieldType";
|
|
59
59
|
import ModelTableColumn from "./Column";
|
|
60
60
|
import Columns from "./Columns";
|
|
61
|
+
import { getExportKeysFromColumn } from "./ExportFromColumns";
|
|
61
62
|
import {
|
|
62
63
|
getRelationSelectFromColumns,
|
|
63
64
|
getSelectFromColumns,
|
|
@@ -115,6 +116,21 @@ export enum ShowAs {
|
|
|
115
116
|
OrderedStatesList,
|
|
116
117
|
}
|
|
117
118
|
|
|
119
|
+
/*
|
|
120
|
+
* "Select All" loads the whole filtered result set - up to LIMIT_PER_PROJECT
|
|
121
|
+
* rows - with the same columns the table itself fetches. Asking for all of
|
|
122
|
+
* them in one request would mean a single wide, relation-joined query for up
|
|
123
|
+
* to 10,000 rows, which is exactly the kind of query the database statement
|
|
124
|
+
* timeout kills. It is paged instead: each request is comparable in cost to a
|
|
125
|
+
* large page load, so a big selection degrades in latency rather than failing.
|
|
126
|
+
*
|
|
127
|
+
* The page size trades those two costs off. Every list request also runs an
|
|
128
|
+
* unbounded count over the filtered set and pays a growing OFFSET, so smaller
|
|
129
|
+
* pages multiply that overhead; 1,000 keeps each query well inside the
|
|
130
|
+
* statement timeout while capping a maxed-out selection at ten round trips.
|
|
131
|
+
*/
|
|
132
|
+
export const BULK_SELECT_ALL_PAGE_SIZE: number = 1000;
|
|
133
|
+
|
|
118
134
|
export interface SaveFilterProps {
|
|
119
135
|
tableId: string;
|
|
120
136
|
}
|
|
@@ -340,6 +356,28 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
340
356
|
[],
|
|
341
357
|
);
|
|
342
358
|
|
|
359
|
+
/*
|
|
360
|
+
* "Select All" gets its own loading / error state instead of sharing
|
|
361
|
+
* `isLoading` and `error` with fetchItems. They ran concurrently before, so
|
|
362
|
+
* a page refresh landing mid-selection flipped the spinner off and cleared
|
|
363
|
+
* the other's error - which is how a failed select-all could leave the user
|
|
364
|
+
* with only the current page selected and nothing on screen to say so.
|
|
365
|
+
*/
|
|
366
|
+
const [isBulkSelectAllLoading, setIsBulkSelectAllLoading] =
|
|
367
|
+
useState<boolean>(false);
|
|
368
|
+
const [bulkSelectionError, setBulkSelectionError] = useState<string>("");
|
|
369
|
+
const [bulkSelectionTotalCount, setBulkSelectionTotalCount] =
|
|
370
|
+
useState<number>(0);
|
|
371
|
+
const [isBulkSelectionTruncated, setIsBulkSelectionTruncated] =
|
|
372
|
+
useState<boolean>(false);
|
|
373
|
+
|
|
374
|
+
/*
|
|
375
|
+
* The paged select-all spans several round trips, so the user can clear the
|
|
376
|
+
* selection or start another one while it is still running. Every run takes
|
|
377
|
+
* a token and drops its results if a newer one has since started.
|
|
378
|
+
*/
|
|
379
|
+
const bulkSelectAllToken: MutableRefObject<number> = React.useRef<number>(0);
|
|
380
|
+
|
|
343
381
|
let showAs: ShowAs | undefined = props.showAs;
|
|
344
382
|
|
|
345
383
|
if (!showAs) {
|
|
@@ -947,6 +985,18 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
947
985
|
...column,
|
|
948
986
|
disableSort: column.disableSort || shouldDisableSort(key),
|
|
949
987
|
key: columnKey,
|
|
988
|
+
/*
|
|
989
|
+
* `key` is only the first declared field, so a cell composed from
|
|
990
|
+
* several of them would export just that one. Hand the exporter
|
|
991
|
+
* every field the column declares (and that we actually selected).
|
|
992
|
+
*/
|
|
993
|
+
exportKeys: getExportKeysFromColumn<TBaseModel>({
|
|
994
|
+
column: column,
|
|
995
|
+
columnKey: columnKey ? String(columnKey) : null,
|
|
996
|
+
hasPermissionToReadField: (field: string): boolean => {
|
|
997
|
+
return hasPermissionToReadField(field as keyof TBaseModel);
|
|
998
|
+
},
|
|
999
|
+
}),
|
|
950
1000
|
tooltipText,
|
|
951
1001
|
getElement: column.getElement,
|
|
952
1002
|
});
|
|
@@ -1304,37 +1354,250 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
1304
1354
|
return fragment;
|
|
1305
1355
|
};
|
|
1306
1356
|
|
|
1307
|
-
|
|
1308
|
-
setError("");
|
|
1309
|
-
setIsLoading(true);
|
|
1357
|
+
type FetchAllBulkItemsFunction = () => Promise<boolean>;
|
|
1310
1358
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
sort: {},
|
|
1327
|
-
requestOptions: props.fetchRequestOptions,
|
|
1328
|
-
});
|
|
1359
|
+
/*
|
|
1360
|
+
* Selects every row matching the table's current query, filters and search -
|
|
1361
|
+
* not just the rows on screen - and returns whether it succeeded.
|
|
1362
|
+
*
|
|
1363
|
+
* It fetches the SAME columns as fetchItems. It used to ask for
|
|
1364
|
+
* `select: { _id: true }`, which picked the right rows but stripped every
|
|
1365
|
+
* field off them, so "Export CSV" over a select-all wrote one blank line per
|
|
1366
|
+
* row and any bulk action that reads a field (or renders an item's name) saw
|
|
1367
|
+
* undefined. Routing the projection through getSelect() also keeps
|
|
1368
|
+
* `selectMoreFields` and per-field read permissions in play, which a
|
|
1369
|
+
* hand-rolled column list would silently drop.
|
|
1370
|
+
*/
|
|
1371
|
+
const fetchAllBulkItems: FetchAllBulkItemsFunction =
|
|
1372
|
+
async (): Promise<boolean> => {
|
|
1373
|
+
const token: number = ++bulkSelectAllToken.current;
|
|
1329
1374
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
}
|
|
1375
|
+
setBulkSelectionError("");
|
|
1376
|
+
setIsBulkSelectionTruncated(false);
|
|
1377
|
+
setIsBulkSelectAllLoading(true);
|
|
1334
1378
|
|
|
1335
|
-
|
|
1379
|
+
const itemsSelected: Array<TBaseModel> = [];
|
|
1380
|
+
|
|
1381
|
+
try {
|
|
1382
|
+
/*
|
|
1383
|
+
* An unsorted table is served in the API's default order, so the
|
|
1384
|
+
* selection has to ask for that order explicitly - once any sort is
|
|
1385
|
+
* sent, the server stops applying its default. Without this the rows
|
|
1386
|
+
* came back in random UUID order, which both scrambled the exported
|
|
1387
|
+
* CSV and, on a table past the selection ceiling, kept an arbitrary
|
|
1388
|
+
* slice of history instead of the newest rows the user was looking at.
|
|
1389
|
+
*/
|
|
1390
|
+
const defaultSortColumn: string =
|
|
1391
|
+
(model as AnalyticsBaseModel).defaultSortColumn || "createdAt";
|
|
1392
|
+
|
|
1393
|
+
const bulkSort: Sort<TBaseModel> = (
|
|
1394
|
+
sortBy
|
|
1395
|
+
? { [sortBy as string]: sortOrder }
|
|
1396
|
+
: { [defaultSortColumn]: SortOrder.Descending }
|
|
1397
|
+
) as Sort<TBaseModel>;
|
|
1398
|
+
|
|
1399
|
+
/*
|
|
1400
|
+
* skip/limit paging is only stable over a total order. The field that
|
|
1401
|
+
* identifies a selected row (`_id` unless a table overrides it) is
|
|
1402
|
+
* appended as a tiebreaker so rows sharing a sort value - 1,200 labels
|
|
1403
|
+
* imported in one transaction all share a createdAt - cannot land on
|
|
1404
|
+
* two pages while another row is never returned at all.
|
|
1405
|
+
*/
|
|
1406
|
+
if (
|
|
1407
|
+
(bulkSort as Dictionary<unknown>)[
|
|
1408
|
+
matchBulkSelectedItemByField as string
|
|
1409
|
+
] === undefined
|
|
1410
|
+
) {
|
|
1411
|
+
(bulkSort as Dictionary<SortOrder>)[
|
|
1412
|
+
matchBulkSelectedItemByField as string
|
|
1413
|
+
] = SortOrder.Ascending;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/*
|
|
1417
|
+
* getSelect() throws on a misconfigured column, so it is built inside
|
|
1418
|
+
* the try: otherwise the button would be left spinning forever with an
|
|
1419
|
+
* unhandled rejection instead of a message.
|
|
1420
|
+
*/
|
|
1421
|
+
const bulkSelect: Select<TBaseModel> = {
|
|
1422
|
+
...getSelect(),
|
|
1423
|
+
...getRelationSelect(),
|
|
1424
|
+
};
|
|
1425
|
+
|
|
1426
|
+
/*
|
|
1427
|
+
* Every ordered column has to be selected for the database to order by
|
|
1428
|
+
* it. `_id` always is; the default sort column usually is not, and a
|
|
1429
|
+
* table matching its selection on some field other than `_id` would
|
|
1430
|
+
* otherwise tick no row checkboxes at all.
|
|
1431
|
+
*/
|
|
1432
|
+
for (const sortColumn of Object.keys(bulkSort)) {
|
|
1433
|
+
if (
|
|
1434
|
+
(bulkSelect as Dictionary<unknown>)[sortColumn] === undefined &&
|
|
1435
|
+
model.hasColumn(sortColumn)
|
|
1436
|
+
) {
|
|
1437
|
+
(bulkSelect as Dictionary<boolean>)[sortColumn] = true;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
let totalCount: number = 0;
|
|
1442
|
+
let hasMore: boolean = false;
|
|
1443
|
+
let rowsFetched: number = 0;
|
|
1444
|
+
|
|
1445
|
+
/*
|
|
1446
|
+
* Offset paging over a table that is still receiving writes can hand
|
|
1447
|
+
* back a row that an earlier page already returned. Selecting it twice
|
|
1448
|
+
* would duplicate it in the export and inflate the selected count.
|
|
1449
|
+
*/
|
|
1450
|
+
const seenItemIds: Set<string> = new Set<string>();
|
|
1451
|
+
|
|
1452
|
+
while (itemsSelected.length < LIMIT_PER_PROJECT) {
|
|
1453
|
+
const limit: number = Math.min(
|
|
1454
|
+
BULK_SELECT_ALL_PAGE_SIZE,
|
|
1455
|
+
LIMIT_PER_PROJECT - itemsSelected.length,
|
|
1456
|
+
);
|
|
1457
|
+
|
|
1458
|
+
const listResult: ListResult<TBaseModel> =
|
|
1459
|
+
await props.callbacks.getList({
|
|
1460
|
+
modelType: props.modelType as
|
|
1461
|
+
| DatabaseBaseModelType
|
|
1462
|
+
| AnalyticsBaseModelType,
|
|
1463
|
+
query: {
|
|
1464
|
+
...props.query,
|
|
1465
|
+
...query,
|
|
1466
|
+
...buildSearchQueryFragment(),
|
|
1467
|
+
},
|
|
1468
|
+
groupBy: {
|
|
1469
|
+
...props.groupBy,
|
|
1470
|
+
},
|
|
1471
|
+
limit: limit,
|
|
1472
|
+
skip: rowsFetched,
|
|
1473
|
+
select: bulkSelect,
|
|
1474
|
+
sort: bulkSort,
|
|
1475
|
+
requestOptions: props.fetchRequestOptions,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
if (token !== bulkSelectAllToken.current) {
|
|
1479
|
+
// A newer select-all, or a cleared selection, superseded this run.
|
|
1480
|
+
return false;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
totalCount = listResult.count || totalCount;
|
|
1484
|
+
hasMore = Boolean(listResult.hasMore);
|
|
1485
|
+
rowsFetched += listResult.data.length;
|
|
1486
|
+
|
|
1487
|
+
for (const item of listResult.data) {
|
|
1488
|
+
const itemId: string =
|
|
1489
|
+
item[matchBulkSelectedItemByField]?.toString() || "";
|
|
1490
|
+
|
|
1491
|
+
if (itemId) {
|
|
1492
|
+
if (seenItemIds.has(itemId)) {
|
|
1493
|
+
continue;
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
seenItemIds.add(itemId);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
itemsSelected.push(item);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
if (listResult.data.length < limit) {
|
|
1503
|
+
// A short page is the last page.
|
|
1504
|
+
hasMore = false;
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (listResult.hasMore === false) {
|
|
1509
|
+
break;
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
if (totalCount > 0 && rowsFetched >= totalCount) {
|
|
1513
|
+
/*
|
|
1514
|
+
* Everything matching has been read. Stopping here saves the
|
|
1515
|
+
* empty round trip a result set that is an exact multiple of the
|
|
1516
|
+
* page size would otherwise cost - and each of those requests
|
|
1517
|
+
* runs a full count over the filtered set.
|
|
1518
|
+
*/
|
|
1519
|
+
break;
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
setBulkSelectionTotalCount(Math.max(totalCount, itemsSelected.length));
|
|
1524
|
+
setIsBulkSelectionTruncated(totalCount > rowsFetched || hasMore);
|
|
1525
|
+
setBulkSelectedItems(itemsSelected);
|
|
1526
|
+
|
|
1527
|
+
return true;
|
|
1528
|
+
} catch (err) {
|
|
1529
|
+
if (token === bulkSelectAllToken.current) {
|
|
1530
|
+
/*
|
|
1531
|
+
* Leave the existing selection alone. A half-loaded selection that
|
|
1532
|
+
* claims to be everything is worse than a failed one, and the user
|
|
1533
|
+
* can retry because the Select All button stays on screen.
|
|
1534
|
+
*/
|
|
1535
|
+
setBulkSelectionError(API.getFriendlyMessage(err));
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
return false;
|
|
1539
|
+
} finally {
|
|
1540
|
+
if (token === bulkSelectAllToken.current) {
|
|
1541
|
+
setIsBulkSelectAllLoading(false);
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
};
|
|
1545
|
+
|
|
1546
|
+
type ClearBulkSelectionStateFunction = () => void;
|
|
1547
|
+
|
|
1548
|
+
const clearBulkSelectionState: ClearBulkSelectionStateFunction = (): void => {
|
|
1549
|
+
// Supersede any select-all still in flight so it cannot repopulate this.
|
|
1550
|
+
bulkSelectAllToken.current++;
|
|
1551
|
+
setIsBulkSelectAllLoading(false);
|
|
1552
|
+
setBulkSelectionError("");
|
|
1553
|
+
setIsBulkSelectionTruncated(false);
|
|
1554
|
+
setBulkSelectionTotalCount(0);
|
|
1555
|
+
setBulkSelectedItems((existingItems: Array<TBaseModel>) => {
|
|
1556
|
+
/*
|
|
1557
|
+
* Keep the same array when there is nothing to clear, so calling this on
|
|
1558
|
+
* every query change (below) costs an idle table no re-render.
|
|
1559
|
+
*/
|
|
1560
|
+
return existingItems.length === 0 ? existingItems : [];
|
|
1561
|
+
});
|
|
1336
1562
|
};
|
|
1337
1563
|
|
|
1564
|
+
/*
|
|
1565
|
+
* Everything that decides *which rows match* - the caller's query, the
|
|
1566
|
+
* column filters, the search term and the label chips. Serialised because
|
|
1567
|
+
* callers routinely pass `query` as a fresh object literal on every render,
|
|
1568
|
+
* and the label chips get their display names hydrated after mount without
|
|
1569
|
+
* the set of ids ever changing.
|
|
1570
|
+
*
|
|
1571
|
+
* Sort and page are deliberately left out: they re-order or re-window the
|
|
1572
|
+
* same matching set, so a selection made under them is still a selection of
|
|
1573
|
+
* rows that match. Selecting rows across pages is a real workflow, and a
|
|
1574
|
+
* truncated select-all keeps saying "selected N of M matching" after a
|
|
1575
|
+
* re-sort, which stays true.
|
|
1576
|
+
*/
|
|
1577
|
+
const effectiveQueryKey: string = JSON.stringify({
|
|
1578
|
+
props: props.query || {},
|
|
1579
|
+
filter: query,
|
|
1580
|
+
search: debouncedSearchText.trim(),
|
|
1581
|
+
labels: selectedLabelIdsKey,
|
|
1582
|
+
});
|
|
1583
|
+
|
|
1584
|
+
/*
|
|
1585
|
+
* A selection refers to the rows the *previous* query returned. Once the
|
|
1586
|
+
* query changes, the table underneath the bulk bar is a different result
|
|
1587
|
+
* set, so carrying the selection over leaves the bar claiming "6,000 Alerts
|
|
1588
|
+
* Selected" above a filtered-down list of 12 - with the Select All button
|
|
1589
|
+
* hidden, as though the selection matched what is on screen, and Delete
|
|
1590
|
+
* still wired to all 6,000 rows the user can no longer see.
|
|
1591
|
+
*
|
|
1592
|
+
* Dropping it here also bumps `bulkSelectAllToken`, so a filter change that
|
|
1593
|
+
* lands while a paged select-all is still running supersedes that run
|
|
1594
|
+
* instead of letting it finish and repopulate the selection against the old
|
|
1595
|
+
* query.
|
|
1596
|
+
*/
|
|
1597
|
+
useEffect(() => {
|
|
1598
|
+
clearBulkSelectionState();
|
|
1599
|
+
}, [effectiveQueryKey]);
|
|
1600
|
+
|
|
1338
1601
|
const fetchItems: PromiseVoidFunction = async (): Promise<void> => {
|
|
1339
1602
|
setError("");
|
|
1340
1603
|
setIsLoading(true);
|
|
@@ -2236,15 +2499,18 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2236
2499
|
};
|
|
2237
2500
|
})()}
|
|
2238
2501
|
onBulkActionEnd={async () => {
|
|
2239
|
-
|
|
2502
|
+
clearBulkSelectionState();
|
|
2240
2503
|
await fetchItems();
|
|
2241
2504
|
}}
|
|
2242
2505
|
onBulkActionStart={() => {}}
|
|
2243
2506
|
bulkSelectedItems={bulkSelectedItems}
|
|
2244
2507
|
onBulkSelectedItemAdded={(item: TBaseModel) => {
|
|
2508
|
+
// The user has moved on from the failed select-all.
|
|
2509
|
+
setBulkSelectionError("");
|
|
2245
2510
|
setBulkSelectedItems([...bulkSelectedItems, item]);
|
|
2246
2511
|
}}
|
|
2247
2512
|
onBulkSelectedItemRemoved={(item: TBaseModel) => {
|
|
2513
|
+
setBulkSelectionError("");
|
|
2248
2514
|
setBulkSelectedItems(
|
|
2249
2515
|
bulkSelectedItems.filter((i: TBaseModel) => {
|
|
2250
2516
|
return (
|
|
@@ -2278,11 +2544,15 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2278
2544
|
setBulkSelectedItems(uniqueItems);
|
|
2279
2545
|
}}
|
|
2280
2546
|
onBulkClearAllItems={() => {
|
|
2281
|
-
|
|
2547
|
+
clearBulkSelectionState();
|
|
2282
2548
|
}}
|
|
2283
|
-
onBulkSelectAllItems={async () => {
|
|
2284
|
-
await fetchAllBulkItems();
|
|
2549
|
+
onBulkSelectAllItems={async (): Promise<boolean> => {
|
|
2550
|
+
return await fetchAllBulkItems();
|
|
2285
2551
|
}}
|
|
2552
|
+
bulkSelectionError={bulkSelectionError}
|
|
2553
|
+
isBulkSelectAllLoading={isBulkSelectAllLoading}
|
|
2554
|
+
isBulkSelectionTruncated={isBulkSelectionTruncated}
|
|
2555
|
+
bulkSelectionTotalCount={bulkSelectionTotalCount}
|
|
2286
2556
|
matchBulkSelectedItemByField={matchBulkSelectedItemByField || "_id"}
|
|
2287
2557
|
bulkItemToString={(item: TBaseModel) => {
|
|
2288
2558
|
const label: string = props.singularName || item.singularName || "";
|
|
@@ -29,6 +29,18 @@ export default interface Columns<
|
|
|
29
29
|
alignItem?: AlignItem | undefined;
|
|
30
30
|
noValueMessage?: string | undefined;
|
|
31
31
|
hideOnMobile?: boolean | undefined; // Hide column on mobile devices
|
|
32
|
+
/*
|
|
33
|
+
* Exact text for this column's CSV cell (the text twin of getElement, and
|
|
34
|
+
* it receives the same item). Set it when the cell renders from something
|
|
35
|
+
* the row does not carry under this column's own `field` - data fetched
|
|
36
|
+
* alongside the table, or a phrase composed from several fields. Columns
|
|
37
|
+
* that render entirely through getElement off a placeholder
|
|
38
|
+
* `field: { _id: true }` are left out of the CSV unless they set this,
|
|
39
|
+
* because their only exportable value is a raw UUID.
|
|
40
|
+
*/
|
|
41
|
+
getExportValue?: ((item: TEntity) => string) | undefined;
|
|
42
|
+
// Leave this column out of the CSV export entirely.
|
|
43
|
+
disableCsvExport?: boolean | undefined;
|
|
32
44
|
getElement?:
|
|
33
45
|
| ((item: TEntity, onBeforeFetchData?: TEntity | undefined) => ReactElement)
|
|
34
46
|
| undefined;
|
|
@@ -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
|
+
}
|
|
@@ -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">
|
|
@@ -78,9 +78,19 @@ export interface ComponentProps<T extends GenericObject> {
|
|
|
78
78
|
bulkSelectedItems?: Array<T> | undefined;
|
|
79
79
|
onBulkSelectedItemAdded?: ((item: T) => void) | undefined;
|
|
80
80
|
onBulkSelectedItemRemoved?: ((item: T) => void) | undefined;
|
|
81
|
-
|
|
81
|
+
/*
|
|
82
|
+
* Resolves to whether every matching row really got selected. The table
|
|
83
|
+
* only claims "all items selected" when it did - otherwise a failed
|
|
84
|
+
* select-all would hide the Select All button while leaving just the
|
|
85
|
+
* current page selected, with no way to retry.
|
|
86
|
+
*/
|
|
87
|
+
onBulkSelectAllItems?: (() => Promise<boolean>) | undefined;
|
|
82
88
|
onBulkSelectItemsOnCurrentPage?: (() => void) | undefined;
|
|
83
89
|
onBulkClearAllItems?: (() => void) | undefined;
|
|
90
|
+
bulkSelectionError?: string | undefined;
|
|
91
|
+
isBulkSelectAllLoading?: boolean | undefined;
|
|
92
|
+
isBulkSelectionTruncated?: boolean | undefined;
|
|
93
|
+
bulkSelectionTotalCount?: number | undefined;
|
|
84
94
|
matchBulkSelectedItemByField?: keyof T | undefined; // which field to use to match selected items. For exmaple this could be '_id'
|
|
85
95
|
onBulkActionEnd?: (() => void) | undefined;
|
|
86
96
|
onBulkActionStart?: (() => void) | undefined;
|
|
@@ -156,6 +166,18 @@ const Table: TableFunction = <T extends GenericObject>(
|
|
|
156
166
|
useEffect(() => {
|
|
157
167
|
if (props.bulkSelectedItems) {
|
|
158
168
|
setBulkSelectedItems(props.bulkSelectedItems);
|
|
169
|
+
|
|
170
|
+
/*
|
|
171
|
+
* "All items selected" is a claim about a selection that exists, so it
|
|
172
|
+
* cannot outlive one. The parent drops the selection whenever the query
|
|
173
|
+
* changes underneath it (and after a bulk action completes), and this
|
|
174
|
+
* state is local to the table - without this, the next single row the
|
|
175
|
+
* user ticked would re-open the bulk bar with the "Select All" button
|
|
176
|
+
* still hidden, as though everything matching were already selected.
|
|
177
|
+
*/
|
|
178
|
+
if (props.bulkSelectedItems.length === 0) {
|
|
179
|
+
setIsAllItemsSelected(false);
|
|
180
|
+
}
|
|
159
181
|
}
|
|
160
182
|
}, [props.bulkSelectedItems]);
|
|
161
183
|
|
|
@@ -238,16 +260,20 @@ const Table: TableFunction = <T extends GenericObject>(
|
|
|
238
260
|
if (props.matchBulkSelectedItemByField === undefined) {
|
|
239
261
|
return;
|
|
240
262
|
}
|
|
241
|
-
const index: number = bulkSelectedItems.findIndex((x: T) => {
|
|
242
|
-
return (
|
|
243
|
-
x[props.matchBulkSelectedItemByField!]?.toString() ===
|
|
244
|
-
item[props.matchBulkSelectedItemByField!]?.toString()
|
|
245
|
-
);
|
|
246
|
-
});
|
|
247
263
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
264
|
+
/*
|
|
265
|
+
* Rebuild rather than splice: this array is the parent's state
|
|
266
|
+
* array by reference (see the sync effect above), so mutating it
|
|
267
|
+
* in place edits React state behind React's back.
|
|
268
|
+
*/
|
|
269
|
+
setBulkSelectedItems(
|
|
270
|
+
bulkSelectedItems.filter((x: T) => {
|
|
271
|
+
return (
|
|
272
|
+
x[props.matchBulkSelectedItemByField!]?.toString() !==
|
|
273
|
+
item[props.matchBulkSelectedItemByField!]?.toString()
|
|
274
|
+
);
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
251
277
|
|
|
252
278
|
props.onBulkSelectedItemRemoved?.(item);
|
|
253
279
|
}}
|
|
@@ -301,14 +327,27 @@ const Table: TableFunction = <T extends GenericObject>(
|
|
|
301
327
|
props.onBulkClearAllItems?.();
|
|
302
328
|
setIsAllItemsSelected(false);
|
|
303
329
|
}}
|
|
304
|
-
onSelectAllClick={() => {
|
|
305
|
-
|
|
306
|
-
|
|
330
|
+
onSelectAllClick={async () => {
|
|
331
|
+
const didSelectAllItems: boolean =
|
|
332
|
+
(await props.onBulkSelectAllItems?.()) ?? false;
|
|
333
|
+
|
|
334
|
+
/*
|
|
335
|
+
* Only on success. Otherwise the bulk bar would hide the Select
|
|
336
|
+
* All button and claim everything was selected while the
|
|
337
|
+
* selection is still just the current page.
|
|
338
|
+
*/
|
|
339
|
+
if (didSelectAllItems) {
|
|
340
|
+
setIsAllItemsSelected(true);
|
|
341
|
+
}
|
|
307
342
|
}}
|
|
308
343
|
selectedItems={bulkSelectedItems}
|
|
309
344
|
singularLabel={translatedSingularLabel}
|
|
310
345
|
pluralLabel={translatedPluralLabel}
|
|
311
346
|
isAllItemsSelected={isAllItemsSelected}
|
|
347
|
+
errorMessage={props.bulkSelectionError}
|
|
348
|
+
isSelectingAllItems={props.isBulkSelectAllLoading}
|
|
349
|
+
isSelectionTruncated={props.isBulkSelectionTruncated}
|
|
350
|
+
totalMatchingItemsCount={props.bulkSelectionTotalCount}
|
|
312
351
|
onActionStart={props.onBulkActionStart}
|
|
313
352
|
onActionEnd={() => {
|
|
314
353
|
setIsAllItemsSelected(false);
|