@oneuptime/common 10.0.31 → 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.
- package/Models/AnalyticsModels/ExceptionInstance.ts +29 -4
- package/Models/AnalyticsModels/Log.ts +110 -4
- package/Models/AnalyticsModels/Metric.ts +16 -9
- package/Models/AnalyticsModels/MonitorLog.ts +4 -2
- package/Models/AnalyticsModels/Span.ts +79 -6
- package/Models/DatabaseModels/Index.ts +8 -0
- package/Models/DatabaseModels/LogDropFilter.ts +480 -0
- package/Models/DatabaseModels/LogPipeline.ts +412 -0
- package/Models/DatabaseModels/LogPipelineProcessor.ts +430 -0
- package/Models/DatabaseModels/LogScrubRule.ts +516 -0
- package/Server/API/TelemetryAPI.ts +261 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.ts +131 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.ts +79 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.ts +41 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.ts +57 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +4 -0
- package/Server/Services/AnalyticsDatabaseService.ts +61 -0
- package/Server/Services/LogAggregationService.ts +238 -1
- package/Server/Services/LogDropFilterService.ts +10 -0
- package/Server/Services/LogPipelineProcessorService.ts +10 -0
- package/Server/Services/LogPipelineService.ts +10 -0
- package/Server/Services/LogScrubRuleService.ts +10 -0
- package/Server/Services/TelemetryAttributeService.ts +4 -6
- package/Server/Utils/AnalyticsDatabase/Statement.ts +15 -1
- package/Server/Utils/AnalyticsDatabase/StatementGenerator.ts +126 -11
- package/Tests/Server/Services/LogAggregationService.test.ts +3 -2
- package/Types/AnalyticsDatabase/AnalyticsTableName.ts +9 -0
- package/Types/AnalyticsDatabase/TableColumnType.ts +4 -0
- package/Types/Date.ts +22 -0
- package/Types/Log/LogDropFilterAction.ts +6 -0
- package/Types/Log/LogPipelineProcessorType.ts +44 -0
- package/Types/Log/LogScrubAction.ts +7 -0
- package/Types/Log/LogScrubPatternType.ts +10 -0
- package/Types/Permission.ts +174 -0
- package/UI/Components/LogsViewer/LogsViewer.tsx +152 -4
- package/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.tsx +92 -0
- package/UI/Components/LogsViewer/components/LogDetailsPanel.tsx +332 -117
- package/UI/Components/LogsViewer/components/LogSearchBar.tsx +294 -274
- package/UI/Components/LogsViewer/components/LogsAnalyticsView.tsx +513 -234
- package/UI/Components/LogsViewer/components/LogsFilterCard.tsx +37 -29
- package/UI/Components/LogsViewer/components/LogsTable.tsx +6 -1
- package/UI/Components/LogsViewer/components/LogsViewerToolbar.tsx +106 -0
- package/UI/Utils/LogExport.ts +160 -0
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js +28 -4
- package/build/dist/Models/AnalyticsModels/ExceptionInstance.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Log.js +97 -4
- package/build/dist/Models/AnalyticsModels/Log.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Metric.js +16 -9
- package/build/dist/Models/AnalyticsModels/Metric.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/MonitorLog.js +4 -2
- package/build/dist/Models/AnalyticsModels/MonitorLog.js.map +1 -1
- package/build/dist/Models/AnalyticsModels/Span.js +73 -6
- package/build/dist/Models/AnalyticsModels/Span.js.map +1 -1
- package/build/dist/Models/DatabaseModels/Index.js +8 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/LogDropFilter.js +508 -0
- package/build/dist/Models/DatabaseModels/LogDropFilter.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LogPipeline.js +438 -0
- package/build/dist/Models/DatabaseModels/LogPipeline.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js +452 -0
- package/build/dist/Models/DatabaseModels/LogPipelineProcessor.js.map +1 -0
- package/build/dist/Models/DatabaseModels/LogScrubRule.js +545 -0
- package/build/dist/Models/DatabaseModels/LogScrubRule.js.map +1 -0
- package/build/dist/Server/API/TelemetryAPI.js +155 -0
- package/build/dist/Server/API/TelemetryAPI.js.map +1 -1
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js +52 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773402621107-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js +34 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773414578773-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js +22 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773500000000-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js +26 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1773676206197-MigrationName.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +4 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/AnalyticsDatabaseService.js +30 -0
- package/build/dist/Server/Services/AnalyticsDatabaseService.js.map +1 -1
- package/build/dist/Server/Services/LogAggregationService.js +188 -1
- package/build/dist/Server/Services/LogAggregationService.js.map +1 -1
- package/build/dist/Server/Services/LogDropFilterService.js +9 -0
- package/build/dist/Server/Services/LogDropFilterService.js.map +1 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js +9 -0
- package/build/dist/Server/Services/LogPipelineProcessorService.js.map +1 -0
- package/build/dist/Server/Services/LogPipelineService.js +9 -0
- package/build/dist/Server/Services/LogPipelineService.js.map +1 -0
- package/build/dist/Server/Services/LogScrubRuleService.js +9 -0
- package/build/dist/Server/Services/LogScrubRuleService.js.map +1 -0
- package/build/dist/Server/Services/TelemetryAttributeService.js +4 -6
- package/build/dist/Server/Services/TelemetryAttributeService.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js +13 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/Statement.js.map +1 -1
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js +89 -2
- package/build/dist/Server/Utils/AnalyticsDatabase/StatementGenerator.js.map +1 -1
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js +3 -2
- package/build/dist/Tests/Server/Services/LogAggregationService.test.js.map +1 -1
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js +10 -0
- package/build/dist/Types/AnalyticsDatabase/AnalyticsTableName.js.map +1 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumnType.js +4 -0
- package/build/dist/Types/AnalyticsDatabase/TableColumnType.js.map +1 -1
- package/build/dist/Types/Date.js +16 -0
- package/build/dist/Types/Date.js.map +1 -1
- package/build/dist/Types/Log/LogDropFilterAction.js +7 -0
- package/build/dist/Types/Log/LogDropFilterAction.js.map +1 -0
- package/build/dist/Types/Log/LogPipelineProcessorType.js +9 -0
- package/build/dist/Types/Log/LogPipelineProcessorType.js.map +1 -0
- package/build/dist/Types/Log/LogScrubAction.js +8 -0
- package/build/dist/Types/Log/LogScrubAction.js.map +1 -0
- package/build/dist/Types/Log/LogScrubPatternType.js +11 -0
- package/build/dist/Types/Log/LogScrubPatternType.js.map +1 -0
- package/build/dist/Types/Permission.js +152 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js +124 -11
- package/build/dist/UI/Components/LogsViewer/LogsViewer.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js +36 -0
- package/build/dist/UI/Components/LogsViewer/components/KeyboardShortcutsHelp.js.map +1 -0
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js +114 -4
- package/build/dist/UI/Components/LogsViewer/components/LogDetailsPanel.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js +17 -5
- package/build/dist/UI/Components/LogsViewer/components/LogSearchBar.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js +229 -122
- package/build/dist/UI/Components/LogsViewer/components/LogsAnalyticsView.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js +5 -4
- package/build/dist/UI/Components/LogsViewer/components/LogsFilterCard.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js +4 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsTable.js.map +1 -1
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js +28 -0
- package/build/dist/UI/Components/LogsViewer/components/LogsViewerToolbar.js.map +1 -1
- package/build/dist/UI/Utils/LogExport.js +129 -0
- package/build/dist/UI/Utils/LogExport.js.map +1 -0
- 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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
222
|
+
// Field name mode: append colon
|
|
223
|
+
const parts: Array<string> = props.value.split(/\s+/);
|
|
161
224
|
|
|
162
|
-
if (
|
|
163
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
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;
|