@oneuptime/common 12.0.19 → 12.0.21
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/Server/API/EnterpriseLicenseAPI.ts +94 -19
- package/Server/EnvironmentConfig.ts +15 -5
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/ProjectService.ts +104 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Response.ts +2 -2
- package/Server/Utils/StartServer.ts +2 -2
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
- package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
- package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -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/MetricSeriesInvestigateMenu.test.tsx +368 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
- package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -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/ResponseRenderAnalytics.test.ts +53 -0
- package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
- package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
- package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
- package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
- package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/Analytics/RevenueEvent.ts +1 -0
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +19 -12
- package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
- package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
- package/UI/Components/Charts/Line/LineChart.tsx +23 -1
- package/UI/Components/Detail/Detail.tsx +18 -1
- package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -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/Server/API/EnterpriseLicenseAPI.js +66 -12
- package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +14 -5
- 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/Index.js +0 -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 +2 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/ProjectService.js +88 -0
- package/build/dist/Server/Services/ProjectService.js.map +1 -1
- 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/Response.js +2 -2
- package/build/dist/Server/Utils/Response.js.map +1 -1
- package/build/dist/Server/Utils/StartServer.js +2 -2
- package/build/dist/Server/Utils/StartServer.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/Types/Analytics/RevenueEvent.js +1 -0
- package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.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 +12 -4
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
- package/build/dist/UI/Components/Charts/Bar/BarChart.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 +114 -51
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
- package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Detail/Detail.js +17 -1
- package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
- package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
- 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
|
@@ -40,6 +40,10 @@ import {
|
|
|
40
40
|
import { cx } from "../Utils/Cx";
|
|
41
41
|
import { getYAxisDomain } from "../Utils/GetYAxisDomain";
|
|
42
42
|
import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
|
|
43
|
+
import {
|
|
44
|
+
PreparedTooltipEntries,
|
|
45
|
+
prepareTooltipEntries,
|
|
46
|
+
} from "../Utils/TooltipEntries";
|
|
43
47
|
import ChartCurve from "../../Types/ChartCurve";
|
|
44
48
|
|
|
45
49
|
/*
|
|
@@ -51,6 +55,24 @@ import ChartCurve from "../../Types/ChartCurve";
|
|
|
51
55
|
*/
|
|
52
56
|
const MAX_LABELED_TIME_REFERENCE_LINES: number = 6;
|
|
53
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
|
+
|
|
54
76
|
//#region Legend
|
|
55
77
|
|
|
56
78
|
interface LegendItemProps {
|
|
@@ -478,9 +500,15 @@ const ChartTooltip: ({
|
|
|
478
500
|
valueFormatter,
|
|
479
501
|
}: ChartTooltipProps): React.JSX.Element | null => {
|
|
480
502
|
if (active && payload && payload.length) {
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
503
|
+
/*
|
|
504
|
+
* Highest value at the hovered timestamp first, capped — on a grouped
|
|
505
|
+
* chart (one series per host/pod), the spiking series must be the
|
|
506
|
+
* first line of the tooltip, not buried at its alphabetical position.
|
|
507
|
+
*/
|
|
508
|
+
const {
|
|
509
|
+
entries: legendPayload,
|
|
510
|
+
overflowCount,
|
|
511
|
+
}: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
|
|
484
512
|
return (
|
|
485
513
|
<div
|
|
486
514
|
className={cx(
|
|
@@ -550,6 +578,11 @@ const ChartTooltip: ({
|
|
|
550
578
|
);
|
|
551
579
|
},
|
|
552
580
|
)}
|
|
581
|
+
{overflowCount > 0 ? (
|
|
582
|
+
<p className={cx("pt-1 text-xs", "text-gray-400")}>
|
|
583
|
+
+{overflowCount} more series — highest values shown
|
|
584
|
+
</p>
|
|
585
|
+
) : null}
|
|
553
586
|
</div>
|
|
554
587
|
</div>
|
|
555
588
|
);
|
|
@@ -616,6 +649,26 @@ interface LineChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
616
649
|
formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
|
|
617
650
|
onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
|
|
618
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;
|
|
619
672
|
}
|
|
620
673
|
|
|
621
674
|
const LineChart: React.ForwardRefExoticComponent<
|
|
@@ -655,6 +708,8 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
655
708
|
formattedTimeReferenceLines,
|
|
656
709
|
formattedReferenceRegions,
|
|
657
710
|
onTimeRangeSelect,
|
|
711
|
+
onBucketClick,
|
|
712
|
+
ghostCategories,
|
|
658
713
|
...other
|
|
659
714
|
} = props;
|
|
660
715
|
const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
|
|
@@ -920,7 +975,12 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
920
975
|
<ResponsiveContainer>
|
|
921
976
|
<RechartsLineChart
|
|
922
977
|
data={data}
|
|
923
|
-
|
|
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() } : {})}
|
|
924
984
|
{...(hasOnTimeRangeSelect
|
|
925
985
|
? {
|
|
926
986
|
onMouseDown: handleRangeSelectMouseDown,
|
|
@@ -928,19 +988,51 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
928
988
|
onMouseUp: handleRangeSelectMouseUp,
|
|
929
989
|
}
|
|
930
990
|
: {})}
|
|
931
|
-
onClick={
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
}}
|
|
944
1036
|
margin={{
|
|
945
1037
|
/*
|
|
946
1038
|
* Tick labels are 10px font with a translate(0, 6) — they
|
|
@@ -1126,85 +1218,95 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1126
1218
|
) as ChartColorValue;
|
|
1127
1219
|
const isCustomColor: boolean = isHexColorValue(lineColor);
|
|
1128
1220
|
const lineHex: string = getColorHex(lineColor);
|
|
1221
|
+
const isGhost: boolean =
|
|
1222
|
+
ghostCategories?.includes(category) || false;
|
|
1129
1223
|
return (
|
|
1130
1224
|
<Line
|
|
1131
1225
|
className={cx(getColorClassName(lineColor, "stroke"))}
|
|
1132
1226
|
strokeOpacity={
|
|
1133
|
-
activeDot || (activeLegend && activeLegend !== category)
|
|
1227
|
+
(activeDot || (activeLegend && activeLegend !== category)
|
|
1134
1228
|
? 0.3
|
|
1135
|
-
: 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
|
+
}
|
|
1136
1309
|
}
|
|
1137
|
-
activeDot={(props: any) => {
|
|
1138
|
-
const {
|
|
1139
|
-
cx: cxCoord,
|
|
1140
|
-
cy: cyCoord,
|
|
1141
|
-
stroke,
|
|
1142
|
-
strokeLinecap,
|
|
1143
|
-
strokeLinejoin,
|
|
1144
|
-
strokeWidth,
|
|
1145
|
-
} = props;
|
|
1146
|
-
return (
|
|
1147
|
-
<Dot
|
|
1148
|
-
className={cx(
|
|
1149
|
-
"stroke-white",
|
|
1150
|
-
onValueChange ? "cursor-pointer" : "",
|
|
1151
|
-
getColorClassName(lineColor, "fill"),
|
|
1152
|
-
)}
|
|
1153
|
-
cx={cxCoord}
|
|
1154
|
-
cy={cyCoord}
|
|
1155
|
-
r={5}
|
|
1156
|
-
fill={isCustomColor ? lineHex : ""}
|
|
1157
|
-
stroke={stroke}
|
|
1158
|
-
strokeLinecap={strokeLinecap}
|
|
1159
|
-
strokeLinejoin={strokeLinejoin}
|
|
1160
|
-
strokeWidth={strokeWidth}
|
|
1161
|
-
onClick={(_: any, event: any) => {
|
|
1162
|
-
return onDotClick(props, event);
|
|
1163
|
-
}}
|
|
1164
|
-
/>
|
|
1165
|
-
);
|
|
1166
|
-
}}
|
|
1167
|
-
dot={(props: any) => {
|
|
1168
|
-
const {
|
|
1169
|
-
stroke,
|
|
1170
|
-
strokeLinecap,
|
|
1171
|
-
strokeLinejoin,
|
|
1172
|
-
strokeWidth,
|
|
1173
|
-
cx: cxCoord,
|
|
1174
|
-
cy: cyCoord,
|
|
1175
|
-
index,
|
|
1176
|
-
} = props;
|
|
1177
|
-
|
|
1178
|
-
if (
|
|
1179
|
-
(hasOnlyOneValueForKey(data, category) &&
|
|
1180
|
-
!(
|
|
1181
|
-
activeDot ||
|
|
1182
|
-
(activeLegend && activeLegend !== category)
|
|
1183
|
-
)) ||
|
|
1184
|
-
(activeDot?.index === index &&
|
|
1185
|
-
activeDot?.dataKey === category)
|
|
1186
|
-
) {
|
|
1187
|
-
return (
|
|
1188
|
-
<Dot
|
|
1189
|
-
key={index}
|
|
1190
|
-
cx={cxCoord}
|
|
1191
|
-
cy={cyCoord}
|
|
1192
|
-
r={5}
|
|
1193
|
-
stroke={stroke}
|
|
1194
|
-
fill={isCustomColor ? lineHex : ""}
|
|
1195
|
-
strokeLinecap={strokeLinecap}
|
|
1196
|
-
strokeLinejoin={strokeLinejoin}
|
|
1197
|
-
strokeWidth={strokeWidth}
|
|
1198
|
-
className={cx(
|
|
1199
|
-
"stroke-white",
|
|
1200
|
-
onValueChange ? "cursor-pointer" : "",
|
|
1201
|
-
getColorClassName(lineColor, "fill"),
|
|
1202
|
-
)}
|
|
1203
|
-
/>
|
|
1204
|
-
);
|
|
1205
|
-
}
|
|
1206
|
-
return <React.Fragment key={index}></React.Fragment>;
|
|
1207
|
-
}}
|
|
1208
1310
|
key={category}
|
|
1209
1311
|
name={category}
|
|
1210
1312
|
type={props.curve || ChartCurve.LINEAR}
|
|
@@ -1214,39 +1316,45 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1214
1316
|
* so the `stroke-*` Tailwind class on the Line applies.
|
|
1215
1317
|
*/
|
|
1216
1318
|
stroke={isCustomColor ? lineHex : ""}
|
|
1217
|
-
strokeWidth={2}
|
|
1319
|
+
strokeWidth={isGhost ? 1.5 : 2}
|
|
1218
1320
|
strokeLinejoin="round"
|
|
1219
1321
|
strokeLinecap="round"
|
|
1220
1322
|
isAnimationActive={false}
|
|
1221
1323
|
connectNulls={connectNulls}
|
|
1324
|
+
{...(isGhost ? { strokeDasharray: "6 4" } : {})}
|
|
1222
1325
|
/>
|
|
1223
1326
|
);
|
|
1224
1327
|
})}
|
|
1225
1328
|
{/* hidden lines to increase clickable target area */}
|
|
1226
1329
|
{onValueChange
|
|
1227
|
-
? categories
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
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
|
+
})
|
|
1250
1358
|
: null}
|
|
1251
1359
|
{props.referenceLines?.map(
|
|
1252
1360
|
(refLine: ChartReferenceLineProps, refIndex: number) => {
|
|
@@ -1288,7 +1396,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1288
1396
|
{...(region.original.onClick
|
|
1289
1397
|
? {
|
|
1290
1398
|
style: { cursor: "pointer" as const },
|
|
1291
|
-
onClick: (): void => {
|
|
1399
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1400
|
+
// Never let an annotation click also pin a bucket.
|
|
1401
|
+
stopChartEventPropagation(...args);
|
|
1292
1402
|
if (suppressNextClickRef.current) {
|
|
1293
1403
|
return;
|
|
1294
1404
|
}
|
|
@@ -1330,7 +1440,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1330
1440
|
{...(timeRefLine.original.onClick
|
|
1331
1441
|
? {
|
|
1332
1442
|
style: { cursor: "pointer" as const },
|
|
1333
|
-
onClick: (): void => {
|
|
1443
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1444
|
+
// Never let an annotation click also pin a bucket.
|
|
1445
|
+
stopChartEventPropagation(...args);
|
|
1334
1446
|
if (suppressNextClickRef.current) {
|
|
1335
1447
|
return;
|
|
1336
1448
|
}
|
|
@@ -1376,7 +1488,9 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1376
1488
|
stroke="var(--ou-chart-marker-ring, #ffffff)"
|
|
1377
1489
|
strokeWidth={2}
|
|
1378
1490
|
style={{ cursor: "pointer" }}
|
|
1379
|
-
onClick={() => {
|
|
1491
|
+
onClick={(...args: Array<unknown>) => {
|
|
1492
|
+
// Never let an exemplar click also pin a bucket.
|
|
1493
|
+
stopChartEventPropagation(...args);
|
|
1380
1494
|
// Ignore the click that follows a drag-to-select.
|
|
1381
1495
|
if (suppressNextClickRef.current) {
|
|
1382
1496
|
return;
|
|
@@ -1419,4 +1533,4 @@ const LineChart: React.ForwardRefExoticComponent<
|
|
|
1419
1533
|
|
|
1420
1534
|
LineChart.displayName = "LineChart";
|
|
1421
1535
|
|
|
1422
|
-
export { LineChart, type LineChartEventProps, type TooltipProps };
|
|
1536
|
+
export { ChartTooltip, LineChart, type LineChartEventProps, type TooltipProps };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared ordering/capping for the hover tooltip of the Area / Line / Bar
|
|
3
|
+
* charts. With a grouped metric (say 20 hosts on one chart) the recharts
|
|
4
|
+
* payload arrives in series-render order — effectively alphabetical — so
|
|
5
|
+
* the series that is actually spiking at the hovered timestamp is buried
|
|
6
|
+
* mid-list. Sorting by the value AT THE HOVERED POINT puts the top series
|
|
7
|
+
* first, and capping the list keeps a high-cardinality chart's tooltip
|
|
8
|
+
* readable (the "+N more" line reports what was elided).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/*
|
|
12
|
+
* The slice of a recharts tooltip payload item the ordering needs. The
|
|
13
|
+
* three chart implementations each declare their own structurally-equal
|
|
14
|
+
* PayloadItem, so this stays generic instead of importing any of them.
|
|
15
|
+
*/
|
|
16
|
+
export interface SortableTooltipItem {
|
|
17
|
+
category: string;
|
|
18
|
+
value: number;
|
|
19
|
+
type?: string | undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_TOOLTIP_MAX_ENTRIES: number = 10;
|
|
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
|
+
|
|
32
|
+
export interface PreparedTooltipEntries<T extends SortableTooltipItem> {
|
|
33
|
+
/** Entries to render, highest value first, capped at maxEntries. */
|
|
34
|
+
entries: Array<T>;
|
|
35
|
+
/** How many entries the cap hid (0 when everything fits). */
|
|
36
|
+
overflowCount: number;
|
|
37
|
+
/** Entry count after filtering, before capping. */
|
|
38
|
+
totalCount: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/*
|
|
42
|
+
* Natural compare so host names order like cpu0 < cpu2 < cpu10 instead of
|
|
43
|
+
* lexicographically — mirrors the series chips' name ordering.
|
|
44
|
+
*/
|
|
45
|
+
function compareCategoryNames(a: string, b: string): number {
|
|
46
|
+
return String(a).localeCompare(String(b), undefined, {
|
|
47
|
+
numeric: true,
|
|
48
|
+
sensitivity: "base",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function prepareTooltipEntries<T extends SortableTooltipItem>(
|
|
53
|
+
payload: Array<T> | undefined | null,
|
|
54
|
+
maxEntries: number = DEFAULT_TOOLTIP_MAX_ENTRIES,
|
|
55
|
+
): PreparedTooltipEntries<T> {
|
|
56
|
+
/*
|
|
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.
|
|
65
|
+
*/
|
|
66
|
+
const visibleEntries: Array<T> = (payload || []).filter((item: T) => {
|
|
67
|
+
return item.type !== "none" && !Array.isArray(item.value);
|
|
68
|
+
});
|
|
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
|
+
|
|
87
|
+
const sortedEntries: Array<T> = visibleEntries
|
|
88
|
+
.slice()
|
|
89
|
+
.sort((a: T, b: T): number => {
|
|
90
|
+
const aTier: number = getTier(a);
|
|
91
|
+
const bTier: number = getTier(b);
|
|
92
|
+
if (aTier !== bTier) {
|
|
93
|
+
return aTier - bTier;
|
|
94
|
+
}
|
|
95
|
+
if (aTier === 2) {
|
|
96
|
+
return compareCategoryNames(a.category, b.category);
|
|
97
|
+
}
|
|
98
|
+
if (b.value !== a.value) {
|
|
99
|
+
return b.value - a.value;
|
|
100
|
+
}
|
|
101
|
+
return compareCategoryNames(a.category, b.category);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const totalCount: number = sortedEntries.length;
|
|
105
|
+
|
|
106
|
+
const cap: number =
|
|
107
|
+
Number.isInteger(maxEntries) && maxEntries > 0
|
|
108
|
+
? maxEntries
|
|
109
|
+
: DEFAULT_TOOLTIP_MAX_ENTRIES;
|
|
110
|
+
|
|
111
|
+
if (totalCount <= cap) {
|
|
112
|
+
return { entries: sortedEntries, overflowCount: 0, totalCount };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
entries: sortedEntries.slice(0, cap),
|
|
117
|
+
overflowCount: totalCount - cap,
|
|
118
|
+
totalCount,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -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).
|
|
@@ -161,7 +174,14 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
|
|
161
174
|
|
|
162
175
|
return (
|
|
163
176
|
<div
|
|
164
|
-
|
|
177
|
+
/*
|
|
178
|
+
* `isolate` opens a stacking context so the recharts tooltip's
|
|
179
|
+
* z-index competes only inside this chart — without it the tooltip
|
|
180
|
+
* (zIndex 10, pinned to the chart's top edge) ties with the app
|
|
181
|
+
* header's sticky z-10 in the root context and paints over the
|
|
182
|
+
* navbar on half-scrolled charts.
|
|
183
|
+
*/
|
|
184
|
+
className="relative isolate flex flex-1"
|
|
165
185
|
style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
|
|
166
186
|
>
|
|
167
187
|
<LineChart
|
|
@@ -201,6 +221,8 @@ const LineChartElement: FunctionComponent<LineInternalProps> = (
|
|
|
201
221
|
}
|
|
202
222
|
onExemplarClick={props.onExemplarClick}
|
|
203
223
|
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
224
|
+
onBucketClick={props.onBucketClick}
|
|
225
|
+
ghostCategories={props.ghostSeriesNames}
|
|
204
226
|
/>
|
|
205
227
|
{hasNoData && <NoDataMessage />}
|
|
206
228
|
</div>
|
|
@@ -715,7 +715,24 @@ const Detail: DetailFunction = <T extends GenericObject>(
|
|
|
715
715
|
}
|
|
716
716
|
|
|
717
717
|
if (field.getElement) {
|
|
718
|
-
|
|
718
|
+
/*
|
|
719
|
+
* getField runs inside Detail's own render, so a throw out of a field
|
|
720
|
+
* renderer unwinds the whole route to the app level ErrorBoundary and
|
|
721
|
+
* replaces the page with "Something went wrong". One misbehaving field
|
|
722
|
+
* must never cost the entire page: log the misconfiguration and keep
|
|
723
|
+
* whatever this field type already rendered - the entity badge, or ""
|
|
724
|
+
* so the placeholder below can stand in.
|
|
725
|
+
*/
|
|
726
|
+
try {
|
|
727
|
+
data = field.getElement(props.item);
|
|
728
|
+
} catch (err) {
|
|
729
|
+
Logger.error(
|
|
730
|
+
"Detail: getElement for '" +
|
|
731
|
+
fieldKeyStr +
|
|
732
|
+
"' threw and was ignored. " +
|
|
733
|
+
(err instanceof Error ? err.message : String(err)),
|
|
734
|
+
);
|
|
735
|
+
}
|
|
719
736
|
}
|
|
720
737
|
|
|
721
738
|
let className: string = "sm:col-span-1";
|