Skip to content

Commit 5a3a3bd

Browse files
pr feedback
1 parent 9c8a0f0 commit 5a3a3bd

6 files changed

Lines changed: 166 additions & 149 deletions

File tree

packages/api/src/controllers/pinnedFilter.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ export async function getPinnedFilters(
1414
return PinnedFilterModel.findOne({
1515
team: new mongoose.Types.ObjectId(teamId),
1616
source: new mongoose.Types.ObjectId(sourceId),
17-
user: null,
1817
});
1918
}
2019

@@ -29,7 +28,6 @@ export async function updatePinnedFilters(
2928
const filter = {
3029
team: new mongoose.Types.ObjectId(teamId),
3130
source: new mongoose.Types.ObjectId(sourceId),
32-
user: null,
3331
};
3432

3533
return PinnedFilterModel.findOneAndUpdate(

packages/api/src/models/pinnedFilter.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ interface IPinnedFilter {
77
_id: ObjectId;
88
team: ObjectId;
99
source: ObjectId;
10-
user: ObjectId | null; // null = team-level, non-null = personal
1110
fields: string[];
1211
filters: PinnedFiltersValue;
1312
createdAt: Date;
@@ -26,11 +25,6 @@ const PinnedFilterSchema = new Schema<IPinnedFilter>(
2625
required: true,
2726
ref: 'Source',
2827
},
29-
user: {
30-
type: mongoose.Schema.Types.ObjectId,
31-
default: null,
32-
ref: 'User',
33-
},
3428
fields: {
3529
type: [String],
3630
default: [],
@@ -46,9 +40,8 @@ const PinnedFilterSchema = new Schema<IPinnedFilter>(
4640
},
4741
);
4842

49-
// One document per team+source+user combination
50-
// user=null means team-level pins
51-
PinnedFilterSchema.index({ team: 1, source: 1, user: 1 }, { unique: true });
43+
// One document per team+source combination
44+
PinnedFilterSchema.index({ team: 1, source: 1 }, { unique: true });
5245

5346
export default mongoose.model<IPinnedFilter>(
5447
'PinnedFilter',

packages/api/src/routers/api/pinnedFilters.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ router.get(
2727
async (req, res, next) => {
2828
try {
2929
const { teamId } = getNonNullUserWithTeam(req);
30-
const source = req.query.source as string;
30+
const { source } = req.query;
3131

3232
// Verify the source belongs to this team
3333
const sourceDoc = await getSource(teamId.toString(), source);
@@ -68,9 +68,7 @@ router.put(
6868
async (req, res, next) => {
6969
try {
7070
const { teamId } = getNonNullUserWithTeam(req);
71-
const source = req.body.source as string;
72-
const fields = req.body.fields as string[];
73-
const filters = req.body.filters as Record<string, (string | boolean)[]>;
71+
const { source, fields, filters } = req.body;
7472

7573
// Verify the source belongs to this team
7674
const sourceDoc = await getSource(teamId.toString(), source);

packages/app/src/components/DBSearchPageFilters.tsx

Lines changed: 38 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -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

5248
import { IS_CLICKHOUSE_BUILD } from '@/config';
@@ -72,6 +68,10 @@ import { mergePath, useLocalStorage } from '@/utils';
7268

7369
import { FilterSettingsPanel } from './DBSearchPageFilters/FilterSettingsPopover';
7470
import { NestedFilterGroup } from './DBSearchPageFilters/NestedFilterGroup';
71+
import {
72+
PinShareIndicator,
73+
PinShareMenu,
74+
} from './DBSearchPageFilters/PinShareMenu';
7575
import { SharedFiltersSection } from './DBSearchPageFilters/SharedFilters';
7676
import { 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-
285167
type 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}

packages/app/src/components/DBSearchPageFilters/NestedFilterGroup.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import { useVirtualizer } from '@tanstack/react-virtual';
44

55
import { FilterState } from '@/searchFilters';
66

7-
import {
8-
type FieldPinHandlers,
9-
FilterGroup,
10-
type ValuePinHandlers,
11-
} from '../DBSearchPageFilters';
7+
import { FilterGroup } from '../DBSearchPageFilters';
128

139
import classes from '../../../styles/SearchPage.module.scss';
1410

@@ -32,6 +28,7 @@ type NestedFilterGroupProps = {
3228
isFieldPinned?: (key: string) => boolean;
3329
onToggleSharedFieldPin?: (key: string) => void;
3430
isSharedFieldPinned?: (key: string) => boolean;
31+
showFilterCounts?: boolean;
3532
onColumnToggle?: (column: string) => void;
3633
displayedColumns?: string[];
3734
onLoadMore: (key: string) => void;
@@ -62,6 +59,7 @@ export const NestedFilterGroup = ({
6259
isFieldPinned,
6360
onToggleSharedFieldPin,
6461
isSharedFieldPinned,
62+
showFilterCounts,
6563
onColumnToggle,
6664
displayedColumns,
6765
onLoadMore,
@@ -255,6 +253,7 @@ export const NestedFilterGroup = ({
255253
loadMoreLoading[child.key] || false
256254
}
257255
hasLoadedMore={hasLoadedMore[child.key] || false}
256+
showFilterCounts={showFilterCounts}
258257
isDefaultExpanded={childHasSelections}
259258
chartConfig={chartConfig}
260259
isLive={isLive}

0 commit comments

Comments
 (0)