@oneuptime/common 10.0.19 → 10.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/Server/API/GitHubAPI.ts +104 -12
- package/Server/API/TelemetryAPI.ts +208 -0
- package/Server/API/UserCallAPI.ts +29 -0
- package/Server/API/UserEmailAPI.ts +29 -0
- package/Server/API/UserSmsAPI.ts +29 -0
- package/Server/API/UserWhatsAppAPI.ts +29 -0
- package/Server/Services/LogAggregationService.ts +251 -0
- package/Server/Utils/VM/VMRunner.ts +45 -0
- package/Types/Log/LogQueryParser.ts +252 -0
- package/Types/Log/LogQueryToFilter.ts +131 -0
- package/UI/Components/CopyTextButton/CopyTextButton.tsx +3 -3
- package/UI/Components/LogsViewer/LogsViewer.tsx +166 -93
- package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +58 -0
- package/UI/Components/LogsViewer/components/FacetSection.tsx +119 -0
- package/UI/Components/LogsViewer/components/FacetValueRow.tsx +102 -0
- package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +122 -0
- package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +4 -4
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +22 -26
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +360 -0
- package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +128 -0
- package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +64 -0
- package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +199 -0
- package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +172 -0
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +27 -57
- package/UI/Components/LogsViewer/components/LogsHistogram.tsx +268 -0
- package/UI/Components/LogsViewer/components/LogsPagination.tsx +12 -10
- package/UI/Components/LogsViewer/components/LogsTable.tsx +33 -32
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +16 -18
- package/UI/Components/LogsViewer/components/severityColors.ts +31 -0
- package/UI/Components/LogsViewer/components/severityTheme.ts +25 -25
- package/UI/Components/LogsViewer/types.ts +20 -0
- package/build/dist/Server/API/GitHubAPI.js +40 -9
- package/build/dist/Server/API/GitHubAPI.js.map +1 -1
- package/build/dist/Server/API/TelemetryAPI.js +136 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/API/UserCallAPI.js +17 -0
- package/build/dist/Server/API/UserCallAPI.js.map +1 -1
- package/build/dist/Server/API/UserEmailAPI.js +17 -0
- package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
- package/build/dist/Server/API/UserSmsAPI.js +17 -0
- package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
- package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
- package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +163 -0
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -0
- package/build/dist/Server/Utils/VM/VMRunner.js +31 -0
- package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
- package/build/dist/Types/Log/LogQueryParser.js +200 -0
- package/build/dist/Types/Log/LogQueryParser.js.map +1 -0
- package/build/dist/Types/Log/LogQueryToFilter.js +96 -0
- package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -0
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +3 -3
- package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -42
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +24 -0
- package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +46 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +35 -0
- package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +64 -0
- package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +4 -4
- package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -21
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +230 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +84 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +27 -0
- package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +100 -0
- package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +104 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +14 -35
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +127 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +9 -9
- package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +31 -30
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +7 -8
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js +22 -0
- package/build/dist/UI/Components/LogsViewer/components/severityColors.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +25 -25
- package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
import { getSeverityColor } from "./severityColors";
|
|
3
|
+
|
|
4
|
+
export interface TooltipEntry {
|
|
5
|
+
severity: string;
|
|
6
|
+
count: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface HistogramTooltipProps {
|
|
10
|
+
active?: boolean;
|
|
11
|
+
label?: string;
|
|
12
|
+
payload?: Array<{
|
|
13
|
+
dataKey: string;
|
|
14
|
+
value: number;
|
|
15
|
+
payload: Record<string, number>;
|
|
16
|
+
}>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function formatTooltipTime(label: string | undefined): string {
|
|
20
|
+
if (!label) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const date: Date = new Date(label);
|
|
25
|
+
|
|
26
|
+
if (isNaN(date.getTime())) {
|
|
27
|
+
return label;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const now: Date = new Date();
|
|
31
|
+
const isToday: boolean = date.toDateString() === now.toDateString();
|
|
32
|
+
|
|
33
|
+
const time: string = date.toLocaleTimeString([], {
|
|
34
|
+
hour: "2-digit",
|
|
35
|
+
minute: "2-digit",
|
|
36
|
+
second: "2-digit",
|
|
37
|
+
hour12: false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (isToday) {
|
|
41
|
+
return time;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const dateStr: string = date.toLocaleDateString([], {
|
|
45
|
+
month: "short",
|
|
46
|
+
day: "numeric",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
return `${dateStr}, ${time}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const HistogramTooltip: FunctionComponent<HistogramTooltipProps> = (
|
|
53
|
+
props: HistogramTooltipProps,
|
|
54
|
+
): ReactElement | null => {
|
|
55
|
+
if (!props.active || !props.payload || props.payload.length === 0) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const entries: Array<TooltipEntry> = props.payload
|
|
60
|
+
.filter((entry: { value: number }): boolean => {
|
|
61
|
+
return entry.value > 0;
|
|
62
|
+
})
|
|
63
|
+
.map((entry: { dataKey: string; value: number }): TooltipEntry => {
|
|
64
|
+
return {
|
|
65
|
+
severity: entry.dataKey,
|
|
66
|
+
count: entry.value,
|
|
67
|
+
};
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (entries.length === 0) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const total: number = entries.reduce(
|
|
75
|
+
(sum: number, e: TooltipEntry): number => {
|
|
76
|
+
return sum + e.count;
|
|
77
|
+
},
|
|
78
|
+
0,
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div className="rounded-md border border-gray-200 bg-white px-3 py-2 shadow-md">
|
|
83
|
+
<p className="mb-1.5 border-b border-gray-100 pb-1.5 font-mono text-[11px] font-medium text-gray-500">
|
|
84
|
+
{formatTooltipTime(props.label)}
|
|
85
|
+
</p>
|
|
86
|
+
<div className="space-y-0.5">
|
|
87
|
+
{entries.map((entry: TooltipEntry) => {
|
|
88
|
+
const color: string = getSeverityColor(entry.severity).fill;
|
|
89
|
+
const colorLabel: string =
|
|
90
|
+
getSeverityColor(entry.severity).label || entry.severity;
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
key={entry.severity}
|
|
94
|
+
className="flex items-center justify-between gap-6 py-0.5"
|
|
95
|
+
>
|
|
96
|
+
<div className="flex items-center gap-1.5">
|
|
97
|
+
<span
|
|
98
|
+
className="inline-block h-2.5 w-2.5 rounded-sm"
|
|
99
|
+
style={{ backgroundColor: color }}
|
|
100
|
+
/>
|
|
101
|
+
<span className="text-xs text-gray-600">{colorLabel}</span>
|
|
102
|
+
</div>
|
|
103
|
+
<span className="font-mono text-xs font-semibold tabular-nums text-gray-800">
|
|
104
|
+
{entry.count.toLocaleString()}
|
|
105
|
+
</span>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</div>
|
|
110
|
+
{entries.length > 1 && (
|
|
111
|
+
<div className="mt-1.5 flex items-center justify-between border-t border-gray-100 pt-1.5">
|
|
112
|
+
<span className="text-xs text-gray-500">Total</span>
|
|
113
|
+
<span className="font-mono text-xs font-semibold tabular-nums text-gray-800">
|
|
114
|
+
{total.toLocaleString()}
|
|
115
|
+
</span>
|
|
116
|
+
</div>
|
|
117
|
+
)}
|
|
118
|
+
</div>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default HistogramTooltip;
|
|
@@ -9,10 +9,10 @@ const LiveLogsToggle: FunctionComponent<LiveLogsToggleProps> = (
|
|
|
9
9
|
const { isLive, onToggle, isDisabled } = props;
|
|
10
10
|
|
|
11
11
|
const baseClasses: string =
|
|
12
|
-
"inline-flex items-center gap-2 rounded-
|
|
12
|
+
"inline-flex items-center gap-2 rounded-lg border px-3 py-1.5 text-sm font-medium transition-all focus:outline-none focus:ring-2 focus:ring-emerald-200";
|
|
13
13
|
const activeClasses: string = isLive
|
|
14
|
-
? "border-emerald-
|
|
15
|
-
: "border-
|
|
14
|
+
? "border-emerald-300 bg-emerald-50 text-emerald-700 hover:bg-emerald-100"
|
|
15
|
+
: "border-gray-200 bg-white text-gray-600 hover:bg-gray-50";
|
|
16
16
|
const disabledClasses: string = isDisabled
|
|
17
17
|
? "cursor-not-allowed opacity-50"
|
|
18
18
|
: "cursor-pointer";
|
|
@@ -33,7 +33,7 @@ const LiveLogsToggle: FunctionComponent<LiveLogsToggleProps> = (
|
|
|
33
33
|
>
|
|
34
34
|
<span
|
|
35
35
|
className={`h-2 w-2 rounded-full ${
|
|
36
|
-
isLive ? "bg-emerald-500 animate-pulse" : "bg-
|
|
36
|
+
isLive ? "bg-emerald-500 animate-pulse" : "bg-gray-300"
|
|
37
37
|
}`}
|
|
38
38
|
/>
|
|
39
39
|
<span className="font-semibold">Live</span>
|
|
@@ -139,19 +139,15 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
139
139
|
|
|
140
140
|
const containerClassName: string =
|
|
141
141
|
variant === "embedded"
|
|
142
|
-
? "rounded-
|
|
143
|
-
: "rounded-
|
|
142
|
+
? "rounded-lg border border-gray-200 bg-white p-5 shadow-sm"
|
|
143
|
+
: "rounded-lg border border-gray-200 bg-white p-5 shadow-md";
|
|
144
144
|
|
|
145
|
-
const headerBorderClass: string =
|
|
146
|
-
variant === "embedded" ? "border-slate-900" : "border-slate-800";
|
|
145
|
+
const headerBorderClass: string = "border-gray-200";
|
|
147
146
|
|
|
148
|
-
const surfaceCardClass: string =
|
|
149
|
-
variant === "embedded"
|
|
150
|
-
? "border-slate-900 bg-slate-950/70"
|
|
151
|
-
: "border-slate-800 bg-slate-950/80";
|
|
147
|
+
const surfaceCardClass: string = "border-gray-200 bg-gray-50";
|
|
152
148
|
|
|
153
149
|
const smallBadgeClass: string =
|
|
154
|
-
"inline-flex items-center gap-1 rounded-full border border-
|
|
150
|
+
"inline-flex items-center gap-1 rounded-full border border-gray-200 bg-gray-50 px-2 py-1 text-[11px] font-mono uppercase tracking-wide text-gray-600";
|
|
155
151
|
|
|
156
152
|
return (
|
|
157
153
|
<div className={containerClassName}>
|
|
@@ -160,13 +156,13 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
160
156
|
>
|
|
161
157
|
<div className="flex flex-1 items-start gap-3">
|
|
162
158
|
<span
|
|
163
|
-
className="mt-1 h-3 w-3 flex-none rounded-full border border-
|
|
159
|
+
className="mt-1 h-3 w-3 flex-none rounded-full border border-gray-200"
|
|
164
160
|
style={{ backgroundColor: serviceColor }}
|
|
165
161
|
aria-hidden="true"
|
|
166
162
|
/>
|
|
167
163
|
<div className="space-y-3">
|
|
168
164
|
<div className="flex flex-wrap items-center gap-3">
|
|
169
|
-
<h3 className="text-lg font-semibold text-
|
|
165
|
+
<h3 className="text-lg font-semibold text-gray-900">
|
|
170
166
|
{serviceName}
|
|
171
167
|
</h3>
|
|
172
168
|
<SeverityBadge severity={props.log.severityText} />
|
|
@@ -200,7 +196,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
200
196
|
<button
|
|
201
197
|
type="button"
|
|
202
198
|
onClick={props.onClose}
|
|
203
|
-
className="flex h-9 w-9 items-center justify-center rounded-full border border-
|
|
199
|
+
className="flex h-9 w-9 items-center justify-center rounded-full border border-gray-200 bg-gray-50 text-gray-400 transition-colors hover:border-gray-300 hover:text-gray-600"
|
|
204
200
|
title="Close details"
|
|
205
201
|
>
|
|
206
202
|
<Icon icon={IconProp.Close} className="h-4 w-4" />
|
|
@@ -208,9 +204,9 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
208
204
|
)}
|
|
209
205
|
</div>
|
|
210
206
|
|
|
211
|
-
<div className="mt-4 space-y-5 text-sm text-
|
|
207
|
+
<div className="mt-4 space-y-5 text-sm text-gray-700">
|
|
212
208
|
<section className="space-y-3">
|
|
213
|
-
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-
|
|
209
|
+
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-gray-400">
|
|
214
210
|
<span>Log Body</span>
|
|
215
211
|
<CopyTextButton
|
|
216
212
|
textToBeCopied={bodyDetails.raw}
|
|
@@ -223,11 +219,11 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
223
219
|
|
|
224
220
|
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
225
221
|
{bodyDetails.isJson ? (
|
|
226
|
-
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-
|
|
222
|
+
<pre className="max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-gray-800">
|
|
227
223
|
{bodyDetails.pretty}
|
|
228
224
|
</pre>
|
|
229
225
|
) : (
|
|
230
|
-
<p className="whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-
|
|
226
|
+
<p className="whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-gray-800">
|
|
231
227
|
{bodyDetails.pretty || "-"}
|
|
232
228
|
</p>
|
|
233
229
|
)}
|
|
@@ -238,7 +234,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
238
234
|
<section className="grid gap-4 md:grid-cols-2">
|
|
239
235
|
{traceId && (
|
|
240
236
|
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
241
|
-
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-
|
|
237
|
+
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-gray-400">
|
|
242
238
|
<span>Trace ID</span>
|
|
243
239
|
<CopyTextButton
|
|
244
240
|
textToBeCopied={traceId}
|
|
@@ -252,14 +248,14 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
252
248
|
{traceRoute ? (
|
|
253
249
|
<Link
|
|
254
250
|
to={traceRoute}
|
|
255
|
-
className="max-w-full truncate font-mono text-xs text-indigo-
|
|
251
|
+
className="max-w-full truncate font-mono text-xs text-indigo-600 hover:text-indigo-500"
|
|
256
252
|
title={`View trace ${traceId}`}
|
|
257
253
|
>
|
|
258
254
|
{traceId}
|
|
259
255
|
</Link>
|
|
260
256
|
) : (
|
|
261
257
|
<span
|
|
262
|
-
className="max-w-full truncate font-mono text-xs text-
|
|
258
|
+
className="max-w-full truncate font-mono text-xs text-gray-700"
|
|
263
259
|
title={traceId}
|
|
264
260
|
>
|
|
265
261
|
{traceId}
|
|
@@ -268,7 +264,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
268
264
|
{traceRoute && (
|
|
269
265
|
<Icon
|
|
270
266
|
icon={IconProp.ExternalLink}
|
|
271
|
-
className="h-4 w-4 flex-none text-indigo-
|
|
267
|
+
className="h-4 w-4 flex-none text-indigo-400"
|
|
272
268
|
/>
|
|
273
269
|
)}
|
|
274
270
|
</div>
|
|
@@ -277,7 +273,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
277
273
|
|
|
278
274
|
{spanId && (
|
|
279
275
|
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
280
|
-
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-
|
|
276
|
+
<div className="mb-2 flex items-center justify-between text-[11px] uppercase tracking-wide text-gray-400">
|
|
281
277
|
<span>Span ID</span>
|
|
282
278
|
<CopyTextButton
|
|
283
279
|
textToBeCopied={spanId}
|
|
@@ -291,14 +287,14 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
291
287
|
{spanRoute ? (
|
|
292
288
|
<Link
|
|
293
289
|
to={spanRoute}
|
|
294
|
-
className="max-w-full truncate font-mono text-xs text-indigo-
|
|
290
|
+
className="max-w-full truncate font-mono text-xs text-indigo-600 hover:text-indigo-500"
|
|
295
291
|
title={`View span ${spanId}`}
|
|
296
292
|
>
|
|
297
293
|
{spanId}
|
|
298
294
|
</Link>
|
|
299
295
|
) : (
|
|
300
296
|
<span
|
|
301
|
-
className="max-w-full truncate font-mono text-xs text-
|
|
297
|
+
className="max-w-full truncate font-mono text-xs text-gray-700"
|
|
302
298
|
title={spanId}
|
|
303
299
|
>
|
|
304
300
|
{spanId}
|
|
@@ -307,7 +303,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
307
303
|
{spanRoute && (
|
|
308
304
|
<Icon
|
|
309
305
|
icon={IconProp.ExternalLink}
|
|
310
|
-
className="h-4 w-4 flex-none text-indigo-
|
|
306
|
+
className="h-4 w-4 flex-none text-indigo-400"
|
|
311
307
|
/>
|
|
312
308
|
)}
|
|
313
309
|
</div>
|
|
@@ -318,7 +314,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
318
314
|
|
|
319
315
|
{prettyAttributes && (
|
|
320
316
|
<section className="space-y-3">
|
|
321
|
-
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-
|
|
317
|
+
<header className="flex items-center justify-between text-[11px] uppercase tracking-wide text-gray-400">
|
|
322
318
|
<span>Attributes</span>
|
|
323
319
|
<CopyTextButton
|
|
324
320
|
textToBeCopied={prettyAttributes}
|
|
@@ -329,7 +325,7 @@ const LogDetailsPanel: FunctionComponent<LogDetailsPanelProps> = (
|
|
|
329
325
|
/>
|
|
330
326
|
</header>
|
|
331
327
|
<div className={`rounded-lg border ${surfaceCardClass} p-4`}>
|
|
332
|
-
<pre className="max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-
|
|
328
|
+
<pre className="max-h-72 overflow-auto whitespace-pre-wrap break-words font-mono text-[13px] leading-6 text-gray-800">
|
|
333
329
|
{prettyAttributes}
|
|
334
330
|
</pre>
|
|
335
331
|
</div>
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
FunctionComponent,
|
|
3
|
+
ReactElement,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
useRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
KeyboardEvent,
|
|
9
|
+
} from "react";
|
|
10
|
+
import Icon from "../../Icon/Icon";
|
|
11
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
12
|
+
import LogSearchSuggestions from "./LogSearchSuggestions";
|
|
13
|
+
import LogSearchHelp from "./LogSearchHelp";
|
|
14
|
+
|
|
15
|
+
export interface LogSearchBarProps {
|
|
16
|
+
value: string;
|
|
17
|
+
onChange: (value: string) => void;
|
|
18
|
+
onSubmit: () => void;
|
|
19
|
+
suggestions?: Array<string> | undefined;
|
|
20
|
+
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
21
|
+
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
22
|
+
placeholder?: string | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
|
|
26
|
+
props: LogSearchBarProps,
|
|
27
|
+
): ReactElement => {
|
|
28
|
+
const [isFocused, setIsFocused] = useState<boolean>(false);
|
|
29
|
+
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
|
|
30
|
+
const [showHelp, setShowHelp] = useState<boolean>(false);
|
|
31
|
+
const [selectedSuggestionIndex, setSelectedSuggestionIndex] =
|
|
32
|
+
useState<number>(-1);
|
|
33
|
+
const inputRef: React.RefObject<HTMLInputElement> = useRef<HTMLInputElement>(
|
|
34
|
+
null!,
|
|
35
|
+
);
|
|
36
|
+
const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
|
37
|
+
null!,
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const currentWord: string = extractCurrentWord(props.value);
|
|
41
|
+
|
|
42
|
+
// Strip leading "@" — treat it as a trigger character for suggestions
|
|
43
|
+
const hasAtPrefix: boolean = currentWord.startsWith("@");
|
|
44
|
+
const normalizedWord: string = hasAtPrefix
|
|
45
|
+
? currentWord.substring(1)
|
|
46
|
+
: currentWord;
|
|
47
|
+
|
|
48
|
+
// Determine if we're in "field:value" mode or "field name" mode
|
|
49
|
+
const colonIndex: number = normalizedWord.indexOf(":");
|
|
50
|
+
const isValueMode: boolean = colonIndex > 0;
|
|
51
|
+
const fieldPrefix: string = isValueMode
|
|
52
|
+
? normalizedWord.substring(0, colonIndex).toLowerCase()
|
|
53
|
+
: "";
|
|
54
|
+
const partialValue: string = isValueMode
|
|
55
|
+
? normalizedWord.substring(colonIndex + 1)
|
|
56
|
+
: "";
|
|
57
|
+
|
|
58
|
+
const filteredSuggestions: Array<string> = isValueMode
|
|
59
|
+
? getValueSuggestions(
|
|
60
|
+
fieldPrefix,
|
|
61
|
+
partialValue,
|
|
62
|
+
props.valueSuggestions || {},
|
|
63
|
+
)
|
|
64
|
+
: (props.suggestions || []).filter((s: string): boolean => {
|
|
65
|
+
if (!normalizedWord && !hasAtPrefix) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
// When just "@" is typed, show all suggestions
|
|
69
|
+
if (hasAtPrefix && normalizedWord.length === 0) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
// Match against the suggestion name, stripping any leading "@" from the suggestion too
|
|
73
|
+
const normalizedSuggestion: string = s.startsWith("@")
|
|
74
|
+
? s.substring(1).toLowerCase()
|
|
75
|
+
: s.toLowerCase();
|
|
76
|
+
return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const shouldShowSuggestions: boolean =
|
|
80
|
+
showSuggestions &&
|
|
81
|
+
isFocused &&
|
|
82
|
+
filteredSuggestions.length > 0 &&
|
|
83
|
+
(isValueMode ? true : currentWord.length > 0);
|
|
84
|
+
|
|
85
|
+
// Show help when focused, input is empty, and no suggestions visible
|
|
86
|
+
const shouldShowHelp: boolean =
|
|
87
|
+
showHelp && isFocused && props.value.length === 0 && !shouldShowSuggestions;
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
setSelectedSuggestionIndex(-1);
|
|
91
|
+
}, [currentWord]);
|
|
92
|
+
|
|
93
|
+
const handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void =
|
|
94
|
+
useCallback(
|
|
95
|
+
(e: KeyboardEvent<HTMLInputElement>): void => {
|
|
96
|
+
if (e.key === "Enter") {
|
|
97
|
+
if (
|
|
98
|
+
shouldShowSuggestions &&
|
|
99
|
+
selectedSuggestionIndex >= 0 &&
|
|
100
|
+
selectedSuggestionIndex < filteredSuggestions.length
|
|
101
|
+
) {
|
|
102
|
+
applySuggestion(filteredSuggestions[selectedSuggestionIndex]!);
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If in value mode with a typed value, try to match and apply as chip
|
|
108
|
+
if (
|
|
109
|
+
isValueMode &&
|
|
110
|
+
partialValue.length > 0 &&
|
|
111
|
+
props.onFieldValueSelect
|
|
112
|
+
) {
|
|
113
|
+
// First try exact case-insensitive match from the available values
|
|
114
|
+
const resolvedField: string =
|
|
115
|
+
FIELD_ALIAS_MAP[fieldPrefix] || fieldPrefix;
|
|
116
|
+
const availableValues: Array<string> =
|
|
117
|
+
(props.valueSuggestions || {})[resolvedField] || [];
|
|
118
|
+
const lowerPartial: string = partialValue.toLowerCase();
|
|
119
|
+
const exactMatch: string | undefined = availableValues.find(
|
|
120
|
+
(v: string): boolean => {
|
|
121
|
+
return v.toLowerCase() === lowerPartial;
|
|
122
|
+
},
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// Use exact match, or if there's exactly one prefix match, use that
|
|
126
|
+
const resolvedMatch: string | undefined =
|
|
127
|
+
exactMatch ||
|
|
128
|
+
(filteredSuggestions.length === 1
|
|
129
|
+
? filteredSuggestions[0]
|
|
130
|
+
: undefined);
|
|
131
|
+
|
|
132
|
+
if (resolvedMatch) {
|
|
133
|
+
props.onFieldValueSelect(fieldPrefix, resolvedMatch);
|
|
134
|
+
// Remove the field:value term from text
|
|
135
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
136
|
+
parts.pop();
|
|
137
|
+
const remaining: string = parts.join(" ");
|
|
138
|
+
props.onChange(remaining ? remaining + " " : "");
|
|
139
|
+
setShowSuggestions(false);
|
|
140
|
+
setShowHelp(false);
|
|
141
|
+
e.preventDefault();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
props.onSubmit();
|
|
147
|
+
setShowSuggestions(false);
|
|
148
|
+
setShowHelp(false);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (e.key === "Escape") {
|
|
153
|
+
setShowSuggestions(false);
|
|
154
|
+
setShowHelp(false);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!shouldShowSuggestions) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (e.key === "ArrowDown") {
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
setSelectedSuggestionIndex((prev: number): number => {
|
|
165
|
+
return Math.min(prev + 1, filteredSuggestions.length - 1);
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (e.key === "ArrowUp") {
|
|
171
|
+
e.preventDefault();
|
|
172
|
+
setSelectedSuggestionIndex((prev: number): number => {
|
|
173
|
+
return Math.max(prev - 1, 0);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
[
|
|
178
|
+
shouldShowSuggestions,
|
|
179
|
+
selectedSuggestionIndex,
|
|
180
|
+
filteredSuggestions,
|
|
181
|
+
isValueMode,
|
|
182
|
+
fieldPrefix,
|
|
183
|
+
partialValue,
|
|
184
|
+
props,
|
|
185
|
+
],
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
const applySuggestion: (suggestion: string) => void = useCallback(
|
|
189
|
+
(suggestion: string): void => {
|
|
190
|
+
if (isValueMode) {
|
|
191
|
+
// Value mode: apply as a chip via onFieldValueSelect
|
|
192
|
+
if (props.onFieldValueSelect) {
|
|
193
|
+
props.onFieldValueSelect(fieldPrefix, suggestion);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Remove the current field:value term from the search text
|
|
197
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
198
|
+
parts.pop(); // remove the field:partialValue
|
|
199
|
+
const remaining: string = parts.join(" ");
|
|
200
|
+
props.onChange(remaining ? remaining + " " : "");
|
|
201
|
+
setShowSuggestions(false);
|
|
202
|
+
setShowHelp(false);
|
|
203
|
+
inputRef.current?.focus();
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Field name mode: append colon
|
|
208
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
209
|
+
|
|
210
|
+
if (parts.length > 0) {
|
|
211
|
+
parts[parts.length - 1] = suggestion + ":";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
props.onChange(parts.join(" "));
|
|
215
|
+
setShowSuggestions(false);
|
|
216
|
+
setShowHelp(false);
|
|
217
|
+
inputRef.current?.focus();
|
|
218
|
+
},
|
|
219
|
+
[props, isValueMode, fieldPrefix],
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const handleExampleClick: (example: string) => void = useCallback(
|
|
223
|
+
(example: string): void => {
|
|
224
|
+
props.onChange(example);
|
|
225
|
+
setShowHelp(false);
|
|
226
|
+
inputRef.current?.focus();
|
|
227
|
+
},
|
|
228
|
+
[props],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
const handleClickOutside: (e: MouseEvent) => void = (
|
|
233
|
+
e: MouseEvent,
|
|
234
|
+
): void => {
|
|
235
|
+
if (
|
|
236
|
+
containerRef.current &&
|
|
237
|
+
!containerRef.current.contains(e.target as Node)
|
|
238
|
+
) {
|
|
239
|
+
setShowSuggestions(false);
|
|
240
|
+
setShowHelp(false);
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
245
|
+
return () => {
|
|
246
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
247
|
+
};
|
|
248
|
+
}, []);
|
|
249
|
+
|
|
250
|
+
return (
|
|
251
|
+
<div ref={containerRef} className="relative">
|
|
252
|
+
<div
|
|
253
|
+
className={`flex items-center gap-2 rounded-lg border bg-white px-3 py-2 transition-colors ${
|
|
254
|
+
isFocused
|
|
255
|
+
? "border-indigo-400 ring-2 ring-indigo-100"
|
|
256
|
+
: "border-gray-200 hover:border-gray-300"
|
|
257
|
+
}`}
|
|
258
|
+
>
|
|
259
|
+
<Icon
|
|
260
|
+
icon={IconProp.Search}
|
|
261
|
+
className="h-4 w-4 flex-none text-gray-400"
|
|
262
|
+
/>
|
|
263
|
+
<input
|
|
264
|
+
ref={inputRef}
|
|
265
|
+
type="text"
|
|
266
|
+
value={props.value}
|
|
267
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
268
|
+
props.onChange(e.target.value);
|
|
269
|
+
setShowSuggestions(true);
|
|
270
|
+
setShowHelp(false);
|
|
271
|
+
}}
|
|
272
|
+
onFocus={() => {
|
|
273
|
+
setIsFocused(true);
|
|
274
|
+
setShowSuggestions(true);
|
|
275
|
+
if (props.value.length === 0) {
|
|
276
|
+
setShowHelp(true);
|
|
277
|
+
}
|
|
278
|
+
}}
|
|
279
|
+
onBlur={() => {
|
|
280
|
+
setIsFocused(false);
|
|
281
|
+
}}
|
|
282
|
+
onKeyDown={handleKeyDown}
|
|
283
|
+
placeholder={
|
|
284
|
+
props.placeholder ||
|
|
285
|
+
'Search logs... (e.g. severity:error service:api "connection refused")'
|
|
286
|
+
}
|
|
287
|
+
className="flex-1 bg-transparent font-mono text-sm text-gray-900 placeholder-gray-400 outline-none"
|
|
288
|
+
spellCheck={false}
|
|
289
|
+
autoComplete="off"
|
|
290
|
+
/>
|
|
291
|
+
{props.value.length > 0 && (
|
|
292
|
+
<button
|
|
293
|
+
type="button"
|
|
294
|
+
className="flex-none rounded-full p-1 text-gray-400 hover:bg-gray-100"
|
|
295
|
+
onClick={() => {
|
|
296
|
+
props.onChange("");
|
|
297
|
+
setShowHelp(true);
|
|
298
|
+
setShowSuggestions(false);
|
|
299
|
+
inputRef.current?.focus();
|
|
300
|
+
}}
|
|
301
|
+
title="Clear search"
|
|
302
|
+
>
|
|
303
|
+
<Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
|
|
304
|
+
</button>
|
|
305
|
+
)}
|
|
306
|
+
</div>
|
|
307
|
+
|
|
308
|
+
{shouldShowSuggestions && (
|
|
309
|
+
<LogSearchSuggestions
|
|
310
|
+
suggestions={filteredSuggestions}
|
|
311
|
+
selectedIndex={selectedSuggestionIndex}
|
|
312
|
+
onSelect={applySuggestion}
|
|
313
|
+
fieldContext={isValueMode ? fieldPrefix : undefined}
|
|
314
|
+
/>
|
|
315
|
+
)}
|
|
316
|
+
|
|
317
|
+
{shouldShowHelp && <LogSearchHelp onExampleClick={handleExampleClick} />}
|
|
318
|
+
</div>
|
|
319
|
+
);
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
function extractCurrentWord(value: string): string {
|
|
323
|
+
const parts: Array<string> = value.split(/\s+/);
|
|
324
|
+
return parts[parts.length - 1] || "";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Field alias mapping (user-facing name → internal key used in valueSuggestions)
|
|
328
|
+
const FIELD_ALIAS_MAP: Record<string, string> = {
|
|
329
|
+
severity: "severityText",
|
|
330
|
+
level: "severityText",
|
|
331
|
+
service: "serviceId",
|
|
332
|
+
trace: "traceId",
|
|
333
|
+
span: "spanId",
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
function getValueSuggestions(
|
|
337
|
+
fieldName: string,
|
|
338
|
+
partialValue: string,
|
|
339
|
+
valueSuggestions: Record<string, Array<string>>,
|
|
340
|
+
): Array<string> {
|
|
341
|
+
// Resolve field name alias
|
|
342
|
+
const resolvedField: string = FIELD_ALIAS_MAP[fieldName] || fieldName;
|
|
343
|
+
|
|
344
|
+
const values: Array<string> | undefined = valueSuggestions[resolvedField];
|
|
345
|
+
|
|
346
|
+
if (!values || values.length === 0) {
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!partialValue || partialValue.length === 0) {
|
|
351
|
+
return values;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const lowerPartial: string = partialValue.toLowerCase();
|
|
355
|
+
return values.filter((v: string): boolean => {
|
|
356
|
+
return v.toLowerCase().startsWith(lowerPartial);
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
export default LogSearchBar;
|