@parca/profile 0.16.0 → 0.16.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 (93) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/Callgraph/Edge/index.d.ts +22 -0
  3. package/dist/Callgraph/Edge/index.js +30 -0
  4. package/dist/Callgraph/Node/index.d.ts +19 -0
  5. package/dist/Callgraph/Node/index.js +37 -0
  6. package/dist/Callgraph/index.d.ts +8 -0
  7. package/dist/Callgraph/index.js +137 -0
  8. package/dist/Callgraph/mockData/index.d.ts +148 -0
  9. package/dist/Callgraph/mockData/index.js +577 -0
  10. package/dist/Callgraph/utils.d.ts +19 -0
  11. package/dist/Callgraph/utils.js +82 -0
  12. package/dist/GraphTooltip/index.d.ts +19 -0
  13. package/dist/GraphTooltip/index.js +119 -0
  14. package/dist/IcicleGraph.d.ts +35 -0
  15. package/dist/IcicleGraph.js +139 -0
  16. package/dist/MatchersInput/index.d.ts +23 -0
  17. package/dist/MatchersInput/index.js +479 -0
  18. package/dist/MetricsCircle/index.d.ts +7 -0
  19. package/dist/MetricsCircle/index.js +18 -0
  20. package/dist/MetricsGraph/index.d.ts +35 -0
  21. package/dist/MetricsGraph/index.js +349 -0
  22. package/dist/MetricsSeries/index.d.ts +11 -0
  23. package/dist/MetricsSeries/index.js +21 -0
  24. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +19 -0
  25. package/dist/ProfileExplorer/ProfileExplorerCompare.js +38 -0
  26. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +15 -0
  27. package/dist/ProfileExplorer/ProfileExplorerSingle.js +19 -0
  28. package/dist/ProfileExplorer/index.d.ts +9 -0
  29. package/dist/ProfileExplorer/index.js +203 -0
  30. package/dist/ProfileIcicleGraph.d.ts +10 -0
  31. package/dist/ProfileIcicleGraph.js +28 -0
  32. package/dist/ProfileMetricsGraph/index.d.ts +22 -0
  33. package/dist/ProfileMetricsGraph/index.js +127 -0
  34. package/dist/ProfileSVG.module.css +3 -0
  35. package/dist/ProfileSelector/CompareButton.d.ts +5 -0
  36. package/dist/ProfileSelector/CompareButton.js +41 -0
  37. package/dist/ProfileSelector/MergeButton.d.ts +5 -0
  38. package/dist/ProfileSelector/MergeButton.js +41 -0
  39. package/dist/ProfileSelector/index.d.ts +29 -0
  40. package/dist/ProfileSelector/index.js +133 -0
  41. package/dist/ProfileSource.d.ts +88 -0
  42. package/dist/ProfileSource.js +239 -0
  43. package/dist/ProfileTypeSelector/index.d.ts +20 -0
  44. package/dist/ProfileTypeSelector/index.js +138 -0
  45. package/dist/ProfileView.d.ts +39 -0
  46. package/dist/ProfileView.js +111 -0
  47. package/dist/ProfileView.styles.css +3 -0
  48. package/dist/ProfileViewWithData.d.ts +11 -0
  49. package/dist/ProfileViewWithData.js +116 -0
  50. package/dist/TopTable.d.ts +9 -0
  51. package/dist/TopTable.js +140 -0
  52. package/dist/TopTable.styles.css +7 -0
  53. package/dist/components/DiffLegend.d.ts +2 -0
  54. package/dist/components/DiffLegend.js +62 -0
  55. package/dist/components/ProfileShareButton/ResultBox.d.ts +6 -0
  56. package/dist/components/ProfileShareButton/ResultBox.js +46 -0
  57. package/dist/components/ProfileShareButton/index.d.ts +7 -0
  58. package/dist/components/ProfileShareButton/index.js +119 -0
  59. package/dist/index.d.ts +13 -0
  60. package/dist/index.js +64 -0
  61. package/dist/styles.css +1 -0
  62. package/dist/useDelayedLoader.d.ts +5 -0
  63. package/dist/useDelayedLoader.js +33 -0
  64. package/dist/useQuery.d.ts +13 -0
  65. package/dist/useQuery.js +41 -0
  66. package/dist/utils.d.ts +4 -0
  67. package/dist/utils.js +83 -0
  68. package/package.json +12 -8
  69. package/src/Callgraph/Edge/index.tsx +59 -0
  70. package/src/Callgraph/Node/index.tsx +66 -0
  71. package/src/Callgraph/index.tsx +169 -0
  72. package/src/Callgraph/mockData/index.ts +605 -0
  73. package/src/Callgraph/utils.ts +116 -0
  74. package/src/GraphTooltip/index.tsx +245 -0
  75. package/src/IcicleGraph.tsx +3 -3
  76. package/src/MatchersInput/index.tsx +698 -0
  77. package/src/MetricsCircle/index.tsx +28 -0
  78. package/src/MetricsGraph/index.tsx +614 -0
  79. package/src/MetricsSeries/index.tsx +38 -0
  80. package/src/ProfileExplorer/ProfileExplorerCompare.tsx +109 -0
  81. package/src/ProfileExplorer/ProfileExplorerSingle.tsx +72 -0
  82. package/src/ProfileExplorer/index.tsx +377 -0
  83. package/src/ProfileMetricsGraph/index.tsx +143 -0
  84. package/src/ProfileSelector/CompareButton.tsx +72 -0
  85. package/src/ProfileSelector/MergeButton.tsx +72 -0
  86. package/src/ProfileSelector/index.tsx +270 -0
  87. package/src/ProfileTypeSelector/index.tsx +180 -0
  88. package/src/ProfileView.tsx +2 -7
  89. package/src/index.tsx +11 -0
  90. package/src/useQuery.tsx +1 -0
  91. package/tailwind.config.js +8 -0
  92. package/tsconfig.json +7 -3
  93. package/typings.d.ts +14 -0
@@ -0,0 +1,698 @@
1
+ // Copyright 2022 The Parca Authors
2
+ // Licensed under the Apache License, Version 2.0 (the "License");
3
+ // you may not use this file except in compliance with the License.
4
+ // You may obtain a copy of the License at
5
+ //
6
+ // http://www.apache.org/licenses/LICENSE-2.0
7
+ //
8
+ // Unless required by applicable law or agreed to in writing, software
9
+ // distributed under the License is distributed on an "AS IS" BASIS,
10
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11
+ // See the License for the specific language governing permissions and
12
+ // limitations under the License.
13
+
14
+ import React, {Fragment, useState, useEffect} from 'react';
15
+ import {Transition} from '@headlessui/react';
16
+ import {Query} from '@parca/parser';
17
+ import {LabelsResponse, QueryServiceClient, ValuesResponse} from '@parca/client';
18
+ import {usePopper} from 'react-popper';
19
+ import cx from 'classnames';
20
+
21
+ import {useParcaTheme, useGrpcMetadata} from '@parca/components';
22
+
23
+ interface MatchersInputProps {
24
+ queryClient: QueryServiceClient;
25
+ setMatchersString: (arg: string) => void;
26
+ runQuery: () => void;
27
+ currentQuery: Query;
28
+ }
29
+
30
+ export interface ILabelNamesResult {
31
+ response?: LabelsResponse;
32
+ error?: Error;
33
+ }
34
+ export interface ILabelValuesResult {
35
+ response?: ValuesResponse;
36
+ error?: Error;
37
+ }
38
+
39
+ interface UseLabelNames {
40
+ result: ILabelNamesResult;
41
+ loading: boolean;
42
+ }
43
+
44
+ interface Matchers {
45
+ key: string;
46
+ matcherType: string;
47
+ value: string;
48
+ }
49
+
50
+ enum Labels {
51
+ labelName = 'labelName',
52
+ labelValue = 'labelValue',
53
+ literal = 'literal',
54
+ }
55
+
56
+ // eslint-disable-next-line no-useless-escape
57
+ const labelNameValueRe = /(^([a-z])\w+)(=~|=|!=|!~)(\")[a-zA-Z0-9_.-:]+(\")$/g; // labelNameValueRe matches the following: labelName=~"labelValue"
58
+ const labelNameValueWithoutQuotesRe = /(^([a-z])\w+)(=~|=|!=|!~)[a-zA-Z0-9_.-:]+$/g; // labelNameValueWithoutQuotesRe matches the following: labelName=~labelValue
59
+ const labelNameLiteralRe = /(^([a-z])\w+)(=~|=|!=|!~)/; // labelNameLiteralRe matches the following: labelName=~, labelName!=~, labelName=, labelName!=
60
+ const literalRe = /(=~|=|!=|!~)/; // literalRe matches the following: =~, =, !=, !~
61
+
62
+ const addQuoteMarks = (labelValue: string): string => {
63
+ // eslint-disable-next-line no-useless-escape
64
+ return `\"${labelValue}\"`;
65
+ };
66
+
67
+ export const useLabelNames = (client: QueryServiceClient): UseLabelNames => {
68
+ const [loading, setLoading] = useState(true);
69
+ const [result, setResult] = useState<ILabelNamesResult>({});
70
+ const metadata = useGrpcMetadata();
71
+
72
+ useEffect(() => {
73
+ const call = client.labels({match: []}, {meta: metadata});
74
+ setLoading(true);
75
+
76
+ call.response
77
+ .then(response => setResult({response}))
78
+ .catch(error => setResult({error}))
79
+ .finally(() => setLoading(false));
80
+ }, [client, metadata]);
81
+
82
+ return {result, loading};
83
+ };
84
+
85
+ class Suggestion {
86
+ type: string;
87
+ typeahead: string;
88
+ value: string;
89
+
90
+ constructor(type: string, typeahead: string, value: string) {
91
+ this.type = type;
92
+ this.typeahead = typeahead;
93
+ this.value = value;
94
+ }
95
+ }
96
+
97
+ class Suggestions {
98
+ literals: Suggestion[];
99
+ labelNames: Suggestion[];
100
+ labelValues: Suggestion[];
101
+
102
+ constructor() {
103
+ this.literals = [];
104
+ this.labelNames = [];
105
+ this.labelValues = [];
106
+ }
107
+ }
108
+
109
+ const MatchersInput = ({
110
+ queryClient,
111
+ setMatchersString,
112
+ runQuery,
113
+ currentQuery,
114
+ }: MatchersInputProps): JSX.Element => {
115
+ const [divInputRef, setDivInputRef] = useState<HTMLDivElement | null>(null);
116
+ const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
117
+ const [localMatchers, setLocalMatchers] = useState<Matchers[] | null>(null);
118
+ const [focusedInput, setFocusedInput] = useState(false);
119
+ const [showSuggest, setShowSuggest] = useState(true);
120
+ const [labelValuesLoading, setLabelValuesLoading] = useState(false);
121
+ const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(-1);
122
+ const [lastCompleted, setLastCompleted] = useState<Suggestion>(new Suggestion('', '', ''));
123
+ const [suggestionSections] = useState<Suggestions>(new Suggestions());
124
+ const [inputRef, setInputRef] = useState<string>('');
125
+ const [labelValuesResponse, setLabelValuesResponse] = useState<string[] | null>(null);
126
+ const [currentLabelsCollection, setCurrentLabelsCollection] = useState<string[] | null>(null); // This is an array that contains query expressions that have been matched i.e. they have been completed and have the blue badge around them in the UI.
127
+ const {styles, attributes} = usePopper(divInputRef, popperElement, {
128
+ placement: 'bottom-start',
129
+ });
130
+ const metadata = useGrpcMetadata();
131
+ const {loader: Spinner} = useParcaTheme();
132
+
133
+ const {loading: labelNamesLoading, result} = useLabelNames(queryClient);
134
+ const {response: labelNamesResponse, error: labelNamesError} = result;
135
+
136
+ const LoadingSpinner = (): JSX.Element => {
137
+ return <div className="pt-2 pb-4">{Spinner}</div>;
138
+ };
139
+
140
+ const getLabelNameValues = (labelName: string): void => {
141
+ const call = queryClient.values({labelName, match: []}, {meta: metadata});
142
+ setLabelValuesLoading(true);
143
+
144
+ call.response
145
+ .then(response => {
146
+ setLabelValuesResponse(response.labelValues);
147
+ })
148
+ .catch(() => setLabelValuesResponse(null))
149
+ .finally(() => setLabelValuesLoading(false));
150
+ };
151
+
152
+ const labelNames =
153
+ (labelNamesError === undefined || labelNamesError == null) &&
154
+ labelNamesResponse !== undefined &&
155
+ labelNamesResponse != null
156
+ ? labelNamesResponse.labelNames.filter(e => e !== '__name__')
157
+ : [];
158
+
159
+ const labelValues =
160
+ labelValuesResponse !== undefined && labelValuesResponse != null ? labelValuesResponse : [];
161
+
162
+ const value = currentQuery.matchersString();
163
+
164
+ Query.suggest(`{${value}`).forEach(function (s) {
165
+ // Skip suggestions that we just completed. This really only works,
166
+ // because we know the language is not repetitive. For a language that
167
+ // has a repeating word, this would not work.
168
+ if (lastCompleted !== null && lastCompleted.type === s.type) {
169
+ return;
170
+ }
171
+
172
+ // Need to figure out if any literal suggestions make sense, but a
173
+ // closing bracket doesn't in the guided query experience because all
174
+ // we have the user do is type the matchers.
175
+ if (s.type === Labels.literal && s.value !== '}') {
176
+ if (suggestionSections.literals.find(e => e.value === s.value) != null) {
177
+ return;
178
+ }
179
+ suggestionSections.literals.push({
180
+ type: s.type,
181
+ typeahead: '',
182
+ value: s.value,
183
+ });
184
+ suggestionSections.labelNames = [];
185
+ suggestionSections.labelValues = [];
186
+ }
187
+
188
+ if (s.type === Labels.labelName) {
189
+ const inputValue = s.typeahead.trim().toLowerCase();
190
+ const inputLength = inputValue.length;
191
+
192
+ const matches = labelNames.filter(function (label) {
193
+ return label.toLowerCase().slice(0, inputLength) === inputValue;
194
+ });
195
+
196
+ matches.forEach(m => {
197
+ if (suggestionSections.labelNames.find(e => e.value === m) != null) {
198
+ return;
199
+ }
200
+
201
+ suggestionSections.labelNames.push({
202
+ type: s.type,
203
+ typeahead: s.typeahead,
204
+ value: m,
205
+ });
206
+ suggestionSections.literals = [];
207
+ suggestionSections.labelValues = [];
208
+ });
209
+ }
210
+
211
+ if (s.type === Labels.labelValue) {
212
+ const inputValue = s.typeahead.trim().toLowerCase();
213
+ const inputLength = inputValue.length;
214
+
215
+ const matches = labelValues.filter(function (label) {
216
+ return label.toLowerCase().slice(0, inputLength) === inputValue;
217
+ });
218
+
219
+ matches.forEach(m => {
220
+ if (suggestionSections.labelValues.find(e => e.value === m) != null) {
221
+ return;
222
+ }
223
+
224
+ suggestionSections.labelValues.push({
225
+ type: s.type,
226
+ typeahead: s.typeahead,
227
+ value: m,
228
+ });
229
+ suggestionSections.labelNames = [];
230
+ suggestionSections.literals = [];
231
+ });
232
+ }
233
+ });
234
+
235
+ const suggestionsLength =
236
+ suggestionSections.literals.length +
237
+ suggestionSections.labelNames.length +
238
+ suggestionSections.labelValues.length;
239
+
240
+ const getLabelsFromMatchers = (matchers: Matchers[]): string[] => {
241
+ return matchers
242
+ .filter(matcher => matcher.key !== '__name__')
243
+ .map(matcher => `${matcher.key}${matcher.matcherType}${addQuoteMarks(matcher.value)}`);
244
+ };
245
+
246
+ useEffect(() => {
247
+ const matchers = currentQuery.matchers.filter(matcher => matcher.key !== '__name__');
248
+
249
+ if (matchers.length > 0) {
250
+ setCurrentLabelsCollection(getLabelsFromMatchers(matchers));
251
+ } else {
252
+ if (localMatchers !== null) setCurrentLabelsCollection(getLabelsFromMatchers(localMatchers));
253
+ }
254
+ // eslint-disable-next-line react-hooks/exhaustive-deps
255
+ }, [currentQuery.matchers]);
256
+
257
+ const resetHighlight = (): void => setHighlightedSuggestionIndex(-1);
258
+ const resetLastCompleted = (): void => setLastCompleted(new Suggestion('', '', ''));
259
+
260
+ const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
261
+ const newValue = e.target.value;
262
+
263
+ // filter out the labelname list and move to the top the labelname that is most similar to what the user is typing.
264
+ if (suggestionSections.labelNames.length > 0) {
265
+ suggestionSections.labelNames = suggestionSections.labelNames.filter(suggestion =>
266
+ suggestion.value.toLowerCase().includes(newValue.toLowerCase())
267
+ );
268
+ }
269
+
270
+ // this checks if the user has typed a label name and a literal (=/!=,=~,!~) i.e labelName=~, labelName!=~, labelName=, labelName!=
271
+ // and is about to type the label value, then it it will filter out the labelvalue list and move to the top
272
+ // the labelvalue that is most similar to what the user is typing.
273
+ if (suggestionSections.labelValues.length > 0 && labelNameLiteralRe.test(newValue)) {
274
+ const labelValueSearch = newValue.split(literalRe)[2];
275
+
276
+ suggestionSections.labelValues = suggestionSections.labelValues.filter(suggestion =>
277
+ suggestion.value.toLowerCase().includes(labelValueSearch.toLowerCase())
278
+ );
279
+ }
280
+
281
+ setInputRef(newValue);
282
+ resetLastCompleted();
283
+ resetHighlight();
284
+ };
285
+
286
+ const complete = (suggestion: Suggestion): string => {
287
+ return value.slice(0, value.length - suggestion.typeahead.length) + suggestion.value;
288
+ };
289
+
290
+ const getSuggestion = (index: number): Suggestion => {
291
+ if (suggestionSections.labelValues.length > 0) {
292
+ if (index < suggestionSections.labelValues.length) {
293
+ return suggestionSections.labelValues[index];
294
+ }
295
+ return suggestionSections.literals[index - suggestionSections.labelValues.length];
296
+ }
297
+
298
+ if (index < suggestionSections.labelNames.length) {
299
+ return suggestionSections.labelNames[index];
300
+ }
301
+ return suggestionSections.literals[index - suggestionSections.labelNames.length];
302
+ };
303
+
304
+ const highlightNext = (): void => {
305
+ const nextIndex = highlightedSuggestionIndex + 1;
306
+
307
+ if (nextIndex === suggestionsLength) {
308
+ resetHighlight();
309
+ return;
310
+ }
311
+ setHighlightedSuggestionIndex(nextIndex);
312
+ };
313
+
314
+ const highlightPrevious = (): void => {
315
+ if (highlightedSuggestionIndex === -1) {
316
+ // Didn't select anything, so starting at the bottom.
317
+ setHighlightedSuggestionIndex(suggestionsLength - 1);
318
+ return;
319
+ }
320
+
321
+ setHighlightedSuggestionIndex(highlightedSuggestionIndex - 1);
322
+ };
323
+
324
+ const applySuggestion = (suggestionIndex: number): void => {
325
+ const suggestion = getSuggestion(suggestionIndex);
326
+
327
+ if (suggestion.type === Labels.labelValue) {
328
+ suggestion.value = addQuoteMarks(suggestion.value);
329
+ }
330
+
331
+ const newValue = complete(suggestion);
332
+ resetHighlight();
333
+
334
+ if (suggestion.type === Labels.labelName) {
335
+ getLabelNameValues(suggestion.value);
336
+ }
337
+
338
+ setLastCompleted(suggestion);
339
+ setMatchersString(newValue);
340
+
341
+ if (suggestion.type === Labels.labelValue) {
342
+ const values = newValue.split(',');
343
+
344
+ if (currentLabelsCollection == null || currentLabelsCollection?.length === 0) {
345
+ setCurrentLabelsCollection(values);
346
+ } else {
347
+ setCurrentLabelsCollection((oldValues: string[] | null) => [
348
+ ...(oldValues ?? []),
349
+ values[values.length - 1],
350
+ ]);
351
+ }
352
+
353
+ setInputRef('');
354
+ focus();
355
+ return;
356
+ }
357
+
358
+ if (lastCompleted.type === Labels.labelValue && suggestion.type === Labels.literal) {
359
+ setInputRef('');
360
+ focus();
361
+ return;
362
+ }
363
+
364
+ if (currentLabelsCollection !== null) {
365
+ setInputRef(newValue.substring(newValue.lastIndexOf(',') + 1));
366
+ focus();
367
+ return;
368
+ }
369
+
370
+ setInputRef(newValue);
371
+ focus();
372
+ };
373
+
374
+ const applyHighlightedSuggestion = (): void => {
375
+ applySuggestion(highlightedSuggestionIndex);
376
+ };
377
+
378
+ // This function adds quotes to the query expression if the user has typed it in manually, i.e. did not use the arrow up / down keys + Enter
379
+ // to choose the label name and value. Therefore, labelName=value becomes labelName="value".
380
+ const addQuotesToInputRefLabelValue = (inputRef: string): string => {
381
+ const labelValue = inputRef.split(literalRe)[2].replaceAll(',', '');
382
+ const labelValueWithQuotes = addQuoteMarks(labelValue);
383
+ return inputRef.replace(labelValue, labelValueWithQuotes);
384
+ };
385
+
386
+ const handleKeyUp = (event: React.KeyboardEvent<HTMLInputElement>): void => {
387
+ const values = inputRef.replaceAll(',', '');
388
+
389
+ if (labelNameValueRe.test(inputRef)) {
390
+ if (currentLabelsCollection === null) {
391
+ setMatchersString(inputRef);
392
+ } else {
393
+ setMatchersString(currentLabelsCollection?.join(',') + ',' + values);
394
+ }
395
+ setInputRef('');
396
+ }
397
+
398
+ if (event.key === ',') {
399
+ if (inputRef.length === 0) event.preventDefault();
400
+
401
+ // If the current typed query expression matches the labelNameValueWithoutQuotesRe regex (i.e. the labelvalue is not quoted), then add quotes to the labelvalue.
402
+ // if not, just use the current inputRef value.
403
+ const inputValues = labelNameValueWithoutQuotesRe.test(inputRef)
404
+ ? inputRef.replaceAll(',', '')
405
+ : addQuotesToInputRefLabelValue(inputRef).replaceAll(',', '');
406
+
407
+ // if the currentLabelsCollection array is null, we don't need to concat the current inputRef value with the currentLabelsCollection array, so we just push to it.
408
+ if (currentLabelsCollection === null) {
409
+ setCurrentLabelsCollection([inputValues]);
410
+ } else {
411
+ setCurrentLabelsCollection((oldValues: string[] | null) => {
412
+ // Don't add the current inputRef value to the currentLabelsCollection array if it doesn't match the regex because that will cause an API error.
413
+ if (!labelNameValueRe.test(inputRef)) return oldValues;
414
+ return [...(oldValues ?? []), inputValues];
415
+ });
416
+ }
417
+
418
+ // update the currentQuery expression with the currentLabelsCollection array if it's not null, otherwise use the current inputRef value.
419
+ setMatchersString(
420
+ currentLabelsCollection !== null
421
+ ? `${currentLabelsCollection?.join(',')},${inputValues}`
422
+ : `${inputValues},`
423
+ );
424
+ setInputRef('');
425
+ }
426
+
427
+ // We suggest the appropriate label names and label values when a user is typing, depending on what the user has typed.
428
+ // For example, if the user types "labelName=", we suggest the label values next.
429
+ // This bit of code is used for the opposite of the above bit of code, when a user is deleting characters by pressing del/backspace
430
+ // We update the currentQuery expression with what's in the inputRef value so that the suggestions are updated accordingly.
431
+ if (event.key === 'Backspace' && inputRef.length > 0) {
432
+ // if the currentLabelsCollection array is not empty i.e has already previously completed expressions, then we first need to turn the array into a string
433
+ // so it can be concatenated with the current inputRef value. that becomes something like "labelName="value",newLabelName="val
434
+ if (currentLabelsCollection != null && currentLabelsCollection.length > 0) {
435
+ setMatchersString(`${currentLabelsCollection?.join(',')},${inputRef}}`);
436
+ } else {
437
+ // if not, we jsut update the currentQuery expression with the current inputRef value.
438
+ setMatchersString(inputRef);
439
+ }
440
+
441
+ if (currentLabelsCollection === null && inputRef.length === 0) {
442
+ setMatchersString('');
443
+ }
444
+ }
445
+ };
446
+
447
+ const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
448
+ // If there is a highlighted suggestion and enter is hit, we complete
449
+ // with the highlighted suggestion.
450
+ if (highlightedSuggestionIndex >= 0 && event.key === 'Enter') {
451
+ applyHighlightedSuggestion();
452
+ if (lastCompleted.type === Labels.labelValue) setLabelValuesResponse(null);
453
+
454
+ const matchers = currentQuery.matchers.filter(matcher => matcher.key !== '__name__');
455
+ setLocalMatchers(prevState => {
456
+ if (inputRef.length > 0) return prevState;
457
+ if (matchers.length === 0) return prevState;
458
+ return matchers;
459
+ });
460
+ }
461
+
462
+ // If a user has manually typed in a label name that actually exists in the list of label name (and did not use the
463
+ // highlight + arrow keys up/down + Enter/Mouse click method to complete it), and has also typed a literal value, i.e. labelName=,
464
+ // then we can apply a suggestion using the typed label name. This will be as if the user had highlighted the label name and hit enter.
465
+ if (event.key === '!' || event.key === '~' || event.key === '=') {
466
+ const labelName = inputRef.split(literalRe)[0];
467
+
468
+ if (suggestionSections.labelNames.length > 0) {
469
+ // Find the label name in the suggestion list and get the index
470
+ const suggestion = suggestionSections.labelNames.find(
471
+ suggestion => suggestion.value === labelName
472
+ );
473
+ // If the typed label name exists, we can apply it using the applySuggestion function
474
+ if (suggestion != null) {
475
+ applySuggestion(suggestionSections.labelNames.indexOf(suggestion));
476
+ }
477
+ }
478
+ }
479
+
480
+ // Same as above, If a user has typed in a label name and literal (and did not use the suggestion box to complete it),
481
+ // we can manually show the next set of suggestions, which are the label values, by applying a literal suggestion.
482
+ if (labelNameLiteralRe.test(inputRef)) {
483
+ const literal = inputRef.split(literalRe)[1];
484
+
485
+ if (suggestionSections.literals.length > 0) {
486
+ // Find the literal in the suggestion list and get the index
487
+ const suggestion = suggestionSections.literals.find(
488
+ suggestion => suggestion.value === literal
489
+ );
490
+ // If the typed literal exists, we can apply it using the applySuggestion function
491
+ if (suggestion != null) {
492
+ applySuggestion(suggestionSections.literals.indexOf(suggestion));
493
+ }
494
+ }
495
+ }
496
+
497
+ // If no suggestions is highlighted and we hit enter, we run the query,
498
+ // and hide suggestions until another actions enables them again.
499
+ if (highlightedSuggestionIndex === -1 && event.key === 'Enter') {
500
+ if (lastCompleted.type === 'labelValue') setLabelValuesResponse(null);
501
+ setShowSuggest(false);
502
+ runQuery();
503
+ return;
504
+ }
505
+
506
+ setShowSuggest(true);
507
+ };
508
+
509
+ const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
510
+ if (event.key === 'Backspace' && inputRef === '') {
511
+ if (currentLabelsCollection === null) return;
512
+
513
+ removeLabel(currentLabelsCollection.length - 1);
514
+ removeLocalMatcher();
515
+ }
516
+
517
+ // Don't need to handle any key interactions if no suggestions there.
518
+ if (suggestionsLength === 0) {
519
+ return;
520
+ }
521
+
522
+ // Handle tabbing through suggestions.
523
+ if (event.key === 'Tab' && suggestionsLength > 0) {
524
+ event.preventDefault();
525
+ if (event.shiftKey) {
526
+ // Shift + tab goes up.
527
+ highlightPrevious();
528
+ return;
529
+ }
530
+ // Just tab goes down.
531
+ highlightNext();
532
+ }
533
+
534
+ // Up arrow highlights previous suggestions.
535
+ if (event.key === 'ArrowUp') {
536
+ highlightPrevious();
537
+ }
538
+
539
+ // Down arrow highlights next suggestions.
540
+ if (event.key === 'ArrowDown') {
541
+ highlightNext();
542
+ }
543
+ };
544
+
545
+ const focus = (): void => {
546
+ setFocusedInput(true);
547
+ };
548
+
549
+ const unfocus = (): void => {
550
+ setFocusedInput(false);
551
+ resetHighlight();
552
+ };
553
+
554
+ const removeLabel = (label: number): void => {
555
+ if (currentLabelsCollection === null) return;
556
+
557
+ const newLabels = [...currentLabelsCollection];
558
+ newLabels.splice(label, 1);
559
+ setCurrentLabelsCollection(newLabels);
560
+
561
+ const newLabelsAsAString = newLabels.join(',');
562
+ setMatchersString(newLabelsAsAString);
563
+ };
564
+
565
+ const removeLocalMatcher = (): void => {
566
+ if (localMatchers === null) return;
567
+
568
+ const newMatchers = [...localMatchers];
569
+ newMatchers.splice(localMatchers.length - 1, 1);
570
+ setLocalMatchers(newMatchers);
571
+ };
572
+
573
+ const profileSelected = currentQuery.profType.profileName === '';
574
+
575
+ return (
576
+ <>
577
+ <div
578
+ ref={setDivInputRef}
579
+ className="w-full flex items-center text-sm border-gray-300 dark:border-gray-600 border-b"
580
+ >
581
+ <ul className="flex space-x-2">
582
+ {currentLabelsCollection?.map((value, i) => (
583
+ <li
584
+ key={i}
585
+ className="bg-indigo-600 w-fit py-1 px-2 text-gray-100 dark-gray-900 rounded-md"
586
+ >
587
+ {value}
588
+ </li>
589
+ ))}
590
+ </ul>
591
+
592
+ <input
593
+ type="text"
594
+ className={cx(
595
+ 'bg-transparent focus:ring-indigo-800 flex-1 block w-full px-2 py-2 text-sm outline-none',
596
+ profileSelected && 'cursor-not-allowed'
597
+ )}
598
+ placeholder={
599
+ profileSelected ? 'Select a profile first to query profiles...' : 'query profiles...'
600
+ }
601
+ onChange={onChange}
602
+ value={inputRef}
603
+ onBlur={unfocus}
604
+ onFocus={focus}
605
+ onKeyPress={handleKeyPress}
606
+ onKeyDown={handleKeyDown}
607
+ onKeyUp={handleKeyUp}
608
+ disabled={profileSelected} // Disable input if no profile has been selected
609
+ title={
610
+ profileSelected ? 'Select a profile first to query profiles...' : 'query profiles...'
611
+ }
612
+ />
613
+ </div>
614
+
615
+ <div
616
+ ref={setPopperElement}
617
+ style={{...styles.popper, marginLeft: 0}}
618
+ {...attributes.popper}
619
+ className="z-50"
620
+ >
621
+ <Transition
622
+ show={focusedInput && showSuggest}
623
+ as={Fragment}
624
+ leave="transition ease-in duration-100"
625
+ leaveFrom="opacity-100"
626
+ leaveTo="opacity-0"
627
+ >
628
+ <div
629
+ style={{width: divInputRef?.offsetWidth}}
630
+ className="absolute z-10 max-h-[400px] mt-1 bg-gray-50 dark:bg-gray-900 shadow-lg rounded-md text-base ring-1 ring-black ring-opacity-5 overflow-auto focus:outline-none sm:text-sm"
631
+ >
632
+ {labelNamesLoading ? (
633
+ <LoadingSpinner />
634
+ ) : (
635
+ <>
636
+ {suggestionSections.labelNames.map((l, i) => (
637
+ <div
638
+ key={i}
639
+ className={cx(
640
+ highlightedSuggestionIndex === i && 'text-white bg-indigo-600',
641
+ 'cursor-default select-none relative py-2 pl-3 pr-9'
642
+ )}
643
+ onMouseOver={() => setHighlightedSuggestionIndex(i)}
644
+ onClick={() => applySuggestion(i)}
645
+ onMouseOut={() => resetHighlight()}
646
+ >
647
+ {l.value}
648
+ </div>
649
+ ))}
650
+ </>
651
+ )}
652
+
653
+ {suggestionSections.literals.map((l, i) => (
654
+ <div
655
+ key={i}
656
+ className={cx(
657
+ highlightedSuggestionIndex === i + suggestionSections.labelNames.length &&
658
+ 'text-white bg-indigo-600',
659
+ 'cursor-default select-none relative py-2 pl-3 pr-9'
660
+ )}
661
+ onMouseOver={() =>
662
+ setHighlightedSuggestionIndex(i + suggestionSections.labelNames.length)
663
+ }
664
+ onClick={() => applySuggestion(i + suggestionSections.labelNames.length)}
665
+ onMouseOut={() => resetHighlight()}
666
+ >
667
+ {l.value}
668
+ </div>
669
+ ))}
670
+
671
+ {labelValuesLoading && lastCompleted.type === 'literal' ? (
672
+ <LoadingSpinner />
673
+ ) : (
674
+ <>
675
+ {suggestionSections.labelValues.map((l, i) => (
676
+ <div
677
+ key={i}
678
+ className={cx(
679
+ highlightedSuggestionIndex === i && 'text-white bg-indigo-600',
680
+ 'cursor-default select-none relative py-2 pl-3 pr-9'
681
+ )}
682
+ onMouseOver={() => setHighlightedSuggestionIndex(i)}
683
+ onClick={() => applySuggestion(i)}
684
+ onMouseOut={() => resetHighlight()}
685
+ >
686
+ {l.value}
687
+ </div>
688
+ ))}
689
+ </>
690
+ )}
691
+ </div>
692
+ </Transition>
693
+ </div>
694
+ </>
695
+ );
696
+ };
697
+
698
+ export default MatchersInput;