@oneuptime/common 12.0.20 → 12.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Models/AnalyticsModels/ChangeEvent.ts +331 -0
- package/Models/AnalyticsModels/Index.ts +2 -0
- package/Models/DatabaseModels/Index.ts +2 -0
- package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
- package/Server/EnvironmentConfig.ts +1 -8
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.ts +42 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +2 -2
- package/Server/Services/ChangeEventService.ts +11 -0
- package/Server/Services/EnterpriseLicenseService.ts +3 -3
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/UserService.ts +42 -0
- package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
- package/Server/Utils/Attribution.ts +11 -11
- package/Server/Utils/Marketing/MarketingEventWebhook.ts +4 -5
- package/Server/Utils/Telemetry/ChangeEventRow.ts +228 -0
- package/Server/Utils/TwoFactorBackupCode.ts +266 -0
- package/Server/Views/Partials/AnalyticsConsent.ejs +3 -95
- package/Tests/App/Dashboard/DashboardChartWidgetZoom.test.tsx +64 -0
- package/Tests/App/Dashboard/EventOverlayHook.test.tsx +249 -0
- package/Tests/App/Dashboard/InvestigationArtifacts.test.tsx +315 -0
- package/Tests/App/Dashboard/InvestigationDrawer.test.tsx +258 -0
- package/Tests/App/Dashboard/MetricChartsCompare.test.tsx +171 -0
- package/Tests/App/Dashboard/MetricViewCompare.test.tsx +233 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
- package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
- package/Tests/Server/Infrastructure/Postgres/AddUserSlackAndMicrosoftTeamsMigration.test.ts +236 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
- package/Tests/Server/Utils/AI/SRE/Insights/Detectors/ExceptionIdentity.test.ts +224 -0
- package/Tests/Server/Utils/Attribution.test.ts +4 -5
- package/Tests/Server/Utils/Marketing/MarketingEventUtil.test.ts +2 -2
- package/Tests/Server/Utils/Monitor/MonitorCriteriaExpectationBuilder.test.ts +300 -0
- package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
- package/Tests/UI/Components/Charts/ChartGhostSeries.test.tsx +103 -0
- package/Tests/UI/Components/Charts/TooltipEntries.test.ts +44 -8
- package/Tests/Utils/Telemetry/CrossSignalScope.test.ts +13 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +7 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/Marketing/Attribution.ts +9 -15
- package/Types/Marketing/MarketingEvent.ts +10 -46
- package/Types/UserAuthenticationStatus.ts +16 -0
- package/UI/Components/Charts/Area/AreaChart.tsx +15 -0
- package/UI/Components/Charts/ChartGroup/ChartGroup.tsx +16 -1
- package/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.tsx +189 -93
- package/UI/Components/Charts/ChartLibrary/BarChart/BarChart.tsx +6 -1
- package/UI/Components/Charts/ChartLibrary/LineChart/LineChart.tsx +213 -114
- package/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.ts +42 -27
- package/UI/Components/Charts/Line/LineChart.tsx +15 -0
- package/Utils/Metrics/MetricExplorerUrl.ts +119 -5
- package/Utils/Telemetry/CrossSignalScope.ts +19 -10
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js +298 -0
- package/build/dist/Models/AnalyticsModels/ChangeEvent.js.map +1 -0
- package/build/dist/Models/AnalyticsModels/Index.js +2 -0
- package/build/dist/Models/AnalyticsModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +2 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
- package/build/dist/Server/EnvironmentConfig.js +1 -7
- package/build/dist/Server/EnvironmentConfig.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js +39 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789000000000-AddUserSlackAndMicrosoftTeams.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +2 -2
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/ChangeEventService.js +9 -0
- package/build/dist/Server/Services/ChangeEventService.js.map +1 -0
- package/build/dist/Server/Services/EnterpriseLicenseService.js +3 -3
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/UserService.js +39 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
- package/build/dist/Server/Utils/Attribution.js +6 -6
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js +4 -5
- package/build/dist/Server/Utils/Marketing/MarketingEventWebhook.js.map +1 -1
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js +150 -0
- package/build/dist/Server/Utils/Telemetry/ChangeEventRow.js.map +1 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +7 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -1
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/Marketing/Attribution.js +9 -15
- package/build/dist/Types/Marketing/Attribution.js.map +1 -1
- package/build/dist/Types/Marketing/MarketingEvent.js +2 -33
- package/build/dist/Types/Marketing/MarketingEvent.js.map +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js +1 -1
- package/build/dist/UI/Components/Charts/Area/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js +9 -1
- package/build/dist/UI/Components/Charts/ChartGroup/ChartGroup.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js +92 -38
- package/build/dist/UI/Components/Charts/ChartLibrary/AreaChart/AreaChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js +8 -8
- package/build/dist/UI/Components/Charts/ChartLibrary/BarChart/BarChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js +95 -37
- package/build/dist/UI/Components/Charts/ChartLibrary/LineChart/LineChart.js.map +1 -1
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js +39 -24
- package/build/dist/UI/Components/Charts/ChartLibrary/Utils/TooltipEntries.js.map +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js +1 -1
- package/build/dist/UI/Components/Charts/Line/LineChart.js.map +1 -1
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js +81 -0
- package/build/dist/Utils/Metrics/MetricExplorerUrl.js.map +1 -1
- package/build/dist/Utils/Telemetry/CrossSignalScope.js +14 -8
- package/build/dist/Utils/Telemetry/CrossSignalScope.js.map +1 -1
- package/package.json +1 -1
- package/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.ts +0 -117
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js +0 -106
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1788900000000-RedactStoredMonitorIngestSecrets.js.map +0 -1
|
@@ -10,6 +10,8 @@ enum EmailTemplateType {
|
|
|
10
10
|
ConfirmStatusPageSubscription = "ConfirmStatusPageSubscription.hbs",
|
|
11
11
|
EmailVerified = "EmailVerified.hbs",
|
|
12
12
|
PasswordChanged = "PasswordChanged.hbs",
|
|
13
|
+
TwoFactorBackupCodeUsed = "TwoFactorBackupCodeUsed.hbs",
|
|
14
|
+
TwoFactorBackupCodesRegenerated = "TwoFactorBackupCodesRegenerated.hbs",
|
|
13
15
|
ProbeOwnerAdded = "ProbeOwnerAdded.hbs",
|
|
14
16
|
InviteMember = "InviteMember.hbs",
|
|
15
17
|
EmailChanged = "EmailChanged.hbs",
|
|
@@ -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;
|
|
@@ -47,6 +47,22 @@ export default interface UserAuthenticationStatus {
|
|
|
47
47
|
*/
|
|
48
48
|
verifiedTwoFactorAuthMethodCount: number;
|
|
49
49
|
|
|
50
|
+
/*
|
|
51
|
+
* How many single-use backup codes the user has left.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately NOT folded into `verifiedTwoFactorAuthMethodCount`, and the
|
|
54
|
+
* separation is the operator's whole decision. The count above answers "can
|
|
55
|
+
* they sign in at all today"; this one answers "can they get themselves back
|
|
56
|
+
* in without me". An operator looking at a lost-phone ticket needs both: a
|
|
57
|
+
* user with codes left should be told to use one, and only a user with none
|
|
58
|
+
* needs the reset button -- which signs them out everywhere and marches them
|
|
59
|
+
* through enrolment.
|
|
60
|
+
*
|
|
61
|
+
* Zero is the normal reading for an account that never generated any, not an
|
|
62
|
+
* error state.
|
|
63
|
+
*/
|
|
64
|
+
unusedTwoFactorBackupCodeCount: number;
|
|
65
|
+
|
|
50
66
|
// Is there an unexpired password-reset link outstanding for this user?
|
|
51
67
|
hasPendingPasswordResetLink: boolean;
|
|
52
68
|
}
|
|
@@ -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
|
|
|
@@ -54,6 +54,24 @@ import ChartCurve from "../../Types/ChartCurve";
|
|
|
54
54
|
*/
|
|
55
55
|
const MAX_LABELED_TIME_REFERENCE_LINES: number = 6;
|
|
56
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
|
+
|
|
57
75
|
//#region Legend
|
|
58
76
|
|
|
59
77
|
interface LegendItemProps {
|
|
@@ -613,6 +631,26 @@ interface AreaChartProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
613
631
|
formattedExemplarPoints?: Array<FormattedExemplarPoint> | undefined;
|
|
614
632
|
onExemplarClick?: ((exemplar: ExemplarPoint) => void) | undefined;
|
|
615
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;
|
|
616
654
|
/**
|
|
617
655
|
* Render a shaded "expected range" band underneath the data lines.
|
|
618
656
|
* Both keys must already be present on every entry of `data` (the
|
|
@@ -661,6 +699,8 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
661
699
|
formattedTimeReferenceLines,
|
|
662
700
|
formattedReferenceRegions,
|
|
663
701
|
onTimeRangeSelect,
|
|
702
|
+
onBucketClick,
|
|
703
|
+
ghostCategories,
|
|
664
704
|
...other
|
|
665
705
|
} = props;
|
|
666
706
|
const CustomTooltip: React.ComponentType<TooltipProps> | undefined =
|
|
@@ -928,7 +968,12 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
928
968
|
<ResponsiveContainer>
|
|
929
969
|
<RechartsAreaChart
|
|
930
970
|
data={data}
|
|
931
|
-
|
|
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() } : {})}
|
|
932
977
|
{...(hasOnTimeRangeSelect
|
|
933
978
|
? {
|
|
934
979
|
onMouseDown: handleRangeSelectMouseDown,
|
|
@@ -936,19 +981,51 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
936
981
|
onMouseUp: handleRangeSelectMouseUp,
|
|
937
982
|
}
|
|
938
983
|
: {})}
|
|
939
|
-
onClick={
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
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
|
+
}}
|
|
952
1029
|
margin={{
|
|
953
1030
|
bottom: (xAxisLabel
|
|
954
1031
|
? 40
|
|
@@ -1173,6 +1250,8 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1173
1250
|
) as ChartColorValue;
|
|
1174
1251
|
const hex: string = getColorHex(colorKey);
|
|
1175
1252
|
const isCustomColor: boolean = isHexColorValue(colorKey);
|
|
1253
|
+
const isGhost: boolean =
|
|
1254
|
+
ghostCategories?.includes(category) || false;
|
|
1176
1255
|
|
|
1177
1256
|
return (
|
|
1178
1257
|
<Area
|
|
@@ -1181,92 +1260,103 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1181
1260
|
type={props.curve || ChartCurve.MONOTONE}
|
|
1182
1261
|
dataKey={category}
|
|
1183
1262
|
stroke={hex}
|
|
1184
|
-
strokeWidth={2}
|
|
1263
|
+
strokeWidth={isGhost ? 1.5 : 2}
|
|
1185
1264
|
strokeLinejoin="round"
|
|
1186
1265
|
strokeLinecap="round"
|
|
1187
1266
|
fill={`url(#${gradientId})`}
|
|
1188
|
-
fillOpacity={1}
|
|
1267
|
+
fillOpacity={isGhost ? 0 : 1}
|
|
1189
1268
|
strokeOpacity={
|
|
1190
|
-
activeDot || (activeLegend && activeLegend !== category)
|
|
1269
|
+
(activeDot || (activeLegend && activeLegend !== category)
|
|
1191
1270
|
? 0.3
|
|
1192
|
-
: 1
|
|
1271
|
+
: 1) * (isGhost ? 0.45 : 1)
|
|
1193
1272
|
}
|
|
1194
1273
|
isAnimationActive={false}
|
|
1195
1274
|
connectNulls={connectNulls}
|
|
1196
1275
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1197
|
-
activeDot={
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
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
|
+
}
|
|
1228
1311
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1229
|
-
dot={
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
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" } : {})}
|
|
1270
1360
|
/>
|
|
1271
1361
|
);
|
|
1272
1362
|
})}
|
|
@@ -1310,7 +1400,9 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1310
1400
|
{...(region.original.onClick
|
|
1311
1401
|
? {
|
|
1312
1402
|
style: { cursor: "pointer" as const },
|
|
1313
|
-
onClick: (): void => {
|
|
1403
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1404
|
+
// Never let an annotation click also pin a bucket.
|
|
1405
|
+
stopChartEventPropagation(...args);
|
|
1314
1406
|
if (suppressNextClickRef.current) {
|
|
1315
1407
|
return;
|
|
1316
1408
|
}
|
|
@@ -1352,7 +1444,9 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1352
1444
|
{...(timeRefLine.original.onClick
|
|
1353
1445
|
? {
|
|
1354
1446
|
style: { cursor: "pointer" as const },
|
|
1355
|
-
onClick: (): void => {
|
|
1447
|
+
onClick: (...args: Array<unknown>): void => {
|
|
1448
|
+
// Never let an annotation click also pin a bucket.
|
|
1449
|
+
stopChartEventPropagation(...args);
|
|
1356
1450
|
if (suppressNextClickRef.current) {
|
|
1357
1451
|
return;
|
|
1358
1452
|
}
|
|
@@ -1398,7 +1492,9 @@ const AreaChart: React.ForwardRefExoticComponent<
|
|
|
1398
1492
|
stroke="var(--ou-chart-marker-ring, #ffffff)"
|
|
1399
1493
|
strokeWidth={2}
|
|
1400
1494
|
style={{ cursor: "pointer" }}
|
|
1401
|
-
onClick={() => {
|
|
1495
|
+
onClick={(...args: Array<unknown>) => {
|
|
1496
|
+
// Never let an exemplar click also pin a bucket.
|
|
1497
|
+
stopChartEventPropagation(...args);
|
|
1402
1498
|
// Ignore the click that follows a drag-to-select.
|
|
1403
1499
|
if (suppressNextClickRef.current) {
|
|
1404
1500
|
return;
|
|
@@ -843,7 +843,12 @@ const BarChart: React.ForwardRefExoticComponent<
|
|
|
843
843
|
<ResponsiveContainer>
|
|
844
844
|
<RechartsBarChart
|
|
845
845
|
data={data}
|
|
846
|
-
|
|
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() } : {})}
|
|
847
852
|
{...(hasOnValueChange && (activeLegend || activeBar)
|
|
848
853
|
? {
|
|
849
854
|
onClick: handleChartClick,
|