Skip to content

Commit 0225550

Browse files
authored
ui: Add refetch support for label names in matchers UI (#5990)
* ui: Add refetch support for label names in matchers UI Introduces a refetch function to the useLabelNames hook and exposes it through context, allowing the SimpleMatchers component to provide a refresh button for label name options. Also updates the Select component to use the new refetch function and adjusts styling for dark mode. * change prop name
1 parent 302edbb commit 0225550

6 files changed

Lines changed: 162 additions & 62 deletions

File tree

ui/packages/shared/profile/src/MatchersInput/SuggestionsList.tsx

Lines changed: 104 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import cx from 'classnames';
1919
import {usePopper} from 'react-popper';
2020

2121
import {useParcaContext} from '@parca/components';
22+
import {TEST_IDS, testId} from '@parca/test-utils';
2223

2324
import SuggestionItem from './SuggestionItem';
2425

@@ -56,6 +57,7 @@ interface Props {
5657
isLabelValuesLoading: boolean;
5758
shouldTrimPrefix: boolean;
5859
refetchLabelValues: () => void;
60+
refetchLabelNames: () => void;
5961
}
6062

6163
const LoadingSpinner = (): JSX.Element => {
@@ -64,6 +66,43 @@ const LoadingSpinner = (): JSX.Element => {
6466
return <div className="pt-2 pb-4">{Spinner}</div>;
6567
};
6668

69+
interface RefreshButtonProps {
70+
onClick: () => void;
71+
disabled: boolean;
72+
title: string;
73+
testId: string;
74+
}
75+
76+
const RefreshButton = ({onClick, disabled, title, testId}: RefreshButtonProps): JSX.Element => {
77+
return (
78+
<div className="absolute w-full flex items-center justify-center bottom-0 px-3 py-2 bg-gray-50 dark:bg-gray-900">
79+
<button
80+
onClick={e => {
81+
e.preventDefault();
82+
e.stopPropagation();
83+
onClick();
84+
}}
85+
disabled={disabled}
86+
className={cx(
87+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
88+
disabled
89+
? 'cursor-wait opacity-50'
90+
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
91+
)}
92+
title={title}
93+
type="button"
94+
data-testid={testId}
95+
>
96+
<Icon
97+
icon="system-uicons:reset"
98+
className={cx('w-3 h-3 text-gray-500 dark:text-gray-400', disabled && 'animate-spin')}
99+
/>
100+
<span className="text-xs text-gray-500 dark:text-gray-400">Refresh results</span>
101+
</button>
102+
</div>
103+
);
104+
};
105+
67106
const transformLabelsForSuggestions = (labelNames: string, shouldTrimPrefix = false): string => {
68107
const trimmedLabel = shouldTrimPrefix ? labelNames.split('.').pop() ?? labelNames : labelNames;
69108
return trimmedLabel;
@@ -79,25 +118,38 @@ const SuggestionsList = ({
79118
isLabelValuesLoading,
80119
shouldTrimPrefix = false,
81120
refetchLabelValues,
121+
refetchLabelNames,
82122
}: Props): JSX.Element => {
83123
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
84124
const {styles, attributes} = usePopper(inputRef, popperElement, {
85125
placement: 'bottom-start',
86126
});
87127
const [highlightedSuggestionIndex, setHighlightedSuggestionIndex] = useState<number>(-1);
88128
const [showSuggest, setShowSuggest] = useState(true);
89-
const [isRefetching, setIsRefetching] = useState(false);
129+
const [isRefetchingValues, setIsRefetchingValues] = useState(false);
130+
const [isRefetchingNames, setIsRefetchingNames] = useState(false);
90131

91-
const handleRefetch = useCallback(async () => {
92-
if (isRefetching) return;
132+
const handleRefetchValues = useCallback(async () => {
133+
if (isRefetchingValues) return;
93134

94-
setIsRefetching(true);
135+
setIsRefetchingValues(true);
95136
try {
96137
await refetchLabelValues();
97138
} finally {
98-
setIsRefetching(false);
139+
setIsRefetchingValues(false);
140+
}
141+
}, [refetchLabelValues, isRefetchingValues]);
142+
143+
const handleRefetchNames = useCallback(async () => {
144+
if (isRefetchingNames) return;
145+
146+
setIsRefetchingNames(true);
147+
try {
148+
await refetchLabelNames();
149+
} finally {
150+
setIsRefetchingNames(false);
99151
}
100-
}, [refetchLabelValues, isRefetching]);
152+
}, [refetchLabelNames, isRefetchingNames]);
101153

102154
const suggestionsLength =
103155
suggestions.literals.length + suggestions.labelNames.length + suggestions.labelValues.length;
@@ -227,6 +279,12 @@ const SuggestionsList = ({
227279
};
228280
}, [inputRef, highlightedSuggestionIndex, suggestions, handleKeyPress, handleKeyDown]);
229281

282+
useEffect(() => {
283+
if (suggestionsLength > 0 && focusedInput) {
284+
setShowSuggest(true);
285+
}
286+
}, [suggestionsLength, focusedInput]);
287+
230288
return (
231289
<>
232290
{suggestionsLength > 0 && (
@@ -247,9 +305,41 @@ const SuggestionsList = ({
247305
style={{width: inputRef?.offsetWidth}}
248306
className="absolute z-10 mt-1 max-h-[400px] overflow-auto rounded-md bg-gray-50 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-gray-900 sm:text-sm"
249307
>
250-
<div className="relative pb-12">
308+
<div
309+
className={cx('relative', {
310+
'pb-12': suggestions.labelNames.length === 0 && suggestions.literals.length === 0,
311+
})}
312+
>
251313
{isLabelNamesLoading ? (
252314
<LoadingSpinner />
315+
) : suggestions.literals.length === 0 && suggestions.labelValues.length === 0 ? (
316+
<>
317+
{suggestions.labelNames.length === 0 ? (
318+
<div
319+
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
320+
{...testId(TEST_IDS.SUGGESTIONS_NO_RESULTS)}
321+
>
322+
No label names found
323+
</div>
324+
) : (
325+
suggestions.labelNames.map((l, i) => (
326+
<SuggestionItem
327+
isHighlighted={highlightedSuggestionIndex === i}
328+
onHighlight={() => setHighlightedSuggestionIndex(i)}
329+
onApplySuggestion={() => applySuggestionWithIndex(i)}
330+
onResetHighlight={() => resetHighlight()}
331+
value={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
332+
key={transformLabelsForSuggestions(l.value, shouldTrimPrefix)}
333+
/>
334+
))
335+
)}
336+
<RefreshButton
337+
onClick={() => void handleRefetchNames()}
338+
disabled={isRefetchingNames}
339+
title="Refresh label names"
340+
testId="suggestions-refresh-names-button"
341+
/>
342+
</>
253343
) : (
254344
<>
255345
{suggestions.labelNames.map((l, i) => (
@@ -287,7 +377,7 @@ const SuggestionsList = ({
287377
{suggestions.labelValues.length === 0 ? (
288378
<div
289379
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
290-
data-testid="suggestions-no-results"
380+
{...testId(TEST_IDS.SUGGESTIONS_NO_RESULTS)}
291381
>
292382
No label values found
293383
</div>
@@ -314,36 +404,12 @@ const SuggestionsList = ({
314404
/>
315405
))
316406
)}
317-
<div className="absolute w-full flex items-center justify-center bottom-0 px-3 py-2 bg-gray-50 dark:bg-gray-800">
318-
<button
319-
onClick={e => {
320-
e.preventDefault();
321-
e.stopPropagation();
322-
void handleRefetch();
323-
}}
324-
disabled={isRefetching}
325-
className={cx(
326-
'p-1 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
327-
isRefetching
328-
? 'cursor-wait opacity-50'
329-
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
330-
)}
331-
title="Refresh label values"
332-
type="button"
333-
data-testid="suggestions-refresh-button"
334-
>
335-
<Icon
336-
icon="system-uicons:reset"
337-
className={cx(
338-
'w-3 h-3 text-gray-500 dark:text-gray-400',
339-
isRefetching && 'animate-spin'
340-
)}
341-
/>
342-
<span className="text-xs text-gray-500 dark:text-gray-400">
343-
Refresh results
344-
</span>
345-
</button>
346-
</div>
407+
<RefreshButton
408+
onClick={() => void handleRefetchValues()}
409+
disabled={isRefetchingValues}
410+
title="Refresh label values"
411+
testId="suggestions-refresh-values-button"
412+
/>
347413
</>
348414
) : (
349415
suggestions.labelValues.map((l, i) => (

ui/packages/shared/profile/src/MatchersInput/index.tsx

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ export interface ILabelNamesResult {
4646
interface UseLabelNames {
4747
result: ILabelNamesResult;
4848
loading: boolean;
49+
refetch: () => void;
4950
}
5051

5152
export const useLabelNames = (
@@ -57,7 +58,7 @@ export const useLabelNames = (
5758
): UseLabelNames => {
5859
const metadata = useGrpcMetadata();
5960

60-
const {data, isLoading, error} = useGrpcQuery<LabelsResponse>({
61+
const {data, isLoading, error, refetch} = useGrpcQuery<LabelsResponse>({
6162
key: ['labelNames', profileType, match?.join(','), start, end],
6263
queryFn: async signal => {
6364
const request: LabelsRequest = {match: match !== undefined ? match : []};
@@ -77,7 +78,13 @@ export const useLabelNames = (
7778
},
7879
});
7980

80-
return {result: {response: data, error: error as Error}, loading: isLoading};
81+
return {
82+
result: {response: data, error: error as Error},
83+
loading: isLoading,
84+
refetch: () => {
85+
void refetch();
86+
},
87+
};
8188
};
8289

8390
interface UseLabelValues {
@@ -163,6 +170,7 @@ const MatchersInput = ({
163170
setCurrentLabelName,
164171
shouldHandlePrefixes,
165172
refetchLabelValues,
173+
refetchLabelNames,
166174
} = useLabels();
167175

168176
const value = currentQuery.matchersString();
@@ -333,9 +341,12 @@ const MatchersInput = ({
333341
inputRef={inputRef.current}
334342
runQuery={runQuery}
335343
focusedInput={focusedInput}
336-
isLabelValuesLoading={isLabelValuesLoading && lastCompleted.type === 'literal'}
344+
isLabelValuesLoading={
345+
isLabelValuesLoading && lastCompleted.type === 'literal' && lastCompleted.value !== ','
346+
}
337347
shouldTrimPrefix={shouldHandlePrefixes}
338348
refetchLabelValues={refetchLabelValues}
349+
refetchLabelNames={refetchLabelNames}
339350
/>
340351
</div>
341352
);

ui/packages/shared/profile/src/SimpleMatchers/Select.tsx

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import cx from 'classnames';
1818
import levenshtein from 'fast-levenshtein';
1919

2020
import {Button, DividerWithLabel, useParcaContext} from '@parca/components';
21+
import {TEST_IDS, testId} from '@parca/test-utils/dist/test-ids';
2122

2223
export interface SelectElement {
2324
active: JSX.Element;
@@ -55,7 +56,7 @@ interface CustomSelectProps {
5556
searchable?: boolean;
5657
onButtonClick?: () => void;
5758
editable?: boolean;
58-
refetchLabelValues?: () => void;
59+
refetchValues?: () => void;
5960
showLoadingInButton?: boolean;
6061
hasRefreshButton?: boolean;
6162
}
@@ -76,7 +77,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
7677
searchable = false,
7778
onButtonClick,
7879
editable = false,
79-
refetchLabelValues,
80+
refetchValues,
8081
showLoadingInButton = false,
8182
hasRefreshButton = false,
8283
}) => {
@@ -91,15 +92,15 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
9192
const optionRefs = useRef<Array<HTMLElement | null>>([]);
9293

9394
const handleRefetch = useCallback(async () => {
94-
if (refetchLabelValues == null || isRefetching) return;
95+
if (refetchValues == null || isRefetching) return;
9596

9697
setIsRefetching(true);
9798
try {
98-
await refetchLabelValues();
99+
await refetchValues();
99100
} finally {
100101
setIsRefetching(false);
101102
}
102-
}, [refetchLabelValues, isRefetching]);
103+
}, [refetchValues, isRefetching]);
103104

104105
let items: TypedSelectItem[] = [];
105106
if (itemsProp[0] != null && 'type' in itemsProp[0]) {
@@ -212,7 +213,10 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
212213
const renderSelection = (selection: SelectItem | string | undefined): string | JSX.Element => {
213214
if (showLoadingInButton && loading === true && selectedKey === '') {
214215
return (
215-
<span className="flex items-center gap-2" data-testid="label-value-loading-indicator">
216+
<span
217+
className="flex items-center gap-2"
218+
{...testId(TEST_IDS.LABEL_VALUE_LOADING_INDICATOR)}
219+
>
216220
<Icon icon="svg-spinners:ring-resize" className="w-4 h-4" />
217221
<span>Loading...</span>
218222
</span>
@@ -334,8 +338,8 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
334338
</div>
335339
</div>
336340
)}
337-
{refetchLabelValues !== undefined && loading !== true && (
338-
<div className="absolute w-full bottom-0 px-3 bg-gray-50 dark:bg-gray-800">
341+
{refetchValues !== undefined && loading !== true && (
342+
<div className="absolute w-full flex items-center justify-center bottom-0 px-3 bg-gray-50 dark:bg-gray-900">
339343
<button
340344
onClick={e => {
341345
e.preventDefault();
@@ -344,14 +348,14 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
344348
}}
345349
disabled={isRefetching}
346350
className={cx(
347-
'p-1 flex items-center gap-1 rounded-full transition-all duration-200 w-full justify-center',
351+
'py-1 px-2 flex items-center gap-1 rounded-full transition-all duration-200 w-auto justify-center',
348352
isRefetching
349353
? 'cursor-wait opacity-50'
350354
: 'hover:bg-gray-200 dark:hover:bg-gray-700 cursor-pointer'
351355
)}
352356
title="Refresh label values"
353357
type="button"
354-
data-testid="label-value-refresh-button"
358+
{...testId(TEST_IDS.LABEL_VALUE_REFRESH_BUTTON)}
355359
>
356360
<Icon
357361
icon="system-uicons:reset"
@@ -369,7 +373,7 @@ const CustomSelect: React.FC<CustomSelectProps> = ({
369373
) : groupedFilteredItems.length === 0 ? (
370374
<div
371375
className="px-4 py-3 text-sm text-gray-500 dark:text-gray-400 text-center"
372-
data-testid="label-value-no-results"
376+
{...testId(TEST_IDS.LABEL_VALUE_NO_RESULTS)}
373377
>
374378
No values found
375379
</div>

ui/packages/shared/profile/src/SimpleMatchers/index.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,12 @@ const SimpleMatchers = ({
203203
[setMatchersString]
204204
);
205205

206-
const {labelNameOptions, isLoading: labelNamesLoading, refetchLabelValues} = useLabels();
206+
const {
207+
labelNameOptions,
208+
isLoading: labelNamesLoading,
209+
refetchLabelValues,
210+
refetchLabelNames,
211+
} = useLabels();
207212

208213
// Helper to ensure selected label name is in the options (for page load before API returns)
209214
const getLabelNameOptionsWithSelected = useCallback(
@@ -451,6 +456,8 @@ const SimpleMatchers = ({
451456
loading={labelNamesLoading}
452457
searchable={true}
453458
{...testId(TEST_IDS.LABEL_NAME_SELECT)}
459+
refetchValues={refetchLabelNames}
460+
hasRefreshButton={true}
454461
/>
455462
<Select
456463
items={operatorOptions}
@@ -478,7 +485,7 @@ const SimpleMatchers = ({
478485
onButtonClick={() => handleLabelValueClick(index)}
479486
editable={isRowRegex(row)}
480487
{...testId(TEST_IDS.LABEL_VALUE_SELECT)}
481-
refetchLabelValues={refetchLabelValues}
488+
refetchValues={refetchLabelValues}
482489
showLoadingInButton={true}
483490
hasRefreshButton={true}
484491
/>

0 commit comments

Comments
 (0)