@oneuptime/common 10.0.30 → 10.0.33

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 (130) hide show
  1. package/Models/AnalyticsModels/ExceptionInstance.ts +29 -4
  2. package/Models/AnalyticsModels/Log.ts +110 -4
  3. package/Models/AnalyticsModels/Metric.ts +16 -9
  4. package/Models/AnalyticsModels/MonitorLog.ts +4 -2
  5. package/Models/AnalyticsModels/Span.ts +79 -6
  6. package/Models/DatabaseModels/Index.ts +8 -0
  7. package/Models/DatabaseModels/LogDropFilter.ts +480 -0
  8. package/Models/DatabaseModels/LogPipeline.ts +412 -0
  9. package/Models/DatabaseModels/LogPipelineProcessor.ts +430 -0
  10. package/Models/DatabaseModels/LogScrubRule.ts +516 -0
  11. package/Server/API/TelemetryAPI.ts +261 -0
  12. package/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.ts +131 -0
  13. package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +79 -0
  14. package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +41 -0
  15. package/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.ts +57 -0
  16. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
  17. package/Server/Services/AnalyticsDatabaseService.ts +61 -0
  18. package/Server/Services/LogAggregationService.ts +238 -1
  19. package/Server/Services/LogDropFilterService.ts +10 -0
  20. package/Server/Services/LogPipelineProcessorService.ts +10 -0
  21. package/Server/Services/LogPipelineService.ts +10 -0
  22. package/Server/Services/LogScrubRuleService.ts +10 -0
  23. package/Server/Services/TelemetryAttributeService.ts +4 -6
  24. package/Server/Utils/AnalyticsDatabase/Statement.ts +15 -1
  25. package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +126 -11
  26. package/Tests/Server/Services/LogAggregationService.test.ts +3 -2
  27. package/Types/AnalyticsDatabase/AnalyticsTableName.ts +9 -0
  28. package/Types/AnalyticsDatabase/TableColumnType.ts +4 -0
  29. package/Types/Date.ts +22 -0
  30. package/Types/Log/LogDropFilterAction.ts +6 -0
  31. package/Types/Log/LogPipelineProcessorType.ts +44 -0
  32. package/Types/Log/LogScrubAction.ts +7 -0
  33. package/Types/Log/LogScrubPatternType.ts +10 -0
  34. package/Types/Permission.ts +174 -0
  35. package/UI/Components/LogsViewer/LogsViewer.tsx +152 -4
  36. package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +92 -0
  37. package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +332 -117
  38. package/UI/Components/LogsViewer/components/LogSearchBar.tsx +294 -274
  39. package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +513 -234
  40. package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +37 -29
  41. package/UI/Components/LogsViewer/components/LogsTable.tsx +6 -1
  42. package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +106 -0
  43. package/UI/Utils/LogExport.ts +160 -0
  44. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +28 -4
  45. package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
  46. package/build/dist/Models/AnalyticsModels/Log.js +97 -4
  47. package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
  48. package/build/dist/Models/AnalyticsModels/Metric.js +16 -9
  49. package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
  50. package/build/dist/Models/AnalyticsModels/MonitorLog.js +4 -2
  51. package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
  52. package/build/dist/Models/AnalyticsModels/Span.js +73 -6
  53. package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
  54. package/build/dist/Models/DatabaseModels/Index.js +8 -0
  55. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  56. package/build/dist/Models/DatabaseModels/LogDropFilter.js +508 -0
  57. package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -0
  58. package/build/dist/Models/DatabaseModels/LogPipeline.js +438 -0
  59. package/build/dist/Models/DatabaseModels/LogPipeline.js.map +1 -0
  60. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js +452 -0
  61. package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js.map +1 -0
  62. package/build/dist/Models/DatabaseModels/LogScrubRule.js +545 -0
  63. package/build/dist/Models/DatabaseModels/LogScrubRule.js.map +1 -0
  64. package/build/dist/Server/API/TelemetryAPI.js +155 -0
  65. package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
  66. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js +52 -0
  67. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js.map +1 -0
  68. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +34 -0
  69. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +1 -0
  70. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +22 -0
  71. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +1 -0
  72. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js +26 -0
  73. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js.map +1 -0
  74. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
  75. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  76. package/build/dist/Server/Services/AnalyticsDatabaseService.js +30 -0
  77. package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
  78. package/build/dist/Server/Services/LogAggregationService.js +188 -1
  79. package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
  80. package/build/dist/Server/Services/LogDropFilterService.js +9 -0
  81. package/build/dist/Server/Services/LogDropFilterService.js.map +1 -0
  82. package/build/dist/Server/Services/LogPipelineProcessorService.js +9 -0
  83. package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -0
  84. package/build/dist/Server/Services/LogPipelineService.js +9 -0
  85. package/build/dist/Server/Services/LogPipelineService.js.map +1 -0
  86. package/build/dist/Server/Services/LogScrubRuleService.js +9 -0
  87. package/build/dist/Server/Services/LogScrubRuleService.js.map +1 -0
  88. package/build/dist/Server/Services/TelemetryAttributeService.js +4 -6
  89. package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
  90. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +13 -1
  91. package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
  92. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +89 -2
  93. package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
  94. package/build/dist/Tests/Server/Services/LogAggregationService.test.js +3 -2
  95. package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
  96. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +10 -0
  97. package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -0
  98. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js +4 -0
  99. package/build/dist/Types/AnalyticsDatabase/TableColumnType.js.map +1 -1
  100. package/build/dist/Types/Date.js +16 -0
  101. package/build/dist/Types/Date.js.map +1 -1
  102. package/build/dist/Types/Log/LogDropFilterAction.js +7 -0
  103. package/build/dist/Types/Log/LogDropFilterAction.js.map +1 -0
  104. package/build/dist/Types/Log/LogPipelineProcessorType.js +9 -0
  105. package/build/dist/Types/Log/LogPipelineProcessorType.js.map +1 -0
  106. package/build/dist/Types/Log/LogScrubAction.js +8 -0
  107. package/build/dist/Types/Log/LogScrubAction.js.map +1 -0
  108. package/build/dist/Types/Log/LogScrubPatternType.js +11 -0
  109. package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -0
  110. package/build/dist/Types/Permission.js +152 -0
  111. package/build/dist/Types/Permission.js.map +1 -1
  112. package/build/dist/UI/Components/LogsViewer/LogsViewer.js +124 -11
  113. package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
  114. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +36 -0
  115. package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -0
  116. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +114 -4
  117. package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
  118. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +17 -5
  119. package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
  120. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +229 -122
  121. package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
  122. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +5 -4
  123. package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
  124. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +4 -1
  125. package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
  126. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +28 -0
  127. package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
  128. package/build/dist/UI/Utils/LogExport.js +129 -0
  129. package/build/dist/UI/Utils/LogExport.js.map +1 -0
  130. package/package.json +1 -1
@@ -1,10 +1,11 @@
1
1
  import React, {
2
- FunctionComponent,
3
2
  ReactElement,
4
3
  useState,
5
4
  useCallback,
6
5
  useRef,
7
6
  useEffect,
7
+ useImperativeHandle,
8
+ forwardRef,
8
9
  KeyboardEvent,
9
10
  } from "react";
10
11
  import Icon from "../../Icon/Icon";
@@ -22,302 +23,319 @@ export interface LogSearchBarProps {
22
23
  placeholder?: string | undefined;
23
24
  }
24
25
 
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
- }
26
+ export interface LogSearchBarRef {
27
+ focus: () => void;
28
+ }
106
29
 
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);
30
+ const LogSearchBar: React.ForwardRefExoticComponent<
31
+ LogSearchBarProps & React.RefAttributes<LogSearchBarRef>
32
+ > = forwardRef<LogSearchBarRef, LogSearchBarProps>(
33
+ (props: LogSearchBarProps, ref: React.Ref<LogSearchBarRef>): ReactElement => {
34
+ const [isFocused, setIsFocused] = useState<boolean>(false);
35
+ const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
36
+ const [showHelp, setShowHelp] = useState<boolean>(false);
37
+ const [selectedSuggestionIndex, setSelectedSuggestionIndex] =
38
+ useState<number>(-1);
39
+ const inputRef: React.RefObject<HTMLInputElement> =
40
+ useRef<HTMLInputElement>(null!);
41
+ const containerRef: React.RefObject<HTMLDivElement> =
42
+ useRef<HTMLDivElement>(null!);
43
+
44
+ useImperativeHandle(ref, () => {
45
+ return {
46
+ focus: (): void => {
47
+ inputRef.current?.focus();
48
+ },
49
+ };
50
+ }, []);
51
+
52
+ const currentWord: string = extractCurrentWord(props.value);
53
+
54
+ // Strip leading "@" — treat it as a trigger character for suggestions
55
+ const hasAtPrefix: boolean = currentWord.startsWith("@");
56
+ const normalizedWord: string = hasAtPrefix
57
+ ? currentWord.substring(1)
58
+ : currentWord;
59
+
60
+ // Determine if we're in "field:value" mode or "field name" mode
61
+ const colonIndex: number = normalizedWord.indexOf(":");
62
+ const isValueMode: boolean = colonIndex > 0;
63
+ const fieldPrefix: string = isValueMode
64
+ ? normalizedWord.substring(0, colonIndex).toLowerCase()
65
+ : "";
66
+ const partialValue: string = isValueMode
67
+ ? normalizedWord.substring(colonIndex + 1)
68
+ : "";
69
+
70
+ const filteredSuggestions: Array<string> = isValueMode
71
+ ? getValueSuggestions(
72
+ fieldPrefix,
73
+ partialValue,
74
+ props.valueSuggestions || {},
75
+ )
76
+ : (props.suggestions || []).filter((s: string): boolean => {
77
+ if (!normalizedWord && !hasAtPrefix) {
78
+ return false;
79
+ }
80
+ // When just "@" is typed, show all suggestions
81
+ if (hasAtPrefix && normalizedWord.length === 0) {
82
+ return true;
83
+ }
84
+ // Match against the suggestion name, stripping any leading "@" from the suggestion too
85
+ const normalizedSuggestion: string = s.startsWith("@")
86
+ ? s.substring(1).toLowerCase()
87
+ : s.toLowerCase();
88
+ return normalizedSuggestion.startsWith(normalizedWord.toLowerCase());
89
+ });
90
+
91
+ const shouldShowSuggestions: boolean =
92
+ showSuggestions &&
93
+ isFocused &&
94
+ filteredSuggestions.length > 0 &&
95
+ (isValueMode ? true : currentWord.length > 0);
96
+
97
+ // Show help when focused, input is empty, and no suggestions visible
98
+ const shouldShowHelp: boolean =
99
+ showHelp &&
100
+ isFocused &&
101
+ props.value.length === 0 &&
102
+ !shouldShowSuggestions;
103
+
104
+ useEffect(() => {
105
+ setSelectedSuggestionIndex(-1);
106
+ }, [currentWord]);
107
+
108
+ const handleKeyDown: (e: KeyboardEvent<HTMLInputElement>) => void =
109
+ useCallback(
110
+ (e: KeyboardEvent<HTMLInputElement>): void => {
111
+ if (e.key === "Enter") {
112
+ if (
113
+ shouldShowSuggestions &&
114
+ selectedSuggestionIndex >= 0 &&
115
+ selectedSuggestionIndex < filteredSuggestions.length
116
+ ) {
117
+ applySuggestion(filteredSuggestions[selectedSuggestionIndex]!);
141
118
  e.preventDefault();
142
119
  return;
143
120
  }
121
+
122
+ // If in value mode with a typed value, try to match and apply as chip
123
+ if (
124
+ isValueMode &&
125
+ partialValue.length > 0 &&
126
+ props.onFieldValueSelect
127
+ ) {
128
+ // First try exact case-insensitive match from the available values
129
+ const resolvedField: string =
130
+ FIELD_ALIAS_MAP[fieldPrefix] || fieldPrefix;
131
+ const availableValues: Array<string> =
132
+ (props.valueSuggestions || {})[resolvedField] || [];
133
+ const lowerPartial: string = partialValue.toLowerCase();
134
+ const exactMatch: string | undefined = availableValues.find(
135
+ (v: string): boolean => {
136
+ return v.toLowerCase() === lowerPartial;
137
+ },
138
+ );
139
+
140
+ // Use exact match, or if there's exactly one prefix match, use that
141
+ const resolvedMatch: string | undefined =
142
+ exactMatch ||
143
+ (filteredSuggestions.length === 1
144
+ ? filteredSuggestions[0]
145
+ : undefined);
146
+
147
+ if (resolvedMatch) {
148
+ props.onFieldValueSelect(fieldPrefix, resolvedMatch);
149
+ // Remove the field:value term from text
150
+ const parts: Array<string> = props.value.split(/\s+/);
151
+ parts.pop();
152
+ const remaining: string = parts.join(" ");
153
+ props.onChange(remaining ? remaining + " " : "");
154
+ setShowSuggestions(false);
155
+ setShowHelp(false);
156
+ e.preventDefault();
157
+ return;
158
+ }
159
+ }
160
+
161
+ props.onSubmit();
162
+ setShowSuggestions(false);
163
+ setShowHelp(false);
164
+ return;
144
165
  }
145
166
 
146
- props.onSubmit();
147
- setShowSuggestions(false);
148
- setShowHelp(false);
149
- return;
150
- }
167
+ if (e.key === "Escape") {
168
+ setShowSuggestions(false);
169
+ setShowHelp(false);
170
+ return;
171
+ }
172
+
173
+ if (!shouldShowSuggestions) {
174
+ return;
175
+ }
176
+
177
+ if (e.key === "ArrowDown") {
178
+ e.preventDefault();
179
+ setSelectedSuggestionIndex((prev: number): number => {
180
+ return Math.min(prev + 1, filteredSuggestions.length - 1);
181
+ });
182
+ return;
183
+ }
184
+
185
+ if (e.key === "ArrowUp") {
186
+ e.preventDefault();
187
+ setSelectedSuggestionIndex((prev: number): number => {
188
+ return Math.max(prev - 1, 0);
189
+ });
190
+ }
191
+ },
192
+ [
193
+ shouldShowSuggestions,
194
+ selectedSuggestionIndex,
195
+ filteredSuggestions,
196
+ isValueMode,
197
+ fieldPrefix,
198
+ partialValue,
199
+ props,
200
+ ],
201
+ );
202
+
203
+ const applySuggestion: (suggestion: string) => void = useCallback(
204
+ (suggestion: string): void => {
205
+ if (isValueMode) {
206
+ // Value mode: apply as a chip via onFieldValueSelect
207
+ if (props.onFieldValueSelect) {
208
+ props.onFieldValueSelect(fieldPrefix, suggestion);
209
+ }
151
210
 
152
- if (e.key === "Escape") {
211
+ // Remove the current field:value term from the search text
212
+ const parts: Array<string> = props.value.split(/\s+/);
213
+ parts.pop(); // remove the field:partialValue
214
+ const remaining: string = parts.join(" ");
215
+ props.onChange(remaining ? remaining + " " : "");
153
216
  setShowSuggestions(false);
154
217
  setShowHelp(false);
218
+ inputRef.current?.focus();
155
219
  return;
156
220
  }
157
221
 
158
- if (!shouldShowSuggestions) {
159
- return;
160
- }
222
+ // Field name mode: append colon
223
+ const parts: Array<string> = props.value.split(/\s+/);
161
224
 
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;
225
+ if (parts.length > 0) {
226
+ parts[parts.length - 1] = suggestion + ":";
168
227
  }
169
228
 
170
- if (e.key === "ArrowUp") {
171
- e.preventDefault();
172
- setSelectedSuggestionIndex((prev: number): number => {
173
- return Math.max(prev - 1, 0);
174
- });
175
- }
229
+ props.onChange(parts.join(" "));
230
+ setShowSuggestions(false);
231
+ setShowHelp(false);
232
+ inputRef.current?.focus();
176
233
  },
177
- [
178
- shouldShowSuggestions,
179
- selectedSuggestionIndex,
180
- filteredSuggestions,
181
- isValueMode,
182
- fieldPrefix,
183
- partialValue,
184
- props,
185
- ],
234
+ [props, isValueMode, fieldPrefix],
186
235
  );
187
236
 
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);
237
+ const handleExampleClick: (example: string) => void = useCallback(
238
+ (example: string): void => {
239
+ props.onChange(example);
202
240
  setShowHelp(false);
203
241
  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();
242
+ },
243
+ [props],
244
+ );
245
+
246
+ useEffect(() => {
247
+ const handleClickOutside: (e: MouseEvent) => void = (
248
+ e: MouseEvent,
249
+ ): void => {
250
+ if (
251
+ containerRef.current &&
252
+ !containerRef.current.contains(e.target as Node)
253
+ ) {
254
+ setShowSuggestions(false);
255
+ setShowHelp(false);
256
+ }
257
+ };
258
+
259
+ document.addEventListener("mousedown", handleClickOutside);
260
+ return () => {
261
+ document.removeEventListener("mousedown", handleClickOutside);
262
+ };
263
+ }, []);
264
+
265
+ return (
266
+ <div ref={containerRef} className="relative">
267
+ <div
268
+ className={`flex items-center gap-2 rounded-lg border bg-white px-3 py-2 transition-colors ${
269
+ isFocused
270
+ ? "border-indigo-400 ring-2 ring-indigo-100"
271
+ : "border-gray-200 hover:border-gray-300"
272
+ }`}
273
+ >
274
+ <Icon
275
+ icon={IconProp.Search}
276
+ className="h-4 w-4 flex-none text-gray-400"
277
+ />
278
+ <input
279
+ ref={inputRef}
280
+ type="text"
281
+ value={props.value}
282
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
283
+ props.onChange(e.target.value);
284
+ setShowSuggestions(true);
285
+ setShowHelp(false);
286
+ }}
287
+ onFocus={() => {
288
+ setIsFocused(true);
289
+ setShowSuggestions(true);
290
+ if (props.value.length === 0) {
291
+ setShowHelp(true);
292
+ }
300
293
  }}
301
- title="Clear search"
302
- >
303
- <Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
304
- </button>
294
+ onBlur={() => {
295
+ setIsFocused(false);
296
+ }}
297
+ onKeyDown={handleKeyDown}
298
+ placeholder={
299
+ props.placeholder ||
300
+ 'Search logs... (e.g. severity:error service:api "connection refused")'
301
+ }
302
+ className="flex-1 bg-transparent font-mono text-sm text-gray-900 placeholder-gray-400 outline-none"
303
+ spellCheck={false}
304
+ autoComplete="off"
305
+ />
306
+ {props.value.length > 0 && (
307
+ <button
308
+ type="button"
309
+ className="flex-none rounded-full p-1 text-gray-400 hover:bg-gray-100"
310
+ onClick={() => {
311
+ props.onChange("");
312
+ setShowHelp(true);
313
+ setShowSuggestions(false);
314
+ inputRef.current?.focus();
315
+ }}
316
+ title="Clear search"
317
+ >
318
+ <Icon icon={IconProp.Close} className="h-3.5 w-3.5" />
319
+ </button>
320
+ )}
321
+ </div>
322
+
323
+ {shouldShowSuggestions && (
324
+ <LogSearchSuggestions
325
+ suggestions={filteredSuggestions}
326
+ selectedIndex={selectedSuggestionIndex}
327
+ onSelect={applySuggestion}
328
+ fieldContext={isValueMode ? fieldPrefix : undefined}
329
+ />
305
330
  )}
306
- </div>
307
331
 
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
- };
332
+ {shouldShowHelp && (
333
+ <LogSearchHelp onExampleClick={handleExampleClick} />
334
+ )}
335
+ </div>
336
+ );
337
+ },
338
+ );
321
339
 
322
340
  function extractCurrentWord(value: string): string {
323
341
  const parts: Array<string> = value.split(/\s+/);
@@ -357,4 +375,6 @@ function getValueSuggestions(
357
375
  });
358
376
  }
359
377
 
378
+ LogSearchBar.displayName = "LogSearchBar";
379
+
360
380
  export default LogSearchBar;