@oneuptime/common 11.3.3 → 11.3.4
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/Server/Services/TraceAggregationService.ts +16 -3
- package/Tests/Server/Services/TraceAggregationService.test.ts +10 -0
- package/Types/Dashboard/DashboardComponentType.ts +1 -0
- package/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.ts +39 -0
- package/UI/Components/Date/RangeStartAndEndDateEdit.tsx +228 -44
- package/UI/Components/Date/RangeStartAndEndDateView.tsx +73 -21
- package/UI/Components/GanttChart/ChartContainer.tsx +8 -1
- package/Utils/Dashboard/Components/DashboardTraceTableComponent.ts +65 -0
- package/Utils/Dashboard/Components/Index.ts +7 -0
- package/build/dist/Server/Services/TraceAggregationService.js +12 -3
- package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
- package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js +2 -0
- package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js.map +1 -0
- package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js +116 -26
- package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js.map +1 -1
- package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js +42 -16
- package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js.map +1 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js +9 -1
- package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
- package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js +51 -0
- package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js.map +1 -0
- package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
- package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
- package/package.json +1 -1
|
@@ -107,8 +107,12 @@ export interface TraceAnalyticsTopItem {
|
|
|
107
107
|
export interface TraceAnalyticsTableRow {
|
|
108
108
|
groupValues: Record<string, string>;
|
|
109
109
|
count: number;
|
|
110
|
+
errorCount: number;
|
|
110
111
|
avgDurationMs: number;
|
|
111
112
|
p50DurationMs: number;
|
|
113
|
+
p90DurationMs: number;
|
|
114
|
+
p95DurationMs: number;
|
|
115
|
+
p99DurationMs: number;
|
|
112
116
|
minDurationMs: number;
|
|
113
117
|
maxDurationMs: number;
|
|
114
118
|
}
|
|
@@ -1344,8 +1348,12 @@ export class TraceAggregationService {
|
|
|
1344
1348
|
return {
|
|
1345
1349
|
groupValues,
|
|
1346
1350
|
count: Number(row["cnt"] || 0),
|
|
1351
|
+
errorCount: Number(row["err_cnt"] || 0),
|
|
1347
1352
|
avgDurationMs: Number(row["avg_ms"] || 0),
|
|
1348
1353
|
p50DurationMs: Number(row["p50_ms"] || 0),
|
|
1354
|
+
p90DurationMs: Number(row["p90_ms"] || 0),
|
|
1355
|
+
p95DurationMs: Number(row["p95_ms"] || 0),
|
|
1356
|
+
p99DurationMs: Number(row["p99_ms"] || 0),
|
|
1349
1357
|
minDurationMs: Number(row["min_ms"] || 0),
|
|
1350
1358
|
maxDurationMs: Number(row["max_ms"] || 0),
|
|
1351
1359
|
};
|
|
@@ -1676,15 +1684,20 @@ export class TraceAggregationService {
|
|
|
1676
1684
|
request.limit ?? TraceAggregationService.DEFAULT_ANALYTICS_LIMIT;
|
|
1677
1685
|
|
|
1678
1686
|
/*
|
|
1679
|
-
* The "top dimensions" table always carries the full
|
|
1680
|
-
*
|
|
1681
|
-
*
|
|
1687
|
+
* The "top dimensions" table always carries the full stat set (count,
|
|
1688
|
+
* errors, avg, p50/p90/p95/p99, min, max) — one query answers "requests,
|
|
1689
|
+
* errors and tail latency per tenant" without a follow-up. errorCount
|
|
1690
|
+
* mirrors the METRIC_EXPRESSIONS convention: statusCode = 2 is an error.
|
|
1682
1691
|
*/
|
|
1683
1692
|
const statement: Statement = new Statement();
|
|
1684
1693
|
statement.append(
|
|
1685
1694
|
"SELECT count() AS cnt" +
|
|
1695
|
+
", countIf(statusCode = 2) AS err_cnt" +
|
|
1686
1696
|
", avg(durationUnixNano) / 1000000 AS avg_ms" +
|
|
1687
1697
|
", quantile(0.5)(durationUnixNano) / 1000000 AS p50_ms" +
|
|
1698
|
+
", quantile(0.9)(durationUnixNano) / 1000000 AS p90_ms" +
|
|
1699
|
+
", quantile(0.95)(durationUnixNano) / 1000000 AS p95_ms" +
|
|
1700
|
+
", quantile(0.99)(durationUnixNano) / 1000000 AS p99_ms" +
|
|
1688
1701
|
", min(durationUnixNano) / 1000000 AS min_ms" +
|
|
1689
1702
|
", max(durationUnixNano) / 1000000 AS max_ms",
|
|
1690
1703
|
);
|
|
@@ -328,10 +328,20 @@ describe("TraceAggregationService", () => {
|
|
|
328
328
|
|
|
329
329
|
const query: string = normalizedQuery(statement);
|
|
330
330
|
expect(query).toContain("count() AS cnt");
|
|
331
|
+
expect(query).toContain("countIf(statusCode = 2) AS err_cnt");
|
|
331
332
|
expect(query).toContain("avg(durationUnixNano) / 1000000 AS avg_ms");
|
|
332
333
|
expect(query).toContain(
|
|
333
334
|
"quantile(0.5)(durationUnixNano) / 1000000 AS p50_ms",
|
|
334
335
|
);
|
|
336
|
+
expect(query).toContain(
|
|
337
|
+
"quantile(0.9)(durationUnixNano) / 1000000 AS p90_ms",
|
|
338
|
+
);
|
|
339
|
+
expect(query).toContain(
|
|
340
|
+
"quantile(0.95)(durationUnixNano) / 1000000 AS p95_ms",
|
|
341
|
+
);
|
|
342
|
+
expect(query).toContain(
|
|
343
|
+
"quantile(0.99)(durationUnixNano) / 1000000 AS p99_ms",
|
|
344
|
+
);
|
|
335
345
|
expect(query).toContain("min(durationUnixNano) / 1000000 AS min_ms");
|
|
336
346
|
expect(query).toContain("max(durationUnixNano) / 1000000 AS max_ms");
|
|
337
347
|
expect(query).toContain(" AND name IN (");
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import ObjectID from "../../ObjectID";
|
|
2
|
+
import DashboardComponentType from "../DashboardComponentType";
|
|
3
|
+
import BaseComponent from "./DashboardBaseComponent";
|
|
4
|
+
|
|
5
|
+
export default interface DashboardTraceTableComponent extends BaseComponent {
|
|
6
|
+
componentType: DashboardComponentType.TraceTable;
|
|
7
|
+
componentId: ObjectID;
|
|
8
|
+
arguments: {
|
|
9
|
+
title?: string | undefined;
|
|
10
|
+
// Substring filter on span name (e.g. "/Shipment/ShipShipment").
|
|
11
|
+
spanNameContains?: string | undefined;
|
|
12
|
+
/*
|
|
13
|
+
* Attribute equality filters, ANDed. The structured editor stores a
|
|
14
|
+
* key/value record (e.g. { "url.host": "torginol.starship.online" }).
|
|
15
|
+
* A legacy "key=value; key2=value2" string is still read for widgets
|
|
16
|
+
* saved before the structured editor existed.
|
|
17
|
+
*/
|
|
18
|
+
attributeFilters?:
|
|
19
|
+
| string
|
|
20
|
+
| Record<string, string | number | boolean>
|
|
21
|
+
| undefined;
|
|
22
|
+
/*
|
|
23
|
+
* The dimension to break the table into rows by: a span attribute key
|
|
24
|
+
* (e.g. url.host, resource.service.instance.id) or a top-level column
|
|
25
|
+
* (name, primaryEntityId, statusCode, kind). One row per value. Unlike
|
|
26
|
+
* the trace chart's optional split, this is REQUIRED — the table is a
|
|
27
|
+
* "top dimensions" breakdown, so an unset group-by renders an empty
|
|
28
|
+
* prompt instead of a query.
|
|
29
|
+
*/
|
|
30
|
+
groupByAttribute?: string | undefined;
|
|
31
|
+
// Cap on the number of rows (default 10).
|
|
32
|
+
topLimit?: number | undefined;
|
|
33
|
+
/*
|
|
34
|
+
* Include non-root spans. Off by default so "Requests" matches the
|
|
35
|
+
* traces explorer, which is root-spans-only.
|
|
36
|
+
*/
|
|
37
|
+
includeChildSpans?: boolean | undefined;
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -1,64 +1,248 @@
|
|
|
1
1
|
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
-
import RangeStartAndEndDateTime
|
|
2
|
+
import RangeStartAndEndDateTime, {
|
|
3
|
+
RangeStartAndEndDateTimeUtil,
|
|
4
|
+
} from "../../../Types/Time/RangeStartAndEndDateTime";
|
|
3
5
|
import StartAndEndDate, {
|
|
4
6
|
StartAndEndDateType,
|
|
5
7
|
} from "../../../UI/Components/Date/StartAndEndDate";
|
|
6
8
|
import InBetween from "../../../Types/BaseDatabase/InBetween";
|
|
7
9
|
import TimeRange from "../../../Types/Time/TimeRange";
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "../../../UI/Components/Dropdown/Dropdown";
|
|
12
|
-
import DropdownUtil from "../../../UI/Utils/Dropdown";
|
|
10
|
+
import OneUptimeDate from "../../../Types/Date";
|
|
11
|
+
import Icon from "../Icon/Icon";
|
|
12
|
+
import IconProp from "../../../Types/Icon/IconProp";
|
|
13
13
|
|
|
14
14
|
export interface ComponentProps {
|
|
15
15
|
value?: RangeStartAndEndDateTime | undefined;
|
|
16
16
|
onChange: (startAndEndDate: RangeStartAndEndDateTime) => void;
|
|
17
|
+
/*
|
|
18
|
+
* When provided, clicking a quick range immediately commits the selection
|
|
19
|
+
* (and the parent is expected to close the picker). Custom ranges always
|
|
20
|
+
* go through onChange so the user can fine-tune before applying.
|
|
21
|
+
*/
|
|
22
|
+
onApply?: ((startAndEndDate: RangeStartAndEndDateTime) => void) | undefined;
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
|
|
25
|
+
/*
|
|
26
|
+
* Quick ranges shown in the left rail, in display order. Custom is handled
|
|
27
|
+
* separately below the list.
|
|
28
|
+
*/
|
|
29
|
+
const QUICK_RANGES: Array<TimeRange> = [
|
|
30
|
+
TimeRange.PAST_FIVE_MINS,
|
|
31
|
+
TimeRange.PAST_FIFTEEN_MINS,
|
|
32
|
+
TimeRange.PAST_THIRTY_MINS,
|
|
33
|
+
TimeRange.PAST_ONE_HOUR,
|
|
34
|
+
TimeRange.PAST_TWO_HOURS,
|
|
35
|
+
TimeRange.PAST_THREE_HOURS,
|
|
36
|
+
TimeRange.PAST_ONE_DAY,
|
|
37
|
+
TimeRange.PAST_TWO_DAYS,
|
|
38
|
+
TimeRange.PAST_ONE_WEEK,
|
|
39
|
+
TimeRange.PAST_TWO_WEEKS,
|
|
40
|
+
TimeRange.PAST_ONE_MONTH,
|
|
41
|
+
TimeRange.PAST_THREE_MONTHS,
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const RangeStartAndEndDateEdit: FunctionComponent<ComponentProps> = (
|
|
20
45
|
props: ComponentProps,
|
|
21
46
|
): ReactElement => {
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
47
|
+
const selectedRange: TimeRange =
|
|
48
|
+
props.value?.range || TimeRange.PAST_ONE_HOUR;
|
|
49
|
+
const isCustom: boolean = selectedRange === TimeRange.CUSTOM;
|
|
50
|
+
|
|
51
|
+
/*
|
|
52
|
+
* Absolute window for the current selection. Used to preview a quick range
|
|
53
|
+
* and to seed the custom editor when the user switches to it.
|
|
54
|
+
*/
|
|
55
|
+
const resolvedRange: InBetween<Date> =
|
|
56
|
+
RangeStartAndEndDateTimeUtil.getStartAndEndDate(
|
|
57
|
+
props.value || { range: TimeRange.PAST_ONE_HOUR },
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
const formatDateTime: (date: Date) => string = (date: Date): string => {
|
|
61
|
+
return OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
62
|
+
date,
|
|
63
|
+
false,
|
|
64
|
+
true,
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getDurationText: (start: Date, end: Date) => string = (
|
|
69
|
+
start: Date,
|
|
70
|
+
end: Date,
|
|
71
|
+
): string => {
|
|
72
|
+
const seconds: number = OneUptimeDate.getDifferenceInSeconds(end, start);
|
|
73
|
+
|
|
74
|
+
if (seconds <= 0) {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return OneUptimeDate.secondsToFormattedFriendlyTimeString(seconds).trim();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const onSelectQuickRange: (range: TimeRange) => void = (
|
|
82
|
+
range: TimeRange,
|
|
83
|
+
): void => {
|
|
84
|
+
const next: RangeStartAndEndDateTime = { range };
|
|
85
|
+
|
|
86
|
+
if (props.onApply) {
|
|
87
|
+
props.onApply(next);
|
|
88
|
+
} else {
|
|
89
|
+
props.onChange(next);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const onSelectCustom: () => void = (): void => {
|
|
94
|
+
props.onChange({
|
|
95
|
+
range: TimeRange.CUSTOM,
|
|
96
|
+
startAndEndDate: props.value?.startAndEndDate || resolvedRange,
|
|
97
|
+
});
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const customStart: Date | undefined =
|
|
101
|
+
props.value?.startAndEndDate?.startValue;
|
|
102
|
+
const customEnd: Date | undefined = props.value?.startAndEndDate?.endValue;
|
|
103
|
+
/*
|
|
104
|
+
* Only show the duration hint for a valid range. getDifferenceInSeconds is
|
|
105
|
+
* absolute, so without this guard an inverted (end-before-start) range would
|
|
106
|
+
* read as a positive "Showing N of data" while Apply is disabled.
|
|
107
|
+
*/
|
|
108
|
+
const customDuration: string =
|
|
109
|
+
customStart && customEnd && OneUptimeDate.isAfter(customEnd, customStart)
|
|
110
|
+
? getDurationText(customStart, customEnd)
|
|
111
|
+
: "";
|
|
112
|
+
|
|
113
|
+
const previewDuration: string = getDurationText(
|
|
114
|
+
resolvedRange.startValue,
|
|
115
|
+
resolvedRange.endValue,
|
|
116
|
+
);
|
|
32
117
|
|
|
33
118
|
return (
|
|
34
|
-
<div>
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
119
|
+
<div className="flex flex-col md:flex-row md:divide-x md:divide-gray-200">
|
|
120
|
+
{/* Left rail: quick ranges */}
|
|
121
|
+
<div
|
|
122
|
+
className="md:w-52 md:shrink-0 md:pr-4 md:max-h-96 md:overflow-y-auto"
|
|
123
|
+
role="radiogroup"
|
|
124
|
+
aria-label="Time range"
|
|
125
|
+
>
|
|
126
|
+
<div className="px-1 pb-2 text-xs font-semibold uppercase tracking-wide text-gray-400">
|
|
127
|
+
Quick ranges
|
|
128
|
+
</div>
|
|
129
|
+
<div className="grid grid-cols-2 gap-1 md:grid-cols-1">
|
|
130
|
+
{QUICK_RANGES.map((range: TimeRange) => {
|
|
131
|
+
const isActive: boolean = !isCustom && selectedRange === range;
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<button
|
|
135
|
+
key={range}
|
|
136
|
+
type="button"
|
|
137
|
+
role="radio"
|
|
138
|
+
aria-checked={isActive}
|
|
139
|
+
onClick={() => {
|
|
140
|
+
return onSelectQuickRange(range);
|
|
141
|
+
}}
|
|
142
|
+
className={`w-full rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
|
|
143
|
+
isActive
|
|
144
|
+
? "bg-indigo-50 font-medium text-indigo-700"
|
|
145
|
+
: "text-gray-700 hover:bg-gray-100"
|
|
146
|
+
}`}
|
|
147
|
+
>
|
|
148
|
+
{range}
|
|
149
|
+
</button>
|
|
150
|
+
);
|
|
151
|
+
})}
|
|
152
|
+
</div>
|
|
153
|
+
<div className="mt-2 border-t border-gray-100 pt-2">
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
role="radio"
|
|
157
|
+
aria-checked={isCustom}
|
|
158
|
+
onClick={onSelectCustom}
|
|
159
|
+
className={`flex w-full items-center gap-2 rounded-md px-3 py-1.5 text-left text-sm transition-colors ${
|
|
160
|
+
isCustom
|
|
161
|
+
? "bg-indigo-50 font-medium text-indigo-700"
|
|
162
|
+
: "text-gray-700 hover:bg-gray-100"
|
|
163
|
+
}`}
|
|
164
|
+
>
|
|
165
|
+
<Icon icon={IconProp.Calendar} className="h-4 w-4" />
|
|
166
|
+
Custom range
|
|
167
|
+
</button>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
{/* Right pane: custom editor or quick-range preview */}
|
|
172
|
+
<div className="mt-4 md:mt-0 md:flex-1 md:pl-4">
|
|
173
|
+
{isCustom ? (
|
|
174
|
+
<div>
|
|
175
|
+
<div className="text-sm font-medium text-gray-900">
|
|
176
|
+
Custom range
|
|
177
|
+
</div>
|
|
178
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
179
|
+
Pick the exact start and end date & time.
|
|
180
|
+
</div>
|
|
181
|
+
<StartAndEndDate
|
|
182
|
+
type={StartAndEndDateType.DateTime}
|
|
183
|
+
value={props.value?.startAndEndDate || resolvedRange}
|
|
184
|
+
hideTimeButtons={true}
|
|
185
|
+
onValueChanged={(startAndEndDate: InBetween<Date> | null) => {
|
|
186
|
+
if (startAndEndDate) {
|
|
187
|
+
props.onChange({
|
|
188
|
+
range: TimeRange.CUSTOM,
|
|
189
|
+
startAndEndDate,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}}
|
|
193
|
+
/>
|
|
194
|
+
{customDuration ? (
|
|
195
|
+
<div className="mt-2 flex items-center gap-1.5 text-xs text-gray-500">
|
|
196
|
+
<Icon icon={IconProp.Clock} className="h-3.5 w-3.5" />
|
|
197
|
+
<span>
|
|
198
|
+
Showing{" "}
|
|
199
|
+
<span className="font-medium text-gray-700">
|
|
200
|
+
{customDuration}
|
|
201
|
+
</span>{" "}
|
|
202
|
+
of data.
|
|
203
|
+
</span>
|
|
204
|
+
</div>
|
|
205
|
+
) : (
|
|
206
|
+
<></>
|
|
207
|
+
)}
|
|
208
|
+
</div>
|
|
209
|
+
) : (
|
|
210
|
+
<div>
|
|
211
|
+
<div className="text-sm font-medium text-gray-900">
|
|
212
|
+
{selectedRange}
|
|
213
|
+
</div>
|
|
214
|
+
<div className="mt-0.5 text-xs text-gray-500">
|
|
215
|
+
Relative to now — updates every time the dashboard loads.
|
|
216
|
+
</div>
|
|
217
|
+
<div className="mt-3 space-y-2 rounded-md border border-gray-200 bg-gray-50 p-3">
|
|
218
|
+
<div className="flex items-center justify-between text-sm">
|
|
219
|
+
<span className="text-gray-500">From</span>
|
|
220
|
+
<span className="font-medium text-gray-900">
|
|
221
|
+
{formatDateTime(resolvedRange.startValue)}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="flex items-center justify-between text-sm">
|
|
225
|
+
<span className="text-gray-500">To</span>
|
|
226
|
+
<span className="font-medium text-gray-900">
|
|
227
|
+
{formatDateTime(resolvedRange.endValue)}
|
|
228
|
+
</span>
|
|
229
|
+
</div>
|
|
230
|
+
{previewDuration ? (
|
|
231
|
+
<div className="flex items-center justify-between border-t border-gray-200 pt-2 text-sm">
|
|
232
|
+
<span className="text-gray-500">Duration</span>
|
|
233
|
+
<span className="font-medium text-gray-900">
|
|
234
|
+
{previewDuration}
|
|
235
|
+
</span>
|
|
236
|
+
</div>
|
|
237
|
+
) : (
|
|
238
|
+
<></>
|
|
239
|
+
)}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
)}
|
|
243
|
+
</div>
|
|
60
244
|
</div>
|
|
61
245
|
);
|
|
62
246
|
};
|
|
63
247
|
|
|
64
|
-
export default
|
|
248
|
+
export default RangeStartAndEndDateEdit;
|
|
@@ -7,7 +7,7 @@ import { GetReactElementFunction } from "../../../UI/Types/FunctionTypes";
|
|
|
7
7
|
import Icon from "../Icon/Icon";
|
|
8
8
|
import Tooltip from "../Tooltip/Tooltip";
|
|
9
9
|
import RangeStartAndEndDateEdit from "./RangeStartAndEndDateEdit";
|
|
10
|
-
import Modal from "../Modal/Modal";
|
|
10
|
+
import Modal, { ModalWidth } from "../Modal/Modal";
|
|
11
11
|
|
|
12
12
|
export interface ComponentProps {
|
|
13
13
|
dashboardStartAndEndDate: RangeStartAndEndDateTime;
|
|
@@ -25,17 +25,65 @@ const DashboardStartAndEndDateView: FunctionComponent<ComponentProps> = (
|
|
|
25
25
|
const isCustomRange: boolean =
|
|
26
26
|
props.dashboardStartAndEndDate.range === TimeRange.CUSTOM;
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
/*
|
|
29
|
+
* The Apply button should only commit a custom range when both ends are set
|
|
30
|
+
* and the start is strictly before the end.
|
|
31
|
+
*/
|
|
32
|
+
const isSelectionValid: (
|
|
33
|
+
selection: RangeStartAndEndDateTime | null,
|
|
34
|
+
) => boolean = (selection: RangeStartAndEndDateTime | null): boolean => {
|
|
35
|
+
if (!selection) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (selection.range !== TimeRange.CUSTOM) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const startAndEndDate: typeof selection.startAndEndDate =
|
|
44
|
+
selection.startAndEndDate;
|
|
45
|
+
|
|
46
|
+
if (!startAndEndDate) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return OneUptimeDate.isAfter(
|
|
51
|
+
startAndEndDate.endValue,
|
|
52
|
+
startAndEndDate.startValue,
|
|
53
|
+
);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const closeModal: () => void = (): void => {
|
|
57
|
+
setTempStartAndEndDate(null);
|
|
58
|
+
setShowTimeSelectModal(false);
|
|
59
|
+
};
|
|
38
60
|
|
|
61
|
+
const commitSelection: (selection: RangeStartAndEndDateTime) => void = (
|
|
62
|
+
selection: RangeStartAndEndDateTime,
|
|
63
|
+
): void => {
|
|
64
|
+
props.onChange(selection);
|
|
65
|
+
closeModal();
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const getButtonTitle: () => string = (): string => {
|
|
69
|
+
if (isCustomRange) {
|
|
70
|
+
return `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
71
|
+
props.dashboardStartAndEndDate.startAndEndDate?.startValue ||
|
|
72
|
+
OneUptimeDate.getCurrentDate(),
|
|
73
|
+
false,
|
|
74
|
+
true,
|
|
75
|
+
)} - ${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
|
|
76
|
+
props.dashboardStartAndEndDate.startAndEndDate?.endValue ||
|
|
77
|
+
OneUptimeDate.getCurrentDate(),
|
|
78
|
+
false,
|
|
79
|
+
true,
|
|
80
|
+
)}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return props.dashboardStartAndEndDate.range;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const getContent: GetReactElementFunction = (): ReactElement => {
|
|
39
87
|
return (
|
|
40
88
|
<div>
|
|
41
89
|
<Tooltip text="Click to change the date and time range of data on this dashboard.">
|
|
@@ -48,30 +96,34 @@ const DashboardStartAndEndDateView: FunctionComponent<ComponentProps> = (
|
|
|
48
96
|
}}
|
|
49
97
|
>
|
|
50
98
|
<Icon icon={IconProp.Clock} className="w-3.5 h-3.5 text-gray-500" />
|
|
51
|
-
<span className="text-xs text-gray-500">{
|
|
99
|
+
<span className="text-xs text-gray-500">{getButtonTitle()}</span>
|
|
52
100
|
</button>
|
|
53
101
|
</Tooltip>
|
|
54
102
|
{showTimeSelectModal && (
|
|
55
103
|
<Modal
|
|
56
|
-
title="Select
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
104
|
+
title="Select Time Range"
|
|
105
|
+
description="Choose a quick range or set an exact start and end date & time."
|
|
106
|
+
icon={IconProp.Clock}
|
|
107
|
+
modalWidth={ModalWidth.Medium}
|
|
108
|
+
submitButtonText="Apply"
|
|
109
|
+
disableSubmitButton={!isSelectionValid(tempStartAndEndDate)}
|
|
110
|
+
onClose={closeModal}
|
|
61
111
|
onSubmit={() => {
|
|
62
112
|
if (tempStartAndEndDate) {
|
|
63
|
-
|
|
113
|
+
commitSelection(tempStartAndEndDate);
|
|
64
114
|
}
|
|
65
|
-
setShowTimeSelectModal(false);
|
|
66
|
-
setTempStartAndEndDate(null);
|
|
67
115
|
}}
|
|
68
116
|
>
|
|
69
|
-
<div className="mt-
|
|
117
|
+
<div className="mt-3">
|
|
70
118
|
<RangeStartAndEndDateEdit
|
|
71
119
|
value={tempStartAndEndDate || undefined}
|
|
72
120
|
onChange={(startAndEndDate: RangeStartAndEndDateTime) => {
|
|
73
121
|
setTempStartAndEndDate(startAndEndDate);
|
|
74
122
|
}}
|
|
123
|
+
onApply={(startAndEndDate: RangeStartAndEndDateTime) => {
|
|
124
|
+
// Quick ranges apply immediately and close the modal.
|
|
125
|
+
commitSelection(startAndEndDate);
|
|
126
|
+
}}
|
|
75
127
|
/>
|
|
76
128
|
</div>
|
|
77
129
|
</Modal>
|
|
@@ -33,7 +33,14 @@ const ChartContainer: FunctionComponent<ComponentProps> = (
|
|
|
33
33
|
}, []);
|
|
34
34
|
|
|
35
35
|
return (
|
|
36
|
-
|
|
36
|
+
/*
|
|
37
|
+
* `isolate` creates a new stacking context so the chart's internal
|
|
38
|
+
* z-indexes (e.g. a highlighted/matched bar uses z-30, see Bar/Index.tsx)
|
|
39
|
+
* stay trapped inside the chart. Without it those bars paint above
|
|
40
|
+
* page-level overlays like the span-details SideOver (z-10), making a
|
|
41
|
+
* matched span "show above" the sidebar.
|
|
42
|
+
*/
|
|
43
|
+
<div ref={divRef} className={"w-full"} style={{ isolation: "isolate" }}>
|
|
37
44
|
{props.children}
|
|
38
45
|
</div>
|
|
39
46
|
);
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import DashboardTraceTableComponent from "../../../Types/Dashboard/DashboardComponents/DashboardTraceTableComponent";
|
|
2
|
+
import { ObjectType } from "../../../Types/JSON";
|
|
3
|
+
import ObjectID from "../../../Types/ObjectID";
|
|
4
|
+
import DashboardBaseComponentUtil from "./DashboardBaseComponent";
|
|
5
|
+
import {
|
|
6
|
+
ComponentArgument,
|
|
7
|
+
ComponentArgumentSection,
|
|
8
|
+
ComponentInputType,
|
|
9
|
+
} from "../../../Types/Dashboard/DashboardComponents/ComponentArgument";
|
|
10
|
+
import DashboardComponentType from "../../../Types/Dashboard/DashboardComponentType";
|
|
11
|
+
|
|
12
|
+
const DisplaySection: ComponentArgumentSection = {
|
|
13
|
+
name: "Display Options",
|
|
14
|
+
description: "Configure the widget title",
|
|
15
|
+
order: 1,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
* The "Query" section (span-name filter, attribute filters, group-by, max
|
|
20
|
+
* rows, include-child-spans) is rendered by the shared structured editor —
|
|
21
|
+
* TraceChartQueryEditor in "table" mode — rather than these declarative
|
|
22
|
+
* free-text fields, so those arguments are deliberately not listed here.
|
|
23
|
+
* See ArgumentsForm.tsx. Unlike the trace chart, there is no "Metric"
|
|
24
|
+
* argument: the table always shows the full duration stat set (Requests,
|
|
25
|
+
* Median, Avg, Min, Max) per row.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
export default class DashboardTraceTableComponentUtil extends DashboardBaseComponentUtil {
|
|
29
|
+
public static override getDefaultComponent(): DashboardTraceTableComponent {
|
|
30
|
+
return {
|
|
31
|
+
_type: ObjectType.DashboardComponent,
|
|
32
|
+
componentType: DashboardComponentType.TraceTable,
|
|
33
|
+
widthInDashboardUnits: 8,
|
|
34
|
+
heightInDashboardUnits: 5,
|
|
35
|
+
topInDashboardUnits: 0,
|
|
36
|
+
leftInDashboardUnits: 0,
|
|
37
|
+
componentId: ObjectID.generate(),
|
|
38
|
+
minHeightInDashboardUnits: 3,
|
|
39
|
+
minWidthInDashboardUnits: 6,
|
|
40
|
+
arguments: {
|
|
41
|
+
groupByAttribute: "name",
|
|
42
|
+
topLimit: 10,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static override getComponentConfigArguments(): Array<
|
|
48
|
+
ComponentArgument<DashboardTraceTableComponent>
|
|
49
|
+
> {
|
|
50
|
+
const componentArguments: Array<
|
|
51
|
+
ComponentArgument<DashboardTraceTableComponent>
|
|
52
|
+
> = [];
|
|
53
|
+
|
|
54
|
+
componentArguments.push({
|
|
55
|
+
name: "Title",
|
|
56
|
+
description: "Header shown above the table",
|
|
57
|
+
required: false,
|
|
58
|
+
type: ComponentInputType.Text,
|
|
59
|
+
id: "title",
|
|
60
|
+
section: DisplaySection,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return componentArguments;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -36,6 +36,7 @@ import DashboardDockerSwarmServiceListComponentUtil from "./DashboardDockerSwarm
|
|
|
36
36
|
import DashboardTableComponentUtil from "./DashboardTableComponent";
|
|
37
37
|
import DashboardTextComponentUtil from "./DashboardTextComponent";
|
|
38
38
|
import DashboardTraceChartComponentUtil from "./DashboardTraceChartComponent";
|
|
39
|
+
import DashboardTraceTableComponentUtil from "./DashboardTraceTableComponent";
|
|
39
40
|
import DashboardTraceListComponentUtil from "./DashboardTraceListComponent";
|
|
40
41
|
import DashboardValueComponentUtil from "./DashboardValueComponent";
|
|
41
42
|
|
|
@@ -91,6 +92,12 @@ export default class DashboardComponentsUtil {
|
|
|
91
92
|
>;
|
|
92
93
|
}
|
|
93
94
|
|
|
95
|
+
if (dashboardComponentType === DashboardComponentType.TraceTable) {
|
|
96
|
+
return DashboardTraceTableComponentUtil.getComponentConfigArguments() as Array<
|
|
97
|
+
ComponentArgument<DashboardBaseComponent>
|
|
98
|
+
>;
|
|
99
|
+
}
|
|
100
|
+
|
|
94
101
|
if (dashboardComponentType === DashboardComponentType.IncidentList) {
|
|
95
102
|
return DashboardIncidentListComponentUtil.getComponentConfigArguments() as Array<
|
|
96
103
|
ComponentArgument<DashboardBaseComponent>
|