@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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
50
|
-
*
|
|
51
|
-
*
|
|
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
|
-
*
|
|
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
|
|
77
|
-
* one
|
|
78
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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={
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
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={
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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 };
|