@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
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import { cleanup, render, screen } from "@testing-library/react";
|
|
11
|
+
import * as React from "react";
|
|
12
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
13
|
+
|
|
14
|
+
/*
|
|
15
|
+
* Ghost overlays end-to-end through the real MetricCharts: previous-
|
|
16
|
+
* period results must render as dashed twins of DISPLAYED series only —
|
|
17
|
+
* never as legend chips, never consuming Top-N slots, and always
|
|
18
|
+
* time-shifted onto the current window's bucket grid (unshifted points
|
|
19
|
+
* would be silently dropped by the bucketer).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
jest.mock("recharts", () => {
|
|
23
|
+
const actual: Record<string, any> = jest.requireActual("recharts") as Record<
|
|
24
|
+
string,
|
|
25
|
+
any
|
|
26
|
+
>;
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
30
|
+
return React.cloneElement(children, {
|
|
31
|
+
width: 600,
|
|
32
|
+
height: 300,
|
|
33
|
+
} as Record<string, unknown>);
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const fetchExemplarsMock: MockFunction = getJestMockFunction();
|
|
39
|
+
|
|
40
|
+
jest.mock(
|
|
41
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
|
|
42
|
+
() => {
|
|
43
|
+
return {
|
|
44
|
+
__esModule: true,
|
|
45
|
+
default: {
|
|
46
|
+
fetchExemplars: (...args: Array<any>) => {
|
|
47
|
+
fetchExemplarsMock(...args);
|
|
48
|
+
return new Promise(() => {
|
|
49
|
+
// Intentionally never settles.
|
|
50
|
+
});
|
|
51
|
+
},
|
|
52
|
+
setQueryTopNOverride: () => {
|
|
53
|
+
return undefined;
|
|
54
|
+
},
|
|
55
|
+
getQueryConfigTopNKey: (
|
|
56
|
+
_queryConfig: unknown,
|
|
57
|
+
index: number,
|
|
58
|
+
scope?: string,
|
|
59
|
+
) => {
|
|
60
|
+
return `${scope || ""}:${index}`;
|
|
61
|
+
},
|
|
62
|
+
clearQueryTopNOverridesForScope: () => {
|
|
63
|
+
return undefined;
|
|
64
|
+
},
|
|
65
|
+
serializeAttributeFiltersForKey: (attributes: unknown) => {
|
|
66
|
+
return JSON.stringify(attributes || {});
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
DEFAULT_TOP_N_SERIES: 10,
|
|
70
|
+
SHOW_ALL_SERIES_TOP_N: 10_000,
|
|
71
|
+
sanitizeAttributeFilters: (attributes: unknown) => {
|
|
72
|
+
return attributes;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
import MetricCharts from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts";
|
|
79
|
+
import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
|
|
80
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
81
|
+
import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
|
|
82
|
+
import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
|
|
83
|
+
import MetricViewData from "../../../Types/Metrics/MetricViewData";
|
|
84
|
+
|
|
85
|
+
const WINDOW_START: Date = new Date("2026-08-20T10:00:00.000Z");
|
|
86
|
+
const WINDOW_END: Date = new Date("2026-08-20T11:00:00.000Z");
|
|
87
|
+
const WINDOW_MS: number = WINDOW_END.getTime() - WINDOW_START.getTime();
|
|
88
|
+
|
|
89
|
+
function buildViewData(): MetricViewData {
|
|
90
|
+
return {
|
|
91
|
+
queryConfigs: [
|
|
92
|
+
{
|
|
93
|
+
metricAliasData: { metricVariable: "a", legend: "CPU" },
|
|
94
|
+
metricQueryData: {
|
|
95
|
+
filterData: {
|
|
96
|
+
metricName: "cpu.usage",
|
|
97
|
+
attributes: {},
|
|
98
|
+
aggegationType: MetricsAggregationType.Avg,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
} as unknown as MetricQueryConfigData,
|
|
102
|
+
],
|
|
103
|
+
formulaConfigs: [],
|
|
104
|
+
startAndEndDate: new InBetween<Date>(WINDOW_START, WINDOW_END),
|
|
105
|
+
} as MetricViewData;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function buildResult(offsetMs: number): AggregatedResult {
|
|
109
|
+
return {
|
|
110
|
+
data: [
|
|
111
|
+
{
|
|
112
|
+
timestamp: new Date(
|
|
113
|
+
new Date("2026-08-20T10:10:00.000Z").getTime() - offsetMs,
|
|
114
|
+
),
|
|
115
|
+
value: 40,
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
timestamp: new Date(
|
|
119
|
+
new Date("2026-08-20T10:30:00.000Z").getTime() - offsetMs,
|
|
120
|
+
),
|
|
121
|
+
value: 70,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
truncated: false,
|
|
125
|
+
} as unknown as AggregatedResult;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
beforeEach(() => {
|
|
129
|
+
fetchExemplarsMock.mockReset();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
afterEach(() => {
|
|
133
|
+
cleanup();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("MetricCharts compare overlays", () => {
|
|
137
|
+
test("renders a dashed ghost twin without adding legend chips", () => {
|
|
138
|
+
const { container } = render(
|
|
139
|
+
<MetricCharts
|
|
140
|
+
metricViewData={buildViewData()}
|
|
141
|
+
metricResults={[buildResult(0)]}
|
|
142
|
+
metricResultsPrevious={[buildResult(WINDOW_MS)]}
|
|
143
|
+
compareOffsetMs={WINDOW_MS}
|
|
144
|
+
metricTypes={[]}
|
|
145
|
+
/>,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// The ghost line is on the chart, dashed…
|
|
149
|
+
expect(
|
|
150
|
+
container.querySelector('path[stroke-dasharray="6 4"]'),
|
|
151
|
+
).toBeInTheDocument();
|
|
152
|
+
|
|
153
|
+
// …but the chip legend shows only the live series.
|
|
154
|
+
expect(screen.getAllByText("CPU").length).toBeGreaterThan(0);
|
|
155
|
+
expect(screen.queryByText("CPU (previous)")).toBeNull();
|
|
156
|
+
// And the series count still reads 1.
|
|
157
|
+
expect(screen.getByText("1 series")).toBeInTheDocument();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("no previous results means no ghosts", () => {
|
|
161
|
+
const { container } = render(
|
|
162
|
+
<MetricCharts
|
|
163
|
+
metricViewData={buildViewData()}
|
|
164
|
+
metricResults={[buildResult(0)]}
|
|
165
|
+
metricTypes={[]}
|
|
166
|
+
/>,
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(container.querySelector('path[stroke-dasharray="6 4"]')).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
jest,
|
|
8
|
+
test,
|
|
9
|
+
} from "@jest/globals";
|
|
10
|
+
import {
|
|
11
|
+
cleanup,
|
|
12
|
+
fireEvent,
|
|
13
|
+
render,
|
|
14
|
+
screen,
|
|
15
|
+
waitFor,
|
|
16
|
+
} from "@testing-library/react";
|
|
17
|
+
import * as React from "react";
|
|
18
|
+
import getJestMockFunction, { MockFunction } from "../../MockType";
|
|
19
|
+
|
|
20
|
+
/*
|
|
21
|
+
* The per-series investigate menu is the chart's bridge from "this series
|
|
22
|
+
* is spiking" to the signals that explain it: the chip's magnifier must
|
|
23
|
+
* open a menu whose pivots carry the series' group-by labels as REAL
|
|
24
|
+
* filters (logs/traces URLs), whose "Filter to this series" narrows the
|
|
25
|
+
* live explorer view, and whose resource entry lands on the host page the
|
|
26
|
+
* series is about. Rendering the real MetricCharts (not a stub) pins the
|
|
27
|
+
* whole chain: series naming → chip → menu → scoped URL.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* ResponsiveContainer measures its parent, which is always 0x0 in jsdom,
|
|
32
|
+
* so the chart renders nothing without a fixed size.
|
|
33
|
+
*/
|
|
34
|
+
jest.mock("recharts", () => {
|
|
35
|
+
const actual: Record<string, any> = jest.requireActual("recharts") as Record<
|
|
36
|
+
string,
|
|
37
|
+
any
|
|
38
|
+
>;
|
|
39
|
+
return {
|
|
40
|
+
...actual,
|
|
41
|
+
ResponsiveContainer: ({ children }: { children: React.ReactElement }) => {
|
|
42
|
+
return React.cloneElement(children, {
|
|
43
|
+
width: 600,
|
|
44
|
+
height: 300,
|
|
45
|
+
} as Record<string, unknown>);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const getListMock: MockFunction = getJestMockFunction();
|
|
51
|
+
const fetchExemplarsMock: MockFunction = getJestMockFunction();
|
|
52
|
+
const showToastMock: MockFunction = getJestMockFunction();
|
|
53
|
+
|
|
54
|
+
/*
|
|
55
|
+
* The arrow wrappers are load bearing: jest.mock is hoisted above the
|
|
56
|
+
* compiled requires, so the mock variables above are still unassigned when
|
|
57
|
+
* the factory runs. Dereferencing them lazily, at call time, is what makes
|
|
58
|
+
* this work.
|
|
59
|
+
*/
|
|
60
|
+
jest.mock("../../../UI/Utils/ModelAPI/ModelAPI", () => {
|
|
61
|
+
return {
|
|
62
|
+
__esModule: true,
|
|
63
|
+
default: {
|
|
64
|
+
getList: (...args: Array<any>) => {
|
|
65
|
+
return getListMock(...args);
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
jest.mock("../../../UI/Components/Toast/ToastInit", () => {
|
|
72
|
+
return {
|
|
73
|
+
__esModule: true,
|
|
74
|
+
ShowToastNotification: (...args: Array<any>) => {
|
|
75
|
+
return showToastMock(...args);
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
/*
|
|
81
|
+
* Keep the exemplar fetch (fired by MetricCharts whenever the view has a
|
|
82
|
+
* resolvable window) off the network; everything else on the Metrics util
|
|
83
|
+
* surface that MetricCharts touches is pure enough to stub inline.
|
|
84
|
+
*/
|
|
85
|
+
jest.mock(
|
|
86
|
+
"../../../../App/FeatureSet/Dashboard/src/Components/Metrics/Utils/Metrics",
|
|
87
|
+
() => {
|
|
88
|
+
return {
|
|
89
|
+
__esModule: true,
|
|
90
|
+
default: {
|
|
91
|
+
fetchExemplars: (...args: Array<any>) => {
|
|
92
|
+
fetchExemplarsMock(...args);
|
|
93
|
+
/*
|
|
94
|
+
* Never resolves: a resolved exemplar set would setState after
|
|
95
|
+
* each test's assertions and trip React's act() warning; no
|
|
96
|
+
* test here cares about exemplar dots.
|
|
97
|
+
*/
|
|
98
|
+
return new Promise(() => {
|
|
99
|
+
// Intentionally never settles.
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
setQueryTopNOverride: () => {
|
|
103
|
+
return undefined;
|
|
104
|
+
},
|
|
105
|
+
getQueryConfigTopNKey: (
|
|
106
|
+
_queryConfig: unknown,
|
|
107
|
+
index: number,
|
|
108
|
+
scope?: string,
|
|
109
|
+
) => {
|
|
110
|
+
return `${scope || ""}:${index}`;
|
|
111
|
+
},
|
|
112
|
+
clearQueryTopNOverridesForScope: () => {
|
|
113
|
+
return undefined;
|
|
114
|
+
},
|
|
115
|
+
serializeAttributeFiltersForKey: (attributes: unknown) => {
|
|
116
|
+
return JSON.stringify(attributes || {});
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
DEFAULT_TOP_N_SERIES: 10,
|
|
120
|
+
SHOW_ALL_SERIES_TOP_N: 10_000,
|
|
121
|
+
sanitizeAttributeFilters: (attributes: unknown) => {
|
|
122
|
+
return attributes;
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
import MetricCharts from "../../../../App/FeatureSet/Dashboard/src/Components/Metrics/MetricCharts";
|
|
129
|
+
import AggregatedResult from "../../../Types/BaseDatabase/AggregatedResult";
|
|
130
|
+
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
131
|
+
import MetricsAggregationType from "../../../Types/Metrics/MetricsAggregationType";
|
|
132
|
+
import MetricQueryConfigData from "../../../Types/Metrics/MetricQueryConfigData";
|
|
133
|
+
import MetricViewData from "../../../Types/Metrics/MetricViewData";
|
|
134
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
135
|
+
import Navigation from "../../../UI/Utils/Navigation";
|
|
136
|
+
import ProjectUtil from "../../../UI/Utils/Project";
|
|
137
|
+
|
|
138
|
+
const PROJECT_ID: ObjectID = new ObjectID(
|
|
139
|
+
"77777777-1111-4111-8111-777777777777",
|
|
140
|
+
);
|
|
141
|
+
const HOST_MODEL_ID: string = "88888888-2222-4222-8222-888888888888";
|
|
142
|
+
|
|
143
|
+
const WINDOW_START: Date = new Date("2026-08-20T10:00:00.000Z");
|
|
144
|
+
const WINDOW_END: Date = new Date("2026-08-20T11:00:00.000Z");
|
|
145
|
+
|
|
146
|
+
function buildQueryConfig(): MetricQueryConfigData {
|
|
147
|
+
return {
|
|
148
|
+
metricAliasData: { metricVariable: "a" },
|
|
149
|
+
metricQueryData: {
|
|
150
|
+
filterData: {
|
|
151
|
+
metricName: "cpu.usage",
|
|
152
|
+
attributes: {},
|
|
153
|
+
aggegationType: MetricsAggregationType.Avg,
|
|
154
|
+
},
|
|
155
|
+
groupByAttributeKeys: ["host.name"],
|
|
156
|
+
},
|
|
157
|
+
} as unknown as MetricQueryConfigData;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function buildViewData(): MetricViewData {
|
|
161
|
+
return {
|
|
162
|
+
queryConfigs: [buildQueryConfig()],
|
|
163
|
+
formulaConfigs: [],
|
|
164
|
+
startAndEndDate: new InBetween<Date>(WINDOW_START, WINDOW_END),
|
|
165
|
+
} as MetricViewData;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function buildResults(): Array<AggregatedResult> {
|
|
169
|
+
const timestamps: Array<string> = [
|
|
170
|
+
"2026-08-20T10:10:00.000Z",
|
|
171
|
+
"2026-08-20T10:20:00.000Z",
|
|
172
|
+
];
|
|
173
|
+
const data: Array<Record<string, unknown>> = [];
|
|
174
|
+
for (const host of ["web-01", "web-02"]) {
|
|
175
|
+
for (const [index, timestamp] of timestamps.entries()) {
|
|
176
|
+
data.push({
|
|
177
|
+
timestamp: new Date(timestamp),
|
|
178
|
+
value: host === "web-01" ? 90 + index : 10 + index,
|
|
179
|
+
attributes: { "host.name": host },
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return [{ data, truncated: false } as unknown as AggregatedResult];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let navigateSpy: ReturnType<typeof jest.spyOn>;
|
|
187
|
+
|
|
188
|
+
beforeEach(() => {
|
|
189
|
+
getListMock.mockReset();
|
|
190
|
+
fetchExemplarsMock.mockReset();
|
|
191
|
+
showToastMock.mockReset();
|
|
192
|
+
navigateSpy = jest.spyOn(Navigation, "navigate").mockImplementation(() => {
|
|
193
|
+
return undefined;
|
|
194
|
+
});
|
|
195
|
+
jest.spyOn(ProjectUtil, "getCurrentProjectId").mockReturnValue(PROJECT_ID);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterEach(() => {
|
|
199
|
+
jest.restoreAllMocks();
|
|
200
|
+
cleanup();
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
function getNavigatedUrl(): string {
|
|
204
|
+
const lastCall: Array<unknown> | undefined =
|
|
205
|
+
navigateSpy.mock.calls[navigateSpy.mock.calls.length - 1];
|
|
206
|
+
return decodeURIComponent(String(lastCall?.[0]));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
describe("per-series investigate menu", () => {
|
|
210
|
+
test("each series chip carries an investigate button that opens the menu", () => {
|
|
211
|
+
render(
|
|
212
|
+
<MetricCharts
|
|
213
|
+
metricViewData={buildViewData()}
|
|
214
|
+
metricResults={buildResults()}
|
|
215
|
+
metricTypes={[]}
|
|
216
|
+
/>,
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
|
|
220
|
+
|
|
221
|
+
const menu: HTMLElement = screen.getByRole("menu", {
|
|
222
|
+
name: "Series investigation actions",
|
|
223
|
+
});
|
|
224
|
+
expect(menu).toBeInTheDocument();
|
|
225
|
+
expect(screen.getByText("View logs")).toBeInTheDocument();
|
|
226
|
+
expect(screen.getByText("View traces")).toBeInTheDocument();
|
|
227
|
+
expect(screen.getByText("View exceptions")).toBeInTheDocument();
|
|
228
|
+
// The menu header names the series and what it is scoped to.
|
|
229
|
+
expect(menu.textContent).toContain("host.name = web-01");
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("read-only hosts get an explorer deep link; read-write hosts filter in place", () => {
|
|
233
|
+
const onQueryConfigsChange: MockFunction = getJestMockFunction();
|
|
234
|
+
|
|
235
|
+
const { unmount } = render(
|
|
236
|
+
<MetricCharts
|
|
237
|
+
metricViewData={buildViewData()}
|
|
238
|
+
metricResults={buildResults()}
|
|
239
|
+
metricTypes={[]}
|
|
240
|
+
/>,
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
|
|
244
|
+
expect(screen.getByText("Open in Metric Explorer")).toBeInTheDocument();
|
|
245
|
+
expect(screen.queryByText("Filter to this series")).toBeNull();
|
|
246
|
+
unmount();
|
|
247
|
+
|
|
248
|
+
render(
|
|
249
|
+
<MetricCharts
|
|
250
|
+
metricViewData={buildViewData()}
|
|
251
|
+
metricResults={buildResults()}
|
|
252
|
+
metricTypes={[]}
|
|
253
|
+
onQueryConfigsChange={(...args: Array<any>) => {
|
|
254
|
+
onQueryConfigsChange(...args);
|
|
255
|
+
}}
|
|
256
|
+
/>,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
|
|
260
|
+
expect(screen.queryByText("Open in Metric Explorer")).toBeNull();
|
|
261
|
+
|
|
262
|
+
fireEvent.click(screen.getByText("Filter to this series"));
|
|
263
|
+
|
|
264
|
+
expect(onQueryConfigsChange).toHaveBeenCalledTimes(1);
|
|
265
|
+
const narrowedConfigs: Array<MetricQueryConfigData> = onQueryConfigsChange
|
|
266
|
+
.mock.calls[0]?.[0] as Array<MetricQueryConfigData>;
|
|
267
|
+
expect(
|
|
268
|
+
(
|
|
269
|
+
narrowedConfigs[0]?.metricQueryData.filterData as Record<
|
|
270
|
+
string,
|
|
271
|
+
unknown
|
|
272
|
+
>
|
|
273
|
+
)["attributes"],
|
|
274
|
+
).toEqual({ "host.name": "web-01" });
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("View logs navigates to the logs explorer scoped to the series and window", async () => {
|
|
278
|
+
render(
|
|
279
|
+
<MetricCharts
|
|
280
|
+
metricViewData={buildViewData()}
|
|
281
|
+
metricResults={buildResults()}
|
|
282
|
+
metricTypes={[]}
|
|
283
|
+
/>,
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-02"));
|
|
287
|
+
fireEvent.click(screen.getByText("View logs"));
|
|
288
|
+
|
|
289
|
+
await waitFor(() => {
|
|
290
|
+
expect(navigateSpy).toHaveBeenCalled();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
const url: string = getNavigatedUrl();
|
|
294
|
+
expect(url).toContain("/logs");
|
|
295
|
+
expect(url).toContain('["attributes.host.name",["web-02"]]');
|
|
296
|
+
expect(url).toContain("range=Custom");
|
|
297
|
+
expect(url).toContain("start=");
|
|
298
|
+
expect(url).toContain("end=");
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("View exceptions reports what the exceptions grammar cannot carry", async () => {
|
|
302
|
+
render(
|
|
303
|
+
<MetricCharts
|
|
304
|
+
metricViewData={buildViewData()}
|
|
305
|
+
metricResults={buildResults()}
|
|
306
|
+
metricTypes={[]}
|
|
307
|
+
/>,
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
|
|
311
|
+
fireEvent.click(screen.getByText("View exceptions"));
|
|
312
|
+
|
|
313
|
+
await waitFor(() => {
|
|
314
|
+
expect(navigateSpy).toHaveBeenCalled();
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const url: string = getNavigatedUrl();
|
|
318
|
+
expect(url).toContain("/exceptions");
|
|
319
|
+
expect(url).toContain("status=all");
|
|
320
|
+
// The host.name narrowing has no exceptions facet — the user is told.
|
|
321
|
+
expect(showToastMock).toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
test("the host series jumps to the matching Host page", async () => {
|
|
325
|
+
getListMock.mockReturnValue(
|
|
326
|
+
Promise.resolve({ data: [{ _id: HOST_MODEL_ID }] }),
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
render(
|
|
330
|
+
<MetricCharts
|
|
331
|
+
metricViewData={buildViewData()}
|
|
332
|
+
metricResults={buildResults()}
|
|
333
|
+
metricTypes={[]}
|
|
334
|
+
/>,
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
fireEvent.click(screen.getByLabelText("Investigate host.name=web-01"));
|
|
338
|
+
fireEvent.click(screen.getByText('Open host "web-01"'));
|
|
339
|
+
|
|
340
|
+
await waitFor(() => {
|
|
341
|
+
expect(navigateSpy).toHaveBeenCalled();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
const listArgs: Record<string, unknown> = getListMock.mock
|
|
345
|
+
.calls[0]?.[0] as Record<string, unknown>;
|
|
346
|
+
expect(listArgs["query"]).toEqual({
|
|
347
|
+
projectId: PROJECT_ID,
|
|
348
|
+
hostIdentifier: "web-01",
|
|
349
|
+
});
|
|
350
|
+
expect(getNavigatedUrl()).toContain(`/host/${HOST_MODEL_ID}`);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("enableSeriesActions={false} renders plain chips with no investigate affordance", () => {
|
|
354
|
+
render(
|
|
355
|
+
<MetricCharts
|
|
356
|
+
metricViewData={buildViewData()}
|
|
357
|
+
metricResults={buildResults()}
|
|
358
|
+
metricTypes={[]}
|
|
359
|
+
enableSeriesActions={false}
|
|
360
|
+
/>,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// The chips themselves still render…
|
|
364
|
+
expect(screen.getByText("host.name=web-01")).toBeInTheDocument();
|
|
365
|
+
// …but nothing offers navigation.
|
|
366
|
+
expect(screen.queryByLabelText("Investigate host.name=web-01")).toBeNull();
|
|
367
|
+
});
|
|
368
|
+
});
|