@oneuptime/common 12.0.31 → 12.0.32
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/AlertState.ts +0 -2
- package/Models/DatabaseModels/Domain.ts +1 -1
- package/Models/DatabaseModels/Incident.ts +1 -1
- package/Models/DatabaseModels/IncidentTemplate.ts +1 -1
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/MetricType.ts +0 -2
- package/Models/DatabaseModels/MonitorStatusTimeline.ts +0 -2
- package/Models/DatabaseModels/MonitorTest.ts +0 -2
- package/Models/DatabaseModels/NetworkDevice.ts +121 -2
- package/Models/DatabaseModels/NetworkDeviceOidTemplate.ts +3 -1
- package/Models/DatabaseModels/NetworkDeviceRole.ts +648 -0
- package/Models/DatabaseModels/ScheduledMaintenance.ts +1 -1
- package/Models/DatabaseModels/ScheduledMaintenanceTemplate.ts +1 -1
- package/Models/DatabaseModels/StatusPageAnnouncement.ts +0 -2
- package/Models/DatabaseModels/StatusPageAnnouncementTemplate.ts +0 -2
- package/Models/DatabaseModels/StatusPagePrivateUser.ts +0 -2
- package/Models/DatabaseModels/StatusPageResource.ts +0 -2
- package/Models/DatabaseModels/StatusPageSubscriber.ts +0 -2
- package/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.ts +72 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -0
- package/Server/Services/DatabaseService.ts +39 -9
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/NetworkDeviceRoleService.ts +90 -0
- package/Server/Services/ProjectService.ts +85 -0
- package/Server/Services/UserNotificationSettingService.ts +0 -6
- package/Tests/App/Dashboard/AddNeighborToMonitoringModal.test.tsx +31 -1
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +35 -8
- package/Tests/App/Dashboard/DashboardCommandPalette.test.tsx +107 -3
- package/Tests/App/Dashboard/DashboardWideChartZoom.test.tsx +401 -0
- package/Tests/App/Dashboard/MonitorTemplateColumnAndFacet.test.tsx +318 -0
- package/Tests/App/Dashboard/NetworkDeviceBulkDelete.test.tsx +357 -0
- package/Tests/App/Dashboard/NetworkSideMenu.test.tsx +590 -0
- package/Tests/App/Dashboard/SideMenuHarness.tsx +11 -3
- package/Tests/Models/DatabaseModels/ModelRegistryInvariants.test.ts +115 -41
- package/Tests/Server/Infrastructure/Postgres/NetworkDeviceRoleTableMigration.test.ts +476 -0
- package/Tests/Server/Services/DatabaseServiceGenerateSlug.test.ts +215 -0
- package/Tests/Server/Services/NetworkDeviceRoleModel.test.ts +672 -0
- package/Tests/Server/Services/NetworkDeviceRuleEngines.test.ts +1 -1
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +67 -4
- package/Tests/Server/Services/ScheduledMaintenanceSubscriberEndTime.test.ts +369 -0
- package/Tests/Server/Services/UserNotificationSettingMonitorDefaults.test.ts +248 -0
- package/Tests/Server/Services/UserNotificationSettingWorkspaceChannels.test.ts +43 -2
- package/Tests/Server/Utils/BasicCron.test.ts +283 -0
- package/Tests/Server/Utils/IncomingCallPhoneNumber.test.ts +174 -0
- package/Tests/Types/NetworkDevice/DefaultNetworkDeviceRole.test.ts +417 -0
- package/Tests/UI/Components/Charts/ChartClickDoubleClickDisambiguation.test.tsx +221 -0
- package/Tests/UI/Components/Charts/ChartDoubleClickReset.test.tsx +220 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableBulkDelete.test.tsx +751 -0
- package/Tests/UI/Components/ModelTable/BaseModelTableColumnCustomization.test.tsx +475 -43
- package/Tests/UI/Components/ModelTable/ColumnPreference.test.ts +89 -0
- package/Tests/UI/Components/NavBarCommandKGating.test.tsx +103 -8
- package/Tests/UI/Components/SideMenuItem.test.tsx +30 -0
- package/Tests/UI/Utils/TableColumnsToCsv.test.ts +30 -0
- package/Tests/UI/Utils/UseDashboardTimeRangeZoom.test.tsx +187 -0
- package/Tests/Utils/Dashboard/DashboardResourceListSharedArgs.test.ts +302 -0
- package/Tests/Utils/Dashboard/DashboardTimeRangeZoom.test.ts +242 -0
- package/Tests/Utils/Monitor/NetworkDeviceRoleCatalog.test.ts +981 -0
- package/Tests/Utils/Monitor/NetworkTopologyRoleCatalog.test.ts +865 -0
- package/Tests/Utils/NetworkDevice/DeviceRoleKeyUtil.test.ts +538 -0
- package/Tests/Utils/Slo/SloEvaluation.test.ts +171 -0
- package/Tests/Utils/Slug.test.ts +61 -1
- package/Tests/jest.setup.ts +19 -0
- package/Types/Monitor/SnmpMonitor/NetworkTopology.ts +61 -0
- package/Types/NetworkDevice/DefaultNetworkDeviceRole.ts +145 -0
- package/Types/Permission.ts +45 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +7 -0
- package/UI/Components/Charts/Bar/BarChart.tsx +6 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +9 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +14 -0
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +97 -35
- package/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.ts +13 -0
- package/UI/Components/Charts/Line/LineChart.tsx +7 -0
- package/UI/Components/ModelTable/BaseModelTable.tsx +50 -15
- package/UI/Components/Navbar/NavBar.tsx +62 -13
- package/UI/Components/Navbar/NavBarMenuModal.tsx +7 -2
- package/UI/Components/SideMenu/CountModelSideMenuItem.tsx +2 -0
- package/UI/Components/SideMenu/SideMenu.tsx +12 -2
- package/UI/Components/SideMenu/SideMenuItem.tsx +4 -1
- package/UI/Components/SideMenu/SideMenuSection.tsx +14 -2
- package/UI/Utils/UseDashboardTimeRangeZoom.ts +77 -0
- package/Utils/Dashboard/DashboardTimeRangeZoom.ts +118 -0
- package/Utils/Monitor/NetworkDeviceRoleCatalog.ts +232 -0
- package/Utils/Monitor/NetworkDeviceRoleUtil.ts +2 -1
- package/Utils/Monitor/NetworkTopologyUtil.ts +116 -33
- package/Utils/NetworkDevice/DeviceRoleKeyUtil.ts +137 -0
- package/Utils/Rules/RulePatternMatchUtil.ts +1 -1
- package/Utils/Slug.ts +34 -1
- package/build/dist/Models/DatabaseModels/AlertState.js +0 -2
- package/build/dist/Models/DatabaseModels/AlertState.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js +1 -1
- package/build/dist/Models/DatabaseModels/Domain.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js +1 -1
- package/build/dist/Models/DatabaseModels/Incident.js.map +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/IncidentTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MetricType.js +0 -2
- package/build/dist/Models/DatabaseModels/MetricType.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorStatusTimeline.js.map +1 -1
- package/build/dist/Models/DatabaseModels/MonitorTest.js +0 -2
- package/build/dist/Models/DatabaseModels/MonitorTest.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +124 -3
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js +3 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceOidTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js +677 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceRole.js.map +1 -0
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenance.js.map +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js +1 -1
- package/build/dist/Models/DatabaseModels/ScheduledMaintenanceTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncement.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageAnnouncementTemplate.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPagePrivateUser.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageResource.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageResource.js.map +1 -1
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js +0 -2
- package/build/dist/Models/DatabaseModels/StatusPageSubscriber.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1790800000000-AddNetworkDeviceRoleTable.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/DatabaseService.js +31 -5
- package/build/dist/Server/Services/DatabaseService.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceRoleService.js +70 -0
- package/build/dist/Server/Services/NetworkDeviceRoleService.js.map +1 -0
- package/build/dist/Server/Services/ProjectService.js +65 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js +0 -1
- package/build/dist/Server/Services/UserNotificationSettingService.js.map +1 -1
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js +105 -0
- package/build/dist/Types/NetworkDevice/DefaultNetworkDeviceRole.js.map +1 -0
- package/build/dist/Types/Permission.js +40 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +6 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +7 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +79 -31
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js +14 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/DoubleClick.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js +39 -13
- package/build/dist/UI/Components/ModelTable/BaseModelTable.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBar.js +44 -9
- package/build/dist/UI/Components/Navbar/NavBar.js.map +1 -1
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js +2 -2
- package/build/dist/UI/Components/Navbar/NavBarMenuModal.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/CountModelSideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenu.js +9 -6
- package/build/dist/UI/Components/SideMenu/SideMenu.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuItem.js.map +1 -1
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js +7 -2
- package/build/dist/UI/Components/SideMenu/SideMenuSection.js.map +1 -1
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js +42 -0
- package/build/dist/UI/Utils/UseDashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js +74 -0
- package/build/dist/Utils/Dashboard/DashboardTimeRangeZoom.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js +126 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleCatalog.js.map +1 -0
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js +2 -1
- package/build/dist/Utils/Monitor/NetworkDeviceRoleUtil.js.map +1 -1
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js +67 -32
- package/build/dist/Utils/Monitor/NetworkTopologyUtil.js.map +1 -1
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js +114 -0
- package/build/dist/Utils/NetworkDevice/DeviceRoleKeyUtil.js.map +1 -0
- package/build/dist/Utils/Rules/RulePatternMatchUtil.js +1 -1
- package/build/dist/Utils/Slug.js +31 -1
- package/build/dist/Utils/Slug.js.map +1 -1
- package/package.json +1 -1
|
@@ -40,6 +40,7 @@ import useChartAnnotations, {
|
|
|
40
40
|
UseChartAnnotationsResult,
|
|
41
41
|
} from "../Annotations/UseChartAnnotations";
|
|
42
42
|
import { cx } from "../Utils/Cx";
|
|
43
|
+
import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../Utils/DoubleClick";
|
|
43
44
|
import { getYAxisDomain } from "../Utils/GetYAxisDomain";
|
|
44
45
|
import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
|
|
45
46
|
import {
|
|
@@ -625,6 +626,15 @@ interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
625
626
|
formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
|
|
626
627
|
onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
|
|
627
628
|
onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
|
|
629
|
+
/**
|
|
630
|
+
* Double-click on the plot surface: the counterpart to drag-to-select,
|
|
631
|
+
* for undoing the zoom it produced. Supplying it makes single clicks
|
|
632
|
+
* wait DOUBLE_CLICK_DISAMBIGUATION_MS before they act, so a double-click
|
|
633
|
+
* cannot also fire the bucket-click path twice on its way past — hosts
|
|
634
|
+
* that have nothing to reset should leave it undefined and keep clicks
|
|
635
|
+
* instant.
|
|
636
|
+
*/
|
|
637
|
+
onTimeRangeReset?: (() => void) | undefined;
|
|
628
638
|
/**
|
|
629
639
|
* Plain click on a time bucket (the plot background — dot/legend clicks
|
|
630
640
|
* and drag-selections never reach this). [bucketStart, bucketEnd) uses
|
|
@@ -693,6 +703,7 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
693
703
|
formattedTimeReferenceLines,
|
|
694
704
|
formattedReferenceRegions,
|
|
695
705
|
onTimeRangeSelect,
|
|
706
|
+
onTimeRangeReset,
|
|
696
707
|
onBucketClick,
|
|
697
708
|
ghostCategories,
|
|
698
709
|
...other
|
|
@@ -723,9 +734,26 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
723
734
|
React.useRef<number | null>(null);
|
|
724
735
|
const suppressNextClickRef: React.MutableRefObject<boolean> =
|
|
725
736
|
React.useRef<boolean>(false);
|
|
737
|
+
const hasOnTimeRangeReset: boolean = Boolean(onTimeRangeReset);
|
|
738
|
+
/*
|
|
739
|
+
* A pending single-click, held open long enough for a second click to
|
|
740
|
+
* cancel it. Only armed when onTimeRangeReset is supplied.
|
|
741
|
+
*/
|
|
742
|
+
const pendingClickTimeoutRef: React.MutableRefObject<ReturnType<
|
|
743
|
+
typeof setTimeout
|
|
744
|
+
> | null> = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
726
745
|
const categoryColors: Map<string, ChartColorValue> =
|
|
727
746
|
constructCategoryColors(categories, colors);
|
|
728
747
|
|
|
748
|
+
React.useEffect(() => {
|
|
749
|
+
return () => {
|
|
750
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
751
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
752
|
+
pendingClickTimeoutRef.current = null;
|
|
753
|
+
}
|
|
754
|
+
};
|
|
755
|
+
}, []);
|
|
756
|
+
|
|
729
757
|
/*
|
|
730
758
|
* Annotations are drawn onto the categorical axis, so the layer needs
|
|
731
759
|
* the same x values recharts is drawing — not the source dates.
|
|
@@ -971,6 +999,51 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
971
999
|
onTimeRangeSelect(startDate, endDate);
|
|
972
1000
|
}
|
|
973
1001
|
|
|
1002
|
+
/*
|
|
1003
|
+
* The plain-click path, split out so it can either run inline (no
|
|
1004
|
+
* reset handler) or be deferred behind the double-click window.
|
|
1005
|
+
*/
|
|
1006
|
+
function handleChartClick(chartState: RangeSelectionChartState): void {
|
|
1007
|
+
/*
|
|
1008
|
+
* A click while a legend/dot selection is active keeps its
|
|
1009
|
+
* long-standing meaning — clear the selection — and never
|
|
1010
|
+
* also pins a bucket.
|
|
1011
|
+
*/
|
|
1012
|
+
if (hasOnValueChange && (activeLegend || activeDot)) {
|
|
1013
|
+
setActiveDot(undefined);
|
|
1014
|
+
setActiveLegend(undefined);
|
|
1015
|
+
onValueChange?.(null);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (!onBucketClick) {
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const rowIndex: number | null = getRowIndexFromChartState(chartState);
|
|
1022
|
+
if (rowIndex === null) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
|
|
1026
|
+
if (!bucketStart) {
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
/*
|
|
1030
|
+
* Cover the full bucket: width from adjacent row dates, the
|
|
1031
|
+
* same derivation drag-to-select uses.
|
|
1032
|
+
*/
|
|
1033
|
+
const adjacentBucketDate: Date | null =
|
|
1034
|
+
rowIndex > 0
|
|
1035
|
+
? getBucketDateAtIndex(rowIndex - 1)
|
|
1036
|
+
: getBucketDateAtIndex(rowIndex + 1);
|
|
1037
|
+
const clickedBucketWidthInMs: number = adjacentBucketDate
|
|
1038
|
+
? Math.abs(bucketStart.getTime() - adjacentBucketDate.getTime())
|
|
1039
|
+
: 0;
|
|
1040
|
+
onBucketClick(
|
|
1041
|
+
bucketStart,
|
|
1042
|
+
new Date(bucketStart.getTime() + clickedBucketWidthInMs),
|
|
1043
|
+
(data[rowIndex] || {}) as Record<string, number | string>,
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
974
1047
|
return (
|
|
975
1048
|
<div
|
|
976
1049
|
ref={ref}
|
|
@@ -1009,46 +1082,35 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1009
1082
|
if (suppressNextClickRef.current) {
|
|
1010
1083
|
return;
|
|
1011
1084
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
* long-standing meaning — clear the selection — and never
|
|
1015
|
-
* also pins a bucket.
|
|
1016
|
-
*/
|
|
1017
|
-
if (hasOnValueChange && (activeLegend || activeDot)) {
|
|
1018
|
-
setActiveDot(undefined);
|
|
1019
|
-
setActiveLegend(undefined);
|
|
1020
|
-
onValueChange?.(null);
|
|
1021
|
-
return;
|
|
1022
|
-
}
|
|
1023
|
-
if (!onBucketClick) {
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
const rowIndex: number | null =
|
|
1027
|
-
getRowIndexFromChartState(chartState);
|
|
1028
|
-
if (rowIndex === null) {
|
|
1029
|
-
return;
|
|
1030
|
-
}
|
|
1031
|
-
const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
|
|
1032
|
-
if (!bucketStart) {
|
|
1085
|
+
if (!hasOnTimeRangeReset) {
|
|
1086
|
+
handleChartClick(chartState);
|
|
1033
1087
|
return;
|
|
1034
1088
|
}
|
|
1035
1089
|
/*
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1090
|
+
* Reset is on, so hold the click open: both clicks of a
|
|
1091
|
+
* double-click arrive before dblclick does, and neither
|
|
1092
|
+
* may pin a bucket. The second click re-arms the timer,
|
|
1093
|
+
* dblclick clears it.
|
|
1038
1094
|
*/
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
onBucketClick(
|
|
1047
|
-
bucketStart,
|
|
1048
|
-
new Date(bucketStart.getTime() + bucketWidthInMs),
|
|
1049
|
-
(data[rowIndex] || {}) as Record<string, number | string>,
|
|
1050
|
-
);
|
|
1095
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
1096
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
1097
|
+
}
|
|
1098
|
+
pendingClickTimeoutRef.current = setTimeout(() => {
|
|
1099
|
+
pendingClickTimeoutRef.current = null;
|
|
1100
|
+
handleChartClick(chartState);
|
|
1101
|
+
}, DOUBLE_CLICK_DISAMBIGUATION_MS);
|
|
1051
1102
|
}}
|
|
1103
|
+
{...(hasOnTimeRangeReset
|
|
1104
|
+
? {
|
|
1105
|
+
onDoubleClick: () => {
|
|
1106
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
1107
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
1108
|
+
pendingClickTimeoutRef.current = null;
|
|
1109
|
+
}
|
|
1110
|
+
onTimeRangeReset?.();
|
|
1111
|
+
},
|
|
1112
|
+
}
|
|
1113
|
+
: {})}
|
|
1052
1114
|
margin={{
|
|
1053
1115
|
bottom: (xAxisLabel
|
|
1054
1116
|
? 40
|
|
@@ -692,6 +692,12 @@ interface BarChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
692
692
|
referenceLines?: Array<ChartReferenceLineProps> | undefined;
|
|
693
693
|
formattedTimeReferenceLines?: Array<FormattedTimeReferenceLine> | undefined;
|
|
694
694
|
formattedReferenceRegions?: Array<FormattedReferenceRegion> | undefined;
|
|
695
|
+
/**
|
|
696
|
+
* Double-click on the plot surface. Bar charts have no drag-to-select of
|
|
697
|
+
* their own, but they sit on the same dashboards as line/area panels and
|
|
698
|
+
* must offer the same way out of a board-wide zoom.
|
|
699
|
+
*/
|
|
700
|
+
onTimeRangeReset?: (() => void) | undefined;
|
|
695
701
|
}
|
|
696
702
|
|
|
697
703
|
const BarChart: React.ForwardRefExoticComponent<
|
|
@@ -735,6 +741,7 @@ const BarChart: React.ForwardRefExoticComponent<
|
|
|
735
741
|
customTooltip,
|
|
736
742
|
formattedTimeReferenceLines,
|
|
737
743
|
formattedReferenceRegions,
|
|
744
|
+
onTimeRangeReset,
|
|
738
745
|
...other
|
|
739
746
|
} = props;
|
|
740
747
|
const CustomTooltip: React.ComponentType<any> | undefined = customTooltip;
|
|
@@ -877,6 +884,13 @@ const BarChart: React.ForwardRefExoticComponent<
|
|
|
877
884
|
onClick: handleChartClick,
|
|
878
885
|
}
|
|
879
886
|
: {})}
|
|
887
|
+
{...(onTimeRangeReset
|
|
888
|
+
? {
|
|
889
|
+
onDoubleClick: () => {
|
|
890
|
+
onTimeRangeReset();
|
|
891
|
+
},
|
|
892
|
+
}
|
|
893
|
+
: {})}
|
|
880
894
|
margin={{
|
|
881
895
|
bottom: xAxisLabel ? 40 : showXAxis ? 24 : 8,
|
|
882
896
|
left: yAxisLabel ? 20 : 0,
|
|
@@ -41,6 +41,7 @@ import useChartAnnotations, {
|
|
|
41
41
|
UseChartAnnotationsResult,
|
|
42
42
|
} from "../Annotations/UseChartAnnotations";
|
|
43
43
|
import { cx } from "../Utils/Cx";
|
|
44
|
+
import { DOUBLE_CLICK_DISAMBIGUATION_MS } from "../Utils/DoubleClick";
|
|
44
45
|
import { getYAxisDomain } from "../Utils/GetYAxisDomain";
|
|
45
46
|
import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
|
|
46
47
|
import {
|
|
@@ -643,6 +644,15 @@ interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
643
644
|
formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
|
|
644
645
|
onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
|
|
645
646
|
onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
|
|
647
|
+
/**
|
|
648
|
+
* Double-click on the plot surface: the counterpart to drag-to-select,
|
|
649
|
+
* for undoing the zoom it produced. Supplying it makes single clicks
|
|
650
|
+
* wait DOUBLE_CLICK_DISAMBIGUATION_MS before they act, so a double-click
|
|
651
|
+
* cannot also fire the bucket-click path twice on its way past — hosts
|
|
652
|
+
* that have nothing to reset should leave it undefined and keep clicks
|
|
653
|
+
* instant.
|
|
654
|
+
*/
|
|
655
|
+
onTimeRangeReset?: (() => void) | undefined;
|
|
646
656
|
/**
|
|
647
657
|
* Plain click on a time bucket (the plot background — dot/legend clicks
|
|
648
658
|
* and drag-selections never reach this). [bucketStart, bucketEnd) uses
|
|
@@ -702,6 +712,7 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
702
712
|
formattedTimeReferenceLines,
|
|
703
713
|
formattedReferenceRegions,
|
|
704
714
|
onTimeRangeSelect,
|
|
715
|
+
onTimeRangeReset,
|
|
705
716
|
onBucketClick,
|
|
706
717
|
ghostCategories,
|
|
707
718
|
...other
|
|
@@ -732,9 +743,26 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
732
743
|
React.useRef<number | null>(null);
|
|
733
744
|
const suppressNextClickRef: React.MutableRefObject<boolean> =
|
|
734
745
|
React.useRef<boolean>(false);
|
|
746
|
+
const hasOnTimeRangeReset: boolean = Boolean(onTimeRangeReset);
|
|
747
|
+
/*
|
|
748
|
+
* A pending single-click, held open long enough for a second click to
|
|
749
|
+
* cancel it. Only armed when onTimeRangeReset is supplied.
|
|
750
|
+
*/
|
|
751
|
+
const pendingClickTimeoutRef: React.MutableRefObject<ReturnType<
|
|
752
|
+
typeof setTimeout
|
|
753
|
+
> | null> = React.useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
735
754
|
const categoryColors: Map<string, ChartColorValue> =
|
|
736
755
|
constructCategoryColors(categories, colors);
|
|
737
756
|
|
|
757
|
+
React.useEffect(() => {
|
|
758
|
+
return () => {
|
|
759
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
760
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
761
|
+
pendingClickTimeoutRef.current = null;
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
}, []);
|
|
765
|
+
|
|
738
766
|
/*
|
|
739
767
|
* Annotations are drawn onto the categorical axis, so the layer needs
|
|
740
768
|
* the same x values recharts is drawing — not the source dates.
|
|
@@ -978,6 +1006,51 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
978
1006
|
onTimeRangeSelect(startDate, endDate);
|
|
979
1007
|
}
|
|
980
1008
|
|
|
1009
|
+
/*
|
|
1010
|
+
* The plain-click path, split out so it can either run inline (no
|
|
1011
|
+
* reset handler) or be deferred behind the double-click window.
|
|
1012
|
+
*/
|
|
1013
|
+
function handleChartClick(chartState: RangeSelectionChartState): void {
|
|
1014
|
+
/*
|
|
1015
|
+
* A click while a legend/dot selection is active keeps its
|
|
1016
|
+
* long-standing meaning — clear the selection — and never
|
|
1017
|
+
* also pins a bucket.
|
|
1018
|
+
*/
|
|
1019
|
+
if (hasOnValueChange && (activeLegend || activeDot)) {
|
|
1020
|
+
setActiveDot(undefined);
|
|
1021
|
+
setActiveLegend(undefined);
|
|
1022
|
+
onValueChange?.(null);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (!onBucketClick) {
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const rowIndex: number | null = getRowIndexFromChartState(chartState);
|
|
1029
|
+
if (rowIndex === null) {
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
|
|
1033
|
+
if (!bucketStart) {
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
/*
|
|
1037
|
+
* Cover the full bucket: width from adjacent row dates, the
|
|
1038
|
+
* same derivation drag-to-select uses.
|
|
1039
|
+
*/
|
|
1040
|
+
const adjacentBucketDate: Date | null =
|
|
1041
|
+
rowIndex > 0
|
|
1042
|
+
? getBucketDateAtIndex(rowIndex - 1)
|
|
1043
|
+
: getBucketDateAtIndex(rowIndex + 1);
|
|
1044
|
+
const clickedBucketWidthInMs: number = adjacentBucketDate
|
|
1045
|
+
? Math.abs(bucketStart.getTime() - adjacentBucketDate.getTime())
|
|
1046
|
+
: 0;
|
|
1047
|
+
onBucketClick(
|
|
1048
|
+
bucketStart,
|
|
1049
|
+
new Date(bucketStart.getTime() + clickedBucketWidthInMs),
|
|
1050
|
+
(data[rowIndex] || {}) as Record<string, number | string>,
|
|
1051
|
+
);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
981
1054
|
return (
|
|
982
1055
|
<div
|
|
983
1056
|
ref={ref}
|
|
@@ -1016,46 +1089,35 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1016
1089
|
if (suppressNextClickRef.current) {
|
|
1017
1090
|
return;
|
|
1018
1091
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
* long-standing meaning — clear the selection — and never
|
|
1022
|
-
* also pins a bucket.
|
|
1023
|
-
*/
|
|
1024
|
-
if (hasOnValueChange && (activeLegend || activeDot)) {
|
|
1025
|
-
setActiveDot(undefined);
|
|
1026
|
-
setActiveLegend(undefined);
|
|
1027
|
-
onValueChange?.(null);
|
|
1028
|
-
return;
|
|
1029
|
-
}
|
|
1030
|
-
if (!onBucketClick) {
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
const rowIndex: number | null =
|
|
1034
|
-
getRowIndexFromChartState(chartState);
|
|
1035
|
-
if (rowIndex === null) {
|
|
1036
|
-
return;
|
|
1037
|
-
}
|
|
1038
|
-
const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
|
|
1039
|
-
if (!bucketStart) {
|
|
1092
|
+
if (!hasOnTimeRangeReset) {
|
|
1093
|
+
handleChartClick(chartState);
|
|
1040
1094
|
return;
|
|
1041
1095
|
}
|
|
1042
1096
|
/*
|
|
1043
|
-
*
|
|
1044
|
-
*
|
|
1097
|
+
* Reset is on, so hold the click open: both clicks of a
|
|
1098
|
+
* double-click arrive before dblclick does, and neither
|
|
1099
|
+
* may pin a bucket. The second click re-arms the timer,
|
|
1100
|
+
* dblclick clears it.
|
|
1045
1101
|
*/
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
onBucketClick(
|
|
1054
|
-
bucketStart,
|
|
1055
|
-
new Date(bucketStart.getTime() + bucketWidthInMs),
|
|
1056
|
-
(data[rowIndex] || {}) as Record<string, number | string>,
|
|
1057
|
-
);
|
|
1102
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
1103
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
1104
|
+
}
|
|
1105
|
+
pendingClickTimeoutRef.current = setTimeout(() => {
|
|
1106
|
+
pendingClickTimeoutRef.current = null;
|
|
1107
|
+
handleChartClick(chartState);
|
|
1108
|
+
}, DOUBLE_CLICK_DISAMBIGUATION_MS);
|
|
1058
1109
|
}}
|
|
1110
|
+
{...(hasOnTimeRangeReset
|
|
1111
|
+
? {
|
|
1112
|
+
onDoubleClick: () => {
|
|
1113
|
+
if (pendingClickTimeoutRef.current !== null) {
|
|
1114
|
+
clearTimeout(pendingClickTimeoutRef.current);
|
|
1115
|
+
pendingClickTimeoutRef.current = null;
|
|
1116
|
+
}
|
|
1117
|
+
onTimeRangeReset?.();
|
|
1118
|
+
},
|
|
1119
|
+
}
|
|
1120
|
+
: {})}
|
|
1059
1121
|
margin={{
|
|
1060
1122
|
/*
|
|
1061
1123
|
* Tick labels are 10px font with a translate(0, 6) — they
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long a chart holds a single click open, waiting to see whether it is
|
|
3
|
+
* really the first half of a double-click.
|
|
4
|
+
*
|
|
5
|
+
* Charts that accept `onTimeRangeReset` have to tell a bucket click apart
|
|
6
|
+
* from a reset gesture, and the browser delivers BOTH clicks of a
|
|
7
|
+
* double-click before it delivers `dblclick` — so the only way a
|
|
8
|
+
* double-click can avoid also pinning a bucket twice is for the click path
|
|
9
|
+
* to wait. Kept just above the ~200ms most platforms use as their
|
|
10
|
+
* double-click threshold, and short enough that a real single click still
|
|
11
|
+
* feels immediate. Charts with no reset handler skip the wait entirely.
|
|
12
|
+
*/
|
|
13
|
+
export const DOUBLE_CLICK_DISAMBIGUATION_MS: number = 250;
|
|
@@ -62,6 +62,12 @@ export interface ComponentProps {
|
|
|
62
62
|
* buckets calls back with the [start, end) of the selected time range.
|
|
63
63
|
*/
|
|
64
64
|
onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
|
|
65
|
+
/*
|
|
66
|
+
* Double-click on the plot: undoes whatever drag-to-select produced.
|
|
67
|
+
* Supply it only when a reset is actually possible — see ChartLibrary
|
|
68
|
+
* onTimeRangeReset for why an idle handler costs click latency.
|
|
69
|
+
*/
|
|
70
|
+
onTimeRangeReset?: (() => void) | undefined;
|
|
65
71
|
// Plain click on a bucket — see ChartLibrary onBucketClick.
|
|
66
72
|
onBucketClick?:
|
|
67
73
|
| ((
|
|
@@ -221,6 +227,7 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
|
|
221
227
|
}
|
|
222
228
|
onExemplarClick={props.onExemplarClick}
|
|
223
229
|
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
230
|
+
onTimeRangeReset={props.onTimeRangeReset}
|
|
224
231
|
onBucketClick={props.onBucketClick}
|
|
225
232
|
ghostCategories={props.ghostSeriesNames}
|
|
226
233
|
/>
|
|
@@ -201,6 +201,13 @@ export enum ModalTableBulkDefaultActions {
|
|
|
201
201
|
export interface BulkActionProps<T extends BaseModel | AnalyticsBaseModel> {
|
|
202
202
|
buttons: Array<BulkActionButtonSchema<T> | ModalTableBulkDefaultActions>;
|
|
203
203
|
matchBulkSelectedItemByField?: keyof T | undefined; // which field to use to match selected items. For exmaple this could be '_id'
|
|
204
|
+
/**
|
|
205
|
+
* Appended to the bulk Delete confirmation, for a surface where deleting the
|
|
206
|
+
* selected rows also removes records the user did not select, or where a
|
|
207
|
+
* reversible alternative exists. The default sentence only says the deletion
|
|
208
|
+
* cannot be undone - it cannot know what else goes with it.
|
|
209
|
+
*/
|
|
210
|
+
deleteConfirmationWarning?: string | undefined;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
export interface BaseTableProps<
|
|
@@ -2923,7 +2930,12 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2923
2930
|
items.length === 1
|
|
2924
2931
|
? props.singularName || model.singularName || "item"
|
|
2925
2932
|
: props.pluralName || model.pluralName || "items";
|
|
2926
|
-
|
|
2933
|
+
|
|
2934
|
+
const warning: string = props.bulkActions?.deleteConfirmationWarning
|
|
2935
|
+
? ` ${props.bulkActions.deleteConfirmationWarning}`
|
|
2936
|
+
: "";
|
|
2937
|
+
|
|
2938
|
+
return `Are you sure you want to delete ${items.length} ${itemLabel}? This action cannot be undone.${warning}`;
|
|
2927
2939
|
},
|
|
2928
2940
|
confirmTitle: (items: Array<TBaseModel>) => {
|
|
2929
2941
|
const itemLabel: string =
|
|
@@ -2946,26 +2958,41 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
2946
2958
|
const failedItems: Array<BulkActionFailed<TBaseModel>> = [];
|
|
2947
2959
|
|
|
2948
2960
|
for (let i: number = 0; i < items.length; i++) {
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2961
|
+
const item: TBaseModel = items[i]!;
|
|
2962
|
+
|
|
2963
|
+
// remove items from inProgressItems
|
|
2964
|
+
inProgressItems.splice(inProgressItems.indexOf(item), 1);
|
|
2953
2965
|
|
|
2966
|
+
try {
|
|
2954
2967
|
await props.callbacks.deleteItem(item);
|
|
2955
2968
|
successItems.push(item);
|
|
2956
|
-
|
|
2957
|
-
onProgressInfo({
|
|
2958
|
-
inProgressItems: inProgressItems,
|
|
2959
|
-
successItems: successItems,
|
|
2960
|
-
failed: failedItems,
|
|
2961
|
-
totalItems: items,
|
|
2962
|
-
});
|
|
2963
2969
|
} catch (err) {
|
|
2964
2970
|
failedItems.push({
|
|
2965
|
-
item:
|
|
2971
|
+
item: item,
|
|
2966
2972
|
failedMessage: API.getFriendlyMessage(err),
|
|
2967
2973
|
});
|
|
2968
2974
|
}
|
|
2975
|
+
|
|
2976
|
+
/*
|
|
2977
|
+
* Reported after a failure as well as after a success. This used to
|
|
2978
|
+
* sit inside the `try`, so a row that failed moved neither the
|
|
2979
|
+
* progress bar nor the failure list until some later row succeeded
|
|
2980
|
+
* - and a run whose trailing rows all failed (or one where every
|
|
2981
|
+
* row failed, which is what a permission or constraint problem
|
|
2982
|
+
* looks like) ended with the modal still showing the counts from
|
|
2983
|
+
* before them. Deleting hundreds of rows at once is exactly where
|
|
2984
|
+
* partial failure is expected, so it has to be visible.
|
|
2985
|
+
*
|
|
2986
|
+
* Copies, not the live arrays: the progress info goes into React
|
|
2987
|
+
* state, and pushing into the same array reference leaves the
|
|
2988
|
+
* previous render's snapshot mutated underneath it.
|
|
2989
|
+
*/
|
|
2990
|
+
onProgressInfo({
|
|
2991
|
+
inProgressItems: [...inProgressItems],
|
|
2992
|
+
successItems: [...successItems],
|
|
2993
|
+
failed: [...failedItems],
|
|
2994
|
+
totalItems: items,
|
|
2995
|
+
});
|
|
2969
2996
|
}
|
|
2970
2997
|
|
|
2971
2998
|
onBulkActionEnd();
|
|
@@ -3116,9 +3143,17 @@ const BaseModelTable: <TBaseModel extends BaseModel | AnalyticsBaseModel>(
|
|
|
3116
3143
|
},
|
|
3117
3144
|
);
|
|
3118
3145
|
|
|
3146
|
+
/*
|
|
3147
|
+
* On a table that already offers bulk actions the gate decides on
|
|
3148
|
+
* its own, because there is no selection column left to grow: the
|
|
3149
|
+
* row checkboxes are already there. That covers the locked case, and
|
|
3150
|
+
* it also covers a master admin - allowed everything by the gate, but
|
|
3151
|
+
* holding no project permission for `userCanDelete` to find, so the
|
|
3152
|
+
* Delete they are entitled to used to be missing from a menu whose
|
|
3153
|
+
* every other action worked (issue #3559).
|
|
3154
|
+
*/
|
|
3119
3155
|
const canAutoInjectDelete: boolean =
|
|
3120
|
-
userCanDelete ||
|
|
3121
|
-
(bulkDeleteGate.disabled && sourceButtons.length > 0);
|
|
3156
|
+
userCanDelete || (bulkDeleteGate.show && sourceButtons.length > 0);
|
|
3122
3157
|
|
|
3123
3158
|
if (canAutoInjectDelete && !alreadyHasDeleteAction) {
|
|
3124
3159
|
sourceButtons.push(ModalTableBulkDefaultActions.Delete);
|
|
@@ -66,11 +66,11 @@ export interface ComponentProps {
|
|
|
66
66
|
link: URL;
|
|
67
67
|
};
|
|
68
68
|
/*
|
|
69
|
-
* When true,
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* shortcut.
|
|
69
|
+
* When true, Cmd/Ctrl+K never opens or toggles the products menu — for apps
|
|
70
|
+
* where another surface (e.g. a command palette) owns that shortcut. The
|
|
71
|
+
* menu itself stays usable via its trigger button; if it is already open,
|
|
72
|
+
* the owner's chord dismisses it without consuming the event. Defaults to
|
|
73
|
+
* false so existing consumers keep the products shortcut.
|
|
74
74
|
*/
|
|
75
75
|
disableCommandKShortcut?: boolean | undefined;
|
|
76
76
|
className?: string;
|
|
@@ -85,6 +85,22 @@ export interface ComponentProps {
|
|
|
85
85
|
const MOBILE_MENU_BOTTOM_GAP_IN_PX: number = 16;
|
|
86
86
|
const MOBILE_MENU_MIN_HEIGHT_IN_PX: number = 160;
|
|
87
87
|
|
|
88
|
+
/*
|
|
89
|
+
* Keep the products-menu shortcut and the dashboard command palette on the
|
|
90
|
+
* same definition of Cmd/Ctrl+K. In particular, modified chords such as
|
|
91
|
+
* Cmd+Shift+K must be left alone for the browser or another feature.
|
|
92
|
+
*/
|
|
93
|
+
const isCommandKShortcut: (event: KeyboardEvent) => boolean = (
|
|
94
|
+
event: KeyboardEvent,
|
|
95
|
+
): boolean => {
|
|
96
|
+
return (
|
|
97
|
+
(event.metaKey || event.ctrlKey) &&
|
|
98
|
+
!event.shiftKey &&
|
|
99
|
+
!event.altKey &&
|
|
100
|
+
event.key.toLowerCase() === "k"
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
|
|
88
104
|
const Navbar: FunctionComponent<ComponentProps> = (
|
|
89
105
|
props: ComponentProps,
|
|
90
106
|
): ReactElement => {
|
|
@@ -96,6 +112,9 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
96
112
|
const [mobileMenuMaxHeight, setMobileMenuMaxHeight] = useState<
|
|
97
113
|
number | undefined
|
|
98
114
|
>(undefined);
|
|
115
|
+
const hasMoreMenu: boolean = Boolean(
|
|
116
|
+
!props.children && props.moreMenuItems && props.moreMenuItems.length > 0,
|
|
117
|
+
);
|
|
99
118
|
|
|
100
119
|
// Use the existing outside click hook for mobile menu
|
|
101
120
|
const {
|
|
@@ -173,27 +192,56 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
173
192
|
|
|
174
193
|
// Open/close the products menu with Cmd/Ctrl + K from anywhere.
|
|
175
194
|
useEffect(() => {
|
|
176
|
-
if (props.disableCommandKShortcut) {
|
|
177
|
-
// Another surface
|
|
195
|
+
if (props.disableCommandKShortcut || !hasMoreMenu) {
|
|
196
|
+
// Another surface owns Cmd/Ctrl+K, or there is no products menu to open.
|
|
178
197
|
return;
|
|
179
198
|
}
|
|
180
199
|
|
|
181
200
|
const handleGlobalKeyDown: (event: KeyboardEvent) => void = (
|
|
182
201
|
event: KeyboardEvent,
|
|
183
202
|
): void => {
|
|
184
|
-
if ((event
|
|
185
|
-
|
|
186
|
-
setIsMoreMenuVisible((visible: boolean) => {
|
|
187
|
-
return !visible;
|
|
188
|
-
});
|
|
203
|
+
if (!isCommandKShortcut(event) || event.defaultPrevented) {
|
|
204
|
+
return;
|
|
189
205
|
}
|
|
206
|
+
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
setIsMoreMenuVisible((visible: boolean) => {
|
|
209
|
+
return !visible;
|
|
210
|
+
});
|
|
190
211
|
};
|
|
191
212
|
|
|
192
213
|
document.addEventListener("keydown", handleGlobalKeyDown);
|
|
193
214
|
return () => {
|
|
194
215
|
document.removeEventListener("keydown", handleGlobalKeyDown);
|
|
195
216
|
};
|
|
196
|
-
}, [props.disableCommandKShortcut]);
|
|
217
|
+
}, [hasMoreMenu, props.disableCommandKShortcut]);
|
|
218
|
+
|
|
219
|
+
/*
|
|
220
|
+
* When the dashboard has surrendered Cmd/Ctrl+K to its command palette, an
|
|
221
|
+
* already-open products modal must get out of the way before the palette
|
|
222
|
+
* opens. This close-only listener deliberately does not prevent or stop the
|
|
223
|
+
* event: the real shortcut owner still needs to receive the same keydown.
|
|
224
|
+
* It also ignores defaultPrevented so the result is deterministic regardless
|
|
225
|
+
* of which document listener React registered first.
|
|
226
|
+
*/
|
|
227
|
+
useEffect(() => {
|
|
228
|
+
if (!props.disableCommandKShortcut || !hasMoreMenu || !isMoreMenuVisible) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const closeProductsOnCommandK: (event: KeyboardEvent) => void = (
|
|
233
|
+
event: KeyboardEvent,
|
|
234
|
+
): void => {
|
|
235
|
+
if (isCommandKShortcut(event)) {
|
|
236
|
+
setIsMoreMenuVisible(false);
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
document.addEventListener("keydown", closeProductsOnCommandK);
|
|
241
|
+
return () => {
|
|
242
|
+
document.removeEventListener("keydown", closeProductsOnCommandK);
|
|
243
|
+
};
|
|
244
|
+
}, [hasMoreMenu, isMoreMenuVisible, props.disableCommandKShortcut]);
|
|
197
245
|
|
|
198
246
|
// More menu open/close.
|
|
199
247
|
const openMoreMenu: () => void = (): void => {
|
|
@@ -488,6 +536,7 @@ const Navbar: FunctionComponent<ComponentProps> = (
|
|
|
488
536
|
footer={props.moreMenuFooter}
|
|
489
537
|
searchPlaceholder={props.moreMenuSearchPlaceholder}
|
|
490
538
|
noResultsText={props.moreMenuNoResultsText}
|
|
539
|
+
showCommandKShortcutHint={!props.disableCommandKShortcut}
|
|
491
540
|
keyboardHint={props.moreMenuKeyboardHint}
|
|
492
541
|
recentLabel={props.moreMenuRecentLabel}
|
|
493
542
|
onClose={closeMoreMenu}
|