@parca/profile 0.16.74 → 0.16.76

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 (37) hide show
  1. package/CHANGELOG.md +8 -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 +75 -185
  10. package/dist/MetricsCircle/index.d.ts +1 -0
  11. package/dist/MetricsGraph/index.d.ts +1 -0
  12. package/dist/MetricsGraph/index.js +3 -2
  13. package/dist/MetricsSeries/index.d.ts +1 -0
  14. package/dist/ProfileExplorer/ProfileExplorerCompare.d.ts +1 -0
  15. package/dist/ProfileExplorer/ProfileExplorerSingle.d.ts +1 -0
  16. package/dist/ProfileExplorer/index.d.ts +1 -0
  17. package/dist/ProfileIcicleGraph.d.ts +1 -0
  18. package/dist/ProfileMetricsGraph/index.d.ts +1 -0
  19. package/dist/ProfileSelector/CompareButton.d.ts +1 -0
  20. package/dist/ProfileSelector/MergeButton.d.ts +1 -0
  21. package/dist/ProfileSelector/index.d.ts +1 -0
  22. package/dist/ProfileSelector/index.js +4 -1
  23. package/dist/ProfileSource.d.ts +1 -0
  24. package/dist/ProfileTypeSelector/index.d.ts +1 -0
  25. package/dist/ProfileView/FilterByFunctionButton.d.ts +1 -0
  26. package/dist/ProfileView/index.d.ts +1 -0
  27. package/dist/ProfileViewWithData.d.ts +1 -0
  28. package/dist/TopTable.d.ts +1 -0
  29. package/dist/components/DiffLegend.d.ts +1 -0
  30. package/dist/components/ProfileShareButton/ResultBox.d.ts +1 -0
  31. package/dist/components/ProfileShareButton/index.d.ts +1 -0
  32. package/dist/styles.css +1 -1
  33. package/package.json +7 -6
  34. package/src/MatchersInput/SuggestionsList.tsx +291 -0
  35. package/src/MatchersInput/index.tsx +85 -287
  36. package/src/MetricsGraph/index.tsx +6 -2
  37. package/src/ProfileSelector/index.tsx +4 -1
@@ -11,15 +11,16 @@
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';
15
+ import TextareaAutosize from 'react-textarea-autosize';
16
+ import cx from 'classnames';
17
+
18
+ import {useGrpcMetadata} from '@parca/components';
19
+ import {sanitizeLabelValue} from '@parca/functions';
16
20
  import {Query} from '@parca/parser';
17
21
  import {LabelsResponse, QueryServiceClient} from '@parca/client';
18
- import {usePopper} from 'react-popper';
19
- import cx from 'classnames';
20
22
 
21
- import {useParcaContext, useGrpcMetadata} from '@parca/components';
22
- import SuggestionItem from './SuggestionItem';
23
+ import SuggestionsList, {Suggestion, Suggestions} from './SuggestionsList';
23
24
 
24
25
  interface MatchersInputProps {
25
26
  queryClient: QueryServiceClient;
@@ -56,58 +57,23 @@ export const useLabelNames = (client: QueryServiceClient): UseLabelNames => {
56
57
  return {result, loading};
57
58
  };
58
59
 
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
60
  const MatchersInput = ({
84
61
  queryClient,
85
62
  setMatchersString,
86
63
  runQuery,
87
64
  currentQuery,
88
65
  }: MatchersInputProps): JSX.Element => {
89
- const [inputRef, setInputRef] = useState<HTMLInputElement | null>(null);
66
+ const inputRef = useRef<HTMLTextAreaElement | null>(null);
90
67
  const [focusedInput, setFocusedInput] = useState(false);
91
- const [showSuggest, setShowSuggest] = useState(true);
92
- const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState(-1);
93
68
  const [labelValuesLoading, setLabelValuesLoading] = useState(false);
94
69
  const [lastCompleted, setLastCompleted] = useState<Suggestion>(new Suggestion('', '', ''));
95
- const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
96
70
  const [labelValues, setLabelValues] = useState<string[] | null>(null);
97
71
  const [currentLabelName, setCurrentLabelName] = useState<string | null>(null);
98
- const {styles, attributes} = usePopper(inputRef, popperElement, {
99
- placement: 'bottom-start',
100
- });
101
72
  const metadata = useGrpcMetadata();
102
- const {loader: Spinner} = useParcaContext();
103
73
 
104
74
  const {loading: labelNamesLoading, result} = useLabelNames(queryClient);
105
75
  const {response: labelNamesResponse, error: labelNamesError} = result;
106
76
 
107
- const LoadingSpinner = (): JSX.Element => {
108
- return <div className="pt-2 pb-4">{Spinner}</div>;
109
- };
110
-
111
77
  useEffect(() => {
112
78
  if (currentLabelName !== null) {
113
79
  const call = queryClient.values({labelName: currentLabelName, match: []}, {meta: metadata});
@@ -115,187 +81,103 @@ const MatchersInput = ({
115
81
 
116
82
  call.response
117
83
  .then(response => {
118
- setLabelValues(response.labelValues);
84
+ // replace single `\` in the `labelValues` string with doubles `\\` if available.
85
+ const newValues = sanitizeLabelValue(response.labelValues);
86
+
87
+ setLabelValues(newValues);
119
88
  })
120
89
  .catch(() => setLabelValues(null))
121
90
  .finally(() => setLabelValuesLoading(false));
122
91
  }
123
92
  }, [currentLabelName, queryClient, metadata]);
124
93
 
125
- const labelNames =
126
- (labelNamesError === undefined || labelNamesError == null) &&
127
- labelNamesResponse !== undefined &&
128
- labelNamesResponse != null
94
+ const labelNames = useMemo(() => {
95
+ return (labelNamesError === undefined || labelNamesError == null) &&
96
+ labelNamesResponse !== undefined &&
97
+ labelNamesResponse != null
129
98
  ? labelNamesResponse.labelNames.filter(e => e !== '__name__')
130
99
  : [];
100
+ }, [labelNamesError, labelNamesResponse]);
131
101
 
132
102
  const value = currentQuery.matchersString();
133
103
 
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
- });
104
+ const suggestionSections = useMemo(() => {
105
+ const suggestionSections = new Suggestions();
106
+ Query.suggest(`${currentQuery.profileName()}{${value}`).forEach(function (s) {
107
+ // Skip suggestions that we just completed. This really only works,
108
+ // because we know the language is not repetitive. For a language that
109
+ // has a repeating word, this would not work.
110
+ if (lastCompleted !== null && lastCompleted.type === s.type) {
111
+ return;
112
+ }
159
113
 
160
- matches.forEach(m =>
161
- suggestionSections.labelNames.push({
114
+ // Need to figure out if any literal suggestions make sense, but a
115
+ // closing bracket doesn't in the guided query experience because all
116
+ // we have the user do is type the matchers.
117
+ if (s.type === 'literal' && s.value !== '}') {
118
+ suggestionSections.literals.push({
162
119
  type: s.type,
163
120
  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;
121
+ value: s.value,
122
+ });
173
123
  }
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
- );
124
+ if (s.type === 'labelName') {
125
+ const inputValue = s.typeahead.trim().toLowerCase();
126
+ const inputLength = inputValue.length;
127
+ const matches = labelNames.filter(function (label) {
128
+ return label.toLowerCase().slice(0, inputLength) === inputValue;
129
+ });
130
+
131
+ matches.forEach(m =>
132
+ suggestionSections.labelNames.push({
133
+ type: s.type,
134
+ typeahead: s.typeahead,
135
+ value: m,
136
+ })
137
+ );
185
138
  }
186
- }
187
- });
188
139
 
189
- const suggestionsLength =
190
- suggestionSections.literals.length +
191
- suggestionSections.labelNames.length +
192
- suggestionSections.labelValues.length;
140
+ if (s.type === 'labelValue') {
141
+ if (currentLabelName === null || s.labelName !== currentLabelName) {
142
+ setCurrentLabelName(s.labelName);
143
+ return;
144
+ }
145
+
146
+ if (labelValues !== null) {
147
+ labelValues
148
+ .filter(v => v.slice(0, s.typeahead.length) === s.typeahead)
149
+ .forEach(v =>
150
+ suggestionSections.labelValues.push({
151
+ type: s.type,
152
+ typeahead: s.typeahead,
153
+ value: v,
154
+ })
155
+ );
156
+ }
157
+ }
158
+ });
159
+ return suggestionSections;
160
+ }, [currentQuery, lastCompleted, labelNames, labelValues, currentLabelName, value]);
193
161
 
194
- const resetHighlight = (): void => setHighlightedSuggestionIndex(-1);
195
162
  const resetLastCompleted = (): void => setLastCompleted(new Suggestion('', '', ''));
196
163
 
197
- const onChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
164
+ const onChange = (e: React.ChangeEvent<HTMLTextAreaElement>): void => {
198
165
  const newValue = e.target.value;
199
166
  setMatchersString(newValue);
200
167
  resetLastCompleted();
201
- resetHighlight();
202
168
  };
203
169
 
204
170
  const complete = (suggestion: Suggestion): string => {
205
171
  return value.slice(0, value.length - suggestion.typeahead.length) + suggestion.value;
206
172
  };
207
173
 
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);
174
+ const applySuggestion = (suggestion: Suggestion): void => {
241
175
  const newValue = complete(suggestion);
242
- resetHighlight();
243
176
  setLastCompleted(suggestion);
244
177
  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();
178
+ if (inputRef.current !== null) {
179
+ inputRef.current.value = newValue;
180
+ inputRef.current.focus();
299
181
  }
300
182
  };
301
183
 
@@ -305,18 +187,16 @@ const MatchersInput = ({
305
187
 
306
188
  const unfocus = (): void => {
307
189
  setFocusedInput(false);
308
- resetHighlight();
309
190
  };
310
191
 
311
192
  const profileSelected = currentQuery.profileName() === '';
312
193
 
313
194
  return (
314
195
  <div className="font-mono flex-1 w-full block">
315
- <input
316
- ref={setInputRef}
317
- type="text"
196
+ <TextareaAutosize
197
+ ref={inputRef}
318
198
  className={cx(
319
- 'bg-transparent focus:ring-indigo-800 flex-1 block w-full px-2 py-2 text-sm outline-none',
199
+ '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
200
  profileSelected && 'cursor-not-allowed'
321
201
  )}
322
202
  placeholder={
@@ -328,8 +208,6 @@ const MatchersInput = ({
328
208
  value={value}
329
209
  onBlur={unfocus}
330
210
  onFocus={focus}
331
- onKeyPress={handleKeyPress}
332
- onKeyDown={handleKeyDown}
333
211
  disabled={profileSelected} // Disable input if no profile has been selected
334
212
  title={
335
213
  profileSelected
@@ -337,95 +215,15 @@ const MatchersInput = ({
337
215
  : 'filter profiles... eg. node="test"'
338
216
  }
339
217
  />
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
- )}
218
+ <SuggestionsList
219
+ isLabelNamesLoading={labelNamesLoading}
220
+ suggestions={suggestionSections}
221
+ applySuggestion={applySuggestion}
222
+ inputRef={inputRef.current}
223
+ runQuery={runQuery}
224
+ focusedInput={focusedInput}
225
+ isLabelValuesLoading={labelValuesLoading && lastCompleted.type === 'literal'}
226
+ />
429
227
  </div>
430
228
  );
431
229
  };
@@ -21,7 +21,7 @@ import throttle from 'lodash.throttle';
21
21
  import {MetricsSeries as MetricsSeriesPb, MetricsSample, Label} from '@parca/client';
22
22
  import {usePopper} from 'react-popper';
23
23
  import type {VirtualElement} from '@popperjs/core';
24
- import {valueFormatter, formatDate} from '@parca/functions';
24
+ import {valueFormatter, formatDate, sanitizeHighlightedValues} from '@parca/functions';
25
25
  import {DateTimeRange} from '@parca/components';
26
26
  import {useContainerDimensions} from '@parca/dynamicsize';
27
27
  import useIsShiftDown from '@parca/components/src/hooks/useIsShiftDown';
@@ -362,7 +362,11 @@ export const RawMetricsGraph = ({
362
362
 
363
363
  const openClosestProfile = (): void => {
364
364
  if (highlighted != null) {
365
- onSampleClick(Math.round(highlighted.timestamp), highlighted.value, highlighted.labels);
365
+ onSampleClick(
366
+ Math.round(highlighted.timestamp),
367
+ highlighted.value,
368
+ sanitizeHighlightedValues(highlighted.labels) // When a user clicks on any sample in the graph, replace single `\` in the `labelValues` string with doubles `\\` if available.
369
+ );
366
370
  }
367
371
  };
368
372
 
@@ -138,7 +138,10 @@ const ProfileSelector = ({
138
138
  };
139
139
 
140
140
  const addLabelMatcher = (key: string, value: string): void => {
141
- const [newQuery, changed] = Query.parse(queryExpressionString).setMatcher(key, value);
141
+ // When a user clicks on a label on the metrics graph tooltip,
142
+ // replace single `\` in the `value` string with doubles `\\` if available.
143
+ const newValue = value.includes('\\') ? value.replaceAll('\\', '\\\\') : value;
144
+ const [newQuery, changed] = Query.parse(queryExpressionString).setMatcher(key, newValue);
142
145
  if (changed) {
143
146
  setNewQueryExpression(newQuery.toString(), false);
144
147
  }