@oneuptime/common 12.0.20 → 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/EnvironmentConfig.ts +1 -8
- 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/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -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/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +10 -46
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
- package/UI/Components/Charts/Line/LineChart.tsx +15 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Server/EnvironmentConfig.js +1 -7
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/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/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/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 +2 -33
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "@jest/globals";
|
|
2
2
|
import {
|
|
3
3
|
DEFAULT_TOOLTIP_MAX_ENTRIES,
|
|
4
|
+
PREVIOUS_PERIOD_SERIES_SUFFIX,
|
|
4
5
|
PreparedTooltipEntries,
|
|
5
6
|
SortableTooltipItem,
|
|
6
7
|
prepareTooltipEntries,
|
|
@@ -53,24 +54,24 @@ describe("prepareTooltipEntries", () => {
|
|
|
53
54
|
expect(categories(prepared)).toEqual(["cpu1", "cpu2", "cpu10"]);
|
|
54
55
|
});
|
|
55
56
|
|
|
56
|
-
test("sorts non-finite values last
|
|
57
|
+
test("sorts non-finite values last and drops band tuples entirely", () => {
|
|
57
58
|
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
58
59
|
item("nan-series", NaN),
|
|
59
60
|
item("real-low", 1),
|
|
60
61
|
item("infinite", Infinity),
|
|
61
62
|
item("real-high", 7),
|
|
62
|
-
|
|
63
|
+
/*
|
|
64
|
+
* The anomaly band's dataKey yields a [low, high] tuple at runtime
|
|
65
|
+
* — a shaded region, not a series, so it must not render as a row
|
|
66
|
+
* nor count toward the overflow footer.
|
|
67
|
+
*/
|
|
63
68
|
item("band", [2, 9] as unknown as number),
|
|
64
69
|
]);
|
|
65
70
|
|
|
66
71
|
expect(categories(prepared).slice(0, 2)).toEqual(["real-high", "real-low"]);
|
|
67
72
|
// Non-finite tail keeps natural name order among itself.
|
|
68
|
-
expect(categories(prepared).slice(2)).toEqual([
|
|
69
|
-
|
|
70
|
-
"infinite",
|
|
71
|
-
"nan-series",
|
|
72
|
-
]);
|
|
73
|
-
expect(prepared.totalCount).toBe(5);
|
|
73
|
+
expect(categories(prepared).slice(2)).toEqual(["infinite", "nan-series"]);
|
|
74
|
+
expect(prepared.totalCount).toBe(4);
|
|
74
75
|
});
|
|
75
76
|
|
|
76
77
|
test('filters out type "none" entries (hidden click-target lines, band fills)', () => {
|
|
@@ -143,6 +144,41 @@ describe("prepareTooltipEntries", () => {
|
|
|
143
144
|
}
|
|
144
145
|
});
|
|
145
146
|
|
|
147
|
+
test("previous-period ghosts sort after every live series, whatever their value", () => {
|
|
148
|
+
const prepared: PreparedTooltipEntries<TestItem> = prepareTooltipEntries([
|
|
149
|
+
item(`host-a${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 999),
|
|
150
|
+
item("host-a", 10),
|
|
151
|
+
item("host-b", 50),
|
|
152
|
+
item(`host-b${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 60),
|
|
153
|
+
]);
|
|
154
|
+
|
|
155
|
+
expect(categories(prepared)).toEqual([
|
|
156
|
+
"host-b",
|
|
157
|
+
"host-a",
|
|
158
|
+
// Ghost tier keeps its own value ordering.
|
|
159
|
+
`host-a${PREVIOUS_PERIOD_SERIES_SUFFIX}`,
|
|
160
|
+
`host-b${PREVIOUS_PERIOD_SERIES_SUFFIX}`,
|
|
161
|
+
]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("ghosts never displace live readings under the cap", () => {
|
|
165
|
+
const payload: Array<TestItem> = [];
|
|
166
|
+
for (let index: number = 0; index < DEFAULT_TOOLTIP_MAX_ENTRIES; index++) {
|
|
167
|
+
payload.push(item(`live-${index}`, index));
|
|
168
|
+
payload.push(item(`live-${index}${PREVIOUS_PERIOD_SERIES_SUFFIX}`, 1e6));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const prepared: PreparedTooltipEntries<TestItem> =
|
|
172
|
+
prepareTooltipEntries(payload);
|
|
173
|
+
|
|
174
|
+
expect(
|
|
175
|
+
prepared.entries.every((entry: TestItem) => {
|
|
176
|
+
return !entry.category.endsWith(PREVIOUS_PERIOD_SERIES_SUFFIX);
|
|
177
|
+
}),
|
|
178
|
+
).toBe(true);
|
|
179
|
+
expect(prepared.overflowCount).toBe(DEFAULT_TOOLTIP_MAX_ENTRIES);
|
|
180
|
+
});
|
|
181
|
+
|
|
146
182
|
test("does not mutate the input payload", () => {
|
|
147
183
|
const payload: Array<TestItem> = [item("low", 1), item("high", 2)];
|
|
148
184
|
prepareTooltipEntries(payload);
|
|
@@ -597,9 +597,20 @@ describe("toMetricsExplorerQueryParams", () => {
|
|
|
597
597
|
});
|
|
598
598
|
|
|
599
599
|
test("reports the signal-specific fields the metric grammar cannot express", () => {
|
|
600
|
+
const result: CrossSignalQueryParams = toMetricsExplorerQueryParams(
|
|
601
|
+
fullScope(),
|
|
602
|
+
"container.cpu.time",
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
/*
|
|
606
|
+
* serviceIds now CARRY: they ride the explorer's one-shot
|
|
607
|
+
* `services` param (resolved to resource.service.name filters on
|
|
608
|
+
* load) instead of being reported dropped.
|
|
609
|
+
*/
|
|
610
|
+
expect(result.dropped).toEqual(["traceIds", "spanIds", "severityTexts"]);
|
|
600
611
|
expect(
|
|
601
|
-
|
|
602
|
-
).
|
|
612
|
+
JSON.parse(result.params["services"] as string).length,
|
|
613
|
+
).toBeGreaterThan(0);
|
|
603
614
|
});
|
|
604
615
|
});
|
|
605
616
|
|
|
@@ -7,6 +7,13 @@ enum AnalyticsTableName {
|
|
|
7
7
|
* attributes with a keys sidecar for arbitrary-field filtering.
|
|
8
8
|
*/
|
|
9
9
|
SecurityEvent = "SecurityEventItemV1",
|
|
10
|
+
/*
|
|
11
|
+
* Deploy/config-change markers posted by CI/CD, rendered as dashed
|
|
12
|
+
* vertical lines on metric charts. Peer of LogItemV3 in layout (tenant
|
|
13
|
+
* projectId, per-row retentionDate TTL, attributes Map + keys sidecar)
|
|
14
|
+
* but tiny in volume.
|
|
15
|
+
*/
|
|
16
|
+
ChangeEvent = "ChangeEventV1",
|
|
10
17
|
Metric = "MetricItemV3",
|
|
11
18
|
ExceptionInstance = "ExceptionItemV3",
|
|
12
19
|
Span = "SpanItemV3",
|
|
@@ -3,19 +3,14 @@
|
|
|
3
3
|
* writes it.
|
|
4
4
|
*
|
|
5
5
|
* Attribution starts in the browser (Home/Views/head-basic.ejs stores it in
|
|
6
|
-
* localStorage) and reaches the server through
|
|
6
|
+
* localStorage) and reaches the server through the signup form
|
|
7
|
+
* (App/FeatureSet/Accounts/src/Pages/Register.tsx), which posts it onto the
|
|
8
|
+
* User record.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* /enterprise/demo, /support, /enterprise/self-hosted — goes through it.
|
|
13
|
-
*
|
|
14
|
-
* Both doors are reachable by an unauthenticated caller, so both must whitelist
|
|
15
|
-
* and length-bound what they accept. Keeping the key lists here rather than in
|
|
16
|
-
* each door means a key added for the browser cannot be silently dropped by one
|
|
17
|
-
* of the readers — which is exactly what happened to UTM parameters on the Cal
|
|
18
|
-
* path, where the webhook parsed click IDs the embed never sent.
|
|
10
|
+
* That door is reachable by an unauthenticated caller, so it must whitelist and
|
|
11
|
+
* length-bound what it accepts. Keeping the key lists here rather than in the
|
|
12
|
+
* door itself means a key added for the browser cannot be silently dropped on
|
|
13
|
+
* arrival.
|
|
19
14
|
*/
|
|
20
15
|
|
|
21
16
|
// Ad-platform click identifiers, in the spelling each platform uses on the URL.
|
|
@@ -31,9 +26,8 @@ export const AdClickIdKeys: Array<string> = [
|
|
|
31
26
|
];
|
|
32
27
|
|
|
33
28
|
/*
|
|
34
|
-
* UTM parameters travel as snake_case on a URL and
|
|
35
|
-
*
|
|
36
|
-
* Both spellings are accepted on the way in — Cal booking questions and hand
|
|
29
|
+
* UTM parameters travel as snake_case on a URL and as camelCase once they are
|
|
30
|
+
* columns on User/Project. Both spellings are accepted on the way in — hand
|
|
37
31
|
* written links produce either — and only the camelCase form is stored.
|
|
38
32
|
*/
|
|
39
33
|
export const UtmWireKeyToPropertyKey: Record<string, string> = {
|
|
@@ -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,6 @@ import { JSONObject } from "../JSON";
|
|
|
26
26
|
*/
|
|
27
27
|
export enum MarketingEventType {
|
|
28
28
|
SignUp = "sign_up",
|
|
29
|
-
MeetingBooked = "meeting_booked",
|
|
30
29
|
/*
|
|
31
30
|
* A project's first paid subscription, created together with the project.
|
|
32
31
|
*
|
|
@@ -45,53 +44,19 @@ export enum MarketingEventType {
|
|
|
45
44
|
* whose email is typed in by a human rather than captured from a session —
|
|
46
45
|
* which is exactly what makes it joinable: set EnterpriseLicense.email to
|
|
47
46
|
* the address the customer booked with and this event shares an identity
|
|
48
|
-
* with
|
|
47
|
+
* with whatever else that address has done, months earlier.
|
|
49
48
|
*/
|
|
50
49
|
EnterpriseLicenseIssued = "enterprise_license_issued",
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
/*
|
|
54
|
-
* Which conversation a booked meeting actually is.
|
|
55
|
-
*
|
|
56
|
-
* All three Cal embeds book the same event type (oneuptimehq/demo), so without
|
|
57
|
-
* this the webhook cannot tell a free user's support call from a net-new
|
|
58
|
-
* enterprise demo — they arrive as one undifferentiated `meeting_booked`, and
|
|
59
|
-
* anything bidding on or reporting against that count values them identically.
|
|
60
|
-
*
|
|
61
|
-
* The browser event has carried this since the embeds were instrumented; the
|
|
62
|
-
* webhook did not, which meant the authoritative record was the one that could
|
|
63
|
-
* not make the distinction.
|
|
64
|
-
*
|
|
65
|
-
* `Unknown` is a real state, not a failure: a booking made through a Cal link
|
|
66
|
-
* that was not one of the instrumented embeds genuinely has no kind, and is
|
|
67
|
-
* still a booking worth emitting.
|
|
68
|
-
*/
|
|
69
|
-
export enum MeetingBookingKind {
|
|
70
|
-
EnterpriseDemo = "enterprise_demo",
|
|
71
|
-
SupportCall = "support_call",
|
|
72
|
-
ArchitectureAssessment = "architecture_assessment",
|
|
73
|
-
Unknown = "unknown",
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/*
|
|
77
|
-
* The Cal booking-metadata key the embeds write and the webhook reads. Shared
|
|
78
|
-
* so a rename cannot land on one side only - the failure mode there is silent,
|
|
79
|
-
* because a booking with no recognised kind still succeeds.
|
|
80
|
-
*/
|
|
81
|
-
export const BOOKING_KIND_METADATA_KEY: string = "ou_booking_kind";
|
|
82
|
-
|
|
83
|
-
export const MeetingBookingKinds: Array<string> =
|
|
84
|
-
Object.values(MeetingBookingKind);
|
|
85
|
-
|
|
86
52
|
export const MARKETING_EVENT_SCHEMA_VERSION: number = 1;
|
|
87
53
|
|
|
88
54
|
/*
|
|
89
55
|
* The campaign the converting visitor carried.
|
|
90
56
|
*
|
|
91
|
-
* Copied from the User or Project row
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* 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.
|
|
95
60
|
*/
|
|
96
61
|
export interface MarketingEventAttribution extends JSONObject {
|
|
97
62
|
utmSource?: string | undefined;
|
|
@@ -110,17 +75,16 @@ export interface MarketingEvent extends JSONObject {
|
|
|
110
75
|
* Stable per real-world occurrence, and the receiver's deduplication key:
|
|
111
76
|
*
|
|
112
77
|
* sign_up:{userId}
|
|
113
|
-
* meeting_booked:{calBookingUid}
|
|
114
78
|
* subscription_started:{projectId}
|
|
115
79
|
* subscription_upgraded:{projectId}:{occurredAt}
|
|
116
80
|
* subscription_downgraded:{projectId}:{occurredAt}
|
|
117
81
|
* enterprise_license_issued:{enterpriseLicenseId}
|
|
118
82
|
*
|
|
119
|
-
*
|
|
120
|
-
* one
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
|
86
|
+
* legitimately recur for one project, so its id carries the instant it
|
|
87
|
+
* happened.
|
|
124
88
|
*/
|
|
125
89
|
eventId: string;
|
|
126
90
|
eventType: MarketingEventType;
|
|
@@ -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.
|
|
@@ -228,6 +241,8 @@ const AreaChartElement: FunctionComponent<AreaInternalProps> = (
|
|
|
228
241
|
}
|
|
229
242
|
onExemplarClick={props.onExemplarClick}
|
|
230
243
|
onTimeRangeSelect={props.onTimeRangeSelect}
|
|
244
|
+
onBucketClick={props.onBucketClick}
|
|
245
|
+
ghostCategories={props.ghostSeriesNames}
|
|
231
246
|
anomalyBandLowerKey={bandLower}
|
|
232
247
|
anomalyBandUpperKey={bandUpper}
|
|
233
248
|
/>
|
|
@@ -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
|
|