Skip to content

Commit be7f0f8

Browse files
authored
review: Fix side panel filtering
1 parent 08c4132 commit be7f0f8

8 files changed

Lines changed: 95 additions & 35 deletions

File tree

‎packages/app/src/DBDashboardPage.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -672,12 +672,15 @@ const Tile = ({
672672
() =>
673673
onFilter && source && (isLogSource(source) || isTraceSource(source))
674674
? {
675+
// Only the cell popover passes an action; the side panel's
676+
// "Add to filters" passes none and shouldn't toggle a condition off.
675677
onPropertyAddClick: (key, value, action) =>
676678
onFilter(
677679
[{ key, value: String(value) }],
678-
action === 'exclude' ? 'exclude' : 'include',
680+
action === 'include' || action === 'exclude' ? action : 'add',
679681
),
680682
source,
683+
isAdhocFilterTarget: true,
681684
}
682685
: undefined,
683686
[onFilter, source],
@@ -1392,7 +1395,6 @@ const Tile = ({
13921395
variant="default"
13931396
errorVariant="collapsible"
13941397
context={searchRowContext}
1395-
isAdhocFilterTarget
13961398
/>
13971399
</ChartContainer>
13981400
)}

‎packages/app/src/components/DBRowSidePanel.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,11 @@ export type RowSidePanelContextProps = {
113113
setChildModalOpen?: (open: boolean) => void;
114114
source?: TLogSource | TTraceSource;
115115
onOpenLinkedTrace?: (link: SpanLinkData) => void;
116+
/**
117+
* onPropertyAddClick updates ad hoc dashboard filters. Tables shape
118+
* each cell's key and value for that with getAdhocCellCondition.
119+
*/
120+
isAdhocFilterTarget?: boolean;
116121
};
117122

118123
export const RowSidePanelContext = createContext<RowSidePanelContextProps>({});

‎packages/app/src/components/DBRowTable.tsx‎

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,6 @@ export const RawLogTable = memo(
378378
showExpandButton = true,
379379
enableRowSelection = false,
380380
selectionResetKey,
381-
isAdhocFilterTarget,
382381
onSelectedRowsChange,
383382
getRowWhere,
384383
variant = 'default',
@@ -414,12 +413,6 @@ export const RawLogTable = memo(
414413
showExpandButton?: boolean;
415414
enableRowSelection?: boolean;
416415
selectionResetKey?: string;
417-
/**
418-
* Cell include / exclude actions update ad hoc dashboard filters, which
419-
* apply outside this query and compare `toString(key)` with the value. See
420-
* getAdhocCellCondition for how that shapes each cell's key and value.
421-
*/
422-
isAdhocFilterTarget?: boolean;
423416
onSelectedRowsChange?: (hasSelectedRows: boolean) => void;
424417
renderRowDetails?: (row: {
425418
id: string;
@@ -521,6 +514,8 @@ export const RawLogTable = memo(
521514
// Get the alias map from the config so we resolve correct column ids
522515
const { data: aliasMap } = useAliasMapFromChartConfig(config);
523516

517+
const { isAdhocFilterTarget } = use(RowSidePanelContext);
518+
524519
const getCellFilter = useCallback(
525520
(
526521
column: string | undefined,
@@ -1653,7 +1648,6 @@ function DBSqlRowTableComponent({
16531648
showExpandButton = true,
16541649
enableRowSelection = false,
16551650
selectionResetKey,
1656-
isAdhocFilterTarget,
16571651
onSelectedRowsChange,
16581652
renderRowDetails,
16591653
onSortingChange,
@@ -1688,7 +1682,6 @@ function DBSqlRowTableComponent({
16881682
showExpandButton?: boolean;
16891683
enableRowSelection?: boolean;
16901684
selectionResetKey?: string;
1691-
isAdhocFilterTarget?: boolean;
16921685
onSelectedRowsChange?: (hasSelectedRows: boolean) => void;
16931686
initialSortBy?: SortingState;
16941687
onSortingChange?: (v: SortingState | null) => void;
@@ -2012,7 +2005,6 @@ function DBSqlRowTableComponent({
20122005
showExpandButton={showExpandButton}
20132006
enableRowSelection={enableRowSelection}
20142007
selectionResetKey={selectionResetKey}
2015-
isAdhocFilterTarget={isAdhocFilterTarget}
20162008
onSelectedRowsChange={onSelectedRowsChange}
20172009
enableSorting={true}
20182010
onSortingChange={_onSortingChange}

‎packages/app/src/components/DBSqlRowTableWithSidebar.tsx‎

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ interface Props {
5454
// it. Enabled by default; pass `false` to opt out.
5555
closeOnClickOutside?: boolean;
5656
keepOpenSelector?: string;
57-
isAdhocFilterTarget?: boolean;
5857
}
5958

6059
// Clicking the results table (selecting/switching rows, scrolling) keeps the
@@ -85,7 +84,6 @@ export default function DBSqlRowTableWithSideBar({
8584
onResolvedColumnsChange,
8685
closeOnClickOutside = true,
8786
keepOpenSelector = DEFAULT_KEEP_OPEN_SELECTOR,
88-
isAdhocFilterTarget,
8987
}: Props) {
9088
const { data: sourceData } = useSource({ id: sourceId });
9189
const [rowId, setRowId] = useQueryState('rowWhere', parseAsStringEncoded);
@@ -165,7 +163,6 @@ export default function DBSqlRowTableWithSideBar({
165163
selectionResetKey={selectionResetKey}
166164
onSelectedRowsChange={onSelectedRowsChange}
167165
onResolvedColumnsChange={onResolvedColumnsChange}
168-
isAdhocFilterTarget={isAdhocFilterTarget}
169166
/>
170167
</RowSidePanelContext>
171168
);

‎packages/app/tests/e2e/features/dashboard-adhoc-click-filters.spec.ts‎

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
INTERESTING_FILTER_KEYS_SOURCE_NAME,
1414
} from '../utils/constants';
1515
import {
16+
adhocConditionsParam,
1617
expectFiltersParam,
1718
type FilterEntry,
1819
filtersParam,
@@ -256,22 +257,60 @@ test.describe(
256257
{ name: 'Conditions', sources: [DEFAULT_LOGS_SOURCE_NAME] },
257258
]);
258259

259-
await new TableComponent(page).clickFirstRow();
260-
await new SidePanelComponent(
261-
page,
262-
'row-side-panel',
263-
).addParsedFieldToFilter('ServiceName');
264-
await expect
265-
.poll(() => filtersParam(page))
266-
.toEqual([
267-
{
268-
type: 'adhoc',
269-
name: 'Conditions',
270-
conditions: [
271-
{ key: 'ServiceName', operator: '=', value: expect.any(String) },
272-
],
273-
},
274-
]);
260+
const sidePanel = new SidePanelComponent(page, 'row-side-panel');
261+
const serviceCondition = {
262+
key: 'ServiceName',
263+
operator: '=',
264+
value: expect.any(String),
265+
};
266+
const expectServiceCondition = () =>
267+
expect
268+
.poll(() => filtersParam(page))
269+
.toEqual([
270+
{
271+
type: 'adhoc',
272+
name: 'Conditions',
273+
conditions: [serviceCondition],
274+
},
275+
]);
276+
277+
await test.step('Add to filters adds a condition', async () => {
278+
await new TableComponent(page).clickFirstRow();
279+
await sidePanel.addParsedFieldToFilter('ServiceName');
280+
await expectServiceCondition();
281+
});
282+
283+
await test.step('Adding it again keeps the condition', async () => {
284+
await sidePanel.addParsedFieldToFilter('ServiceName');
285+
await expectServiceCondition();
286+
});
287+
288+
await test.step('Surrounding context cells are shaped like tile cells', async () => {
289+
const [{ value: serviceName }] = adhocConditionsParam(
290+
page,
291+
'Conditions',
292+
);
293+
await sidePanel.clickTab('context');
294+
const contextTable = sidePanel.contextTab.getByTestId(
295+
'search-results-table',
296+
);
297+
const includeAction = contextTable.getByRole('button', {
298+
name: 'Toggle filter for this value',
299+
});
300+
await contextTable
301+
.getByText(serviceName, { exact: true })
302+
.first()
303+
.hover();
304+
await expect(includeAction.last()).toBeVisible();
305+
await contextTable
306+
.getByText(/\d:\d{2}:\d{2}\.\d{3}/)
307+
.first()
308+
.hover();
309+
await expect(
310+
contextTable.getByRole('button', { name: 'Copy field value' }).last(),
311+
).toBeVisible();
312+
await expect(includeAction).toHaveCount(0);
313+
});
275314
});
276315

277316
test('offers filter actions only where an ad hoc filter applies', async () => {

‎packages/app/tests/e2e/utils/filters-param.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ export const filtersParam = (page: Page): FilterEntry[] | null => {
2626
return JSON.parse(decodeURIComponent(raw));
2727
};
2828

29+
/** The conditions of ad hoc filter `name` in the `filters=` param. */
30+
export const adhocConditionsParam = (page: Page, name: string) =>
31+
filtersParam(page)?.find(
32+
(entry): entry is Extract<FilterEntry, { type: 'adhoc' }> =>
33+
entry.type === 'adhoc' && entry.name === name,
34+
)?.conditions ?? [];
35+
2936
/** Wait for the param to settle on `expected` — nuqs writes it asynchronously. */
3037
export const expectFiltersParam = async (
3138
page: Page,

‎packages/common-utils/src/__tests__/filters.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1884,6 +1884,23 @@ describe('filters', () => {
18841884
).toEqual([svc('=')]);
18851885
});
18861886

1887+
it('adds a value without toggling an existing condition off', () => {
1888+
expect(
1889+
mergeAdhocConditions(
1890+
[svc('='), level],
1891+
[{ key: 'ServiceName', value: 'api' }],
1892+
'add',
1893+
),
1894+
).toEqual([svc('='), level]);
1895+
expect(
1896+
mergeAdhocConditions(
1897+
[svc('!=')],
1898+
[{ key: 'ServiceName', value: 'api' }],
1899+
'add',
1900+
),
1901+
).toEqual([svc('=')]);
1902+
});
1903+
18871904
it('toggles an excluded value off', () => {
18881905
expect(
18891906
mergeAdhocConditions(

‎packages/common-utils/src/filters.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,7 +1004,7 @@ export function adhocConditionsToFilters(
10041004
}
10051005

10061006
/** How a value clicked in a tile updates an ad hoc filter. */
1007-
export type AdhocConditionAction = 'include' | 'exclude';
1007+
export type AdhocConditionAction = 'include' | 'exclude' | 'add';
10081008

10091009
/** A clicked `key = value` pair, before an action gives it an operator. */
10101010
export type AdhocConditionEntry = Pick<AdhocFilterCondition, 'key' | 'value'>;
@@ -1016,13 +1016,14 @@ export type AdhocConditionEntry = Pick<AdhocFilterCondition, 'key' | 'value'>;
10161016
* `include` / `exclude` add a condition per entry, replacing the opposite
10171017
* condition on the same key and value. When every entry's condition already
10181018
* exists, they are removed instead, so a second click undoes the first.
1019+
* `add` is `include` without the undo, for actions labeled as adding.
10191020
*/
10201021
export function mergeAdhocConditions(
10211022
existingConditions: AdhocFilterCondition[],
10221023
newConditions: AdhocConditionEntry[],
10231024
action: AdhocConditionAction,
10241025
): AdhocFilterCondition[] {
1025-
const operator: AdhocFilterOperator = action === 'include' ? '=' : '!=';
1026+
const operator: AdhocFilterOperator = action === 'exclude' ? '!=' : '=';
10261027
const matches = (
10271028
condition: AdhocFilterCondition,
10281029
entry: AdhocConditionEntry,
@@ -1035,14 +1036,14 @@ export function mergeAdhocConditions(
10351036
const isApplied = newConditions.every(entry =>
10361037
existingConditions.some(condition => matches(condition, entry, operator)),
10371038
);
1038-
if (isApplied) {
1039+
if (isApplied && action !== 'add') {
10391040
return existingConditions.filter(
10401041
condition =>
10411042
!newConditions.some(entry => matches(condition, entry, operator)),
10421043
);
10431044
}
10441045

1045-
const opposite: AdhocFilterOperator = action === 'include' ? '!=' : '=';
1046+
const opposite: AdhocFilterOperator = action === 'exclude' ? '=' : '!=';
10461047
const kept = existingConditions.filter(
10471048
condition =>
10481049
!newConditions.some(entry => matches(condition, entry, opposite)),

0 commit comments

Comments
 (0)