@@ -19,7 +19,6 @@ import {
1919 Group ,
2020 Loader ,
2121 MantineStyleProps ,
22- Menu ,
2322 NumberInput ,
2423 ScrollArea ,
2524 Stack ,
@@ -39,14 +38,11 @@ import {
3938 IconChevronUp ,
4039 IconFilterOff ,
4140 IconMinus ,
42- IconPin ,
43- IconPinFilled ,
4441 IconPlus ,
4542 IconRefresh ,
4643 IconSearch ,
4744 IconShadow ,
4845 IconSitemap ,
49- IconUsers ,
5046} from '@tabler/icons-react' ;
5147
5248import { IS_CLICKHOUSE_BUILD } from '@/config' ;
@@ -72,6 +68,10 @@ import { mergePath, useLocalStorage } from '@/utils';
7268
7369import { FilterSettingsPanel } from './DBSearchPageFilters/FilterSettingsPopover' ;
7470import { NestedFilterGroup } from './DBSearchPageFilters/NestedFilterGroup' ;
71+ import {
72+ PinShareIndicator ,
73+ PinShareMenu ,
74+ } from './DBSearchPageFilters/PinShareMenu' ;
7575import { SharedFiltersSection } from './DBSearchPageFilters/SharedFilters' ;
7676import { groupFacetsByBaseName } from './DBSearchPageFilters/utils' ;
7777
@@ -164,124 +164,6 @@ const TextButton = ({
164164 ) ;
165165} ;
166166
167- /**
168- * Shared pin/share dropdown menu used on both value rows and group headers.
169- * Shows contextual actions: "Remove from Shared" / "Pin for me" / "Share with team"
170- * with the most relevant action first.
171- *
172- * Icon logic:
173- * - sharedPinned → IconUsers (people icon)
174- * - personalPinned → IconPinFilled
175- * - neither → IconPin (outline)
176- */
177- function PinShareMenu ( {
178- personalPinned,
179- sharedPinned,
180- onTogglePersonalPin,
181- onToggleSharedPin,
182- size = 14 ,
183- onChange,
184- 'data-testid' : dataTestId ,
185- 'aria-label' : ariaLabel ,
186- } : {
187- personalPinned : boolean ;
188- sharedPinned : boolean ;
189- onTogglePersonalPin : VoidFunction ;
190- onToggleSharedPin ?: VoidFunction ;
191- size ?: number ;
192- onChange ?: ( opened : boolean ) => void ;
193- 'data-testid' ?: string ;
194- 'aria-label' ?: string ;
195- } ) {
196- const isPinnedAny = personalPinned || sharedPinned ;
197-
198- // Personal pin icon takes priority over shared icon
199- const triggerIcon = personalPinned ? (
200- < IconPinFilled size = { size } />
201- ) : sharedPinned ? (
202- < IconUsers size = { size } />
203- ) : (
204- < IconPin size = { size } />
205- ) ;
206-
207- return (
208- < Menu
209- position = "right"
210- withArrow
211- shadow = "sm"
212- width = { 200 }
213- onChange = { onChange }
214- >
215- < Menu . Target >
216- < ActionIcon
217- size = "xs"
218- variant = "subtle"
219- color = "gray"
220- aria-label = { ariaLabel ?? ( isPinnedAny ? 'Unpin' : 'Pin' ) }
221- data-testid = { dataTestId }
222- >
223- { triggerIcon }
224- </ ActionIcon >
225- </ Menu . Target >
226- < Menu . Dropdown >
227- { onToggleSharedPin && sharedPinned && (
228- < Menu . Item
229- leftSection = { < IconUsers size = { 14 } /> }
230- onClick = { onToggleSharedPin }
231- fz = "xs"
232- >
233- Remove from Shared
234- </ Menu . Item >
235- ) }
236- < Menu . Item
237- leftSection = {
238- personalPinned ? < IconPinFilled size = { 14 } /> : < IconPin size = { 14 } />
239- }
240- onClick = { onTogglePersonalPin }
241- fz = "xs"
242- >
243- { personalPinned ? 'Unpin for me' : 'Pin for me' }
244- </ Menu . Item >
245- { onToggleSharedPin && ! sharedPinned && (
246- < Menu . Item
247- leftSection = { < IconUsers size = { 14 } /> }
248- onClick = { onToggleSharedPin }
249- fz = "xs"
250- >
251- Share with team
252- </ Menu . Item >
253- ) }
254- </ Menu . Dropdown >
255- </ Menu >
256- ) ;
257- }
258-
259- /**
260- * Small indicator icon shown persistently on pinned/shared values.
261- */
262- function PinShareIndicator ( {
263- personalPinned,
264- sharedPinned,
265- 'data-testid' : dataTestId ,
266- } : {
267- personalPinned : boolean ;
268- sharedPinned : boolean ;
269- 'data-testid' ?: string ;
270- } ) {
271- if ( ! personalPinned && ! sharedPinned ) return null ;
272-
273- // Personal pin icon takes priority over shared icon
274- return (
275- < Center me = "1px" >
276- { personalPinned ? (
277- < IconPinFilled size = { 12 } data-testid = { dataTestId } />
278- ) : (
279- < IconUsers size = { 12 } data-testid = { dataTestId } />
280- ) }
281- </ Center >
282- ) ;
283- }
284-
285167type FilterPercentageProps = {
286168 percentage : number ;
287169 isLoading ?: boolean ;
@@ -1291,15 +1173,23 @@ const DBSearchPageFiltersComponent = ({
12911173 field . type . includes ( 'LowCardinality' ) || // query only low cardinality fields by default
12921174 field . isMapSubField || // always include Map/JSON sub-fields (e.g. LogAttributes, ResourceAttributes keys)
12931175 Object . keys ( filterState ) . includes ( field . path ) || // keep selected fields
1294- isFieldPinned ( field . path ) , // keep pinned fields
1176+ isFieldPinned ( field . path ) || // keep personally pinned fields
1177+ isSharedFieldPinned ( field . path ) , // keep team-shared fields
12951178 )
12961179 . map ( ( { path } ) => path )
12971180 . filter (
12981181 path =>
12991182 ! [ 'body' , 'timestamp' , '_hdx_body' ] . includes ( path . toLowerCase ( ) ) ,
13001183 ) ;
13011184 return strings ;
1302- } , [ data , jsonColumns , filterState , showMoreFields , isFieldPinned ] ) ;
1185+ } , [
1186+ data ,
1187+ jsonColumns ,
1188+ filterState ,
1189+ showMoreFields ,
1190+ isFieldPinned ,
1191+ isSharedFieldPinned ,
1192+ ] ) ;
13031193
13041194 // Special case for live tail
13051195 const [ dateRange , setDateRange ] = useState < [ Date , Date ] > (
@@ -1413,19 +1303,37 @@ const DBSearchPageFiltersComponent = ({
14131303 const teamVals = pinnedFiltersApiData ?. team ?. filters [ key ] ?? [ ] ;
14141304 const dynamicValues = facetMap . get ( key ) ?? [ ] ;
14151305
1306+ let merged : ( string | boolean ) [ ] ;
14161307 if ( teamVals . length > 0 ) {
1417- const merged = [ ...teamVals ] ;
1308+ merged = [ ...teamVals ] ;
14181309 for ( const v of dynamicValues ) {
14191310 if ( ! merged . some ( existing => existing === v ) ) {
14201311 merged . push ( v ) ;
14211312 }
14221313 }
1423- return { key, value : merged } ;
1314+ } else {
1315+ // Field-only pin — show dynamic values
1316+ merged = [ ...dynamicValues ] ;
14241317 }
1425- // Field-only pin — show dynamic values
1426- return { key, value : dynamicValues } ;
1318+
1319+ // Merge in extra values loaded via "Load more"
1320+ const extraValues = extraFacets [ key ] ;
1321+ if ( extraValues && extraValues . length > 0 ) {
1322+ for ( const v of extraValues ) {
1323+ if ( ! merged . includes ( v ) ) {
1324+ merged . push ( v ) ;
1325+ }
1326+ }
1327+ }
1328+
1329+ return { key, value : merged } ;
14271330 } ) ;
1428- } , [ sharedFilterKeys , facetsWithPinnedValues , pinnedFiltersApiData ] ) ;
1331+ } , [
1332+ sharedFilterKeys ,
1333+ facetsWithPinnedValues ,
1334+ pinnedFiltersApiData ,
1335+ extraFacets ,
1336+ ] ) ;
14291337
14301338 const shownFacets = useMemo ( ( ) => {
14311339 const _facets : { key : string ; value : ( string | boolean ) [ ] } [ ] = [ ] ;
@@ -1656,6 +1564,7 @@ const DBSearchPageFiltersComponent = ({
16561564 isFieldPinned = { key => isFieldPinned ( key ) }
16571565 onToggleSharedFieldPin = { key => toggleSharedFieldPin ( key ) }
16581566 isSharedFieldPinned = { key => isSharedFieldPinned ( key ) }
1567+ showFilterCounts = { showFilterCounts }
16591568 onColumnToggle = { onColumnToggle }
16601569 displayedColumns = { displayedColumns }
16611570 onLoadMore = { loadMoreFilterValuesForKey }
0 commit comments