Compare commits

..

1 Commits

Author SHA1 Message Date
Ashwin Bhatkal
de6fbfeb2d fix(dashboard): keep ALL on a dynamic variable that holds a value outside its list 2026-10-19 13:02:38 +05:30
9 changed files with 59 additions and 84 deletions

View File

@@ -3,10 +3,7 @@ import { Button, Skeleton } from 'antd';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { FieldKeysConfig } from 'api/querySuggestions/types';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import {
EXCLUDED_QUICK_FILTER_NAMES,
SIGNAL_DATA_SOURCE_MAP,
} from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { buildCompositeKey } from 'container/OptionsMenu/utils';
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
@@ -83,11 +80,7 @@ function OtherFilters({
buildCompositeKey(filter.name, filter.fieldContext, filter.fieldDataType),
),
);
return suggestions.filter(
(attr) =>
!addedKeys.has(attr.key as string) &&
!EXCLUDED_QUICK_FILTER_NAMES.has(attr.name),
);
return suggestions.filter((attr) => !addedKeys.has(attr.key as string));
}, [fetchedKeys, addedFilters]);
const handleAddFilter = (filter: TelemetryFieldKey): void => {

View File

@@ -9,8 +9,3 @@ export const SIGNAL_DATA_SOURCE_MAP = {
[SignalType.METER_EXPLORER]: DataSource.METRICS,
[SignalType.AI_OBSERVABILITY]: DataSource.TRACES,
};
export const EXCLUDED_QUICK_FILTER_NAMES: ReadonlySet<string> = new Set([
'trace_id',
'span_id',
]);

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { useGetQuickFilters } from 'api/generated/services/quick-filter';
import { EXCLUDED_QUICK_FILTER_NAMES } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { IQuickFiltersConfig, SignalType } from '../types';
@@ -32,10 +31,7 @@ const useFilterConfig = ({
);
const customFilters = useMemo<TelemetryFieldKey[]>(
() =>
((data?.data?.filters ?? []) as TelemetryFieldKey[]).filter(
(filter) => !EXCLUDED_QUICK_FILTER_NAMES.has(filter.name),
),
() => (data?.data?.filters ?? []) as TelemetryFieldKey[],
[data],
);

View File

@@ -395,70 +395,6 @@ describe('Quick Filters with custom filters', () => {
});
});
it('hides trace_id and span_id from saved and suggested filters', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
server.use(
rest.get(quickFiltersListURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
...quickFiltersListResponse,
data: {
...quickFiltersListResponse.data,
filters: [
...quickFiltersListResponse.data.filters,
{ name: 'trace_id', fieldDataType: 'string', fieldContext: 'span' },
],
},
}),
),
),
rest.get(quickFiltersSuggestionsURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
complete: true,
keys: {
[FILTER_K8S_DEPLOYMENT_NAME]: [
{
name: FILTER_K8S_DEPLOYMENT_NAME,
fieldContext: 'resource',
fieldDataType: 'string',
},
],
trace_id: [
{ name: 'trace_id', fieldContext: 'log', fieldDataType: 'string' },
{
name: 'trace_id',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
span_id: [
{ name: 'span_id', fieldContext: 'log', fieldDataType: 'string' },
],
},
},
}),
),
),
);
render(<TestQuickFilters signal={SIGNAL} />);
await screen.findByText(FILTER_SERVICE_NAME);
expect(screen.queryByText('Trace Id')).not.toBeInTheDocument();
const icon = await screen.findByTestId(SETTINGS_ICON_TEST_ID);
const settingsButton = icon.closest('button') ?? icon;
await user.click(settingsButton);
await screen.findByText(FILTER_K8S_DEPLOYMENT_NAME);
expect(screen.queryByText('trace_id')).not.toBeInTheDocument();
expect(screen.queryByText('span_id')).not.toBeInTheDocument();
});
it('adds a filter from OTHER FILTERS to ADDED FILTERS when clicked', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });

View File

@@ -193,6 +193,40 @@ describe('ValueSelector', () => {
expect(onSearch).toHaveBeenLastCalledWith('pay');
});
it('commits ALL even when the selection holds a value outside the list', async () => {
// A search pick, or one an older time window offered: ALL must still go out as
// `__all__`, not as a list that filters the panels to the truncated options.
const onChange = jest.fn();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
render(
<ValueSelector
options={OPTIONS}
variableType="dynamic"
multiSelect
showAllOption
selection={{ value: ['searched'], allSelected: false }}
onChange={onChange}
emptyFallback={{ value: null, allSelected: true }}
testId="variable-select-env"
dynamic={{
values: OPTIONS,
relatedValues: [],
complete: false,
onSearch: jest.fn(),
onSearchReset: jest.fn(),
}}
/>,
);
await openDropdown();
await user.click(screen.getByText('ALL'));
await user.keyboard('{Escape}');
expect(onChange).toHaveBeenLastCalledWith(
expect.objectContaining({ allSelected: true }),
);
});
});
describe('clearing', () => {

View File

@@ -12,6 +12,7 @@ function commit(
values,
options: OPTIONS,
showAllOption: true,
isDynamic: false,
emptyFallback: FALLBACK,
...overrides,
});
@@ -60,6 +61,20 @@ describe('selectionFromCommittedValues', () => {
});
});
// A dynamic ALL is sent as `__all__`, so reading it as a list would filter the
// panels down to the truncated option set.
it('reads every option PLUS a value outside the list as ALL for a dynamic variable', () => {
expect(
commit([...OPTIONS, 'searched'], { isDynamic: true }).allSelected,
).toBe(true);
});
it('does not read a dynamic subset PLUS a value outside the list as ALL', () => {
expect(
commit(['checkout', 'searched'], { isDynamic: true }).allSelected,
).toBe(false);
});
// Derived from the values + options at commit time, never from the old selection.
it('recomputes the marker: a typed value the data now offers is a normal pick', () => {
expect(

View File

@@ -102,6 +102,7 @@ function ValueSelector({
values,
options,
showAllOption,
isDynamic: !!dynamic,
emptyFallback,
});

View File

@@ -51,6 +51,7 @@ interface CommittedValues {
values: string[];
options: string[];
showAllOption: boolean;
isDynamic: boolean;
emptyFallback: VariableSelection;
}
@@ -62,6 +63,7 @@ export function selectionFromCommittedValues({
values,
options,
showAllOption,
isDynamic,
emptyFallback,
}: CommittedValues): VariableSelection {
if (values.length === 0) {
@@ -71,10 +73,12 @@ export function selectionFromCommittedValues({
const customValues = values.filter((value) => !options.includes(value));
// ALL re-materializes to the option set, so a set carrying a typed value is not ALL
// — the next refetch would expand it back and drop what the user typed.
// A dynamic ALL travels as `__all__` and is never re-materialized, so a value outside
// its truncated list (a search pick, an older window's pick) loses nothing.
const allSelected =
showAllOption &&
options.length > 0 &&
customValues.length === 0 &&
(isDynamic || customValues.length === 0) &&
options.every((option) => values.includes(option));
return {

View File

@@ -184,6 +184,7 @@ func NewDefaultQuickFilter(orgID valuer.UUID) ([]*StorableQuickFilter, error) {
{Name: "http.method", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http.route", FieldContext: telemetrytypes.FieldContextAttribute, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "http_url", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString},
{Name: "trace_id", FieldContext: telemetrytypes.FieldContextSpan, FieldDataType: telemetrytypes.FieldDataTypeString},
}
logsFilters := []telemetrytypes.TelemetryFieldKey{