@parca/profile 0.16.74 → 0.16.75

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 (33) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/Callgraph/index.d.ts +1 -0
  3. package/dist/GraphTooltip/index.d.ts +1 -0
  4. package/dist/IcicleGraph.d.ts +1 -0
  5. package/dist/MatchersInput/SuggestionItem.d.ts +1 -0
  6. package/dist/MatchersInput/SuggestionsList.d.ts +24 -0
  7. package/dist/MatchersInput/SuggestionsList.js +162 -0
  8. package/dist/MatchersInput/index.d.ts +1 -0
  9. package/dist/MatchersInput/index.js +70 -183
  10. package/dist/MetricsCircle/index.d.ts +1 -0
  11. package/dist/MetricsGraph/index.d.ts +1 -0
  12. package/dist/MetricsSeries/index.d.ts +1 -0
  13. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +1 -0
  14. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +1 -0
  15. package/dist/ProfileExplorer/index.d.ts +1 -0
  16. package/dist/ProfileIcicleGraph.d.ts +1 -0
  17. package/dist/ProfileMetricsGraph/index.d.ts +1 -0
  18. package/dist/ProfileSelector/CompareButton.d.ts +1 -0
  19. package/dist/ProfileSelector/MergeButton.d.ts +1 -0
  20. package/dist/ProfileSelector/index.d.ts +1 -0
  21. package/dist/ProfileSource.d.ts +1 -0
  22. package/dist/ProfileTypeSelector/index.d.ts +1 -0
  23. package/dist/ProfileView/FilterByFunctionButton.d.ts +1 -0
  24. package/dist/ProfileView/index.d.ts +1 -0
  25. package/dist/ProfileViewWithData.d.ts +1 -0
  26. package/dist/TopTable.d.ts +1 -0
  27. package/dist/components/DiffLegend.d.ts +1 -0
  28. package/dist/components/ProfileShareButton/ResultBox.d.ts +1 -0
  29. package/dist/components/ProfileShareButton/index.d.ts +1 -0
  30. package/dist/styles.css +1 -1
  31. package/package.json +6 -5
  32. package/src/MatchersInput/SuggestionsList.tsx +291 -0
  33. package/src/MatchersInput/index.tsx +78 -285
@@ -11,15 +11,14 @@
11
11
  // See the License for the specific language governing permissions and
12
12
  // limitations under the License.
13
13
 
14
- import React, {Fragment, useState, useEffect} from 'react';
15
- import {Transition} from '@headlessui/react';
14
+ import React, {useState, useEffect, useMemo, useRef} from 'react';
16
15
  import {Query} from '@parca/parser';
17
16
  import {LabelsResponse, QueryServiceClient} from '@parca/client';
18
- import {usePopper} from 'react-popper';
17
+ import TextareaAutosize from 'react-textarea-autosize';
19
18
  import cx from 'classnames';
20
19
 
21
- import {useParcaContext, useGrpcMetadata} from '@parca/components';
22
- import SuggestionItem from './SuggestionItem';
20
+ import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
21
+ import {useGrpcMetadata} from '@parca/components';
23
22
 
24
23
  interface MatchersInputProps {
25
24
  queryClient: QueryServiceClient;
@@ -56,58 +55,23 @@ export const useLabelNames = (client: QueryServiceClient): UseLabelNames => {
56
55
  return {result, loading};
57
56
  };
58
57
 
59
- class Suggestion {
60
- type: string;
61
- typeahead: string;
62
- value: string;
63
-
64
- constructor(type: string, typeahead: string, value: string) {
65
- this.type = type;
66
- this.typeahead = typeahead;
67
- this.value = value;
68
- }
69
- }
70
-
71
- class Suggestions {
72
- literals: Suggestion[];
73
- labelNames: Suggestion[];
74
- labelValues: Suggestion[];
75
-
76
- constructor() {
77
- this.literals = [];
78
- this.labelNames = [];
79
- this.labelValues = [];
80
- }
81
- }
82
-
83
58
  const MatchersInput = ({
84
59
  queryClient,
85
60
  setMatchersString,
86
61
  runQuery,
87
62
  currentQuery,
88
63
  }: MatchersInputProps): JSX.Element => {
89
- const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
64
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
90
65
  const [focusedInput, setFocusedInput] = useState(false);
91
- const [showSuggest, setShowSuggest] = useState(true);
92
- const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(-1);
93
66
  const [labelValuesLoading, setLabelValuesLoading] = useState(false);
94
67
  const [lastCompleted, setLastCompleted] = useState<Suggestion>(new Suggestion('', '', ''));
95
- const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
96
68
  const [labelValues, setLabelValues] = useState<string[] | null>(null);
97
69
  const [currentLabelName, setCurrentLabelName] = useState<string | null>(null);
98
- const {styles, attributes} = usePopper(inputRef, popperElement, {
99
- placement: 'bottom-start',
100
- });
101
70
  const metadata = useGrpcMetadata();
102
- const {loader: Spinner} = useParcaContext();
103
71
 
104
72
  const {loading: labelNamesLoading, result} = useLabelNames(queryClient);
105
73
  const {response: labelNamesResponse, error: labelNamesError} = result;
106
74
 
107
- const LoadingSpinner = (): JSX.Element => {
108
- return <div className="pt-2 pb-4">{Spinner}</div>;
109
- };
110
-
111
75
  useEffect(() => {
112
76
  if (currentLabelName !== null) {
113
77
  const call = queryClient.values({labelName: currentLabelName, match: []}, {meta: metadata});
@@ -122,180 +86,93 @@ const MatchersInput = ({
122
86
  }
123
87
  }, [currentLabelName, queryClient, metadata]);
124
88
 
125
- const labelNames =
126
- (labelNamesError === undefined || labelNamesError == null) &&
127
- labelNamesResponse !== undefined &&
128
- labelNamesResponse != null
89
+ const labelNames = useMemo(() => {
90
+ return (labelNamesError === undefined || labelNamesError == null) &&
91
+ labelNamesResponse !== undefined &&
92
+ labelNamesResponse != null
129
93
  ? labelNamesResponse.labelNames.filter(e => e !== '__name__')
130
94
  : [];
95
+ }, [labelNamesError, labelNamesResponse]);
131
96
 
132
97
  const value = currentQuery.matchersString();
133
98
 
134
- const suggestionSections = new Suggestions();
135
- Query.suggest(`${currentQuery.profileName()}{${value}`).forEach(function (s) {
136
- // Skip suggestions that we just completed. This really only works,
137
- // because we know the language is not repetitive. For a language that
138
- // has a repeating word, this would not work.
139
- if (lastCompleted !== null && lastCompleted.type === s.type) {
140
- return;
141
- }
142
-
143
- // Need to figure out if any literal suggestions make sense, but a
144
- // closing bracket doesn't in the guided query experience because all
145
- // we have the user do is type the matchers.
146
- if (s.type === 'literal' && s.value !== '}') {
147
- suggestionSections.literals.push({
148
- type: s.type,
149
- typeahead: s.typeahead,
150
- value: s.value,
151
- });
152
- }
153
- if (s.type === 'labelName') {
154
- const inputValue = s.typeahead.trim().toLowerCase();
155
- const inputLength = inputValue.length;
156
- const matches = labelNames.filter(function (label) {
157
- return label.toLowerCase().slice(0, inputLength) === inputValue;
158
- });
99
+ const suggestionSections = useMemo(() => {
100
+ const suggestionSections = new Suggestions();
101
+ Query.suggest(`${currentQuery.profileName()}{${value}`).forEach(function (s) {
102
+ // Skip suggestions that we just completed. This really only works,
103
+ // because we know the language is not repetitive. For a language that
104
+ // has a repeating word, this would not work.
105
+ if (lastCompleted !== null && lastCompleted.type === s.type) {
106
+ return;
107
+ }
159
108
 
160
- matches.forEach(m =>
161
- suggestionSections.labelNames.push({
109
+ // Need to figure out if any literal suggestions make sense, but a
110
+ // closing bracket doesn't in the guided query experience because all
111
+ // we have the user do is type the matchers.
112
+ if (s.type === 'literal' && s.value !== '}') {
113
+ suggestionSections.literals.push({
162
114
  type: s.type,
163
115
  typeahead: s.typeahead,
164
- value: m,
165
- })
166
- );
167
- }
168
-
169
- if (s.type === 'labelValue') {
170
- if (currentLabelName === null || s.labelName !== currentLabelName) {
171
- setCurrentLabelName(s.labelName);
172
- return;
116
+ value: s.value,
117
+ });
173
118
  }
174
-
175
- if (labelValues !== null) {
176
- labelValues
177
- .filter(v => v.slice(0, s.typeahead.length) === s.typeahead)
178
- .forEach(v =>
179
- suggestionSections.labelValues.push({
180
- type: s.type,
181
- typeahead: s.typeahead,
182
- value: v,
183
- })
184
- );
119
+ if (s.type === 'labelName') {
120
+ const inputValue = s.typeahead.trim().toLowerCase();
121
+ const inputLength = inputValue.length;
122
+ const matches = labelNames.filter(function (label) {
123
+ return label.toLowerCase().slice(0, inputLength) === inputValue;
124
+ });
125
+
126
+ matches.forEach(m =>
127
+ suggestionSections.labelNames.push({
128
+ type: s.type,
129
+ typeahead: s.typeahead,
130
+ value: m,
131
+ })
132
+ );
185
133
  }
186
- }
187
- });
188
134
 
189
- const suggestionsLength =
190
- suggestionSections.literals.length +
191
- suggestionSections.labelNames.length +
192
- suggestionSections.labelValues.length;
135
+ if (s.type === 'labelValue') {
136
+ if (currentLabelName === null || s.labelName !== currentLabelName) {
137
+ setCurrentLabelName(s.labelName);
138
+ return;
139
+ }
140
+
141
+ if (labelValues !== null) {
142
+ labelValues
143
+ .filter(v => v.slice(0, s.typeahead.length) === s.typeahead)
144
+ .forEach(v =>
145
+ suggestionSections.labelValues.push({
146
+ type: s.type,
147
+ typeahead: s.typeahead,
148
+ value: v,
149
+ })
150
+ );
151
+ }
152
+ }
153
+ });
154
+ return suggestionSections;
155
+ }, [currentQuery, lastCompleted, labelNames, labelValues, currentLabelName, value]);
193
156
 
194
- const resetHighlight = (): void => setHighlightedSuggestionIndex(-1);
195
157
  const resetLastCompleted = (): void => setLastCompleted(new Suggestion('', '', ''));
196
158
 
197
- const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
159
+ const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
198
160
  const newValue = e.target.value;
199
161
  setMatchersString(newValue);
200
162
  resetLastCompleted();
201
- resetHighlight();
202
163
  };
203
164
 
204
165
  const complete = (suggestion: Suggestion): string => {
205
166
  return value.slice(0, value.length - suggestion.typeahead.length) + suggestion.value;
206
167
  };
207
168
 
208
- const getSuggestion = (index: number): Suggestion => {
209
- if (index < suggestionSections.labelNames.length) {
210
- return suggestionSections.labelNames[index];
211
- }
212
- if (index < suggestionSections.labelNames.length + suggestionSections.literals.length) {
213
- return suggestionSections.literals[index - suggestionSections.labelNames.length];
214
- }
215
- return suggestionSections.labelValues[
216
- index - suggestionSections.labelNames.length - suggestionSections.literals.length
217
- ];
218
- };
219
-
220
- const highlightNext = (): void => {
221
- const nextIndex = highlightedSuggestionIndex + 1;
222
- if (nextIndex === suggestionsLength) {
223
- resetHighlight();
224
- return;
225
- }
226
- setHighlightedSuggestionIndex(nextIndex);
227
- };
228
-
229
- const highlightPrevious = (): void => {
230
- if (highlightedSuggestionIndex === -1) {
231
- // Didn't select anything, so starting at the bottom.
232
- setHighlightedSuggestionIndex(suggestionsLength - 1);
233
- return;
234
- }
235
-
236
- setHighlightedSuggestionIndex(highlightedSuggestionIndex - 1);
237
- };
238
-
239
- const applySuggestion = (suggestionIndex: number): void => {
240
- const suggestion = getSuggestion(suggestionIndex);
169
+ const applySuggestion = (suggestion: Suggestion): void => {
241
170
  const newValue = complete(suggestion);
242
- resetHighlight();
243
171
  setLastCompleted(suggestion);
244
172
  setMatchersString(newValue);
245
- if (inputRef !== null) {
246
- inputRef.value = newValue;
247
- inputRef.focus();
248
- }
249
- };
250
-
251
- const applyHighlightedSuggestion = (): void => {
252
- applySuggestion(highlightedSuggestionIndex);
253
- };
254
-
255
- const handleKeyPress = (event: React.KeyboardEvent<HTMLInputElement>): void => {
256
- // If there is a highlighted suggestion and enter is hit, we complete
257
- // with the highlighted suggestion.
258
- if (highlightedSuggestionIndex >= 0 && event.key === 'Enter') {
259
- applyHighlightedSuggestion();
260
- }
261
-
262
- // If no suggestions is highlighted and we hit enter, we run the query,
263
- // and hide suggestions until another actions enables them again.
264
- if (highlightedSuggestionIndex === -1 && event.key === 'Enter') {
265
- setShowSuggest(false);
266
- runQuery();
267
- return;
268
- }
269
-
270
- setShowSuggest(true);
271
- };
272
-
273
- const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>): void => {
274
- // Don't need to handle any key interactions if no suggestions there.
275
- if (suggestionsLength === 0) {
276
- return;
277
- }
278
-
279
- // Handle tabbing through suggestions.
280
- if (event.key === 'Tab' && suggestionsLength > 0) {
281
- event.preventDefault();
282
- if (event.shiftKey) {
283
- // Shift + tab goes up.
284
- highlightPrevious();
285
- return;
286
- }
287
- // Just tab goes down.
288
- highlightNext();
289
- }
290
-
291
- // Up arrow highlights previous suggestions.
292
- if (event.key === 'ArrowUp') {
293
- highlightPrevious();
294
- }
295
-
296
- // Down arrow highlights next suggestions.
297
- if (event.key === 'ArrowDown') {
298
- highlightNext();
173
+ if (inputRef.current !== null) {
174
+ inputRef.current.value = newValue;
175
+ inputRef.current.focus();
299
176
  }
300
177
  };
301
178
 
@@ -305,18 +182,16 @@ const MatchersInput = ({
305
182
 
306
183
  const unfocus = (): void => {
307
184
  setFocusedInput(false);
308
- resetHighlight();
309
185
  };
310
186
 
311
187
  const profileSelected = currentQuery.profileName() === '';
312
188
 
313
189
  return (
314
190
  <div className="font-mono flex-1 w-full block">
315
- <input
316
- ref={setInputRef}
317
- type="text"
191
+ <TextareaAutosize
192
+ ref={inputRef}
318
193
  className={cx(
319
- 'bg-transparent focus:ring-indigo-800 flex-1 block w-full px-2 py-2 text-sm outline-none',
194
+ 'bg-gray-50 dark:bg-gray-900 focus:ring-indigo-800 flex-1 block w-full px-2 py-2 text-sm outline-none rounded',
320
195
  profileSelected && 'cursor-not-allowed'
321
196
  )}
322
197
  placeholder={
@@ -328,8 +203,6 @@ const MatchersInput = ({
328
203
  value={value}
329
204
  onBlur={unfocus}
330
205
  onFocus={focus}
331
- onKeyPress={handleKeyPress}
332
- onKeyDown={handleKeyDown}
333
206
  disabled={profileSelected} // Disable input if no profile has been selected
334
207
  title={
335
208
  profileSelected
@@ -337,95 +210,15 @@ const MatchersInput = ({
337
210
  : 'filter profiles... eg. node="test"'
338
211
  }
339
212
  />
340
- {suggestionsLength > 0 && (
341
- <div
342
- ref={setPopperElement}
343
- style={{...styles.popper, marginLeft: 0}}
344
- {...attributes.popper}
345
- className="z-50"
346
- >
347
- <Transition
348
- show={focusedInput && showSuggest}
349
- as={Fragment}
350
- leave="transition ease-in duration-100"
351
- leaveFrom="opacity-100"
352
- leaveTo="opacity-0"
353
- >
354
- <div
355
- style={{width: inputRef?.offsetWidth}}
356
- 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"
357
- >
358
- {labelNamesLoading ? (
359
- <LoadingSpinner />
360
- ) : (
361
- <>
362
- {suggestionSections.labelNames.map((l, i) => (
363
- <SuggestionItem
364
- isHighlighted={highlightedSuggestionIndex === i}
365
- onHighlight={() => setHighlightedSuggestionIndex(i)}
366
- onApplySuggestion={() => applySuggestion(i)}
367
- onResetHighlight={() => resetHighlight()}
368
- value={l.value}
369
- key={l.value}
370
- />
371
- ))}
372
- </>
373
- )}
374
-
375
- {suggestionSections.literals.map((l, i) => (
376
- <SuggestionItem
377
- isHighlighted={
378
- highlightedSuggestionIndex === i + suggestionSections.labelNames.length
379
- }
380
- onHighlight={() =>
381
- setHighlightedSuggestionIndex(i + suggestionSections.labelNames.length)
382
- }
383
- onApplySuggestion={() =>
384
- applySuggestion(i + suggestionSections.labelNames.length)
385
- }
386
- onResetHighlight={() => resetHighlight()}
387
- value={l.value}
388
- key={l.value}
389
- />
390
- ))}
391
-
392
- {labelValuesLoading && lastCompleted.type === 'literal' ? (
393
- <LoadingSpinner />
394
- ) : (
395
- <>
396
- {suggestionSections.labelValues.map((l, i) => (
397
- <SuggestionItem
398
- isHighlighted={
399
- highlightedSuggestionIndex ===
400
- i +
401
- suggestionSections.labelNames.length +
402
- suggestionSections.literals.length
403
- }
404
- onHighlight={() =>
405
- setHighlightedSuggestionIndex(
406
- i +
407
- suggestionSections.labelNames.length +
408
- suggestionSections.literals.length
409
- )
410
- }
411
- onApplySuggestion={() =>
412
- applySuggestion(
413
- i +
414
- suggestionSections.labelNames.length +
415
- suggestionSections.literals.length
416
- )
417
- }
418
- onResetHighlight={() => resetHighlight()}
419
- value={l.value}
420
- key={l.value}
421
- />
422
- ))}
423
- </>
424
- )}
425
- </div>
426
- </Transition>
427
- </div>
428
- )}
213
+ <SuggestionsList
214
+ isLabelNamesLoading={labelNamesLoading}
215
+ suggestions={suggestionSections}
216
+ applySuggestion={applySuggestion}
217
+ inputRef={inputRef.current}
218
+ runQuery={runQuery}
219
+ focusedInput={focusedInput}
220
+ isLabelValuesLoading={labelValuesLoading && lastCompleted.type === 'literal'}
221
+ />
429
222
  </div>
430
223
  );
431
224
  };