@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.
Files changed (124) hide show
  1. package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
  2. package/Models/AnalyticsModels/Index.ts +2 -0
  3. package/Server/API/EnterpriseLicenseAPI.ts +94 -19
  4. package/Server/EnvironmentConfig.ts +15 -5
  5. package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
  6. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +0 -2
  7. package/Server/Services/ChangeEventService.ts +11 -0
  8. package/Server/Services/EnterpriseLicenseService.ts +3 -3
  9. package/Server/Services/Index.ts +2 -0
  10. package/Server/Services/ProjectService.ts +104 -0
  11. package/Server/Utils/Attribution.ts +11 -11
  12. package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
  13. package/Server/Utils/Response.ts +2 -2
  14. package/Server/Utils/StartServer.ts +2 -2
  15. package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
  16. package/Server/Views/Partials/AnalyticsConsent.ejs +441 -0
  17. package/Tests/App/Dashboard/AlertEpisodeViewFields.test.tsx +465 -0
  18. package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +366 -0
  19. package/Tests/App/Dashboard/DiscoveryScanWizardValidation.test.tsx +408 -0
  20. package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
  21. package/Tests/App/Dashboard/IncidentEpisodeViewFields.test.tsx +474 -0
  22. package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
  23. package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
  24. package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
  25. package/Tests/App/Dashboard/MetricSeriesInvestigateMenu.test.tsx +368 -0
  26. package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
  27. package/Tests/Server/API/EnterpriseLicenseReportUserCount.test.ts +763 -0
  28. package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
  29. package/Tests/Server/Services/ProjectServiceChangePlan.test.ts +138 -0
  30. package/Tests/Server/Services/ProjectServiceCreateSubscriptionStarted.test.ts +516 -0
  31. package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
  32. package/Tests/Server/Utils/Attribution.test.ts +4 -5
  33. package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
  34. package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
  35. package/Tests/Server/Utils/ResponseRenderAnalytics.test.ts +53 -0
  36. package/Tests/Server/Views/AnalyticsConsentPartial.test.ts +241 -0
  37. package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
  38. package/Tests/UI/Components/Charts/ChartTooltipSorted.test.tsx +129 -0
  39. package/Tests/UI/Components/Charts/TooltipEntries.test.ts +191 -0
  40. package/Tests/UI/Components/Detail/DetailFieldErrors.test.tsx +347 -0
  41. package/Tests/UI/Components/Forms/BasicFormStepValidation.test.tsx +392 -0
  42. package/Tests/UI/Components/Forms/ValidationFormSteps.test.ts +549 -0
  43. package/Tests/Utils/EnterpriseLicenseSync.test.ts +562 -0
  44. package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
  45. package/Types/Analytics/RevenueEvent.ts +1 -0
  46. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
  47. package/Types/Marketing/Attribution.ts +9 -15
  48. package/Types/Marketing/MarketingEvent.ts +19 -12
  49. package/UI/Components/Charts/Area/AreaChart.tsx +23 -1
  50. package/UI/Components/Charts/Bar/BarChart.tsx +8 -1
  51. package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
  52. package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +208 -97
  53. package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +26 -3
  54. package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +232 -118
  55. package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +120 -0
  56. package/UI/Components/Charts/Line/LineChart.tsx +23 -1
  57. package/UI/Components/Detail/Detail.tsx +18 -1
  58. package/Utils/EnterpriseLicense/EnterpriseLicenseSync.ts +244 -0
  59. package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
  60. package/Utils/Telemetry/CrossSignalScope.ts +19 -10
  61. package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
  62. package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
  63. package/build/dist/Models/AnalyticsModels/Index.js +2 -0
  64. package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
  65. package/build/dist/Server/API/EnterpriseLicenseAPI.js +66 -12
  66. package/build/dist/Server/API/EnterpriseLicenseAPI.js.map +1 -1
  67. package/build/dist/Server/EnvironmentConfig.js +14 -5
  68. package/build/dist/Server/EnvironmentConfig.js.map +1 -1
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +0 -2
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  73. package/build/dist/Server/Services/ChangeEventService.js +9 -0
  74. package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
  75. package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
  76. package/build/dist/Server/Services/Index.js +2 -0
  77. package/build/dist/Server/Services/Index.js.map +1 -1
  78. package/build/dist/Server/Services/ProjectService.js +88 -0
  79. package/build/dist/Server/Services/ProjectService.js.map +1 -1
  80. package/build/dist/Server/Utils/Attribution.js +6 -6
  81. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
  82. package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
  83. package/build/dist/Server/Utils/Response.js +2 -2
  84. package/build/dist/Server/Utils/Response.js.map +1 -1
  85. package/build/dist/Server/Utils/StartServer.js +2 -2
  86. package/build/dist/Server/Utils/StartServer.js.map +1 -1
  87. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
  88. package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
  89. package/build/dist/Types/Analytics/RevenueEvent.js +1 -0
  90. package/build/dist/Types/Analytics/RevenueEvent.js.map +1 -1
  91. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
  92. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
  93. package/build/dist/Types/Marketing/Attribution.js +9 -15
  94. package/build/dist/Types/Marketing/Attribution.js.map +1 -1
  95. package/build/dist/Types/Marketing/MarketingEvent.js +12 -4
  96. package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
  97. package/build/dist/UI/Components/Charts/Area/AreaChart.js +10 -2
  98. package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
  99. package/build/dist/UI/Components/Charts/Bar/BarChart.js +9 -1
  100. package/build/dist/UI/Components/Charts/Bar/BarChart.js.map +1 -1
  101. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
  102. package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
  103. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +114 -51
  104. package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
  105. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +35 -23
  106. package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
  107. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +122 -55
  108. package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
  109. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +86 -0
  110. package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -0
  111. package/build/dist/UI/Components/Charts/Line/LineChart.js +10 -2
  112. package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
  113. package/build/dist/UI/Components/Detail/Detail.js +17 -1
  114. package/build/dist/UI/Components/Detail/Detail.js.map +1 -1
  115. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js +174 -0
  116. package/build/dist/Utils/EnterpriseLicense/EnterpriseLicenseSync.js.map +1 -0
  117. package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
  118. package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
  119. package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
  120. package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
  121. package/package.json +1 -1
  122. package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
  123. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
  124. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
@@ -3,7 +3,7 @@ import { JSONObject } from "../JSON";
3
3
  /*
4
4
  * The outbound marketing event contract.
5
5
  *
6
- * OneUptime does not keep a conversion ledger. The four moments worth
6
+ * OneUptime does not keep a conversion ledger. The moments worth
7
7
  * measuring are emitted as signed webhooks at the instant they happen and are
8
8
  * not stored here afterwards, so this interface — not a database table — is
9
9
  * the whole record of what a conversion looks like.
@@ -13,7 +13,7 @@ import { JSONObject } from "../JSON";
13
13
  *
14
14
  * - DEDUPLICATION. `eventId` is stable for a given real-world occurrence, so
15
15
  * a redelivery carries the id the first attempt did. Retries are expected
16
- * (the queue retries, and Cal retries into us), so a receiver that keys on
16
+ * (the delivery queue retries), so a receiver that keys on
17
17
  * anything else will double-count.
18
18
  * - ORDERING. Events are delivered per-occurrence with no sequence number.
19
19
  * A signup and a plan change seconds apart may arrive either way round;
@@ -26,7 +26,15 @@ import { JSONObject } from "../JSON";
26
26
  */
27
27
  export enum MarketingEventType {
28
28
  SignUp = "sign_up",
29
- MeetingBooked = "meeting_booked",
29
+ /*
30
+ * A project's first paid subscription, created together with the project.
31
+ *
32
+ * Deliberately not a subscription_upgraded. An upgrade is a movement between
33
+ * two tiers and reports expansion; this has no previous tier at all and is
34
+ * new business. Reporting one as the other mixes net-new with expansion
35
+ * revenue, and nothing downstream could separate them again.
36
+ */
37
+ SubscriptionStarted = "subscription_started",
30
38
  SubscriptionUpgraded = "subscription_upgraded",
31
39
  SubscriptionDowngraded = "subscription_downgraded",
32
40
  /*
@@ -36,7 +44,7 @@ export enum MarketingEventType {
36
44
  * whose email is typed in by a human rather than captured from a session —
37
45
  * which is exactly what makes it joinable: set EnterpriseLicense.email to
38
46
  * the address the customer booked with and this event shares an identity
39
- * with the meeting_booked that preceded it, months earlier.
47
+ * with whatever else that address has done, months earlier.
40
48
  */
41
49
  EnterpriseLicenseIssued = "enterprise_license_issued",
42
50
  }
@@ -46,10 +54,9 @@ export const MARKETING_EVENT_SCHEMA_VERSION: number = 1;
46
54
  /*
47
55
  * The campaign the converting visitor carried.
48
56
  *
49
- * Copied from the User or Project row for a signup or a plan change, and read
50
- * out of Cal booking metadata for a booked meeting. Every field is optional:
51
- * a conversion with no attribution at all is still a conversion, and is still
52
- * emitted.
57
+ * Copied from the User or Project row the conversion happened on. Every field
58
+ * is optional: a conversion with no attribution at all is still a conversion,
59
+ * and is still emitted.
53
60
  */
54
61
  export interface MarketingEventAttribution extends JSONObject {
55
62
  utmSource?: string | undefined;
@@ -68,14 +75,14 @@ export interface MarketingEvent extends JSONObject {
68
75
  * Stable per real-world occurrence, and the receiver's deduplication key:
69
76
  *
70
77
  * sign_up:{userId}
71
- * meeting_booked:{calBookingUid}
78
+ * subscription_started:{projectId}
72
79
  * subscription_upgraded:{projectId}:{occurredAt}
73
80
  * subscription_downgraded:{projectId}:{occurredAt}
74
81
  * enterprise_license_issued:{enterpriseLicenseId}
75
82
  *
76
- * Three of these are naturally unique — a user signs up once, a booking has
77
- * one uid, a licence row is issued once — so Cal retrying a delivery or the
78
- * queue retrying a job cannot produce a second conversion. A plan change can
83
+ * Three of these are naturally unique — a user signs up once, a project has
84
+ * one first subscription, a licence row is issued once — so the queue
85
+ * retrying a job cannot produce a second conversion. A plan change can
79
86
  * legitimately recur for one project, so its id carries the instant it
80
87
  * happened.
81
88
  */
@@ -63,6 +63,19 @@ export interface ComponentProps {
63
63
  * buckets calls back with the [start, end) of the selected time range.
64
64
  */
65
65
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
66
+ // Plain click on a bucket — see ChartLibrary onBucketClick.
67
+ onBucketClick?:
68
+ | ((
69
+ bucketStart: Date,
70
+ bucketEnd: Date,
71
+ valuesAtBucket: Record<string, number | string>,
72
+ ) => void)
73
+ | undefined;
74
+ /*
75
+ * Series rendered as compare-to-previous-period ghosts (dashed, faded,
76
+ * dotless). Names must exist in `data`.
77
+ */
78
+ ghostSeriesNames?: Array<string> | undefined;
66
79
  showLegend?: boolean | undefined;
67
80
  /**
68
81
  * Render a shaded "expected range" band underneath the plotted lines.
@@ -181,7 +194,14 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
181
194
 
182
195
  return (
183
196
  <div
184
- className="relative flex flex-1"
197
+ /*
198
+ * `isolate` opens a stacking context so the recharts tooltip's
199
+ * z-index competes only inside this chart — without it the tooltip
200
+ * (zIndex 10, pinned to the chart's top edge) ties with the app
201
+ * header's sticky z-10 in the root context and paints over the
202
+ * navbar on half-scrolled charts.
203
+ */
204
+ className="relative isolate flex flex-1"
185
205
  style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
186
206
  >
187
207
  <AreaChart
@@ -221,6 +241,8 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
221
241
  }
222
242
  onExemplarClick={props.onExemplarClick}
223
243
  onTimeRangeSelect={props.onTimeRangeSelect}
244
+ onBucketClick={props.onBucketClick}
245
+ ghostCategories={props.ghostSeriesNames}
224
246
  anomalyBandLowerKey={bandLower}
225
247
  anomalyBandUpperKey={bandUpper}
226
248
  />
@@ -115,7 +115,14 @@ const BarChartElement: FunctionComponent<BarInternalProps> = (
115
115
 
116
116
  return (
117
117
  <div
118
- className="relative flex flex-1"
118
+ /*
119
+ * `isolate` opens a stacking context so the recharts tooltip's
120
+ * z-index competes only inside this chart — without it the tooltip
121
+ * (zIndex 10, pinned to the chart's top edge) ties with the app
122
+ * header's sticky z-10 in the root context and paints over the
123
+ * navbar on half-scrolled charts.
124
+ */
125
+ className="relative isolate flex flex-1"
119
126
  style={props.heightInPx ? { height: `${props.heightInPx}px` } : undefined}
120
127
  >
121
128
  <BarChart
@@ -57,12 +57,27 @@ export interface ComponentProps {
57
57
  charts: Array<Chart>;
58
58
  hideCard?: boolean | undefined;
59
59
  chartCssClass?: string | undefined;
60
+ /**
61
+ * Share one crosshair-sync channel across SEVERAL ChartGroup instances
62
+ * (e.g. every metric widget on one dashboard passes the dashboard id,
63
+ * so hovering an instant on one widget highlights it on all of them).
64
+ * Absent → this group syncs only within itself.
65
+ */
66
+ syncId?: string | undefined;
60
67
  }
61
68
 
62
69
  const ChartGroup: FunctionComponent<ComponentProps> = (
63
70
  props: ComponentProps,
64
71
  ): ReactElement => {
65
- const syncId: string = Text.generateRandomText(10);
72
+ /*
73
+ * Stable per-mount fallback: a plain const regenerated the id on every
74
+ * render, which forced recharts to tear down and re-subscribe its sync
75
+ * listeners on each parent re-render (auto-refresh ticks included).
76
+ */
77
+ const [fallbackSyncId] = useState<string>(() => {
78
+ return Text.generateRandomText(10);
79
+ });
80
+ const syncId: string = props.syncId || fallbackSyncId;
66
81
  const [metricInfoModalChart, setMetricInfoModalChart] =
67
82
  useState<ChartMetricInfo | null>(null);
68
83
 
@@ -39,6 +39,10 @@ import {
39
39
  import { cx } from "../Utils/Cx";
40
40
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
41
41
  import { hasOnlyOneValueForKey } from "../Utils/HasOnlyOneValueForKey";
42
+ import {
43
+ PreparedTooltipEntries,
44
+ prepareTooltipEntries,
45
+ } from "../Utils/TooltipEntries";
42
46
  import ChartCurve from "../../Types/ChartCurve";
43
47
 
44
48
  /*
@@ -50,6 +54,24 @@ import ChartCurve from "../../Types/ChartCurve";
50
54
  */
51
55
  const MAX_LABELED_TIME_REFERENCE_LINES: number = 6;
52
56
 
57
+ /*
58
+ * recharts invokes child event handlers with varying shapes ((event) for
59
+ * plain SVG passthrough, (data, event) for adapted children) — find the
60
+ * DOM event wherever it is so propagation can be stopped reliably.
61
+ */
62
+ function stopChartEventPropagation(...args: Array<unknown>): void {
63
+ for (const candidate of args) {
64
+ if (
65
+ candidate &&
66
+ typeof (candidate as { stopPropagation?: unknown }).stopPropagation ===
67
+ "function"
68
+ ) {
69
+ (candidate as { stopPropagation: () => void }).stopPropagation();
70
+ return;
71
+ }
72
+ }
73
+ }
74
+
53
75
  //#region Legend
54
76
 
55
77
  interface LegendItemProps {
@@ -475,9 +497,15 @@ const ChartTooltip: ({
475
497
  valueFormatter,
476
498
  }: ChartTooltipProps): React.JSX.Element | null => {
477
499
  if (active && payload && payload.length) {
478
- const legendPayload: PayloadItem[] = payload.filter((item: PayloadItem) => {
479
- return item.type !== "none";
480
- });
500
+ /*
501
+ * Highest value at the hovered timestamp first, capped — on a grouped
502
+ * chart (one series per host/pod), the spiking series must be the
503
+ * first line of the tooltip, not buried at its alphabetical position.
504
+ */
505
+ const {
506
+ entries: legendPayload,
507
+ overflowCount,
508
+ }: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
481
509
  return (
482
510
  <div
483
511
  className={cx(
@@ -531,6 +559,11 @@ const ChartTooltip: ({
531
559
  );
532
560
  },
533
561
  )}
562
+ {overflowCount > 0 ? (
563
+ <p className={cx("pt-1 text-xs", "text-gray-400")}>
564
+ +{overflowCount} more series — highest values shown
565
+ </p>
566
+ ) : null}
534
567
  </div>
535
568
  </div>
536
569
  );
@@ -598,6 +631,26 @@ interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
598
631
  formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
599
632
  onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
600
633
  onTimeRangeSelect?: ((startTime: Date, endTime: Date) => void) | undefined;
634
+ /**
635
+ * Plain click on a time bucket (the plot background — dot/legend clicks
636
+ * and drag-selections never reach this). [bucketStart, bucketEnd) uses
637
+ * the same adjacent-row width derivation as drag-to-select. Fires only
638
+ * when no toggle-selection is active: a click that clears an active
639
+ * legend/dot selection keeps its existing meaning.
640
+ */
641
+ onBucketClick?:
642
+ | ((
643
+ bucketStart: Date,
644
+ bucketEnd: Date,
645
+ // The clicked row: one numeric entry per series + the axis keys.
646
+ valuesAtBucket: Record<string, number | string>,
647
+ ) => void)
648
+ | undefined;
649
+ /**
650
+ * Categories rendered as compare-to-previous-period ghosts: dashed,
651
+ * faded, no fill, no dots — context, not data.
652
+ */
653
+ ghostCategories?: Array<string> | undefined;
601
654
  /**
602
655
  * Render a shaded "expected range" band underneath the data lines.
603
656
  * Both keys must already be present on every entry of `data` (the
@@ -646,6 +699,8 @@ const AreaChart: React.ForwardRefExoticComponent<
646
699
  formattedTimeReferenceLines,
647
700
  formattedReferenceRegions,
648
701
  onTimeRangeSelect,
702
+ onBucketClick,
703
+ ghostCategories,
649
704
  ...other
650
705
  } = props;
651
706
  const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
@@ -913,7 +968,12 @@ const AreaChart: React.ForwardRefExoticComponent<
913
968
  <ResponsiveContainer>
914
969
  <RechartsAreaChart
915
970
  data={data}
916
- syncId={props.syncid?.toString() || ""}
971
+ /*
972
+ * Omitted, not "": recharts treats "" as a real sync
973
+ * channel, so every unsynced chart page-wide would
974
+ * accidentally sync with every other one.
975
+ */
976
+ {...(props.syncid ? { syncId: props.syncid.toString() } : {})}
917
977
  {...(hasOnTimeRangeSelect
918
978
  ? {
919
979
  onMouseDown: handleRangeSelectMouseDown,
@@ -921,19 +981,51 @@ const AreaChart: React.ForwardRefExoticComponent<
921
981
  onMouseUp: handleRangeSelectMouseUp,
922
982
  }
923
983
  : {})}
924
- onClick={
925
- hasOnValueChange && (activeLegend || activeDot)
926
- ? () => {
927
- // Ignore the click that follows a drag-to-select.
928
- if (suppressNextClickRef.current) {
929
- return;
930
- }
931
- setActiveDot(undefined);
932
- setActiveLegend(undefined);
933
- onValueChange?.(null);
934
- }
935
- : () => {}
936
- }
984
+ onClick={(chartState: RangeSelectionChartState) => {
985
+ // Ignore the click that follows a drag-to-select.
986
+ if (suppressNextClickRef.current) {
987
+ return;
988
+ }
989
+ /*
990
+ * A click while a legend/dot selection is active keeps its
991
+ * long-standing meaning — clear the selection — and never
992
+ * also pins a bucket.
993
+ */
994
+ if (hasOnValueChange && (activeLegend || activeDot)) {
995
+ setActiveDot(undefined);
996
+ setActiveLegend(undefined);
997
+ onValueChange?.(null);
998
+ return;
999
+ }
1000
+ if (!onBucketClick) {
1001
+ return;
1002
+ }
1003
+ const rowIndex: number | null =
1004
+ getRowIndexFromChartState(chartState);
1005
+ if (rowIndex === null) {
1006
+ return;
1007
+ }
1008
+ const bucketStart: Date | null = getBucketDateAtIndex(rowIndex);
1009
+ if (!bucketStart) {
1010
+ return;
1011
+ }
1012
+ /*
1013
+ * Cover the full bucket: width from adjacent row dates, the
1014
+ * same derivation drag-to-select uses.
1015
+ */
1016
+ const adjacentDate: Date | null =
1017
+ rowIndex > 0
1018
+ ? getBucketDateAtIndex(rowIndex - 1)
1019
+ : getBucketDateAtIndex(rowIndex + 1);
1020
+ const bucketWidthInMs: number = adjacentDate
1021
+ ? Math.abs(bucketStart.getTime() - adjacentDate.getTime())
1022
+ : 0;
1023
+ onBucketClick(
1024
+ bucketStart,
1025
+ new Date(bucketStart.getTime() + bucketWidthInMs),
1026
+ (data[rowIndex] || {}) as Record<string, number | string>,
1027
+ );
1028
+ }}
937
1029
  margin={{
938
1030
  bottom: (xAxisLabel
939
1031
  ? 40
@@ -1158,6 +1250,8 @@ const AreaChart: React.ForwardRefExoticComponent<
1158
1250
  ) as ChartColorValue;
1159
1251
  const hex: string = getColorHex(colorKey);
1160
1252
  const isCustomColor: boolean = isHexColorValue(colorKey);
1253
+ const isGhost: boolean =
1254
+ ghostCategories?.includes(category) || false;
1161
1255
 
1162
1256
  return (
1163
1257
  <Area
@@ -1166,92 +1260,103 @@ const AreaChart: React.ForwardRefExoticComponent<
1166
1260
  type={props.curve || ChartCurve.MONOTONE}
1167
1261
  dataKey={category}
1168
1262
  stroke={hex}
1169
- strokeWidth={2}
1263
+ strokeWidth={isGhost ? 1.5 : 2}
1170
1264
  strokeLinejoin="round"
1171
1265
  strokeLinecap="round"
1172
1266
  fill={`url(#${gradientId})`}
1173
- fillOpacity={1}
1267
+ fillOpacity={isGhost ? 0 : 1}
1174
1268
  strokeOpacity={
1175
- activeDot || (activeLegend && activeLegend !== category)
1269
+ (activeDot || (activeLegend && activeLegend !== category)
1176
1270
  ? 0.3
1177
- : 1
1271
+ : 1) * (isGhost ? 0.45 : 1)
1178
1272
  }
1179
1273
  isAnimationActive={false}
1180
1274
  connectNulls={connectNulls}
1181
1275
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1182
- activeDot={(dotProps: any) => {
1183
- const {
1184
- cx: cxCoord,
1185
- cy: cyCoord,
1186
- stroke,
1187
- strokeLinecap: slc,
1188
- strokeLinejoin: slj,
1189
- strokeWidth,
1190
- } = dotProps;
1191
- return (
1192
- <Dot
1193
- className={cx(
1194
- "stroke-white",
1195
- onValueChange ? "cursor-pointer" : "",
1196
- getColorClassName(colorKey, "fill"),
1197
- )}
1198
- cx={cxCoord}
1199
- cy={cyCoord}
1200
- r={5}
1201
- fill={isCustomColor ? hex : ""}
1202
- stroke={stroke}
1203
- strokeLinecap={slc}
1204
- strokeLinejoin={slj}
1205
- strokeWidth={strokeWidth}
1206
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1207
- onClick={(_: any, event: any) => {
1208
- return onDotClick(dotProps, event);
1209
- }}
1210
- />
1211
- );
1212
- }}
1276
+ activeDot={
1277
+ isGhost
1278
+ ? false
1279
+ : (dotProps: any) => {
1280
+ const {
1281
+ cx: cxCoord,
1282
+ cy: cyCoord,
1283
+ stroke,
1284
+ strokeLinecap: slc,
1285
+ strokeLinejoin: slj,
1286
+ strokeWidth,
1287
+ } = dotProps;
1288
+ return (
1289
+ <Dot
1290
+ className={cx(
1291
+ "stroke-white",
1292
+ onValueChange ? "cursor-pointer" : "",
1293
+ getColorClassName(colorKey, "fill"),
1294
+ )}
1295
+ cx={cxCoord}
1296
+ cy={cyCoord}
1297
+ r={5}
1298
+ fill={isCustomColor ? hex : ""}
1299
+ stroke={stroke}
1300
+ strokeLinecap={slc}
1301
+ strokeLinejoin={slj}
1302
+ strokeWidth={strokeWidth}
1303
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1304
+ onClick={(_: any, event: any) => {
1305
+ return onDotClick(dotProps, event);
1306
+ }}
1307
+ />
1308
+ );
1309
+ }
1310
+ }
1213
1311
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
1214
- dot={(dotProps: any) => {
1215
- const {
1216
- stroke,
1217
- strokeLinecap: slc,
1218
- strokeLinejoin: slj,
1219
- strokeWidth,
1220
- cx: cxCoord,
1221
- cy: cyCoord,
1222
- index: dotIndex,
1223
- } = dotProps;
1224
-
1225
- if (
1226
- (hasOnlyOneValueForKey(data, category) &&
1227
- !(
1228
- activeDot ||
1229
- (activeLegend && activeLegend !== category)
1230
- )) ||
1231
- (activeDot?.index === dotIndex &&
1232
- activeDot?.dataKey === category)
1233
- ) {
1234
- return (
1235
- <Dot
1236
- key={dotIndex}
1237
- cx={cxCoord}
1238
- cy={cyCoord}
1239
- r={5}
1240
- stroke={stroke}
1241
- fill={isCustomColor ? hex : ""}
1242
- strokeLinecap={slc}
1243
- strokeLinejoin={slj}
1244
- strokeWidth={strokeWidth}
1245
- className={cx(
1246
- "stroke-white",
1247
- onValueChange ? "cursor-pointer" : "",
1248
- getColorClassName(colorKey, "fill"),
1249
- )}
1250
- />
1251
- );
1252
- }
1253
- return <React.Fragment key={dotIndex}></React.Fragment>;
1254
- }}
1312
+ dot={
1313
+ isGhost
1314
+ ? false
1315
+ : (dotProps: any) => {
1316
+ const {
1317
+ stroke,
1318
+ strokeLinecap: slc,
1319
+ strokeLinejoin: slj,
1320
+ strokeWidth,
1321
+ cx: cxCoord,
1322
+ cy: cyCoord,
1323
+ index: dotIndex,
1324
+ } = dotProps;
1325
+
1326
+ if (
1327
+ (hasOnlyOneValueForKey(data, category) &&
1328
+ !(
1329
+ activeDot ||
1330
+ (activeLegend && activeLegend !== category)
1331
+ )) ||
1332
+ (activeDot?.index === dotIndex &&
1333
+ activeDot?.dataKey === category)
1334
+ ) {
1335
+ return (
1336
+ <Dot
1337
+ key={dotIndex}
1338
+ cx={cxCoord}
1339
+ cy={cyCoord}
1340
+ r={5}
1341
+ stroke={stroke}
1342
+ fill={isCustomColor ? hex : ""}
1343
+ strokeLinecap={slc}
1344
+ strokeLinejoin={slj}
1345
+ strokeWidth={strokeWidth}
1346
+ className={cx(
1347
+ "stroke-white",
1348
+ onValueChange ? "cursor-pointer" : "",
1349
+ getColorClassName(colorKey, "fill"),
1350
+ )}
1351
+ />
1352
+ );
1353
+ }
1354
+ return (
1355
+ <React.Fragment key={dotIndex}></React.Fragment>
1356
+ );
1357
+ }
1358
+ }
1359
+ {...(isGhost ? { strokeDasharray: "6 4" } : {})}
1255
1360
  />
1256
1361
  );
1257
1362
  })}
@@ -1295,7 +1400,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1295
1400
  {...(region.original.onClick
1296
1401
  ? {
1297
1402
  style: { cursor: "pointer" as const },
1298
- onClick: (): void => {
1403
+ onClick: (...args: Array<unknown>): void => {
1404
+ // Never let an annotation click also pin a bucket.
1405
+ stopChartEventPropagation(...args);
1299
1406
  if (suppressNextClickRef.current) {
1300
1407
  return;
1301
1408
  }
@@ -1337,7 +1444,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1337
1444
  {...(timeRefLine.original.onClick
1338
1445
  ? {
1339
1446
  style: { cursor: "pointer" as const },
1340
- onClick: (): void => {
1447
+ onClick: (...args: Array<unknown>): void => {
1448
+ // Never let an annotation click also pin a bucket.
1449
+ stopChartEventPropagation(...args);
1341
1450
  if (suppressNextClickRef.current) {
1342
1451
  return;
1343
1452
  }
@@ -1383,7 +1492,9 @@ const AreaChart: React.ForwardRefExoticComponent<
1383
1492
  stroke="var(--ou-chart-marker-ring, #ffffff)"
1384
1493
  strokeWidth={2}
1385
1494
  style={{ cursor: "pointer" }}
1386
- onClick={() => {
1495
+ onClick={(...args: Array<unknown>) => {
1496
+ // Never let an exemplar click also pin a bucket.
1497
+ stopChartEventPropagation(...args);
1387
1498
  // Ignore the click that follows a drag-to-select.
1388
1499
  if (suppressNextClickRef.current) {
1389
1500
  return;
@@ -1426,4 +1537,4 @@ const AreaChart: React.ForwardRefExoticComponent<
1426
1537
 
1427
1538
  AreaChart.displayName = "AreaChart";
1428
1539
 
1429
- export { AreaChart, type AreaChartEventProps, type TooltipProps };
1540
+ export { AreaChart, ChartTooltip, type AreaChartEventProps, type TooltipProps };
@@ -33,6 +33,10 @@ import {
33
33
  } from "../Utils/ChartColors";
34
34
  import { cx } from "../Utils/Cx";
35
35
  import { getYAxisDomain } from "../Utils/GetYAxisDomain";
36
+ import {
37
+ PreparedTooltipEntries,
38
+ prepareTooltipEntries,
39
+ } from "../Utils/TooltipEntries";
36
40
  import { useOnWindowResize } from "../Utils/UseWindowOnResize";
37
41
 
38
42
  /*
@@ -562,6 +566,15 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
562
566
  valueFormatter,
563
567
  }: ChartTooltipProps): React.ReactElement | null => {
564
568
  if (active && payload && payload.length) {
569
+ /*
570
+ * Highest value at the hovered bucket first, capped — on a grouped
571
+ * chart (one series per host/pod), the spiking series must be the
572
+ * first line of the tooltip, not buried at its alphabetical position.
573
+ */
574
+ const {
575
+ entries: sortedPayload,
576
+ overflowCount,
577
+ }: PreparedTooltipEntries<PayloadItem> = prepareTooltipEntries(payload);
565
578
  return (
566
579
  <div
567
580
  className={cx(
@@ -586,7 +599,7 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
586
599
  </p>
587
600
  </div>
588
601
  <div className={cx("space-y-1 px-4 py-2")}>
589
- {payload.map(
602
+ {sortedPayload.map(
590
603
  ({ value, category, color }: PayloadItem, index: number) => {
591
604
  return (
592
605
  <div
@@ -631,6 +644,11 @@ const ChartTooltip: React.FunctionComponent<ChartTooltipProps> = ({
631
644
  );
632
645
  },
633
646
  )}
647
+ {overflowCount > 0 ? (
648
+ <p className={cx("pt-1 text-xs", "text-gray-400")}>
649
+ +{overflowCount} more series — highest values shown
650
+ </p>
651
+ ) : null}
634
652
  </div>
635
653
  </div>
636
654
  );
@@ -825,7 +843,12 @@ const BarChart: React.ForwardRefExoticComponent<
825
843
  <ResponsiveContainer>
826
844
  <RechartsBarChart
827
845
  data={data}
828
- syncId={props.syncid?.toString() || ""}
846
+ /*
847
+ * Omitted, not "": recharts treats "" as a real sync
848
+ * channel, so every unsynced chart page-wide would
849
+ * accidentally sync with every other one.
850
+ */
851
+ {...(props.syncid ? { syncId: props.syncid.toString() } : {})}
829
852
  {...(hasOnValueChange && (activeLegend || activeBar)
830
853
  ? {
831
854
  onClick: handleChartClick,
@@ -1194,4 +1217,4 @@ const BarChart: React.ForwardRefExoticComponent<
1194
1217
 
1195
1218
  BarChart.displayName = "BarChart";
1196
1219
 
1197
- export { BarChart, type BarChartEventProps, type TooltipProps };
1220
+ export { BarChart, ChartTooltip, type BarChartEventProps, type TooltipProps };