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
2 changes: 2 additions & 0 deletions packages/scenes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ export { SceneTimeZoneOverride } from './core/SceneTimeZoneOverride';

export { SceneQueryRunner, type QueryRunnerState } from './querying/SceneQueryRunner';
export { DataProviderProxy } from './querying/DataProviderProxy';
export { buildApplicabilityMatcher } from './variables/applicabilityUtils';
export { findClosestAdHocFilterInHierarchy } from './variables/adhoc/patchGetAdhocFilters';
export {
type ExtraQueryDescriptor,
type ExtraQueryProvider,
Expand Down
105 changes: 105 additions & 0 deletions packages/scenes/src/variables/DrilldownDependenciesManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { AdHocFiltersVariable, AdHocFilterWithLabels } from './adhoc/AdHocFiltersVariable';
import { DrilldownDependenciesManager } from './DrilldownDependenciesManager';
import { GroupByVariable } from './groupby/GroupByVariable';
import { VariableDependencyConfig } from './VariableDependencyConfig';

function createManager(opts: { adhocVar?: AdHocFiltersVariable; groupByVar?: GroupByVariable }) {
const mockDependencyConfig = { setVariableNames: jest.fn() } as unknown as VariableDependencyConfig<any>;
const manager = new DrilldownDependenciesManager(mockDependencyConfig);

if (opts.adhocVar) {
manager['_adhocFiltersVar'] = opts.adhocVar;
}
if (opts.groupByVar) {
manager['_groupByVar'] = opts.groupByVar;
}

return manager;
}

function createAdhocVar(
filters: AdHocFilterWithLabels[],
originFilters?: AdHocFilterWithLabels[],
applicabilityEnabled?: boolean
) {
return new AdHocFiltersVariable({
datasource: { uid: 'my-ds-uid' },
name: 'filters',
filters,
originFilters,
applicabilityEnabled,
});
}

function createGroupByVar(value: string[], keysApplicability?: any[], applicabilityEnabled?: boolean) {
return new GroupByVariable({
datasource: { uid: 'my-ds-uid' },
name: 'groupby',
key: 'testGroupBy',
value,
text: value,
keysApplicability,
applicabilityEnabled,
});
}

describe('DrilldownDependenciesManager', () => {
describe('getFilters', () => {
it('should return undefined when no adhocFiltersVar', () => {
const manager = createManager({});
expect(manager.getFilters()).toBeUndefined();
});

it('should return all complete filters when no applicability results', () => {
const filters: AdHocFilterWithLabels[] = [
{ key: 'env', value: 'prod', operator: '=' },
{ key: 'cluster', value: 'us', operator: '=' },
];

const manager = createManager({ adhocVar: createAdhocVar(filters) });

const result = manager.getFilters() ?? [];
expect(result).toHaveLength(2);
expect(result[0].key).toBe('env');
expect(result[1].key).toBe('cluster');
});

it('should exclude incomplete filters', () => {
const filters: AdHocFilterWithLabels[] = [
{ key: 'env', value: 'prod', operator: '=' },
{ key: '', value: '', operator: '' },
];

const manager = createManager({ adhocVar: createAdhocVar(filters) });

const result = manager.getFilters() ?? [];
expect(result).toHaveLength(1);
expect(result[0].key).toBe('env');
});

it('should exclude variable-level nonApplicable filters', () => {
const filters: AdHocFilterWithLabels[] = [
{ key: 'env', value: 'prod', operator: '=' },
{ key: 'pod', value: 'abc', operator: '=', nonApplicable: true },
];

const manager = createManager({ adhocVar: createAdhocVar(filters) });

const result = manager.getFilters() ?? [];
expect(result).toHaveLength(1);
expect(result[0].key).toBe('env');
});
});

describe('getGroupByKeys', () => {
it('should return undefined when no groupByVar', () => {
const manager = createManager({});
expect(manager.getGroupByKeys()).toBeUndefined();
});

it('should return all applicable keys', () => {
const manager = createManager({ groupByVar: createGroupByVar(['ns', 'pod']) });
expect(manager.getGroupByKeys()).toEqual(['ns', 'pod']);
});
});
});
18 changes: 12 additions & 6 deletions packages/scenes/src/variables/DrilldownDependenciesManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,21 @@ export class DrilldownDependenciesManager<TState extends SceneObjectState> {
}

public getFilters(): AdHocFilterWithLabels[] | undefined {
return this._adhocFiltersVar
? [...(this._adhocFiltersVar.state.originFilters ?? []), ...this._adhocFiltersVar.state.filters].filter(
(f) => isFilterComplete(f) && isFilterApplicable(f)
)
: undefined;
if (!this._adhocFiltersVar) {
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this entire manager will get very thin after the unification, basically it would just deal with adhoc subscription and fetching filters... might not even need it at all we'll see

return undefined;
}

const stateFilters = this._adhocFiltersVar.state.filters;
const originFilters = this._adhocFiltersVar.state.originFilters ?? [];
const allFilters = [...originFilters, ...stateFilters];
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sanity check - does it matter that in the adhoc filters we create const filters = [...this.state.filters, ...(this.state.originFilters ?? [])]; ? Reversed order

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good call, yeah this should be consistent and originfilters should be first by convention

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've modified the adhoc to be consistent, the origin filters were put last mainly because it was easier and order didnt matter -- it still doesnt for origin ones vs non origin ones -- it matters mostly for same type of filters with same key (e.g. I have 2 user filters env=prod and then env=dev, the latter one should take priority and overwrite the first, this doesnt happen for origin and user ones because of the origin property delimitation)

return allFilters.filter((f) => isFilterComplete(f) && isFilterApplicable(f));
}

public getGroupByKeys(): string[] | undefined {
return this._groupByVar ? this._groupByVar.getApplicableKeys() : undefined;
if (!this._groupByVar) {
return undefined;
}
return this._groupByVar.getApplicableKeys();
}

public cleanup(): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React from 'react';
import { render, waitFor } from '@testing-library/react';
import { DataSourceSrv, locationService, setDataSourceSrv, setRunRequest, setTemplateSrv } from '@grafana/runtime';
import { DataQueryRequest, DataSourceApi, getDefaultTimeRange, LoadingState, PanelData } from '@grafana/data';
import {
DataQueryRequest,
DataSourceApi,
getDefaultTimeRange,
LoadingState,
PanelData,
// @ts-expect-error (temporary till we update grafana/data)
DEFAULT_APPLICABILITY_KEY,
} from '@grafana/data';
import { Observable, of } from 'rxjs';
import { EmbeddedScene } from '../../components/EmbeddedScene';
import { SceneFlexLayout, SceneFlexItem } from '../../components/layout/SceneFlexLayout';
Expand Down Expand Up @@ -234,7 +242,7 @@ let runRequestSet = false;
function setup(overrides?: Partial<AdHocFiltersVariableState>) {
const getTagKeysSpy = jest.fn();
const getTagValuesSpy = jest.fn();
const getDrilldownsApplicabilitySpy = jest.fn().mockResolvedValue([]);
const getDrilldownsApplicabilitySpy = jest.fn().mockResolvedValue(new Map([[DEFAULT_APPLICABILITY_KEY, []]]));
const getRecommendedDrilldownsSpy = jest.fn().mockResolvedValue({ filters: [] });

setDataSourceSrv({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
import React from 'react';
import {
// @ts-expect-error (temporary till we update grafana/data)
DrilldownsApplicability,
store,
} from '@grafana/data';
import { store } from '@grafana/data';
import { sceneGraph } from '../../core/sceneGraph';
import { getEnrichedDataRequest } from '../../querying/getEnrichedDataRequest';
import { getQueriesForVariables } from '../utils';
import { getDataSource } from '../../utils/getDataSource';

import { DrilldownRecommendations, DrilldownPill } from '../components/DrilldownRecommendations';
import { ScopesVariable } from '../variants/ScopesVariable';
import { SCOPES_VARIABLE_NAME } from '../constants';
import { AdHocFilterWithLabels, AdHocFiltersVariable } from './AdHocFiltersVariable';
import { SceneObjectBase } from '../../core/SceneObjectBase';
import { SceneComponentProps, SceneObjectState } from '../../core/types';
import { wrapInSafeSerializableSceneObject } from '../../utils/wrapInSafeSerializableSceneObject';

import { Unsubscribable } from 'rxjs';
import { buildApplicabilityMatcher } from '../applicabilityUtils';

export const MAX_RECENT_DRILLDOWNS = 3;
export const MAX_STORED_RECENT_DRILLDOWNS = 10;
Expand Down Expand Up @@ -45,10 +42,6 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec
return this.parent;
}

private get _scopedVars() {
return { __sceneObject: wrapInSafeSerializableSceneObject(this._adHocFilter) };
}

private _activationHandler = () => {
const json = store.get(this._getStorageKey());
const storedFilters = json ? JSON.parse(json) : [];
Expand Down Expand Up @@ -110,7 +103,7 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec

private async _fetchRecommendedDrilldowns() {
const adhoc = this._adHocFilter;
const ds = await getDataSource(adhoc.state.datasource, this._scopedVars);
const ds = await adhoc.getResolvedDataSource();

// @ts-expect-error (temporary till we update grafana/data)
if (!ds || !ds.getRecommendedDrilldowns) {
Expand Down Expand Up @@ -153,15 +146,12 @@ export class AdHocFiltersRecommendations extends SceneObjectBase<AdHocFiltersRec
return;
}

const applicabilityMap = new Map<string, boolean>();
response.forEach((item: DrilldownsApplicability) => {
applicabilityMap.set(item.key, item.applicable !== false);
});
const matcher = buildApplicabilityMatcher(response);

const applicableFilters = storedFilters
.filter((f) => {
const isApplicable = applicabilityMap.get(f.key);
return isApplicable === undefined || isApplicable === true;
const result = matcher(f.key, f.origin);
return !result || result.applicable;
})
.slice(-MAX_RECENT_DRILLDOWNS);

Expand Down
20 changes: 14 additions & 6 deletions packages/scenes/src/variables/adhoc/AdHocFiltersVariable.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import {
PanelData,
Scope,
ScopeSpecFilter,
// @ts-expect-error (temporary till we update grafana/data)
DEFAULT_APPLICABILITY_KEY,
} from '@grafana/data';
import { Observable, of } from 'rxjs';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -2354,6 +2356,7 @@ describe.each(['11.1.2', '11.1.1'])('AdHocFiltersVariable', (v) => {
//pod and static are non-applicable
const { filtersVar, getDrilldownsApplicabilitySpy } = setup(
{
applicabilityEnabled: true,
filters: [
{
key: 'cluster',
Expand Down Expand Up @@ -2400,6 +2403,7 @@ describe.each(['11.1.2', '11.1.1'])('AdHocFiltersVariable', (v) => {
//pod and static are non-applicable
const { filtersVar, getDrilldownsApplicabilitySpy, getTagKeysSpy } = setup(
{
applicabilityEnabled: true,
filters: [
{
key: 'cluster',
Expand Down Expand Up @@ -2462,6 +2466,7 @@ describe.each(['11.1.2', '11.1.1'])('AdHocFiltersVariable', (v) => {
//pod and static are non-applicable
const { filtersVar, getDrilldownsApplicabilitySpy } = setup(
{
applicabilityEnabled: true,
filters: [
{
key: 'cluster',
Expand Down Expand Up @@ -2685,6 +2690,7 @@ describe.each(['11.1.2', '11.1.1'])('AdHocFiltersVariable', (v) => {
it('should set non-applicable filters on activation', async () => {
setup(
{
applicabilityEnabled: true,
filters: [
{ key: 'pod', operator: '=', value: 'val1' },
{ key: 'container', operator: '=', value: 'val3' },
Expand Down Expand Up @@ -3316,12 +3322,14 @@ function setup(
...(useGetDrilldownsApplicability && {
getDrilldownsApplicability(options: any) {
getDrilldownsApplicabilitySpy(options);
return [
{ key: 'cluster', applicable: true },
{ key: 'container', applicable: true },
{ key: 'pod', applicable: false, reason: 'reason' },
{ key: 'static', applicable: false, origin: 'dashboard' },
];
const nonApplicableKeys = new Set(['pod', 'static']);
const results = (options.filters ?? []).map((f: any) => ({
key: f.key,
applicable: !nonApplicableKeys.has(f.key),
...(nonApplicableKeys.has(f.key) && { reason: 'reason' }),
...(f.origin && { origin: f.origin }),
}));
return new Map([[DEFAULT_APPLICABILITY_KEY, results]]);
},
}),
};
Expand Down
Loading