Skip to content
Draft
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
6 changes: 6 additions & 0 deletions .changeset/adhoc-filter-inputs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@hyperdx/common-utils': patch
'@hyperdx/app': patch
---

feat: Render adhoc dashboard filter inputs
13 changes: 6 additions & 7 deletions packages/app/src/DBDashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ import {
isTimeSeriesDisplayType,
} from '@hyperdx/common-utils/dist/core/utils';
import { getBlockingRequiredFilterNames } from '@hyperdx/common-utils/dist/dashboardFilterValues';
import { isAdhocFilter } from '@hyperdx/common-utils/dist/filters';
import {
displayTypeRequiresSource,
isBuilderChartConfig,
Expand Down Expand Up @@ -1979,14 +1978,12 @@ function DBDashboardPage({

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

// Ad hoc filters aren't rendered or applied yet
const filters = useMemo(
() => (dashboard?.filters ?? []).filter(filter => !isAdhocFilter(filter)),
[dashboard?.filters],
);
const filters = useMemo(() => dashboard?.filters ?? [], [dashboard?.filters]);
const {
selectionByFilterId,
setFilterValue,
adhocConditionsByFilterId,
setAdhocConditions,
setFilterValueEntries,
filterValueEntries,
ignoredFilterExpressions,
Expand Down Expand Up @@ -3351,6 +3348,8 @@ function DBDashboardPage({
filters={filters}
selectionByFilterId={selectionByFilterId}
onSetFilterValue={setFilterValue}
adhocConditionsByFilterId={adhocConditionsByFilterId}
onSetAdhocConditions={setAdhocConditions}
dateRange={searchedTimeRange}
variables={variables}
/>
Expand Down Expand Up @@ -3524,7 +3523,7 @@ function DBDashboardPage({
<DashboardFiltersModal
opened={showFiltersModal}
onClose={() => setShowFiltersModal(false)}
filters={dashboard?.filters ?? []}
filters={filters}
onSaveFilter={handleSaveFilter}
onRemoveFilter={handleRemoveFilter}
isLoading={isSavingDashboard || isFetchingDashboard}
Expand Down
73 changes: 32 additions & 41 deletions packages/app/src/DashboardFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,27 @@ import {
getFilterBroadcastTarget,
getFilterVariableName,
getPendingFilterValuesVariables,
isAdhocFilter,
isFilterGlobalRequirement,
isFilterRequired,
isFilterVariableEnabled,
isQueryExpressionFilter,
isStaticListFilter,
} from '@hyperdx/common-utils/dist/filters';
import {
AdhocFilterCondition,
ChartVariable,
DashboardFilter,
} from '@hyperdx/common-utils/dist/types';
import { isFilterSingleSelect } from '@hyperdx/common-utils/dist/variables';
import { Group, Stack, Text, Tooltip } from '@mantine/core';
import { IconAlertTriangle, IconHelp, IconRefresh } from '@tabler/icons-react';
import { Group, Stack, Text } from '@mantine/core';
import { IconRefresh } from '@tabler/icons-react';

import { AdhocDashboardFilter } from './components/AdhocDashboardFilter/AdhocDashboardFilter';
import {
DashboardFilterLabel,
FilterCaution,
} from './components/DashboardFilterLabel';
import { FilterLinkToggle } from './components/FilterLinkToggle';
import { VirtualMultiSelect } from './components/VirtualMultiSelect/VirtualMultiSelect';
import { useDashboardFilterValues } from './hooks/useDashboardFilterValues';
Expand Down Expand Up @@ -99,25 +106,6 @@ export const getFilterEffect = (
return { hasEffect: true, tooltip: parts.join(', ') };
};

/** One of the caution icons a filter's label row can carry, with its tooltip. */
const FilterCaution = ({
label,
testId,
variant = 'warning',
}: {
label: string;
testId: string;
variant?: 'warning' | 'danger';
}) => (
<Tooltip label={label} withinPortal multiline maw={400}>
<IconAlertTriangle
size={12}
color={`var(--color-text-${variant})`}
data-testid={testId}
/>
</Tooltip>
);

const DashboardFilterSelect = ({
filter,
onChange,
Expand All @@ -134,25 +122,7 @@ const DashboardFilterSelect = ({

return (
<Stack gap={2}>
<Group gap={4} align="center" wrap="nowrap">
<Text size="xs" c="dimmed">
{filter.name}
</Text>
<Tooltip label={effect.tooltip} withinPortal>
{effect.hasEffect ? (
<IconHelp
size={12}
color="var(--color-text-muted)"
data-testid={`dashboard-filter-help-${filter.name}`}
/>
) : (
<IconAlertTriangle
size={12}
color="var(--color-text-warning)"
data-testid={`dashboard-filter-no-effect-${filter.name}`}
/>
)}
</Tooltip>
<DashboardFilterLabel name={filter.name} effect={effect}>
{isMissingRequiredValue && (
<FilterCaution
label={getRequiredFilterTooltip(filter)}
Expand All @@ -175,7 +145,7 @@ const DashboardFilterSelect = ({
variant="danger"
/>
)}
</Group>
</DashboardFilterLabel>
<div style={{ width: 250 }}>
<VirtualMultiSelect
placeholder={value.length === 0 ? filter.name : undefined}
Expand Down Expand Up @@ -203,6 +173,11 @@ interface DashboardFilterProps {
filters: DashboardFilter[];
selectionByFilterId: ReadonlyMap<string, FilterSelection>;
onSetFilterValue: (filterId: string, values: string[]) => void;
adhocConditionsByFilterId?: ReadonlyMap<string, AdhocFilterCondition[]>;
onSetAdhocConditions?: (
filterId: string,
conditions: AdhocFilterCondition[],
) => void;
dateRange: [Date, Date];
/**
* The dashboard's variables and their current selections. Defined only when
Expand All @@ -217,6 +192,8 @@ const DashboardFilters = ({
dateRange,
selectionByFilterId,
onSetFilterValue,
adhocConditionsByFilterId,
onSetAdhocConditions,
variables,
}: DashboardFilterProps) => {
// "Link" mode (opt-in, off by default): each dropdown's values are narrowed by
Expand Down Expand Up @@ -248,6 +225,20 @@ const DashboardFilters = ({
return (
<Group align="start">
{filters.map(filter => {
if (isAdhocFilter(filter)) {
return (
<AdhocDashboardFilter
key={filter.id}
filter={filter}
conditions={adhocConditionsByFilterId?.get(filter.id) ?? []}
onChange={conditions =>
onSetAdhocConditions?.(filter.id, conditions)
}
dateRange={dateRange}
effect={getFilterEffect(filter)}
/>
);
}
const queriedFilterValues = filterValuesById?.get(filter.id);
const included = selectionByFilterId.get(filter.id)?.included;
const selectedValues = included
Expand Down
17 changes: 17 additions & 0 deletions packages/app/src/__tests__/DashboardFilters.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,23 @@ describe('getFilterEffect', () => {
}).tooltip,
).toEqual('Available as variable ($svc)');
});

it('counts the sources an ad hoc filter broadcasts to', () => {
expect(
getFilterEffect({
id: 'adhoc1',
type: 'ADHOC',
name: 'Conditions',
sourceType: 'sql',
sources: ['logs', 'traces'],
isVariableEnabled: true,
variableName: 'conds',
}),
).toEqual({
hasEffect: true,
tooltip: 'Filters 2 sources, available as variable ($conds)',
});
});
});

describe('getPendingVariablesTooltip', () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { useState } from 'react';
import {
AdhocDashboardFilter,
AdhocFilterCondition,
} from '@hyperdx/common-utils/dist/types';

import {
FilterConditionEditor,
FilterOperator,
getFilterOperators,
} from '@/components/FilterPill';

import {
useAdhocFilterKeys,
useAdhocFilterValues,
} from './useAdhocFilterOptions';

type AdhocConditionEditorProps = {
filter: AdhocDashboardFilter;
dateRange: [Date, Date];
/** The condition being edited; unset when adding one. */
initial?: AdhocFilterCondition;
onSubmit: (condition: AdhocFilterCondition) => void;
};

/**
* Pick a condition from the filter's keys and values. Mounted only while its
* popover is open, so keys load on open and values once a key is picked.
*/
export function AdhocConditionEditor({
filter,
dateRange,
initial,
onSubmit,
}: AdhocConditionEditorProps) {
const [valueKey, setValueKey] = useState(initial?.key ?? '');
const keys = useAdhocFilterKeys(filter, dateRange);
const values = useAdhocFilterValues(
filter,
valueKey,
dateRange,
keys.keysBySourceId,
);
const isPromql = filter.sourceType === 'promql';

return (
<FilterConditionEditor<FilterOperator>
operators={getFilterOperators(filter.sourceType)}
initial={initial}
keyOptions={keys.data}
isLoadingKeys={keys.isLoading}
isKeysError={keys.isError}
valueOptions={values.data}
isLoadingValues={values.isLoading}
isValuesError={values.isError}
onKeyChange={setValueKey}
keyLabel={isPromql ? 'Label' : 'Key'}
keyPlaceholder={
isPromql ? 'Select a label' : 'Select a column or map key'
}
onSubmit={onSubmit}
data-testid={`adhoc-condition-editor-${filter.name}`}
/>
);
}
Loading
Loading