Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

AdhocVariable: baseFilters with origin appear readonly in the UI #1060

Merged
merged 5 commits into from
Feb 27, 2025
Merged
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
2 changes: 1 addition & 1 deletion packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export {
} from './variables/variants/MultiValueVariable';
export { LocalValueVariable } from './variables/variants/LocalValueVariable';
export { IntervalVariable } from './variables/variants/IntervalVariable';
export { AdHocFiltersVariable } from './variables/adhoc/AdHocFiltersVariable';
export { AdHocFiltersVariable, FilterOrigin } from './variables/adhoc/AdHocFiltersVariable';
export type { AdHocFilterWithLabels } from './variables/adhoc/AdHocFiltersVariable';
export { GroupByVariable } from './variables/groupby/GroupByVariable';
export { type MacroVariableConstructor } from './variables/macros/types';
Expand Down
60 changes: 59 additions & 1 deletion packages/scenes/src/querying/SceneQueryRunner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { SceneTimeRangeCompare } from '../components/SceneTimeRangeCompare';
import { SceneDataLayerSet } from './SceneDataLayerSet';
import { TestAlertStatesDataLayer, TestAnnotationsDataLayer } from './layers/TestDataLayer';
import { TestSceneWithRequestEnricher } from '../utils/test/TestSceneWithRequestEnricher';
import { AdHocFiltersVariable } from '../variables/adhoc/AdHocFiltersVariable';
import { AdHocFiltersVariable, FilterOrigin } from '../variables/adhoc/AdHocFiltersVariable';
import { emptyPanelData } from '../core/SceneDataNode';
import { GroupByVariable } from '../variables/groupby/GroupByVariable';
import { SceneQueryController } from '../behaviors/SceneQueryController';
Expand Down Expand Up @@ -529,6 +529,64 @@ describe.each(['11.1.2', '11.1.1'])('SceneQueryRunner', (v) => {
expect(runRequestCall2[1].filters).toEqual(filtersVar.state.filters);
});

it('should pass adhoc baseFilters via request object if they have a source defined', async () => {
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'test-uid' },
queries: [{ refId: 'A' }],
});

const filtersVar = new AdHocFiltersVariable({
datasource: { uid: 'test-uid' },
applyMode: 'auto',
filters: [{ key: 'A', operator: '=', value: 'B', condition: '' }],
baseFilters: [{ key: 'C', operator: '=', value: 'D', condition: '', origin: FilterOrigin.Scopes }],
});

const scene = new EmbeddedScene({
$data: queryRunner,
$variables: new SceneVariableSet({ variables: [filtersVar] }),
body: new SceneCanvasText({ text: 'hello' }),
});

const deactivate = activateFullSceneTree(scene);
deactivationHandlers.push(deactivate);

await new Promise((r) => setTimeout(r, 1));

const runRequestCall = runRequestMock.mock.calls[0];

expect(runRequestCall[1].filters).toEqual([...filtersVar.state.baseFilters!, ...filtersVar.state.filters]);
});

it('should not pass adhoc baseFilters via request object if they do not have a source defined', async () => {
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'test-uid' },
queries: [{ refId: 'A' }],
});

const filtersVar = new AdHocFiltersVariable({
datasource: { uid: 'test-uid' },
applyMode: 'auto',
filters: [{ key: 'A', operator: '=', value: 'B', condition: '' }],
baseFilters: [{ key: 'C', operator: '=', value: 'D', condition: '' }],
});

const scene = new EmbeddedScene({
$data: queryRunner,
$variables: new SceneVariableSet({ variables: [filtersVar] }),
body: new SceneCanvasText({ text: 'hello' }),
});

const deactivate = activateFullSceneTree(scene);
deactivationHandlers.push(deactivate);

await new Promise((r) => setTimeout(r, 1));

const runRequestCall = runRequestMock.mock.calls[0];

expect(runRequestCall[1].filters).toEqual(filtersVar.state.filters);
});

it('only passes fully completed adhoc filters', async () => {
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'test-uid' },
Expand Down
9 changes: 8 additions & 1 deletion packages/scenes/src/querying/SceneQueryRunner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,9 +531,16 @@ export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> implemen
};

if (this._adhocFiltersVar) {
request.filters = [];

if (this._adhocFiltersVar.state.baseFilters?.length) {
const injectedBaseFilters = this._adhocFiltersVar.state.baseFilters.filter((filter) => filter.origin);
request.filters = request.filters.concat(injectedBaseFilters);
}

// only pass filters that have both key and value
// @ts-ignore (Temporary ignore until we update @grafana/data)
request.filters = this._adhocFiltersVar.state.filters.filter(isFilterComplete);
request.filters = request.filters.concat(this._adhocFiltersVar.state.filters.filter(isFilterComplete));
}

if (this._groupByVar) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ exports[`SceneQueryRunner when running query should build DataQueryRequest objec
"from": "now-6h",
"to": "now",
},
"requestId": "SQR178",
"requestId": "SQR180",
"startTime": 1689063488000,
"targets": [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import React, { useState, useRef, useCallback, useEffect } from 'react';
import { AdHocCombobox } from './AdHocFiltersCombobox';
import { AdHocFilterWithLabels, AdHocFiltersVariable } from '../AdHocFiltersVariable';

const LABEL_MAX_VISIBLE_LENGTH = 20;

interface Props {
filter: AdHocFilterWithLabels;
model: AdHocFiltersVariable;
Expand All @@ -25,14 +27,14 @@ export function AdHocFilterPill({ filter, model, readOnly, focusOnWipInputRef }:
const handleChangeViewMode = useCallback(
(event?: React.MouseEvent, shouldFocusOnPillWrapperOverride?: boolean) => {
event?.stopPropagation();
if (readOnly) {
if (readOnly || filter.origin) {
return;
}

setShouldFocusOnPillWrapper(shouldFocusOnPillWrapperOverride ?? !viewMode);
setViewMode(!viewMode);
},
[readOnly, viewMode]
[readOnly, viewMode, filter.origin]
);

useEffect(() => {
Expand Down Expand Up @@ -66,7 +68,7 @@ export function AdHocFilterPill({ filter, model, readOnly, focusOnWipInputRef }:
);
return (
<div
className={cx(styles.combinedFilterPill, { [styles.readOnlyCombinedFilter]: readOnly })}
className={cx(styles.combinedFilterPill, readOnly && styles.readOnlyCombinedFilter)}
onClick={(e) => {
e.stopPropagation();
setPopulateInputOnEdit(true);
Expand All @@ -83,15 +85,15 @@ export function AdHocFilterPill({ filter, model, readOnly, focusOnWipInputRef }:
tabIndex={0}
ref={pillWrapperRef}
>
{valueLabel.length < 20 ? (
{valueLabel.length < LABEL_MAX_VISIBLE_LENGTH ? (
pillText
) : (
<Tooltip content={<div className={styles.tooltipText}>{valueLabel}</div>} placement="top">
{pillText}
</Tooltip>
)}

{!readOnly ? (
{!readOnly && !filter.origin ? (
<IconButton
onClick={(e) => {
e.stopPropagation();
Expand All @@ -108,10 +110,19 @@ export function AdHocFilterPill({ filter, model, readOnly, focusOnWipInputRef }:
}}
name="times"
size="md"
className={styles.removeButton}
className={styles.pillIcon}
tooltip={`Remove filter with key ${keyLabel}`}
/>
) : null}

{filter.origin && (
<IconButton
name="info-circle"
size="md"
className={styles.pillIcon}
tooltip={`This is a ${filter.origin} injected filter`}
/>
)}
</div>
);
}
Expand Down Expand Up @@ -154,7 +165,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
background: theme.colors.action.selected,
},
}),
removeButton: css({
pillIcon: css({
marginInline: theme.spacing(0.5),
cursor: 'pointer',
'&:hover': {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ interface Props {
}

export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRenderer({ model }: Props) {
const { filters, readOnly } = model.useState();
const { baseFilters, filters, readOnly } = model.useState();
const styles = useStyles2(getStyles);

// ref that focuses on the always wip filter input
Expand All @@ -27,6 +27,17 @@ export const AdHocFiltersComboboxRenderer = memo(function AdHocFiltersComboboxRe
>
<Icon name="filter" className={styles.filterIcon} size="lg" />

{baseFilters?.map((filter, index) =>
filter.origin ? (
<AdHocFilterPill
key={`${index}-${filter.key}`}
filter={filter}
model={model}
focusOnWipInputRef={focusOnWipInputRef.current}
/>
) : null
)}

{filters.map((filter, index) => (
<AdHocFilterPill
key={`${index}-${filter.key}`}
Expand Down
10 changes: 9 additions & 1 deletion packages/scenes/src/variables/adhoc/AdHocFiltersVariable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,18 @@ export interface AdHocFilterWithLabels<M extends Record<string, any> = {}> exten
// hide the filter from AdHocFiltersVariableRenderer and the URL
hidden?: boolean;
meta?: M;
// filter origin, it can be either scopes, dashboards or undefined,
// which means it won't appear in the UI
origin?: FilterOrigin;
}

export type AdHocControlsLayout = ControlsLayout | 'combobox';

export enum FilterOrigin {
Scopes = 'scopes',
Dashboards = 'dashboards',
}

export interface AdHocFiltersVariableState extends SceneVariableState {
/** Optional text to display on the 'add filter' button */
addFilterButtonText?: string;
Expand Down Expand Up @@ -233,7 +241,7 @@ export class AdHocFiltersVariable
}
): void {
let filterExpressionChanged = false;
let filterExpression = undefined;
let filterExpression: string | undefined = undefined;

if (filters && filters !== this.state.filters) {
filterExpression = renderExpression(this.state.expressionBuilder, filters);
Expand Down