@oneuptime/common 12.0.20 → 12.0.22
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/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
- package/Server/EnvironmentConfig.ts +1 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/UserService.ts +42 -0
- package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Utils/TwoFactorBackupCode.ts +266 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
- package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +10 -46
- package/Types/UserAuthenticationStatus.ts +16 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
- package/UI/Components/Charts/Line/LineChart.tsx +15 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.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/UserTwoFactorBackupCode.js +277 -0
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +1 -7
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +39 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +2 -33
- package/build/dist/Types/Marketing/MarketingEvent.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/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
- 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/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -55,6 +55,24 @@ import ChartCurve from "../../Types/ChartCurve";
|
|
|
55
55
|
*/
|
|
56
56
|
const MAX_LABELED_TIME_REFERENCE_LINES: number = 6;
|
|
57
57
|
|
|
58
|
+
/*
|
|
59
|
+
* recharts invokes child event handlers with varying shapes ((event) for
|
|
60
|
+
* plain SVG passthrough, (data, event) for adapted children) — find the
|
|
61
|
+
* DOM event wherever it is so propagation can be stopped reliably.
|
|
62
|
+
*/
|
|
63
|
+
function stopChartEventPropagation(...args: Array<unknown>): void {
|
|
64
|
+
for (const candidate of args) {
|
|
65
|
+
if (
|
|
66
|
+
candidate &&
|
|
67
|
+
typeof (candidate as { stopPropagation?: unknown }).stopPropagation ===
|
|
68
|
+
"function"
|
|
69
|
+
) {
|
|
70
|
+
(candidate as { stopPropagation: () => void }).stopPropagation();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
58
76
|
//#region Legend
|
|
59
77
|
|
|
60
78
|
interface LegendItemProps {
|
|
@@ -631,6 +649,26 @@ interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
631
649
|
formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
|
|
632
650
|
onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
|
|
633
651
|
onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
|
|
652
|
+
/**
|
|
653
|
+
* Plain click on a time bucket (the plot background — dot/legend clicks
|
|
654
|
+
* and drag-selections never reach this). [bucketStart, bucketEnd) uses
|
|
655
|
+
* the same adjacent-row width derivation as drag-to-select. Fires only
|
|
656
|
+
* when no toggle-selection is active: a click that clears an active
|
|
657
|
+
* legend/dot selection keeps its existing meaning.
|
|
658
|
+
*/
|
|
659
|
+
onBucketClick?:
|
|
660
|
+
| ((
|
|
661
|
+
bucketStart: Date,
|
|
662
|
+
bucketEnd: Date,
|
|
663
|
+
// The clicked row: one numeric entry per series + the axis keys.
|
|
664
|
+
valuesAtBucket: Record<string, number | string>,
|
|
665
|
+
) => void)
|
|
666
|
+
| undefined;
|
|
667
|
+
/**
|
|
668
|
+
* Categories rendered as compare-to-previous-period ghosts: dashed,
|
|
669
|
+
* faded, no dots, not click-selectable — context, not data.
|
|
670
|
+
*/
|
|
671
|
+
ghostCategories?: Array<string> | undefined;
|
|
634
672
|
}
|
|
635
673
|
|
|
636
674
|
const LineChart: React.ForwardRefExoticComponent<
|
|
@@ -670,6 +708,8 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
670
708
|
formattedTimeReferenceLines,
|
|
671
709
|
formattedReferenceRegions,
|
|
672
710
|
onTimeRangeSelect,
|
|
711
|
+
onBucketClick,
|
|
712
|
+
ghostCategories,
|
|
673
713
|
...other
|
|
674
714
|
} = props;
|
|
675
715
|
const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
|
|
@@ -935,7 +975,12 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
935
975
|
<ResponsiveContainer>
|
|
936
976
|
<RechartsLineChart
|
|
937
977
|
data={data}
|
|
938
|
-
|
|
978
|
+
/*
|
|
979
|
+
* Omitted, not "": recharts treats "" as a real sync
|
|
980
|
+
* channel, so every unsynced chart page-wide would
|
|
981
|
+
* accidentally sync with every other one.
|
|
982
|
+
*/
|
|
983
|
+
{...(props.syncid ? { syncId: props.syncid.toString() } : {})}
|
|
939
984
|
{...(hasOnTimeRangeSelect
|
|
940
985
|
? {
|
|
941
986
|
onMouseDown: handleRangeSelectMouseDown,
|
|
@@ -943,19 +988,51 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
943
988
|
onMouseUp: handleRangeSelectMouseUp,
|
|
944
989
|
}
|
|
945
990
|
: {})}
|
|
946
|
-
onClick={
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
991
|
+
onClick={(chartState: RangeSelectionChartState) => {
|
|
992
|
+
// Ignore the click that follows a drag-to-select.
|
|
993
|
+
if (suppressNextClickRef.current) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
/*
|
|
997
|
+
* A click while a legend/dot selection is active keeps its
|
|
998
|
+
* long-standing meaning — clear the selection — and never
|
|
999
|
+
* also pins a bucket.
|
|
1000
|
+
*/
|
|
1001
|
+
if (hasOnValueChange && (activeLegend || activeDot)) {
|
|
1002
|
+
setActiveDot(undefined);
|
|
1003
|
+
setActiveLegend(undefined);
|
|
1004
|
+
onValueChange?.(null);
|
|
1005
|
+
return;
|
|
1006
|
+
}
|
|
1007
|
+
if (!onBucketClick) {
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
1010
|
+
const rowIndex: number | null =
|
|
1011
|
+
getRowIndexFromChartState(chartState);
|
|
1012
|
+
if (rowIndex === null) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
|
|
1016
|
+
if (!bucketStart) {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
/*
|
|
1020
|
+
* Cover the full bucket: width from adjacent row dates, the
|
|
1021
|
+
* same derivation drag-to-select uses.
|
|
1022
|
+
*/
|
|
1023
|
+
const adjacentDate: Date | null =
|
|
1024
|
+
rowIndex > 0
|
|
1025
|
+
? getBucketDateAtIndex(rowIndex - 1)
|
|
1026
|
+
: getBucketDateAtIndex(rowIndex + 1);
|
|
1027
|
+
const bucketWidthInMs: number = adjacentDate
|
|
1028
|
+
? Math.abs(bucketStart.getTime() - adjacentDate.getTime())
|
|
1029
|
+
: 0;
|
|
1030
|
+
onBucketClick(
|
|
1031
|
+
bucketStart,
|
|
1032
|
+
new Date(bucketStart.getTime() + bucketWidthInMs),
|
|
1033
|
+
(data[rowIndex] || {}) as Record<string, number | string>,
|
|
1034
|
+
);
|
|
1035
|
+
}}
|
|
959
1036
|
margin={{
|
|
960
1037
|
/*
|
|
961
1038
|
* Tick labels are 10px font with a translate(0, 6) — they
|
|
@@ -1141,85 +1218,95 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1141
1218
|
) as ChartColorValue;
|
|
1142
1219
|
const isCustomColor: boolean = isHexColorValue(lineColor);
|
|
1143
1220
|
const lineHex: string = getColorHex(lineColor);
|
|
1221
|
+
const isGhost: boolean =
|
|
1222
|
+
ghostCategories?.includes(category) || false;
|
|
1144
1223
|
return (
|
|
1145
1224
|
<Line
|
|
1146
1225
|
className={cx(getColorClassName(lineColor, "stroke"))}
|
|
1147
1226
|
strokeOpacity={
|
|
1148
|
-
activeDot || (activeLegend && activeLegend !== category)
|
|
1227
|
+
(activeDot || (activeLegend && activeLegend !== category)
|
|
1149
1228
|
? 0.3
|
|
1150
|
-
: 1
|
|
1229
|
+
: 1) * (isGhost ? 0.45 : 1)
|
|
1230
|
+
}
|
|
1231
|
+
activeDot={
|
|
1232
|
+
isGhost
|
|
1233
|
+
? false
|
|
1234
|
+
: (props: any) => {
|
|
1235
|
+
const {
|
|
1236
|
+
cx: cxCoord,
|
|
1237
|
+
cy: cyCoord,
|
|
1238
|
+
stroke,
|
|
1239
|
+
strokeLinecap,
|
|
1240
|
+
strokeLinejoin,
|
|
1241
|
+
strokeWidth,
|
|
1242
|
+
} = props;
|
|
1243
|
+
return (
|
|
1244
|
+
<Dot
|
|
1245
|
+
className={cx(
|
|
1246
|
+
"stroke-white",
|
|
1247
|
+
onValueChange ? "cursor-pointer" : "",
|
|
1248
|
+
getColorClassName(lineColor, "fill"),
|
|
1249
|
+
)}
|
|
1250
|
+
cx={cxCoord}
|
|
1251
|
+
cy={cyCoord}
|
|
1252
|
+
r={5}
|
|
1253
|
+
fill={isCustomColor ? lineHex : ""}
|
|
1254
|
+
stroke={stroke}
|
|
1255
|
+
strokeLinecap={strokeLinecap}
|
|
1256
|
+
strokeLinejoin={strokeLinejoin}
|
|
1257
|
+
strokeWidth={strokeWidth}
|
|
1258
|
+
onClick={(_: any, event: any) => {
|
|
1259
|
+
return onDotClick(props, event);
|
|
1260
|
+
}}
|
|
1261
|
+
/>
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
dot={
|
|
1266
|
+
isGhost
|
|
1267
|
+
? false
|
|
1268
|
+
: (props: any) => {
|
|
1269
|
+
const {
|
|
1270
|
+
stroke,
|
|
1271
|
+
strokeLinecap,
|
|
1272
|
+
strokeLinejoin,
|
|
1273
|
+
strokeWidth,
|
|
1274
|
+
cx: cxCoord,
|
|
1275
|
+
cy: cyCoord,
|
|
1276
|
+
index,
|
|
1277
|
+
} = props;
|
|
1278
|
+
|
|
1279
|
+
if (
|
|
1280
|
+
(hasOnlyOneValueForKey(data, category) &&
|
|
1281
|
+
!(
|
|
1282
|
+
activeDot ||
|
|
1283
|
+
(activeLegend && activeLegend !== category)
|
|
1284
|
+
)) ||
|
|
1285
|
+
(activeDot?.index === index &&
|
|
1286
|
+
activeDot?.dataKey === category)
|
|
1287
|
+
) {
|
|
1288
|
+
return (
|
|
1289
|
+
<Dot
|
|
1290
|
+
key={index}
|
|
1291
|
+
cx={cxCoord}
|
|
1292
|
+
cy={cyCoord}
|
|
1293
|
+
r={5}
|
|
1294
|
+
stroke={stroke}
|
|
1295
|
+
fill={isCustomColor ? lineHex : ""}
|
|
1296
|
+
strokeLinecap={strokeLinecap}
|
|
1297
|
+
strokeLinejoin={strokeLinejoin}
|
|
1298
|
+
strokeWidth={strokeWidth}
|
|
1299
|
+
className={cx(
|
|
1300
|
+
"stroke-white",
|
|
1301
|
+
onValueChange ? "cursor-pointer" : "",
|
|
1302
|
+
getColorClassName(lineColor, "fill"),
|
|
1303
|
+
)}
|
|
1304
|
+
/>
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
return <React.Fragment key={index}></React.Fragment>;
|
|
1308
|
+
}
|
|
1151
1309
|
}
|
|
1152
|
-
activeDot={(props: any) => {
|
|
1153
|
-
const {
|
|
1154
|
-
cx: cxCoord,
|
|
1155
|
-
cy: cyCoord,
|
|
1156
|
-
stroke,
|
|
1157
|
-
strokeLinecap,
|
|
1158
|
-
strokeLinejoin,
|
|
1159
|
-
strokeWidth,
|
|
1160
|
-
} = props;
|
|
1161
|
-
return (
|
|
1162
|
-
<Dot
|
|
1163
|
-
className={cx(
|
|
1164
|
-
"stroke-white",
|
|
1165
|
-
onValueChange ? "cursor-pointer" : "",
|
|
1166
|
-
getColorClassName(lineColor, "fill"),
|
|
1167
|
-
)}
|
|
1168
|
-
cx={cxCoord}
|
|
1169
|
-
cy={cyCoord}
|
|
1170
|
-
r={5}
|
|
1171
|
-
fill={isCustomColor ? lineHex : ""}
|
|
1172
|
-
stroke={stroke}
|
|
1173
|
-
strokeLinecap={strokeLinecap}
|
|
1174
|
-
strokeLinejoin={strokeLinejoin}
|
|
1175
|
-
strokeWidth={strokeWidth}
|
|
1176
|
-
onClick={(_: any, event: any) => {
|
|
1177
|
-
return onDotClick(props, event);
|
|
1178
|
-
}}
|
|
1179
|
-
/>
|
|
1180
|
-
);
|
|
1181
|
-
}}
|
|
1182
|
-
dot={(props: any) => {
|
|
1183
|
-
const {
|
|
1184
|
-
stroke,
|
|
1185
|
-
strokeLinecap,
|
|
1186
|
-
strokeLinejoin,
|
|
1187
|
-
strokeWidth,
|
|
1188
|
-
cx: cxCoord,
|
|
1189
|
-
cy: cyCoord,
|
|
1190
|
-
index,
|
|
1191
|
-
} = props;
|
|
1192
|
-
|
|
1193
|
-
if (
|
|
1194
|
-
(hasOnlyOneValueForKey(data, category) &&
|
|
1195
|
-
!(
|
|
1196
|
-
activeDot ||
|
|
1197
|
-
(activeLegend && activeLegend !== category)
|
|
1198
|
-
)) ||
|
|
1199
|
-
(activeDot?.index === index &&
|
|
1200
|
-
activeDot?.dataKey === category)
|
|
1201
|
-
) {
|
|
1202
|
-
return (
|
|
1203
|
-
<Dot
|
|
1204
|
-
key={index}
|
|
1205
|
-
cx={cxCoord}
|
|
1206
|
-
cy={cyCoord}
|
|
1207
|
-
r={5}
|
|
1208
|
-
stroke={stroke}
|
|
1209
|
-
fill={isCustomColor ? lineHex : ""}
|
|
1210
|
-
strokeLinecap={strokeLinecap}
|
|
1211
|
-
strokeLinejoin={strokeLinejoin}
|
|
1212
|
-
strokeWidth={strokeWidth}
|
|
1213
|
-
className={cx(
|
|
1214
|
-
"stroke-white",
|
|
1215
|
-
onValueChange ? "cursor-pointer" : "",
|
|
1216
|
-
getColorClassName(lineColor, "fill"),
|
|
1217
|
-
)}
|
|
1218
|
-
/>
|
|
1219
|
-
);
|
|
1220
|
-
}
|
|
1221
|
-
return <React.Fragment key={index}></React.Fragment>;
|
|
1222
|
-
}}
|
|
1223
1310
|
key={category}
|
|
1224
1311
|
name={category}
|
|
1225
1312
|
type={props.curve || ChartCurve.LINEAR}
|
|
@@ -1229,39 +1316,45 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1229
1316
|
* so the `stroke-*` Tailwind class on the Line applies.
|
|
1230
1317
|
*/
|
|
1231
1318
|
stroke={isCustomColor ? lineHex : ""}
|
|
1232
|
-
strokeWidth={2}
|
|
1319
|
+
strokeWidth={isGhost ? 1.5 : 2}
|
|
1233
1320
|
strokeLinejoin="round"
|
|
1234
1321
|
strokeLinecap="round"
|
|
1235
1322
|
isAnimationActive={false}
|
|
1236
1323
|
connectNulls={connectNulls}
|
|
1324
|
+
{...(isGhost ? { strokeDasharray: "6 4" } : {})}
|
|
1237
1325
|
/>
|
|
1238
1326
|
);
|
|
1239
1327
|
})}
|
|
1240
1328
|
{/* hidden lines to increase clickable target area */}
|
|
1241
1329
|
{onValueChange
|
|
1242
|
-
? categories
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1330
|
+
? categories
|
|
1331
|
+
.filter((category: string) => {
|
|
1332
|
+
// Ghosts are context, not click-selectable series.
|
|
1333
|
+
return !(ghostCategories?.includes(category) || false);
|
|
1334
|
+
})
|
|
1335
|
+
.map((category: string) => {
|
|
1336
|
+
return (
|
|
1337
|
+
<Line
|
|
1338
|
+
className={cx("cursor-pointer")}
|
|
1339
|
+
strokeOpacity={0}
|
|
1340
|
+
key={category}
|
|
1341
|
+
name={category}
|
|
1342
|
+
type={props.curve || ChartCurve.LINEAR}
|
|
1343
|
+
dataKey={category}
|
|
1344
|
+
stroke="transparent"
|
|
1345
|
+
fill="transparent"
|
|
1346
|
+
legendType="none"
|
|
1347
|
+
tooltipType="none"
|
|
1348
|
+
strokeWidth={12}
|
|
1349
|
+
connectNulls={connectNulls}
|
|
1350
|
+
onClick={(props: any, event: any) => {
|
|
1351
|
+
event.stopPropagation();
|
|
1352
|
+
const { name } = props;
|
|
1353
|
+
onCategoryClick(name);
|
|
1354
|
+
}}
|
|
1355
|
+
/>
|
|
1356
|
+
);
|
|
1357
|
+
})
|
|
1265
1358
|
: null}
|
|
1266
1359
|
{props.referenceLines?.map(
|
|
1267
1360
|
(refLine: ChartReferenceLineProps, refIndex: number) => {
|
|
@@ -1303,7 +1396,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1303
1396
|
{...(region.original.onClick
|
|
1304
1397
|
? {
|
|
1305
1398
|
style: { cursor: "pointer" as const },
|
|
1306
|
-
onClick: (): void => {
|
|
1399
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1400
|
+
// Never let an annotation click also pin a bucket.
|
|
1401
|
+
stopChartEventPropagation(...args);
|
|
1307
1402
|
if (suppressNextClickRef.current) {
|
|
1308
1403
|
return;
|
|
1309
1404
|
}
|
|
@@ -1345,7 +1440,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1345
1440
|
{...(timeRefLine.original.onClick
|
|
1346
1441
|
? {
|
|
1347
1442
|
style: { cursor: "pointer" as const },
|
|
1348
|
-
onClick: (): void => {
|
|
1443
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1444
|
+
// Never let an annotation click also pin a bucket.
|
|
1445
|
+
stopChartEventPropagation(...args);
|
|
1349
1446
|
if (suppressNextClickRef.current) {
|
|
1350
1447
|
return;
|
|
1351
1448
|
}
|
|
@@ -1391,7 +1488,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1391
1488
|
stroke="var(--ou-chart-marker-ring, #ffffff)"
|
|
1392
1489
|
strokeWidth={2}
|
|
1393
1490
|
style={{ cursor: "pointer" }}
|
|
1394
|
-
onClick={() => {
|
|
1491
|
+
onClick={(...args: Array<unknown>) => {
|
|
1492
|
+
// Never let an exemplar click also pin a bucket.
|
|
1493
|
+
stopChartEventPropagation(...args);
|
|
1395
1494
|
// Ignore the click that follows a drag-to-select.
|
|
1396
1495
|
if (suppressNextClickRef.current) {
|
|
1397
1496
|
return;
|
|
@@ -21,6 +21,14 @@ export interface SortableTooltipItem {
|
|
|
21
21
|
|
|
22
22
|
export const DEFAULT_TOOLTIP_MAX_ENTRIES: number = 10;
|
|
23
23
|
|
|
24
|
+
/*
|
|
25
|
+
* Series carrying this suffix are compare-to-previous-period ghosts.
|
|
26
|
+
* They still read out in the tooltip (that is the point of comparing)
|
|
27
|
+
* but sort after every live series so they can never displace a real
|
|
28
|
+
* reading under the entry cap.
|
|
29
|
+
*/
|
|
30
|
+
export const PREVIOUS_PERIOD_SERIES_SUFFIX: string = " (previous)";
|
|
31
|
+
|
|
24
32
|
export interface PreparedTooltipEntries<T extends SortableTooltipItem> {
|
|
25
33
|
/** Entries to render, highest value first, capped at maxEntries. */
|
|
26
34
|
entries: Array<T>;
|
|
@@ -46,42 +54,49 @@ export function prepareTooltipEntries<T extends SortableTooltipItem>(
|
|
|
46
54
|
maxEntries: number = DEFAULT_TOOLTIP_MAX_ENTRIES,
|
|
47
55
|
): PreparedTooltipEntries<T> {
|
|
48
56
|
/*
|
|
49
|
-
*
|
|
50
|
-
* (recharts tooltipType) —
|
|
51
|
-
*
|
|
57
|
+
* Two kinds of non-series entries are excluded:
|
|
58
|
+
* - `type === "none"` (recharts tooltipType) — series explicitly
|
|
59
|
+
* excluded from tooltips; every chart filtered these before this
|
|
60
|
+
* module existed, so the filter lives here now to stay uniform.
|
|
61
|
+
* - Array values — the anomaly band's range Area yields a
|
|
62
|
+
* [low, high] tuple per point; it is a shaded region, not a series
|
|
63
|
+
* reading, so it must neither render as a row nor count toward the
|
|
64
|
+
* "+N more series" overflow.
|
|
52
65
|
*/
|
|
53
66
|
const visibleEntries: Array<T> = (payload || []).filter((item: T) => {
|
|
54
|
-
return item.type !== "none";
|
|
67
|
+
return item.type !== "none" && !Array.isArray(item.value);
|
|
55
68
|
});
|
|
56
69
|
|
|
70
|
+
/*
|
|
71
|
+
* Sort tiers: live finite readings first (value desc), then
|
|
72
|
+
* previous-period ghosts (value desc), then non-finite entries
|
|
73
|
+
* (missing points) — lower tiers must never displace a real reading
|
|
74
|
+
* under the entry cap.
|
|
75
|
+
*/
|
|
76
|
+
const getTier: (item: T) => number = (item: T): number => {
|
|
77
|
+
const hasFiniteValue: boolean =
|
|
78
|
+
typeof item.value === "number" && Number.isFinite(item.value);
|
|
79
|
+
if (!hasFiniteValue) {
|
|
80
|
+
return 2;
|
|
81
|
+
}
|
|
82
|
+
return String(item.category).endsWith(PREVIOUS_PERIOD_SERIES_SUFFIX)
|
|
83
|
+
? 1
|
|
84
|
+
: 0;
|
|
85
|
+
};
|
|
86
|
+
|
|
57
87
|
const sortedEntries: Array<T> = visibleEntries
|
|
58
88
|
.slice()
|
|
59
89
|
.sort((a: T, b: T): number => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
*/
|
|
65
|
-
const aValue: number | null =
|
|
66
|
-
typeof a.value === "number" && Number.isFinite(a.value)
|
|
67
|
-
? a.value
|
|
68
|
-
: null;
|
|
69
|
-
const bValue: number | null =
|
|
70
|
-
typeof b.value === "number" && Number.isFinite(b.value)
|
|
71
|
-
? b.value
|
|
72
|
-
: null;
|
|
73
|
-
|
|
74
|
-
if (aValue === null && bValue === null) {
|
|
75
|
-
return compareCategoryNames(a.category, b.category);
|
|
90
|
+
const aTier: number = getTier(a);
|
|
91
|
+
const bTier: number = getTier(b);
|
|
92
|
+
if (aTier !== bTier) {
|
|
93
|
+
return aTier - bTier;
|
|
76
94
|
}
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
79
|
-
}
|
|
80
|
-
if (bValue === null) {
|
|
81
|
-
return -1;
|
|
95
|
+
if (aTier === 2) {
|
|
96
|
+
return compareCategoryNames(a.category, b.category);
|
|
82
97
|
}
|
|
83
|
-
if (
|
|
84
|
-
return
|
|
98
|
+
if (b.value !== a.value) {
|
|
99
|
+
return b.value - a.value;
|
|
85
100
|
}
|
|
86
101
|
return compareCategoryNames(a.category, b.category);
|
|
87
102
|
});
|
|
@@ -62,6 +62,19 @@ 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
|
+
// Plain click on a bucket — see ChartLibrary onBucketClick.
|
|
66
|
+
onBucketClick?:
|
|
67
|
+
| ((
|
|
68
|
+
bucketStart: Date,
|
|
69
|
+
bucketEnd: Date,
|
|
70
|
+
valuesAtBucket: Record<string, number | string>,
|
|
71
|
+
) => void)
|
|
72
|
+
| undefined;
|
|
73
|
+
/*
|
|
74
|
+
* Series rendered as compare-to-previous-period ghosts (dashed, faded,
|
|
75
|
+
* dotless). Names must exist in `data`.
|
|
76
|
+
*/
|
|
77
|
+
ghostSeriesNames?: Array<string> | undefined;
|
|
65
78
|
showLegend?: boolean | undefined;
|
|
66
79
|
/*
|
|
67
80
|
* Optional per-series color override (named palette keys or hex strings).
|
|
@@ -208,6 +221,8 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
|
|
208
221
|
}
|
|
209
222
|
onExemplarClick={props.onExemplarClick}
|
|
210
223
|
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
224
|
+
onBucketClick={props.onBucketClick}
|
|
225
|
+
ghostCategories={props.ghostSeriesNames}
|
|
211
226
|
/>
|
|
212
227
|
{hasNoData && <NoDataMessage />}
|
|
213
228
|
</div>
|