Skip to content

Commit d90319b

Browse files
committed
feat: Support click-to-adhoc-filter on search tiles
1 parent fc360c9 commit d90319b

18 files changed

Lines changed: 865 additions & 32 deletions
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@hyperdx/common-utils': patch
3+
'@hyperdx/app': patch
4+
---
5+
6+
feat: Filter search tile cells into adhoc dashboard filters

‎packages/app/src/DBDashboardPage.tsx‎

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ import {
3434
Granularity,
3535
isTimeSeriesDisplayType,
3636
} from '@hyperdx/common-utils/dist/core/utils';
37-
import { getBlockingRequiredFilterNames } from '@hyperdx/common-utils/dist/dashboardFilterValues';
37+
import {
38+
getAdhocFilterTargetsForTile,
39+
getBlockingRequiredFilterNames,
40+
} from '@hyperdx/common-utils/dist/dashboardFilterValues';
3841
import {
3942
getPromqlFilters,
4043
getSqlFilters,
@@ -143,6 +146,7 @@ import { DashboardKioskHeader } from '@/components/DashboardKioskHeader';
143146
import DashboardTableOfContents from '@/components/DashboardTableOfContents';
144147
import EditTimeChartForm from '@/components/DBEditTimeChartForm';
145148
import DBNumberChart from '@/components/DBNumberChart';
149+
import type { RowSidePanelContextProps } from '@/components/DBRowSidePanel';
146150
import DBTableChart from '@/components/DBTableChart';
147151
import { DBTimeChart } from '@/components/DBTimeChart';
148152
import { FavoriteButton } from '@/components/FavoriteButton';
@@ -172,6 +176,7 @@ import useDashboardContainers, {
172176
import { useDashboardKioskMode } from '@/hooks/useDashboardKioskMode';
173177
import { useReleaseAnnotations } from '@/hooks/useReleaseAnnotations';
174178
import { QueryAttributionProvider } from '@/queryAttribution';
179+
import type { TileFilterHandler } from '@/types';
175180
import { calculateNextTilePosition, makeId } from '@/utils/tilePositioning';
176181

177182
import ChartContainer, {
@@ -450,6 +455,7 @@ const Tile = ({
450455
showReleaseAnnotations,
451456
isLive,
452457
readOnly,
458+
onFilter,
453459

454460
// Properties forwarded by grid layout
455461
className,
@@ -485,6 +491,8 @@ const Tile = ({
485491
showReleaseAnnotations?: boolean;
486492
isLive?: boolean;
487493
readOnly?: boolean;
494+
/** Set when an ad hoc filter broadcasts to this tile; clicked values update it. */
495+
onFilter?: TileFilterHandler;
488496

489497
// Properties forwarded by grid layout
490498
className?: string;
@@ -662,6 +670,22 @@ const Tile = ({
662670
() => (filters ? getSqlFilters(filters) : undefined),
663671
[filters],
664672
);
673+
674+
// `source` keeps the handler off rows from other sources, e.g. a linked trace
675+
const searchRowContext = useMemo<RowSidePanelContextProps | undefined>(
676+
() =>
677+
onFilter && source && (isLogSource(source) || isTraceSource(source))
678+
? {
679+
onPropertyAddClick: (key, value, action) =>
680+
onFilter(
681+
[{ key, value: String(value) }],
682+
action === 'exclude' ? 'exclude' : 'include',
683+
),
684+
source,
685+
}
686+
: undefined,
687+
[onFilter, source],
688+
);
665689
const promqlFilters = useMemo(
666690
() => (filters ? getPromqlFilters(filters) : undefined),
667691
[filters],
@@ -1475,6 +1499,8 @@ const Tile = ({
14751499
queryKeyPrefix={'search'}
14761500
variant="default"
14771501
errorVariant="collapsible"
1502+
context={searchRowContext}
1503+
filterOnAliasedExpressions
14781504
/>
14791505
</ChartContainer>
14801506
)}
@@ -1556,6 +1582,7 @@ const Tile = ({
15561582
readOnly,
15571583
tileVariables,
15581584
variableNames,
1585+
searchRowContext,
15591586
],
15601587
);
15611588

@@ -2006,6 +2033,7 @@ function DBDashboardPage({
20062033
setFilterValue,
20072034
adhocConditionsByFilterId,
20082035
setAdhocConditions,
2036+
applyAdhocAction,
20092037
setFilterValueEntries,
20102038
filterValueEntries,
20112039
ignoredFilterExpressions,
@@ -2467,6 +2495,25 @@ function DBDashboardPage({
24672495
[where, whereLanguage, getFilterQueriesForSource],
24682496
);
24692497

2498+
const getTileFilterHandler = useCallback(
2499+
(
2500+
chart: Tile,
2501+
sourceId: string | undefined,
2502+
): TileFilterHandler | undefined => {
2503+
// The filter bar is hidden in kiosk mode, so a click would add conditions
2504+
// the viewer can't see or remove.
2505+
if (isKioskMode) return undefined;
2506+
const targetIds = getAdhocFilterTargetsForTile(
2507+
filters,
2508+
chart.config,
2509+
sourceId,
2510+
).map(filter => filter.id);
2511+
if (targetIds.length === 0) return undefined;
2512+
return (entries, action) => applyAdhocAction(targetIds, entries, action);
2513+
},
2514+
[isKioskMode, filters, applyAdhocAction],
2515+
);
2516+
24702517
const renderTileComponent = useCallback(
24712518
(chart: Tile) => {
24722519
// Resolve the tile's source ID so per-source-scoped filters can be
@@ -2486,6 +2533,7 @@ function DBDashboardPage({
24862533
isRefreshEnabled ? granularityOverride : (granularity ?? undefined)
24872534
}
24882535
filters={getTileFilters(tileSourceId)}
2536+
onFilter={getTileFilterHandler(chart, tileSourceId)}
24892537
variables={variables}
24902538
unsatisfiedRequiredFilters={unsatisfiedRequiredFilters}
24912539
onTimeRangeSelect={onTimeRangeSelect}
@@ -2588,6 +2636,7 @@ function DBDashboardPage({
25882636
showAlertAnnotations,
25892637
showReleaseAnnotations,
25902638
getTileFilters,
2639+
getTileFilterHandler,
25912640
variables,
25922641
unsatisfiedRequiredFilters,
25932642
moveTargetContainers,

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

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
Text,
1616
Tooltip,
1717
} from '@mantine/core';
18-
import { notifications } from '@mantine/notifications';
1918
import { IconPlus } from '@tabler/icons-react';
2019

2120
import {
@@ -25,6 +24,10 @@ import {
2524
import { FilterPill, getFilterOperatorDisplay } from '@/components/FilterPill';
2625

2726
import { AdhocConditionEditor } from './AdhocConditionEditor';
27+
import {
28+
notifyAdhocConditionTooLong,
29+
notifyAdhocFilterFull,
30+
} from './notifications';
2831

2932
type AdhocDashboardFilterProps = {
3033
filter: AdhocDashboardFilterType;
@@ -47,12 +50,7 @@ export function AdhocDashboardFilter({
4750

4851
const toggleAdding = () => {
4952
if (isAtLimit) {
50-
notifications.show({
51-
id: `adhoc-filter-full-${filter.id}`,
52-
color: 'yellow',
53-
title: `${filter.name} is full`,
54-
message: `A filter holds up to ${ADHOC_FILTER_MAX_CONDITIONS} conditions. Remove one to add another.`,
55-
});
53+
notifyAdhocFilterFull(filter);
5654
return;
5755
}
5856
setIsAdding(!isAdding);
@@ -61,13 +59,7 @@ export function AdhocDashboardFilter({
6159
// lose the filter's others on reload. Keep the editor open to fix it instead.
6260
const isStorable = (condition: AdhocFilterCondition) => {
6361
if (AdhocFilterConditionSchema.safeParse(condition).success) return true;
64-
const maxLength = AdhocFilterConditionSchema.shape.value.maxLength;
65-
notifications.show({
66-
id: `adhoc-filter-too-long-${filter.id}`,
67-
color: 'yellow',
68-
title: 'Condition is too long',
69-
message: `Keys and values can be up to ${maxLength?.toLocaleString()} characters.`,
70-
});
62+
notifyAdhocConditionTooLong(filter);
7163
return false;
7264
};
7365
const replace = (index: number, condition: AdhocFilterCondition) =>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import {
2+
ADHOC_FILTER_MAX_CONDITIONS,
3+
AdhocDashboardFilter,
4+
AdhocFilterConditionSchema,
5+
} from '@hyperdx/common-utils/dist/types';
6+
import { notifications } from '@mantine/notifications';
7+
8+
export function notifyAdhocFilterFull(
9+
filter: Pick<AdhocDashboardFilter, 'id' | 'name'>,
10+
) {
11+
notifications.show({
12+
id: `adhoc-filter-full-${filter.id}`,
13+
color: 'yellow',
14+
title: `${filter.name} is full`,
15+
message: `A filter holds up to ${ADHOC_FILTER_MAX_CONDITIONS} conditions. Remove one to add another.`,
16+
});
17+
}
18+
19+
export function notifyAdhocConditionTooLong(
20+
filter: Pick<AdhocDashboardFilter, 'id'>,
21+
) {
22+
const maxLength = AdhocFilterConditionSchema.shape.value.maxLength;
23+
notifications.show({
24+
id: `adhoc-filter-too-long-${filter.id}`,
25+
color: 'yellow',
26+
title: 'Condition is too long',
27+
message: `Keys and values can be up to ${maxLength?.toLocaleString()} characters.`,
28+
});
29+
}

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

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
isJSDataTypeJSONStringifiable,
3030
JSDataType,
3131
} from '@hyperdx/common-utils/dist/clickhouse';
32+
import { quoteResultColumnNameIfNeeded } from '@hyperdx/common-utils/dist/core/metadata';
3233
import { splitAndTrimWithBracket } from '@hyperdx/common-utils/dist/core/utils';
3334
import {
3435
DENOISE_NOISE_THRESHOLD,
@@ -377,6 +378,7 @@ export const RawLogTable = memo(
377378
showExpandButton = true,
378379
enableRowSelection = false,
379380
selectionResetKey,
381+
filterOnAliasedExpressions,
380382
onSelectedRowsChange,
381383
getRowWhere,
382384
variant = 'default',
@@ -397,7 +399,7 @@ export const RawLogTable = memo(
397399
tableId?: string;
398400
columnNameMap?: Record<string, string>;
399401
dedupRows?: boolean;
400-
columnTypeMap: Map<string, { _type: JSDataType | null }>;
402+
columnTypeMap: Map<string, { _type: JSDataType | null; type?: string }>;
401403

402404
isError?: boolean;
403405
error?: ClickHouseQueryError | Error;
@@ -412,6 +414,11 @@ export const RawLogTable = memo(
412414
showExpandButton?: boolean;
413415
enableRowSelection?: boolean;
414416
selectionResetKey?: string;
417+
/**
418+
* Pass the expression behind an aliased column as the cell's filter key,
419+
* for filters applied outside this query, where the alias isn't defined.
420+
*/
421+
filterOnAliasedExpressions?: boolean;
415422
onSelectedRowsChange?: (hasSelectedRows: boolean) => void;
416423
renderRowDetails?: (row: {
417424
id: string;
@@ -513,6 +520,19 @@ export const RawLogTable = memo(
513520
// Get the alias map from the config so we resolve correct column ids
514521
const { data: aliasMap } = useAliasMapFromChartConfig(config);
515522

523+
const getCellFilterKey = useCallback(
524+
(column: string | undefined) => {
525+
if (!filterOnAliasedExpressions || column == null) return column;
526+
// Cells hold ISO UTC timestamps, which never equal toString() of a
527+
// DateTime (space-separated, in the column's timezone).
528+
if (/\bDateTime/.test(columnTypeMap.get(column)?.type ?? '')) {
529+
return undefined;
530+
}
531+
return aliasMap?.[column] ?? quoteResultColumnNameIfNeeded(column);
532+
},
533+
[filterOnAliasedExpressions, aliasMap, columnTypeMap],
534+
);
535+
516536
// Reset scroll when live tail is enabled for the first time
517537
const prevIsLive = usePrevious(isLive);
518538
useEffect(() => {
@@ -1271,10 +1291,10 @@ export const RawLogTable = memo(
12711291
cellValue={cellValue}
12721292
wrapLinesEnabled={wrapLinesEnabled}
12731293
tableContainerRef={tableContainerRef}
1274-
columnName={
1294+
columnName={getCellFilterKey(
12751295
(cell.column.columnDef.meta as any)
1276-
?.column
1277-
}
1296+
?.column,
1297+
)}
12781298
isChart={
12791299
(cell.column.columnDef.meta as any)
12801300
?.column === '__hdx_pattern_trend'
@@ -1587,6 +1607,7 @@ function selectColumnMapWithoutAdditionalKeys(
15871607
string,
15881608
{
15891609
_type: JSDataType | null;
1610+
type: string;
15901611
}
15911612
> {
15921613
if (selectMeta == null || additionalKeysLength == null) {
@@ -1624,6 +1645,7 @@ function DBSqlRowTableComponent({
16241645
showExpandButton = true,
16251646
enableRowSelection = false,
16261647
selectionResetKey,
1648+
filterOnAliasedExpressions,
16271649
onSelectedRowsChange,
16281650
renderRowDetails,
16291651
onSortingChange,
@@ -1658,6 +1680,7 @@ function DBSqlRowTableComponent({
16581680
showExpandButton?: boolean;
16591681
enableRowSelection?: boolean;
16601682
selectionResetKey?: string;
1683+
filterOnAliasedExpressions?: boolean;
16611684
onSelectedRowsChange?: (hasSelectedRows: boolean) => void;
16621685
initialSortBy?: SortingState;
16631686
onSortingChange?: (v: SortingState | null) => void;
@@ -1981,6 +2004,7 @@ function DBSqlRowTableComponent({
19812004
showExpandButton={showExpandButton}
19822005
enableRowSelection={enableRowSelection}
19832006
selectionResetKey={selectionResetKey}
2007+
filterOnAliasedExpressions={filterOnAliasedExpressions}
19842008
onSelectedRowsChange={onSelectedRowsChange}
19852009
enableSorting={true}
19862010
onSortingChange={_onSortingChange}

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

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

5960
// Clicking the results table (selecting/switching rows, scrolling) keeps the
@@ -84,6 +85,7 @@ export default function DBSqlRowTableWithSideBar({
8485
onResolvedColumnsChange,
8586
closeOnClickOutside = true,
8687
keepOpenSelector = DEFAULT_KEEP_OPEN_SELECTOR,
88+
filterOnAliasedExpressions,
8789
}: Props) {
8890
const { data: sourceData } = useSource({ id: sourceId });
8991
const [rowId, setRowId] = useQueryState('rowWhere', parseAsStringEncoded);
@@ -163,6 +165,7 @@ export default function DBSqlRowTableWithSideBar({
163165
selectionResetKey={selectionResetKey}
164166
onSelectedRowsChange={onSelectedRowsChange}
165167
onResolvedColumnsChange={onResolvedColumnsChange}
168+
filterOnAliasedExpressions={filterOnAliasedExpressions}
166169
/>
167170
</RowSidePanelContext>
168171
);

0 commit comments

Comments
 (0)