@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.
Files changed (26) hide show
  1. package/Server/Services/TraceAggregationService.ts +16 -3
  2. package/Tests/Server/Services/TraceAggregationService.test.ts +10 -0
  3. package/Types/Dashboard/DashboardComponentType.ts +1 -0
  4. package/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.ts +39 -0
  5. package/UI/Components/Date/RangeStartAndEndDateEdit.tsx +228 -44
  6. package/UI/Components/Date/RangeStartAndEndDateView.tsx +73 -21
  7. package/UI/Components/GanttChart/ChartContainer.tsx +8 -1
  8. package/Utils/Dashboard/Components/DashboardTraceTableComponent.ts +65 -0
  9. package/Utils/Dashboard/Components/Index.ts +7 -0
  10. package/build/dist/Server/Services/TraceAggregationService.js +12 -3
  11. package/build/dist/Server/Services/TraceAggregationService.js.map +1 -1
  12. package/build/dist/Types/Dashboard/DashboardComponentType.js +1 -0
  13. package/build/dist/Types/Dashboard/DashboardComponentType.js.map +1 -1
  14. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js +2 -0
  15. package/build/dist/Types/Dashboard/DashboardComponents/DashboardTraceTableComponent.js.map +1 -0
  16. package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js +116 -26
  17. package/build/dist/UI/Components/Date/RangeStartAndEndDateEdit.js.map +1 -1
  18. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js +42 -16
  19. package/build/dist/UI/Components/Date/RangeStartAndEndDateView.js.map +1 -1
  20. package/build/dist/UI/Components/GanttChart/ChartContainer.js +9 -1
  21. package/build/dist/UI/Components/GanttChart/ChartContainer.js.map +1 -1
  22. package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js +51 -0
  23. package/build/dist/Utils/Dashboard/Components/DashboardTraceTableComponent.js.map +1 -0
  24. package/build/dist/Utils/Dashboard/Components/Index.js +4 -0
  25. package/build/dist/Utils/Dashboard/Components/Index.js.map +1 -1
  26. 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 duration stat set
1680
- * (count, avg, median, min, max) — one query answers "requests and
1681
- * median response time per tenant" without a follow-up.
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 (");
@@ -7,6 +7,7 @@ enum DashboardComponentType {
7
7
  LogStream = `LogStream`,
8
8
  TraceList = `TraceList`,
9
9
  TraceChart = `TraceChart`,
10
+ TraceTable = `TraceTable`,
10
11
  IncidentList = `IncidentList`,
11
12
  AlertList = `AlertList`,
12
13
  MonitorList = `MonitorList`,
@@ -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 from "../../../Types/Time/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 Dropdown, {
9
- DropdownOption,
10
- DropdownValue,
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
- const DashboardStartAndEndDateEditElement: FunctionComponent<ComponentProps> = (
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 dropdownOptions: DropdownOption[] =
23
- DropdownUtil.getDropdownOptionsFromEnum(TimeRange);
24
- const defaultDropdownOption: DropdownOption =
25
- dropdownOptions.find((option: DropdownOption) => {
26
- return option.value === TimeRange.PAST_ONE_HOUR;
27
- }) || dropdownOptions[0]!;
28
- const selectedDropdownnOption: DropdownOption =
29
- dropdownOptions.find((option: DropdownOption) => {
30
- return option.value === props.value?.range;
31
- }) || defaultDropdownOption;
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
- <Dropdown
36
- value={selectedDropdownnOption}
37
- onChange={(range: DropdownValue | Array<DropdownValue> | null) => {
38
- props.onChange({
39
- range: range as TimeRange,
40
- });
41
- }}
42
- options={dropdownOptions}
43
- />
44
- {/* Start and End Date */}
45
- {props.value?.range === TimeRange.CUSTOM && (
46
- <StartAndEndDate
47
- type={StartAndEndDateType.DateTime}
48
- value={props.value?.startAndEndDate || undefined}
49
- hideTimeButtons={true}
50
- onValueChanged={(startAndEndDate: InBetween<Date> | null) => {
51
- if (startAndEndDate) {
52
- props.onChange({
53
- range: TimeRange.CUSTOM,
54
- startAndEndDate,
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 &amp; 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 &mdash; 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 DashboardStartAndEndDateEditElement;
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
- const getContent: GetReactElementFunction = (): ReactElement => {
29
- const title: string = isCustomRange
30
- ? `${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
31
- props.dashboardStartAndEndDate.startAndEndDate?.startValue ||
32
- OneUptimeDate.getCurrentDate(),
33
- )} - ${OneUptimeDate.getDateAsUserFriendlyLocalFormattedString(
34
- props.dashboardStartAndEndDate.startAndEndDate?.endValue ||
35
- OneUptimeDate.getCurrentDate(),
36
- )}`
37
- : props.dashboardStartAndEndDate.range;
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">{title}</span>
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 Start and End Time"
57
- onClose={() => {
58
- setTempStartAndEndDate(null);
59
- setShowTimeSelectModal(false);
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
- props.onChange(tempStartAndEndDate);
113
+ commitSelection(tempStartAndEndDate);
64
114
  }
65
- setShowTimeSelectModal(false);
66
- setTempStartAndEndDate(null);
67
115
  }}
68
116
  >
69
- <div className="mt-5">
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
- <div ref={divRef} className={"w-full"}>
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>