@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.
Files changed (95) hide show
  1. package/Server/API/TelemetryAPI.ts +208 -0
  2. package/Server/API/UserCallAPI.ts +29 -0
  3. package/Server/API/UserEmailAPI.ts +29 -0
  4. package/Server/API/UserSmsAPI.ts +29 -0
  5. package/Server/API/UserWhatsAppAPI.ts +29 -0
  6. package/Server/Services/LogAggregationService.ts +251 -0
  7. package/Server/Types/AnalyticsDatabase/ModelPermission.ts +2 -2
  8. package/Server/Types/Database/Permissions/TenantPermission.ts +2 -2
  9. package/Server/Utils/VM/VMRunner.ts +10 -0
  10. package/Types/Log/LogQueryParser.ts +252 -0
  11. package/Types/Log/LogQueryToFilter.ts +131 -0
  12. package/UI/Components/CopyTextButton/CopyTextButton.tsx +3 -3
  13. package/UI/Components/LogsViewer/LogsViewer.tsx +166 -93
  14. package/UI/Components/LogsViewer/components/ActiveFilterChips.tsx +58 -0
  15. package/UI/Components/LogsViewer/components/FacetSection.tsx +119 -0
  16. package/UI/Components/LogsViewer/components/FacetValueRow.tsx +102 -0
  17. package/UI/Components/LogsViewer/components/HistogramTooltip.tsx +122 -0
  18. package/UI/Components/LogsViewer/components/LiveLogsToggle.tsx +4 -4
  19. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +22 -26
  20. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +360 -0
  21. package/UI/Components/LogsViewer/components/LogSearchHelp.tsx +128 -0
  22. package/UI/Components/LogsViewer/components/LogSearchSuggestions.tsx +64 -0
  23. package/UI/Components/LogsViewer/components/LogTimeRangePicker.tsx +199 -0
  24. package/UI/Components/LogsViewer/components/LogsFacetSidebar.tsx +172 -0
  25. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +27 -57
  26. package/UI/Components/LogsViewer/components/LogsHistogram.tsx +268 -0
  27. package/UI/Components/LogsViewer/components/LogsPagination.tsx +12 -10
  28. package/UI/Components/LogsViewer/components/LogsTable.tsx +33 -32
  29. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +16 -18
  30. package/UI/Components/LogsViewer/components/severityColors.ts +31 -0
  31. package/UI/Components/LogsViewer/components/severityTheme.ts +25 -25
  32. package/UI/Components/LogsViewer/types.ts +20 -0
  33. package/build/dist/Server/API/TelemetryAPI.js +136 -0
  34. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  35. package/build/dist/Server/API/UserCallAPI.js +17 -0
  36. package/build/dist/Server/API/UserCallAPI.js.map +1 -1
  37. package/build/dist/Server/API/UserEmailAPI.js +17 -0
  38. package/build/dist/Server/API/UserEmailAPI.js.map +1 -1
  39. package/build/dist/Server/API/UserSmsAPI.js +17 -0
  40. package/build/dist/Server/API/UserSmsAPI.js.map +1 -1
  41. package/build/dist/Server/API/UserWhatsAppAPI.js +17 -0
  42. package/build/dist/Server/API/UserWhatsAppAPI.js.map +1 -1
  43. package/build/dist/Server/Services/LogAggregationService.js +163 -0
  44. package/build/dist/Server/Services/LogAggregationService.js.map +1 -0
  45. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js +2 -2
  46. package/build/dist/Server/Types/AnalyticsDatabase/ModelPermission.js.map +1 -1
  47. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js +2 -2
  48. package/build/dist/Server/Types/Database/Permissions/TenantPermission.js.map +1 -1
  49. package/build/dist/Server/Utils/VM/VMRunner.js +10 -0
  50. package/build/dist/Server/Utils/VM/VMRunner.js.map +1 -1
  51. package/build/dist/Types/Log/LogQueryParser.js +200 -0
  52. package/build/dist/Types/Log/LogQueryParser.js.map +1 -0
  53. package/build/dist/Types/Log/LogQueryToFilter.js +96 -0
  54. package/build/dist/Types/Log/LogQueryToFilter.js.map +1 -0
  55. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js +3 -3
  56. package/build/dist/UI/Components/CopyTextButton/CopyTextButton.js.map +1 -1
  57. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +64 -42
  58. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  59. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js +24 -0
  60. package/build/dist/UI/Components/LogsViewer/components/ActiveFilterChips.js.map +1 -0
  61. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js +46 -0
  62. package/build/dist/UI/Components/LogsViewer/components/FacetSection.js.map +1 -0
  63. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js +35 -0
  64. package/build/dist/UI/Components/LogsViewer/components/FacetValueRow.js.map +1 -0
  65. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js +64 -0
  66. package/build/dist/UI/Components/LogsViewer/components/HistogramTooltip.js.map +1 -0
  67. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js +4 -4
  68. package/build/dist/UI/Components/LogsViewer/components/LiveLogsToggle.js.map +1 -1
  69. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +19 -21
  70. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  71. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +230 -0
  72. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -0
  73. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js +84 -0
  74. package/build/dist/UI/Components/LogsViewer/components/LogSearchHelp.js.map +1 -0
  75. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js +27 -0
  76. package/build/dist/UI/Components/LogsViewer/components/LogSearchSuggestions.js.map +1 -0
  77. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js +100 -0
  78. package/build/dist/UI/Components/LogsViewer/components/LogTimeRangePicker.js.map +1 -0
  79. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js +104 -0
  80. package/build/dist/UI/Components/LogsViewer/components/LogsFacetSidebar.js.map +1 -0
  81. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +14 -35
  82. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  83. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js +127 -0
  84. package/build/dist/UI/Components/LogsViewer/components/LogsHistogram.js.map +1 -0
  85. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js +9 -9
  86. package/build/dist/UI/Components/LogsViewer/components/LogsPagination.js.map +1 -1
  87. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +31 -30
  88. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  89. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +7 -8
  90. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  91. package/build/dist/UI/Components/LogsViewer/components/severityColors.js +22 -0
  92. package/build/dist/UI/Components/LogsViewer/components/severityColors.js.map +1 -0
  93. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js +25 -25
  94. package/build/dist/UI/Components/LogsViewer/components/severityTheme.js.map +1 -1
  95. 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 &quot;timeout&quot;
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 Card from "../../Card/Card";
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
- <Card>
26
- <div className="-mt-8">
27
- <FiltersForm<Log>
28
- id="logs-filter"
29
- showFilter={true}
30
- filterData={props.filterData}
31
- onFilterChanged={props.onFilterChanged}
32
- onAdvancedFiltersToggle={props.onAdvancedFiltersToggle}
33
- isFilterLoading={props.isFilterLoading}
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
- <div className="-mx-6 -mb-6 border-t border-slate-200 bg-white/60 px-6 py-3 backdrop-blur">
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