@@ -19,6 +19,7 @@ import cx from 'classnames';
1919import { usePopper } from 'react-popper' ;
2020
2121import { useParcaContext } from '@parca/components' ;
22+ import { TEST_IDS , testId } from '@parca/test-utils' ;
2223
2324import SuggestionItem from './SuggestionItem' ;
2425
@@ -56,6 +57,7 @@ interface Props {
5657 isLabelValuesLoading : boolean ;
5758 shouldTrimPrefix : boolean ;
5859 refetchLabelValues : ( ) => void ;
60+ refetchLabelNames : ( ) => void ;
5961}
6062
6163const 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+
67106const 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 ) => (
0 commit comments