Skip to content

Commit a682ce1

Browse files
authored
feat: Render adhoc dashboard filter inputs (#3343)
1 parent 43b09a7 commit a682ce1

33 files changed

Lines changed: 1852 additions & 138 deletions

‎.changeset/adhoc-filter-inputs.md‎

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: Render adhoc dashboard filter inputs

‎packages/app/src/DBDashboardPage.tsx‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import {
3535
isTimeSeriesDisplayType,
3636
} from '@hyperdx/common-utils/dist/core/utils';
3737
import { getBlockingRequiredFilterNames } from '@hyperdx/common-utils/dist/dashboardFilterValues';
38-
import { isAdhocFilter } from '@hyperdx/common-utils/dist/filters';
3938
import {
4039
displayTypeRequiresSource,
4140
isBuilderChartConfig,
@@ -1979,14 +1978,12 @@ function DBDashboardPage({
19791978

19801979
const [showFiltersModal, setShowFiltersModal] = useState(false);
19811980

1982-
// Ad hoc filters aren't rendered or applied yet
1983-
const filters = useMemo(
1984-
() => (dashboard?.filters ?? []).filter(filter => !isAdhocFilter(filter)),
1985-
[dashboard?.filters],
1986-
);
1981+
const filters = useMemo(() => dashboard?.filters ?? [], [dashboard?.filters]);
19871982
const {
19881983
selectionByFilterId,
19891984
setFilterValue,
1985+
adhocConditionsByFilterId,
1986+
setAdhocConditions,
19901987
setFilterValueEntries,
19911988
filterValueEntries,
19921989
ignoredFilterExpressions,
@@ -3351,6 +3348,8 @@ function DBDashboardPage({
33513348
filters={filters}
33523349
selectionByFilterId={selectionByFilterId}
33533350
onSetFilterValue={setFilterValue}
3351+
adhocConditionsByFilterId={adhocConditionsByFilterId}
3352+
onSetAdhocConditions={setAdhocConditions}
33543353
dateRange={searchedTimeRange}
33553354
variables={variables}
33563355
/>
@@ -3524,7 +3523,7 @@ function DBDashboardPage({
35243523
<DashboardFiltersModal
35253524
opened={showFiltersModal}
35263525
onClose={() => setShowFiltersModal(false)}
3527-
filters={dashboard?.filters ?? []}
3526+
filters={filters}
35283527
onSaveFilter={handleSaveFilter}
35293528
onRemoveFilter={handleRemoveFilter}
35303529
isLoading={isSavingDashboard || isFetchingDashboard}

‎packages/app/src/DashboardFilters.tsx‎

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,28 @@ import {
44
getFilterBroadcastTarget,
55
getFilterVariableName,
66
getPendingFilterValuesVariables,
7+
isAdhocFilter,
78
isFilterGlobalRequirement,
89
isFilterRequired,
910
isFilterVariableEnabled,
1011
isQueryExpressionFilter,
1112
isStaticListFilter,
1213
} from '@hyperdx/common-utils/dist/filters';
1314
import {
15+
AdhocFilterCondition,
1416
ChartVariable,
1517
DashboardFilter,
1618
} from '@hyperdx/common-utils/dist/types';
1719
import { isFilterSingleSelect } from '@hyperdx/common-utils/dist/variables';
18-
import { Group, Stack, Text, Tooltip } from '@mantine/core';
19-
import { IconAlertTriangle, IconHelp, IconRefresh } from '@tabler/icons-react';
20+
import { Group, Stack, Text } from '@mantine/core';
21+
import { IconRefresh } from '@tabler/icons-react';
2022

23+
import { AdhocDashboardFilter } from './components/AdhocDashboardFilter/AdhocDashboardFilter';
24+
import {
25+
DashboardFilterLabel,
26+
FilterCaution,
27+
FilterEffect,
28+
} from './components/DashboardFilterLabel';
2129
import { FilterLinkToggle } from './components/FilterLinkToggle';
2230
import { VirtualMultiSelect } from './components/VirtualMultiSelect/VirtualMultiSelect';
2331
import { useDashboardFilterValues } from './hooks/useDashboardFilterValues';
@@ -65,9 +73,7 @@ export const getRequiredFilterTooltip = (filter: DashboardFilter): string =>
6573
* Describe what a filter does with the value you pick: broadcast it as a
6674
* condition, expose it as a variable, both, or neither.
6775
*/
68-
export const getFilterEffect = (
69-
filter: DashboardFilter,
70-
): { hasEffect: boolean; tooltip: string } => {
76+
export const getFilterEffect = (filter: DashboardFilter): FilterEffect => {
7177
const parts: string[] = [];
7278

7379
const broadcast = getFilterBroadcastTarget(filter);
@@ -99,25 +105,6 @@ export const getFilterEffect = (
99105
return { hasEffect: true, tooltip: parts.join(', ') };
100106
};
101107

102-
/** One of the caution icons a filter's label row can carry, with its tooltip. */
103-
const FilterCaution = ({
104-
label,
105-
testId,
106-
variant = 'warning',
107-
}: {
108-
label: string;
109-
testId: string;
110-
variant?: 'warning' | 'danger';
111-
}) => (
112-
<Tooltip label={label} withinPortal multiline maw={400}>
113-
<IconAlertTriangle
114-
size={12}
115-
color={`var(--color-text-${variant})`}
116-
data-testid={testId}
117-
/>
118-
</Tooltip>
119-
);
120-
121108
const DashboardFilterSelect = ({
122109
filter,
123110
onChange,
@@ -134,25 +121,7 @@ const DashboardFilterSelect = ({
134121

135122
return (
136123
<Stack gap={2}>
137-
<Group gap={4} align="center" wrap="nowrap">
138-
<Text size="xs" c="dimmed">
139-
{filter.name}
140-
</Text>
141-
<Tooltip label={effect.tooltip} withinPortal>
142-
{effect.hasEffect ? (
143-
<IconHelp
144-
size={12}
145-
color="var(--color-text-muted)"
146-
data-testid={`dashboard-filter-help-${filter.name}`}
147-
/>
148-
) : (
149-
<IconAlertTriangle
150-
size={12}
151-
color="var(--color-text-warning)"
152-
data-testid={`dashboard-filter-no-effect-${filter.name}`}
153-
/>
154-
)}
155-
</Tooltip>
124+
<DashboardFilterLabel name={filter.name} effect={effect}>
156125
{isMissingRequiredValue && (
157126
<FilterCaution
158127
label={getRequiredFilterTooltip(filter)}
@@ -175,7 +144,7 @@ const DashboardFilterSelect = ({
175144
variant="danger"
176145
/>
177146
)}
178-
</Group>
147+
</DashboardFilterLabel>
179148
<div style={{ width: 250 }}>
180149
<VirtualMultiSelect
181150
placeholder={value.length === 0 ? filter.name : undefined}
@@ -203,6 +172,11 @@ interface DashboardFilterProps {
203172
filters: DashboardFilter[];
204173
selectionByFilterId: ReadonlyMap<string, FilterSelection>;
205174
onSetFilterValue: (filterId: string, values: string[]) => void;
175+
adhocConditionsByFilterId?: ReadonlyMap<string, AdhocFilterCondition[]>;
176+
onSetAdhocConditions?: (
177+
filterId: string,
178+
conditions: AdhocFilterCondition[],
179+
) => void;
206180
dateRange: [Date, Date];
207181
/**
208182
* The dashboard's variables and their current selections. Defined only when
@@ -217,6 +191,8 @@ const DashboardFilters = ({
217191
dateRange,
218192
selectionByFilterId,
219193
onSetFilterValue,
194+
adhocConditionsByFilterId,
195+
onSetAdhocConditions,
220196
variables,
221197
}: DashboardFilterProps) => {
222198
// "Link" mode (opt-in, off by default): each dropdown's values are narrowed by
@@ -248,6 +224,20 @@ const DashboardFilters = ({
248224
return (
249225
<Group align="start">
250226
{filters.map(filter => {
227+
if (isAdhocFilter(filter)) {
228+
return (
229+
<AdhocDashboardFilter
230+
key={filter.id}
231+
filter={filter}
232+
conditions={adhocConditionsByFilterId?.get(filter.id) ?? []}
233+
onChange={conditions =>
234+
onSetAdhocConditions?.(filter.id, conditions)
235+
}
236+
dateRange={dateRange}
237+
effect={getFilterEffect(filter)}
238+
/>
239+
);
240+
}
251241
const queriedFilterValues = filterValuesById?.get(filter.id);
252242
const included = selectionByFilterId.get(filter.id)?.included;
253243
const selectedValues = included

‎packages/app/src/__tests__/DashboardFilters.test.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,23 @@ describe('getFilterEffect', () => {
117117
}).tooltip,
118118
).toEqual('Available as variable ($svc)');
119119
});
120+
121+
it('counts the sources an ad hoc filter broadcasts to', () => {
122+
expect(
123+
getFilterEffect({
124+
id: 'adhoc1',
125+
type: 'ADHOC',
126+
name: 'Conditions',
127+
sourceType: 'sql',
128+
sources: ['logs', 'traces'],
129+
isVariableEnabled: true,
130+
variableName: 'conds',
131+
}),
132+
).toEqual({
133+
hasEffect: true,
134+
tooltip: 'Filters 2 sources, available as variable ($conds)',
135+
});
136+
});
120137
});
121138

122139
describe('getPendingVariablesTooltip', () => {
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { useState } from 'react';
2+
import {
3+
AdhocDashboardFilter,
4+
AdhocFilterCondition,
5+
} from '@hyperdx/common-utils/dist/types';
6+
7+
import {
8+
FilterConditionEditor,
9+
FilterOperator,
10+
getFilterOperators,
11+
} from '@/components/FilterPill';
12+
13+
import { useAdhocFilterOptions } from './useAdhocFilterOptions';
14+
15+
type AdhocConditionEditorProps = {
16+
filter: AdhocDashboardFilter;
17+
dateRange: [Date, Date];
18+
/** The condition being edited; unset when adding one. */
19+
initial?: AdhocFilterCondition;
20+
onSubmit: (condition: AdhocFilterCondition) => void;
21+
};
22+
23+
/**
24+
* Pick a condition from the filter's keys and values. Mounted only while its
25+
* popover is open, so keys load on open and values once a key is picked.
26+
*/
27+
export function AdhocConditionEditor({
28+
filter,
29+
dateRange,
30+
initial,
31+
onSubmit,
32+
}: AdhocConditionEditorProps) {
33+
const [valueKey, setValueKey] = useState(initial?.key ?? '');
34+
const { keys, values } = useAdhocFilterOptions(filter, valueKey, dateRange);
35+
const isPromql = filter.sourceType === 'promql';
36+
37+
return (
38+
<FilterConditionEditor<FilterOperator>
39+
operators={getFilterOperators(filter.sourceType)}
40+
initial={initial}
41+
keyOptions={keys.data}
42+
isLoadingKeys={keys.isLoading}
43+
isKeysError={keys.isError}
44+
valueOptions={values.data}
45+
isLoadingValues={values.isLoading}
46+
isValuesError={values.isError}
47+
onKeyChange={setValueKey}
48+
keyLabel={isPromql ? 'Label' : 'Key'}
49+
keyPlaceholder={
50+
isPromql ? 'Select a label' : 'Select a column or map key'
51+
}
52+
onSubmit={onSubmit}
53+
data-testid={`adhoc-condition-editor-${filter.name}`}
54+
/>
55+
);
56+
}

0 commit comments

Comments
 (0)