Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/adhoc-filter-suggestions-where.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@hyperdx/common-utils': minor
'@hyperdx/app': minor
---

feat: narrow ad hoc dashboard filter suggestions with a condition

An ad hoc filter on SQL sources can now carry a SQL or Lucene "Keys and values
filter", like a queried filter's dropdown values filter, that limits the rows
its keys and values are suggested from. It can reference the dashboard's other
variables. While it references a required filter with nothing selected, the ad
hoc filter can't add or edit conditions and names the filter it is waiting on.
Failed suggestions now show the database's error message.
1 change: 1 addition & 0 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3331,6 +3331,7 @@ function DBDashboardPage({
onSetAdhocConditions={setAdhocConditions}
dateRange={searchedTimeRange}
variables={variables}
unsatisfiedRequiredFilters={unsatisfiedRequiredFilters}
/>
)}
{/* Selection indicator */}
Expand Down
20 changes: 6 additions & 14 deletions packages/app/src/DashboardFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
DashboardFilterLabel,
FilterCaution,
FilterEffect,
getPendingVariablesTooltip,
} from './components/DashboardFilterLabel';
import { FilterLinkToggle } from './components/FilterLinkToggle';
import { VirtualMultiSelect } from './components/VirtualMultiSelect/VirtualMultiSelect';
Expand All @@ -46,20 +47,6 @@ interface DashboardFilterSelectProps {
pendingVariables?: string[];
}

/**
* Explain a dropdown query that may list nothing until one of the variables it
* depends on is selected. Only the bare SQL reference form gets here — the
* macros and Lucene both have an empty state that lists every value.
*/
export const getPendingVariablesTooltip = (
pendingVariables: string[],
): string => {
const names = pendingVariables.map(name => `$${name}`).join(', ');
return `Filter depends on ${names}, which ${
pendingVariables.length === 1 ? 'has' : 'have'
} no selected value.`;
};

/**
* Explain a required filter that has nothing selected, naming how far the block
* it imposes reaches.
Expand Down Expand Up @@ -184,6 +171,8 @@ interface DashboardFilterProps {
* as the gate for variable-specific copy.
*/
variables?: ChartVariable[];
/** Required filters with nothing selected, which can block ad hoc filters. */
unsatisfiedRequiredFilters?: DashboardFilter[];
}

const DashboardFilters = ({
Expand All @@ -194,6 +183,7 @@ const DashboardFilters = ({
adhocConditionsByFilterId,
onSetAdhocConditions,
variables,
unsatisfiedRequiredFilters,
}: DashboardFilterProps) => {
// "Link" mode (opt-in, off by default): each dropdown's values are narrowed by
// the others' selections. Off by default because contingent value lookups
Expand Down Expand Up @@ -235,6 +225,8 @@ const DashboardFilters = ({
}
dateRange={dateRange}
effect={getFilterEffect(filter)}
variables={variables}
unsatisfiedRequiredFilters={unsatisfiedRequiredFilters}
/>
);
}
Expand Down
7 changes: 2 additions & 5 deletions packages/app/src/__tests__/DashboardFilters.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { DashboardFilter } from '@hyperdx/common-utils/dist/types';

import {
getFilterEffect,
getPendingVariablesTooltip,
getRequiredFilterTooltip,
} from '@/DashboardFilters';
import { getPendingVariablesTooltip } from '@/components/DashboardFilterLabel';
import { getFilterEffect, getRequiredFilterTooltip } from '@/DashboardFilters';

const baseFilter: DashboardFilter = {
id: 'filter1',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from 'react';
import { ResolvedFilterValuesQuery } from '@hyperdx/common-utils/dist/filters';
import {
AdhocDashboardFilter,
AdhocFilterCondition,
Expand All @@ -15,6 +16,8 @@ import { useAdhocFilterOptions } from './useAdhocFilterOptions';
type AdhocConditionEditorProps = {
filter: AdhocDashboardFilter;
dateRange: [Date, Date];
/** The filter's `where`, variables expanded, narrowing the suggestions. */
suggestionsWhere?: ResolvedFilterValuesQuery;
/** The condition being edited; unset when adding one. */
initial?: AdhocFilterCondition;
onSubmit: (condition: AdhocFilterCondition) => void;
Expand All @@ -27,11 +30,17 @@ type AdhocConditionEditorProps = {
export function AdhocConditionEditor({
filter,
dateRange,
suggestionsWhere,
initial,
onSubmit,
}: AdhocConditionEditorProps) {
const [valueKey, setValueKey] = useState(initial?.key ?? '');
const { keys, values } = useAdhocFilterOptions(filter, valueKey, dateRange);
const { keys, values } = useAdhocFilterOptions(
filter,
valueKey,
dateRange,
suggestionsWhere,
);
const isPromql = filter.sourceType === 'promql';

return (
Expand All @@ -41,9 +50,11 @@ export function AdhocConditionEditor({
keyOptions={keys.data}
isLoadingKeys={keys.isLoading}
isKeysError={keys.isError}
keysErrorMessage={keys.error?.message}
valueOptions={values.data}
isLoadingValues={values.isLoading}
isValuesError={values.isError}
valuesErrorMessage={values.error?.message}
onKeyChange={setValueKey}
keyLabel={isPromql ? 'Label' : 'Key'}
keyPlaceholder={
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { useState } from 'react';
import { isAdhocConditionSupported } from '@hyperdx/common-utils/dist/filters';
import { useMemo, useState } from 'react';
import {
getDashboardVariableFilters,
getPendingFilterValuesVariables,
isAdhocConditionSupported,
resolveFilterValuesWhere,
} from '@hyperdx/common-utils/dist/filters';
import {
ADHOC_FILTER_MAX_CONDITIONS,
AdhocDashboardFilter as AdhocDashboardFilterType,
AdhocFilterCondition,
AdhocFilterConditionSchema,
ChartVariable,
DashboardFilter,
} from '@hyperdx/common-utils/dist/types';
import { getVariableReferences } from '@hyperdx/common-utils/dist/variables';
import {
ActionIcon,
CloseButton,
Expand All @@ -19,7 +27,9 @@ import { IconPlus } from '@tabler/icons-react';

import {
DashboardFilterLabel,
FilterCaution,
FilterEffect,
getPendingVariablesTooltip,
} from '@/components/DashboardFilterLabel';
import { FilterPill, getFilterOperatorDisplay } from '@/components/FilterPill';

Expand All @@ -35,6 +45,10 @@ type AdhocDashboardFilterProps = {
onChange: (conditions: AdhocFilterCondition[]) => void;
dateRange: [Date, Date];
effect: FilterEffect;
/** The dashboard's variables, which the filter's `where` may reference. */
variables?: ChartVariable[];
/** Required filters with nothing selected. */
unsatisfiedRequiredFilters?: DashboardFilter[];
};

/** A dashboard filter bar control holding one ad hoc filter's conditions. */
Expand All @@ -44,11 +58,46 @@ export function AdhocDashboardFilter({
onChange,
dateRange,
effect,
variables,
unsatisfiedRequiredFilters,
}: AdhocDashboardFilterProps) {
const [isAdding, setIsAdding] = useState(false);
const isAtLimit = conditions.length >= ADHOC_FILTER_MAX_CONDITIONS;

// The `where` narrowing the suggestions, and what it is still waiting on.
// Blocked only by the required filters it references: a global requirement
// blocks tiles, not the filter bar, same as queried dropdown filters.
const { suggestionsWhere, pendingVariables, blockingFilterNames } =
useMemo(() => {
const where = filter.sourceType === 'sql' ? filter.where : undefined;
const query = { where, whereLanguage: filter.whereLanguage };
const referencedNames = new Set(
getVariableReferences(where ?? '').map(reference => reference.name),
);
Comment thread
knudtty marked this conversation as resolved.
Resolved
return {
suggestionsWhere: resolveFilterValuesWhere(query, variables),
pendingVariables: getPendingFilterValuesVariables(query, variables),
blockingFilterNames: getDashboardVariableFilters(
unsatisfiedRequiredFilters,
)
.filter(({ name }) => referencedNames.has(name))
.map(({ filter: blocking }) => blocking.name),
};
}, [
filter.sourceType,
filter.where,
filter.whereLanguage,
variables,
unsatisfiedRequiredFilters,
]);
const dependsOn = blockingFilterNames.length
? `This filter depends on: ${blockingFilterNames.join(', ')}`
: undefined;
const disabledReason = dependsOn ?? suggestionsWhere.error;
const isDisabled = !!disabledReason;

const toggleAdding = () => {
if (isDisabled) return;
if (isAtLimit) {
notifyAdhocFilterFull(filter);
return;
Expand All @@ -71,7 +120,27 @@ export function AdhocDashboardFilter({

return (
<Stack gap={2} data-testid={`adhoc-filter-${filter.name}`}>
<DashboardFilterLabel name={filter.name} effect={effect} />
<DashboardFilterLabel name={filter.name} effect={effect}>
{dependsOn && (
<FilterCaution
label={dependsOn}
testId={`dashboard-filter-blocked-${filter.name}`}
/>
)}
{!isDisabled && pendingVariables.length > 0 && (
<FilterCaution
label={getPendingVariablesTooltip(pendingVariables)}
testId={`dashboard-filter-pending-variable-${filter.name}`}
/>
)}
{suggestionsWhere.error && (
<FilterCaution
label={suggestionsWhere.error}
testId={`dashboard-filter-error-${filter.name}`}
variant="danger"
/>
)}
</DashboardFilterLabel>
<Group
gap={4}
w={250}
Expand Down Expand Up @@ -101,18 +170,24 @@ export function AdhocDashboardFilter({
}
invalidReason={`Filter not applied: PromQL has no ${condition.operator} operator. Remove this condition and add it again with a supported operator.`}
onRemove={() => remove(index)}
renderPopover={close => (
<AdhocConditionEditor
filter={filter}
dateRange={dateRange}
initial={condition}
onSubmit={next => {
if (!isStorable(next)) return;
replace(index, next);
close();
}}
/>
)}
// Editing would look up suggestions; removing still works.
renderPopover={
isDisabled
? undefined
: close => (
<AdhocConditionEditor
filter={filter}
dateRange={dateRange}
suggestionsWhere={suggestionsWhere}
initial={condition}
onSubmit={next => {
if (!isStorable(next)) return;
replace(index, next);
close();
}}
/>
)
}
data-testid="adhoc-condition-pill"
/>
);
Expand All @@ -121,8 +196,8 @@ export function AdhocDashboardFilter({
<Text
size="xs"
c="dimmed"
onClick={() => setIsAdding(true)}
style={{ cursor: 'pointer', flex: 1 }}
onClick={isDisabled ? undefined : () => setIsAdding(true)}
style={{ cursor: isDisabled ? 'default' : 'pointer', flex: 1 }}
>
{filter.name}
</Text>
Expand All @@ -137,7 +212,7 @@ export function AdhocDashboardFilter({
/>
)}
<Popover
opened={isAdding}
opened={isAdding && !isDisabled}
onChange={setIsAdding}
position="bottom-start"
withArrow
Expand All @@ -146,16 +221,21 @@ export function AdhocDashboardFilter({
>
<Popover.Target>
<Tooltip
label={`Up to ${ADHOC_FILTER_MAX_CONDITIONS} conditions`}
disabled={!isAtLimit}
label={
Comment thread
knudtty marked this conversation as resolved.
Resolved
disabledReason ??
`Up to ${ADHOC_FILTER_MAX_CONDITIONS} conditions`
}
disabled={!isDisabled && !isAtLimit}
withinPortal
multiline
maw={400}
>
<ActionIcon
variant="subtle"
size="sm"
aria-label={`Add ${filter.name} condition`}
aria-disabled={isAtLimit}
data-disabled={isAtLimit || undefined}
aria-disabled={isDisabled || isAtLimit}
data-disabled={isDisabled || isAtLimit || undefined}
onClick={toggleAdding}
data-testid={`adhoc-filter-add-${filter.name}`}
>
Expand All @@ -167,6 +247,7 @@ export function AdhocDashboardFilter({
<AdhocConditionEditor
filter={filter}
dateRange={dateRange}
suggestionsWhere={suggestionsWhere}
onSubmit={condition => {
if (!isStorable(condition)) return;
onChange([...conditions, condition]);
Expand Down
Loading
Loading