@oneuptime/common 10.0.20 → 10.0.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/Types/AnalyticsDatabase/ModelPermission.ts +2 -2
- package/Server/Types/Database/Permissions/TenantPermission.ts +2 -2
- package/Server/Utils/VM/VMRunner.ts +10 -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/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/Types/AnalyticsDatabase/ModelPermission.js +2 -2
- package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +2 -2
- package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
- package/build/dist/Server/Utils/VM/VMRunner.js +10 -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,128 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface LogSearchHelpProps {
|
|
4
|
+
onExampleClick?: ((example: string) => void) | undefined;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
interface HelpRow {
|
|
8
|
+
syntax: string;
|
|
9
|
+
description: string;
|
|
10
|
+
example: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const HELP_ROWS: Array<HelpRow> = [
|
|
14
|
+
{
|
|
15
|
+
syntax: "free text",
|
|
16
|
+
description: "Search log messages",
|
|
17
|
+
example: "connection refused",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
syntax: '"quoted phrase"',
|
|
21
|
+
description: "Exact phrase match",
|
|
22
|
+
example: '"out of memory"',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
syntax: "severity:<level>",
|
|
26
|
+
description: "Filter by log level",
|
|
27
|
+
example: "severity:error",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
syntax: "service:<name>",
|
|
31
|
+
description: "Filter by service",
|
|
32
|
+
example: "service:api",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
syntax: "trace:<id>",
|
|
36
|
+
description: "Filter by trace ID",
|
|
37
|
+
example: "trace:abc123def456",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
syntax: "span:<id>",
|
|
41
|
+
description: "Filter by span ID",
|
|
42
|
+
example: "span:e1f7f671fe78",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
syntax: "@<attr>:<value>",
|
|
46
|
+
description: "Filter by attribute",
|
|
47
|
+
example: "@http.status_code:500",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
syntax: "-field:value",
|
|
51
|
+
description: "Exclude matching logs",
|
|
52
|
+
example: "-severity:debug",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
syntax: "field:value*",
|
|
56
|
+
description: "Wildcard match",
|
|
57
|
+
example: "service:api-*",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
syntax: "@attr:>N",
|
|
61
|
+
description: "Numeric comparison",
|
|
62
|
+
example: "@duration:>1000",
|
|
63
|
+
},
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const LogSearchHelp: FunctionComponent<LogSearchHelpProps> = (
|
|
67
|
+
props: LogSearchHelpProps,
|
|
68
|
+
): ReactElement => {
|
|
69
|
+
return (
|
|
70
|
+
<div className="absolute left-0 top-full z-50 mt-1 w-[36rem] overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg">
|
|
71
|
+
<div className="border-b border-gray-100 px-3 py-2">
|
|
72
|
+
<span className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
|
|
73
|
+
Search syntax
|
|
74
|
+
</span>
|
|
75
|
+
</div>
|
|
76
|
+
|
|
77
|
+
<table className="w-full">
|
|
78
|
+
<tbody>
|
|
79
|
+
{HELP_ROWS.map((row: HelpRow) => {
|
|
80
|
+
return (
|
|
81
|
+
<tr
|
|
82
|
+
key={row.syntax}
|
|
83
|
+
className="cursor-pointer transition-colors hover:bg-gray-50"
|
|
84
|
+
onMouseDown={(e: React.MouseEvent) => {
|
|
85
|
+
e.preventDefault();
|
|
86
|
+
if (props.onExampleClick) {
|
|
87
|
+
props.onExampleClick(row.example);
|
|
88
|
+
}
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<td className="whitespace-nowrap py-1.5 pl-3 pr-2">
|
|
92
|
+
<code className="font-mono text-xs text-indigo-600">
|
|
93
|
+
{row.syntax}
|
|
94
|
+
</code>
|
|
95
|
+
</td>
|
|
96
|
+
<td className="py-1.5 px-2">
|
|
97
|
+
<span className="text-xs text-gray-500">
|
|
98
|
+
{row.description}
|
|
99
|
+
</span>
|
|
100
|
+
</td>
|
|
101
|
+
<td className="whitespace-nowrap py-1.5 pl-2 pr-3 text-right">
|
|
102
|
+
<code className="font-mono text-[11px] text-gray-400">
|
|
103
|
+
{row.example}
|
|
104
|
+
</code>
|
|
105
|
+
</td>
|
|
106
|
+
</tr>
|
|
107
|
+
);
|
|
108
|
+
})}
|
|
109
|
+
</tbody>
|
|
110
|
+
</table>
|
|
111
|
+
|
|
112
|
+
<div className="border-t border-gray-100 px-3 py-1.5">
|
|
113
|
+
<span className="text-[10px] text-gray-400">
|
|
114
|
+
Press{" "}
|
|
115
|
+
<kbd className="rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-mono text-[10px]">
|
|
116
|
+
Enter
|
|
117
|
+
</kbd>{" "}
|
|
118
|
+
to search · Combine filters:{" "}
|
|
119
|
+
<code className="font-mono text-[10px] text-gray-500">
|
|
120
|
+
severity:error service:api "timeout"
|
|
121
|
+
</code>
|
|
122
|
+
</span>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
export default LogSearchHelp;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement } from "react";
|
|
2
|
+
|
|
3
|
+
export interface LogSearchSuggestionsProps {
|
|
4
|
+
suggestions: Array<string>;
|
|
5
|
+
selectedIndex: number;
|
|
6
|
+
onSelect: (suggestion: string) => void;
|
|
7
|
+
fieldContext?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const MAX_VISIBLE_SUGGESTIONS: number = 8;
|
|
11
|
+
|
|
12
|
+
const LogSearchSuggestions: FunctionComponent<LogSearchSuggestionsProps> = (
|
|
13
|
+
props: LogSearchSuggestionsProps,
|
|
14
|
+
): ReactElement => {
|
|
15
|
+
const visible: Array<string> = props.suggestions.slice(
|
|
16
|
+
0,
|
|
17
|
+
MAX_VISIBLE_SUGGESTIONS,
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="absolute left-0 right-0 top-full z-50 mt-1 max-h-56 overflow-y-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg">
|
|
22
|
+
{visible.map((suggestion: string, index: number) => {
|
|
23
|
+
const isSelected: boolean = index === props.selectedIndex;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<button
|
|
27
|
+
key={suggestion}
|
|
28
|
+
type="button"
|
|
29
|
+
className={`flex w-full items-center gap-2 px-3 py-1.5 text-left text-sm transition-colors ${
|
|
30
|
+
isSelected
|
|
31
|
+
? "bg-indigo-50 text-indigo-700"
|
|
32
|
+
: "text-gray-700 hover:bg-gray-50"
|
|
33
|
+
}`}
|
|
34
|
+
onMouseDown={(e: React.MouseEvent) => {
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
props.onSelect(suggestion);
|
|
37
|
+
}}
|
|
38
|
+
>
|
|
39
|
+
{props.fieldContext ? (
|
|
40
|
+
<>
|
|
41
|
+
<span className="font-mono text-xs text-gray-400">
|
|
42
|
+
{props.fieldContext}:
|
|
43
|
+
</span>
|
|
44
|
+
<span className="font-mono">{suggestion}</span>
|
|
45
|
+
</>
|
|
46
|
+
) : (
|
|
47
|
+
<>
|
|
48
|
+
<span className="font-mono text-xs text-indigo-400">@</span>
|
|
49
|
+
<span className="font-mono">{suggestion}</span>
|
|
50
|
+
</>
|
|
51
|
+
)}
|
|
52
|
+
</button>
|
|
53
|
+
);
|
|
54
|
+
})}
|
|
55
|
+
{props.suggestions.length > MAX_VISIBLE_SUGGESTIONS && (
|
|
56
|
+
<div className="px-3 py-1 text-[11px] text-gray-400">
|
|
57
|
+
+{props.suggestions.length - MAX_VISIBLE_SUGGESTIONS} more...
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default LogSearchSuggestions;
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
FunctionComponent,
|
|
3
|
+
ReactElement,
|
|
4
|
+
useState,
|
|
5
|
+
useRef,
|
|
6
|
+
useEffect,
|
|
7
|
+
useCallback,
|
|
8
|
+
} from "react";
|
|
9
|
+
import Icon from "../../Icon/Icon";
|
|
10
|
+
import IconProp from "../../../../Types/Icon/IconProp";
|
|
11
|
+
import RangeStartAndEndDateTime from "../../../../Types/Time/RangeStartAndEndDateTime";
|
|
12
|
+
import TimeRange from "../../../../Types/Time/TimeRange";
|
|
13
|
+
import InBetween from "../../../../Types/BaseDatabase/InBetween";
|
|
14
|
+
import StartAndEndDate, {
|
|
15
|
+
StartAndEndDateType,
|
|
16
|
+
} from "../../Date/StartAndEndDate";
|
|
17
|
+
|
|
18
|
+
export interface LogTimeRangePickerProps {
|
|
19
|
+
value: RangeStartAndEndDateTime;
|
|
20
|
+
onChange: (value: RangeStartAndEndDateTime) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Preset options to show in the dropdown (ordered for log investigation use)
|
|
24
|
+
const PRESET_OPTIONS: Array<{ range: TimeRange; label: string }> = [
|
|
25
|
+
{ range: TimeRange.PAST_THIRTY_MINS, label: "Past 30 Minutes" },
|
|
26
|
+
{ range: TimeRange.PAST_ONE_HOUR, label: "Past 1 Hour" },
|
|
27
|
+
{ range: TimeRange.PAST_TWO_HOURS, label: "Past 2 Hours" },
|
|
28
|
+
{ range: TimeRange.PAST_THREE_HOURS, label: "Past 3 Hours" },
|
|
29
|
+
{ range: TimeRange.PAST_ONE_DAY, label: "Past 1 Day" },
|
|
30
|
+
{ range: TimeRange.PAST_TWO_DAYS, label: "Past 2 Days" },
|
|
31
|
+
{ range: TimeRange.PAST_ONE_WEEK, label: "Past 1 Week" },
|
|
32
|
+
{ range: TimeRange.PAST_TWO_WEEKS, label: "Past 2 Weeks" },
|
|
33
|
+
{ range: TimeRange.PAST_ONE_MONTH, label: "Past 1 Month" },
|
|
34
|
+
{ range: TimeRange.PAST_THREE_MONTHS, label: "Past 3 Months" },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
function formatDateShort(date: Date): string {
|
|
38
|
+
const month: string = date.toLocaleString("en-US", { month: "short" });
|
|
39
|
+
const day: number = date.getDate();
|
|
40
|
+
const hours: string = date.getHours().toString().padStart(2, "0");
|
|
41
|
+
const minutes: string = date.getMinutes().toString().padStart(2, "0");
|
|
42
|
+
return `${month} ${day}, ${hours}:${minutes}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getButtonLabel(value: RangeStartAndEndDateTime): string {
|
|
46
|
+
if (value.range === TimeRange.CUSTOM && value.startAndEndDate) {
|
|
47
|
+
const start: string = formatDateShort(value.startAndEndDate.startValue);
|
|
48
|
+
const end: string = formatDateShort(value.startAndEndDate.endValue);
|
|
49
|
+
return `${start} – ${end}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const preset: { range: TimeRange; label: string } | undefined =
|
|
53
|
+
PRESET_OPTIONS.find((opt: { range: TimeRange; label: string }) => {
|
|
54
|
+
return opt.range === value.range;
|
|
55
|
+
});
|
|
56
|
+
return preset ? preset.label : value.range;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const LogTimeRangePicker: FunctionComponent<LogTimeRangePickerProps> = (
|
|
60
|
+
props: LogTimeRangePickerProps,
|
|
61
|
+
): ReactElement => {
|
|
62
|
+
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
63
|
+
const [showCustom, setShowCustom] = useState<boolean>(
|
|
64
|
+
props.value.range === TimeRange.CUSTOM,
|
|
65
|
+
);
|
|
66
|
+
const containerRef: React.RefObject<HTMLDivElement> = useRef<HTMLDivElement>(
|
|
67
|
+
null!,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// Close on click outside
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
const handleClickOutside: (e: MouseEvent) => void = (
|
|
73
|
+
e: MouseEvent,
|
|
74
|
+
): void => {
|
|
75
|
+
if (
|
|
76
|
+
containerRef.current &&
|
|
77
|
+
!containerRef.current.contains(e.target as Node)
|
|
78
|
+
) {
|
|
79
|
+
setIsOpen(false);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
84
|
+
return () => {
|
|
85
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
86
|
+
};
|
|
87
|
+
}, []);
|
|
88
|
+
|
|
89
|
+
// Sync showCustom when value changes externally (e.g., histogram drag)
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
setShowCustom(props.value.range === TimeRange.CUSTOM);
|
|
92
|
+
}, [props.value.range]);
|
|
93
|
+
|
|
94
|
+
const handlePresetSelect: (range: TimeRange) => void = useCallback(
|
|
95
|
+
(range: TimeRange): void => {
|
|
96
|
+
props.onChange({ range });
|
|
97
|
+
setShowCustom(false);
|
|
98
|
+
setIsOpen(false);
|
|
99
|
+
},
|
|
100
|
+
[props],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const handleCustomDateChange: (dateRange: InBetween<Date> | null) => void =
|
|
104
|
+
useCallback(
|
|
105
|
+
(dateRange: InBetween<Date> | null): void => {
|
|
106
|
+
if (dateRange) {
|
|
107
|
+
props.onChange({
|
|
108
|
+
range: TimeRange.CUSTOM,
|
|
109
|
+
startAndEndDate: dateRange,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
[props],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const buttonLabel: string = getButtonLabel(props.value);
|
|
117
|
+
|
|
118
|
+
return (
|
|
119
|
+
<div ref={containerRef} className="relative">
|
|
120
|
+
<button
|
|
121
|
+
type="button"
|
|
122
|
+
className={`flex items-center gap-1.5 rounded-md border px-2.5 py-1.5 text-xs font-medium transition-colors ${
|
|
123
|
+
isOpen
|
|
124
|
+
? "border-indigo-300 bg-indigo-50 text-indigo-700"
|
|
125
|
+
: "border-gray-200 bg-white text-gray-600 hover:border-gray-300 hover:bg-gray-50"
|
|
126
|
+
}`}
|
|
127
|
+
onClick={() => {
|
|
128
|
+
setIsOpen(!isOpen);
|
|
129
|
+
}}
|
|
130
|
+
>
|
|
131
|
+
<Icon icon={IconProp.Clock} className="h-3.5 w-3.5" />
|
|
132
|
+
<span>{buttonLabel}</span>
|
|
133
|
+
<Icon
|
|
134
|
+
icon={IconProp.ChevronDown}
|
|
135
|
+
className={`h-3 w-3 transition-transform ${isOpen ? "rotate-180" : ""}`}
|
|
136
|
+
/>
|
|
137
|
+
</button>
|
|
138
|
+
|
|
139
|
+
{isOpen && (
|
|
140
|
+
<div className="absolute right-0 top-full z-50 mt-1 w-72 rounded-lg border border-gray-200 bg-white shadow-lg">
|
|
141
|
+
{/* Preset options */}
|
|
142
|
+
<div className="max-h-64 overflow-y-auto py-1">
|
|
143
|
+
{PRESET_OPTIONS.map(
|
|
144
|
+
(option: { range: TimeRange; label: string }) => {
|
|
145
|
+
const isActive: boolean = props.value.range === option.range;
|
|
146
|
+
|
|
147
|
+
return (
|
|
148
|
+
<button
|
|
149
|
+
key={option.range}
|
|
150
|
+
type="button"
|
|
151
|
+
className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
|
|
152
|
+
isActive
|
|
153
|
+
? "bg-indigo-50 font-medium text-indigo-700"
|
|
154
|
+
: "text-gray-700 hover:bg-gray-50"
|
|
155
|
+
}`}
|
|
156
|
+
onClick={() => {
|
|
157
|
+
handlePresetSelect(option.range);
|
|
158
|
+
}}
|
|
159
|
+
>
|
|
160
|
+
{option.label}
|
|
161
|
+
</button>
|
|
162
|
+
);
|
|
163
|
+
},
|
|
164
|
+
)}
|
|
165
|
+
|
|
166
|
+
{/* Custom option */}
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
className={`flex w-full items-center px-3 py-1.5 text-left text-sm transition-colors ${
|
|
170
|
+
props.value.range === TimeRange.CUSTOM
|
|
171
|
+
? "bg-indigo-50 font-medium text-indigo-700"
|
|
172
|
+
: "text-gray-700 hover:bg-gray-50"
|
|
173
|
+
}`}
|
|
174
|
+
onClick={() => {
|
|
175
|
+
setShowCustom(true);
|
|
176
|
+
}}
|
|
177
|
+
>
|
|
178
|
+
Custom Range...
|
|
179
|
+
</button>
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
{/* Custom date inputs */}
|
|
183
|
+
{showCustom && (
|
|
184
|
+
<div className="border-t border-gray-100 p-3">
|
|
185
|
+
<StartAndEndDate
|
|
186
|
+
type={StartAndEndDateType.DateTime}
|
|
187
|
+
value={props.value.startAndEndDate}
|
|
188
|
+
hideTimeButtons={true}
|
|
189
|
+
onValueChanged={handleCustomDateChange}
|
|
190
|
+
/>
|
|
191
|
+
</div>
|
|
192
|
+
)}
|
|
193
|
+
</div>
|
|
194
|
+
)}
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
export default LogTimeRangePicker;
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import React, { FunctionComponent, ReactElement, useMemo } from "react";
|
|
2
|
+
import { FacetData, FacetValue, ActiveFilter } from "../types";
|
|
3
|
+
import FacetSection from "./FacetSection";
|
|
4
|
+
import Service from "../../../../Models/DatabaseModels/Service";
|
|
5
|
+
import Dictionary from "../../../../Types/Dictionary";
|
|
6
|
+
import ComponentLoader from "../../ComponentLoader/ComponentLoader";
|
|
7
|
+
import { getSeverityColor } from "./severityColors";
|
|
8
|
+
import LogSeverity from "../../../../Types/Log/LogSeverity";
|
|
9
|
+
|
|
10
|
+
export interface LogsFacetSidebarProps {
|
|
11
|
+
facetData: FacetData;
|
|
12
|
+
isLoading: boolean;
|
|
13
|
+
serviceMap: Dictionary<Service>;
|
|
14
|
+
onIncludeFilter: (facetKey: string, value: string) => void;
|
|
15
|
+
onExcludeFilter: (facetKey: string, value: string) => void;
|
|
16
|
+
activeFilters?: Array<ActiveFilter> | undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const SEVERITY_ORDER: Array<string> = [
|
|
20
|
+
LogSeverity.Fatal,
|
|
21
|
+
LogSeverity.Error,
|
|
22
|
+
LogSeverity.Warning,
|
|
23
|
+
LogSeverity.Information,
|
|
24
|
+
LogSeverity.Debug,
|
|
25
|
+
LogSeverity.Trace,
|
|
26
|
+
LogSeverity.Unspecified,
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function buildSeverityColorMap(): Record<string, string> {
|
|
30
|
+
const map: Record<string, string> = {};
|
|
31
|
+
|
|
32
|
+
for (const severity of SEVERITY_ORDER) {
|
|
33
|
+
map[severity] = getSeverityColor(severity).fill;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return map;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildServiceDisplayMap(
|
|
40
|
+
serviceMap: Dictionary<Service>,
|
|
41
|
+
): Record<string, string> {
|
|
42
|
+
const map: Record<string, string> = {};
|
|
43
|
+
|
|
44
|
+
for (const [id, service] of Object.entries(serviceMap)) {
|
|
45
|
+
if (service?.name) {
|
|
46
|
+
map[id] = service.name;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return map;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function buildServiceColorMap(
|
|
54
|
+
serviceMap: Dictionary<Service>,
|
|
55
|
+
): Record<string, string> {
|
|
56
|
+
const map: Record<string, string> = {};
|
|
57
|
+
|
|
58
|
+
for (const [id, service] of Object.entries(serviceMap)) {
|
|
59
|
+
if (service?.serviceColor) {
|
|
60
|
+
map[id] = service.serviceColor.toString();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return map;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getFacetTitle(key: string): string {
|
|
68
|
+
const titleMap: Record<string, string> = {
|
|
69
|
+
severityText: "Severity",
|
|
70
|
+
serviceId: "Service",
|
|
71
|
+
traceId: "Trace ID",
|
|
72
|
+
spanId: "Span ID",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return titleMap[key] || key;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const LogsFacetSidebar: FunctionComponent<LogsFacetSidebarProps> = (
|
|
79
|
+
props: LogsFacetSidebarProps,
|
|
80
|
+
): ReactElement => {
|
|
81
|
+
const severityColorMap: Record<string, string> = useMemo(() => {
|
|
82
|
+
return buildSeverityColorMap();
|
|
83
|
+
}, []);
|
|
84
|
+
|
|
85
|
+
const serviceDisplayMap: Record<string, string> = useMemo(() => {
|
|
86
|
+
return buildServiceDisplayMap(props.serviceMap);
|
|
87
|
+
}, [props.serviceMap]);
|
|
88
|
+
|
|
89
|
+
const serviceColorMap: Record<string, string> = useMemo(() => {
|
|
90
|
+
return buildServiceColorMap(props.serviceMap);
|
|
91
|
+
}, [props.serviceMap]);
|
|
92
|
+
|
|
93
|
+
const facetKeys: Array<string> = useMemo(() => {
|
|
94
|
+
const priorityKeys: Array<string> = ["severityText", "serviceId"];
|
|
95
|
+
const otherKeys: Array<string> = Object.keys(props.facetData).filter(
|
|
96
|
+
(key: string) => {
|
|
97
|
+
return !priorityKeys.includes(key);
|
|
98
|
+
},
|
|
99
|
+
);
|
|
100
|
+
return [
|
|
101
|
+
...priorityKeys.filter((key: string) => {
|
|
102
|
+
return props.facetData[key] !== undefined;
|
|
103
|
+
}),
|
|
104
|
+
...otherKeys.sort(),
|
|
105
|
+
];
|
|
106
|
+
}, [props.facetData]);
|
|
107
|
+
|
|
108
|
+
// Build a map of facetKey -> Set<activeValue>
|
|
109
|
+
const activeValuesByKey: Record<string, Set<string>> = useMemo(() => {
|
|
110
|
+
const map: Record<string, Set<string>> = {};
|
|
111
|
+
|
|
112
|
+
if (props.activeFilters) {
|
|
113
|
+
for (const filter of props.activeFilters) {
|
|
114
|
+
if (!map[filter.facetKey]) {
|
|
115
|
+
map[filter.facetKey] = new Set<string>();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
map[filter.facetKey]!.add(filter.value);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return map;
|
|
123
|
+
}, [props.activeFilters]);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="flex h-full w-56 flex-none flex-col overflow-y-auto rounded-lg border border-gray-200 bg-white">
|
|
127
|
+
<div className="border-b border-gray-100 px-3 py-2.5">
|
|
128
|
+
<h3 className="text-[11px] font-semibold uppercase tracking-widest text-gray-400">
|
|
129
|
+
Filters
|
|
130
|
+
</h3>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
{props.isLoading && Object.keys(props.facetData).length === 0 && (
|
|
134
|
+
<div className="flex flex-1 items-center justify-center py-8">
|
|
135
|
+
<ComponentLoader />
|
|
136
|
+
</div>
|
|
137
|
+
)}
|
|
138
|
+
|
|
139
|
+
<div className="flex-1 overflow-y-auto">
|
|
140
|
+
{facetKeys.map((key: string) => {
|
|
141
|
+
const values: Array<FacetValue> = props.facetData[key] || [];
|
|
142
|
+
|
|
143
|
+
let valueDisplayMap: Record<string, string> | undefined;
|
|
144
|
+
let valueColorMap: Record<string, string> | undefined;
|
|
145
|
+
|
|
146
|
+
if (key === "serviceId") {
|
|
147
|
+
valueDisplayMap = serviceDisplayMap;
|
|
148
|
+
valueColorMap = serviceColorMap;
|
|
149
|
+
} else if (key === "severityText") {
|
|
150
|
+
valueColorMap = severityColorMap;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<FacetSection
|
|
155
|
+
key={key}
|
|
156
|
+
facetKey={key}
|
|
157
|
+
title={getFacetTitle(key)}
|
|
158
|
+
values={values}
|
|
159
|
+
onIncludeValue={props.onIncludeFilter}
|
|
160
|
+
onExcludeValue={props.onExcludeFilter}
|
|
161
|
+
valueDisplayMap={valueDisplayMap}
|
|
162
|
+
valueColorMap={valueColorMap}
|
|
163
|
+
activeValues={activeValuesByKey[key]}
|
|
164
|
+
/>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default LogsFacetSidebar;
|
|
@@ -1,73 +1,43 @@
|
|
|
1
1
|
import React, { FunctionComponent, ReactElement, ReactNode } from "react";
|
|
2
|
-
import
|
|
3
|
-
import FiltersForm from "../../Filters/FiltersForm";
|
|
4
|
-
import FieldType from "../../Types/FieldType";
|
|
5
|
-
import DropdownUtil from "../../../Utils/Dropdown";
|
|
6
|
-
import LogSeverity from "../../../../Types/Log/LogSeverity";
|
|
7
|
-
import Query from "../../../../Types/BaseDatabase/Query";
|
|
8
|
-
import Log from "../../../../Models/AnalyticsModels/Log";
|
|
2
|
+
import LogSearchBar from "./LogSearchBar";
|
|
9
3
|
|
|
10
4
|
export interface LogsFilterCardProps {
|
|
11
|
-
filterData: Query<Log>;
|
|
12
|
-
onFilterChanged: (filterData: Query<Log>) => void;
|
|
13
|
-
onAdvancedFiltersToggle: (show: boolean) => void;
|
|
14
|
-
isFilterLoading: boolean;
|
|
15
|
-
filterError?: string | undefined;
|
|
16
|
-
onFilterRefreshClick?: (() => void) | undefined;
|
|
17
5
|
logAttributes: Array<string>;
|
|
18
6
|
toolbar: ReactNode;
|
|
7
|
+
searchQuery: string;
|
|
8
|
+
onSearchQueryChange: (query: string) => void;
|
|
9
|
+
onSearchSubmit: () => void;
|
|
10
|
+
valueSuggestions?: Record<string, Array<string>> | undefined;
|
|
11
|
+
onFieldValueSelect?: ((fieldKey: string, value: string) => void) | undefined;
|
|
19
12
|
}
|
|
20
13
|
|
|
21
14
|
const LogsFilterCard: FunctionComponent<LogsFilterCardProps> = (
|
|
22
15
|
props: LogsFilterCardProps,
|
|
23
16
|
): ReactElement => {
|
|
17
|
+
const searchBarSuggestions: Array<string> = [
|
|
18
|
+
"severity",
|
|
19
|
+
"service",
|
|
20
|
+
"trace",
|
|
21
|
+
"span",
|
|
22
|
+
...props.logAttributes.map((attr: string) => {
|
|
23
|
+
return `@${attr}`;
|
|
24
|
+
}),
|
|
25
|
+
];
|
|
26
|
+
|
|
24
27
|
return (
|
|
25
|
-
<
|
|
26
|
-
<div className="-
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
filterError={props.filterError}
|
|
35
|
-
onFilterRefreshClick={props.onFilterRefreshClick}
|
|
36
|
-
filters={[
|
|
37
|
-
{
|
|
38
|
-
key: "body",
|
|
39
|
-
type: FieldType.Text,
|
|
40
|
-
title: "Search Log",
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
key: "severityText",
|
|
44
|
-
filterDropdownOptions:
|
|
45
|
-
DropdownUtil.getDropdownOptionsFromEnum(LogSeverity),
|
|
46
|
-
type: FieldType.Dropdown,
|
|
47
|
-
title: "Log Severity",
|
|
48
|
-
isAdvancedFilter: true,
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
key: "time",
|
|
52
|
-
type: FieldType.DateTime,
|
|
53
|
-
title: "Start and End Date",
|
|
54
|
-
isAdvancedFilter: true,
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
key: "attributes",
|
|
58
|
-
type: FieldType.JSON,
|
|
59
|
-
title: "Filter by Attributes",
|
|
60
|
-
jsonKeys: props.logAttributes,
|
|
61
|
-
isAdvancedFilter: true,
|
|
62
|
-
},
|
|
63
|
-
]}
|
|
28
|
+
<div className="flex items-start gap-3">
|
|
29
|
+
<div className="min-w-0 flex-1">
|
|
30
|
+
<LogSearchBar
|
|
31
|
+
value={props.searchQuery}
|
|
32
|
+
onChange={props.onSearchQueryChange}
|
|
33
|
+
onSubmit={props.onSearchSubmit}
|
|
34
|
+
suggestions={searchBarSuggestions}
|
|
35
|
+
valueSuggestions={props.valueSuggestions}
|
|
36
|
+
onFieldValueSelect={props.onFieldValueSelect}
|
|
64
37
|
/>
|
|
65
38
|
</div>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
{props.toolbar}
|
|
69
|
-
</div>
|
|
70
|
-
</Card>
|
|
39
|
+
<div className="flex-none pt-0.5">{props.toolbar}</div>
|
|
40
|
+
</div>
|
|
71
41
|
);
|
|
72
42
|
};
|
|
73
43
|
|