Compare commits

...

62 Commits

Author SHA1 Message Date
Dominik Prokop
bb945c7882 Merge remote-tracking branch 'origin/main' into mdvictor/per-panel-filter 2025-12-05 12:14:15 +01:00
Victor Marin
a229cba9d5 extra ff check 2025-12-02 10:58:54 +02:00
Victor Marin
a3f3f5ad99 Merge branch 'main' into mdvictor/per-panel-filter 2025-12-02 10:43:22 +02:00
Victor Marin
1aa6306bb6 translations 2025-11-27 17:47:22 +02:00
Victor Marin
6c015adb2d translations 2025-11-27 17:46:07 +02:00
Victor Marin
147fef1f0f fixes 2025-11-27 17:37:47 +02:00
Victor Marin
6eb464ef29 Merge branch 'mdvictor/chart-group-by' into mdvictor/per-panel-filter 2025-11-27 17:29:18 +02:00
Victor Marin
a5307276ae fix 2025-11-27 17:25:15 +02:00
Victor Marin
3e1d76d7c2 fix 2025-11-27 17:24:24 +02:00
Victor Marin
a11e4bfd0c Merge branch 'main' into mdvictor/chart-group-by 2025-11-27 17:19:48 +02:00
Victor Marin
d11b0dae0a translations 2025-11-27 17:18:17 +02:00
Victor Marin
3815b5b10e pr mods 2025-11-27 16:51:06 +02:00
Victor Marin
f9e6f63e01 tests 2025-11-27 14:26:54 +02:00
Victor Marin
6207f6d250 wip 2025-11-27 12:10:56 +02:00
Victor Marin
e7c44468f3 wip 2025-11-27 12:00:34 +02:00
Victor Marin
19073aed55 wip 2025-11-27 11:56:35 +02:00
Victor Marin
7654462f04 wip 2025-11-27 11:50:14 +02:00
Victor Marin
f599bfddd0 wip 2025-11-26 17:49:05 +02:00
Victor Marin
46dd6f00ef i18n 2025-11-26 12:49:54 +02:00
Victor Marin
ff0495d67f cleanup + action redesign 2025-11-26 11:02:34 +02:00
Victor Marin
67cc78649a cleanup 2025-11-25 11:01:50 +02:00
Victor Marin
54a96f9502 cleanup 2025-11-25 10:53:56 +02:00
Victor Marin
940c78fc02 refactor showing menu using css, remove header deactivation code from panel-edit 2025-11-25 10:53:23 +02:00
Victor Marin
21a78d2b36 Merge branch 'main' into mdvictor/chart-group-by 2025-11-24 22:21:03 +02:00
Victor Marin
3c953e60b5 list 2025-11-24 19:04:27 +02:00
Victor Marin
4a94c1a35d Merge branch 'main' into mdvictor/chart-group-by 2025-11-24 18:31:18 +02:00
Victor Marin
a880486452 properly deactivate header actions on panel edit 2025-11-24 18:19:40 +02:00
Victor Marin
297f83b1af fixes 2025-11-24 17:05:12 +02:00
Victor Marin
bb6da7722b Merge branch 'main' into mdvictor/chart-group-by 2025-11-24 14:57:49 +02:00
Victor Marin
b65ce6bd0a Merge branch 'main' into mdvictor/chart-group-by 2025-11-21 16:13:02 +02:00
Victor Marin
22011e8289 scenes bump 2025-11-21 16:05:35 +02:00
Victor Marin
f9f8936dcf refactor 2025-11-21 16:03:37 +02:00
Victor Marin
99024f96ba refactor subscriptions 2025-11-21 16:03:04 +02:00
Dominik Prokop
4498c812a2 Reset options if they are not applied 2025-11-19 14:23:21 +01:00
Dominik Prokop
2966bcf2b1 Optimise handlers 2025-11-19 14:17:16 +01:00
Dominik Prokop
6842b3b524 Merge remote-tracking branch 'origin/main' into mdvictor/chart-group-by 2025-11-19 12:09:19 +01:00
Victor Marin
be5d52e069 fix test 2025-11-19 09:57:52 +02:00
Victor Marin
babb924926 canary scenes 2025-11-19 09:55:58 +02:00
Victor Marin
a49dfcd393 Merge branch 'main' into mdvictor/chart-group-by 2025-11-19 09:55:22 +02:00
Victor Marin
38409e089a refactor components - do not make async call unless queries/groupByOptions change 2025-11-19 09:43:34 +02:00
Victor Marin
5accfcff64 refactor 2025-11-17 19:30:53 +02:00
Victor Marin
26e549827f memoize values/ refactor 2025-11-17 17:03:11 +02:00
Victor Marin
63ac4f5d73 Merge branch 'main' into mdvictor/chart-group-by 2025-11-17 15:09:59 +02:00
Victor Marin
b68bdc477e refactor 2025-11-17 12:30:05 +02:00
Victor Marin
8757fcb056 refactor 2025-11-15 00:51:29 +02:00
Sergej-Vlasov
ca2708e088 optimise action logic to avoid unnecessary triggers
(cherry picked from commit c4de2dfff8)
2025-11-15 00:48:53 +02:00
Victor Marin
93aa1f4508 wip
(cherry picked from commit 51a00db93d0805f481a9e48213382468f1eb2986)
2025-11-15 00:47:21 +02:00
Victor Marin
8d9d0ba952 canary scenes 2025-11-15 00:30:18 +02:00
Sergej-Vlasov
4d8e7322bc Merge branch 'main' into mdvictor/chart-group-by 2025-11-14 12:04:53 +00:00
Sergej-Vlasov
c4de2dfff8 optimise action logic to avoid unnecessary triggers 2025-11-14 11:28:16 +00:00
Sergej-Vlasov
f4e3964edf adjust apply 2025-11-12 10:49:11 +00:00
Victor Marin
dcd0152e4b switch to dropdown
Some checks failed
CodeQL checks / Detect whether code changed (push) Has been cancelled
CodeQL checks / Analyze (actions) (push) Has been cancelled
CodeQL checks / Analyze (go) (push) Has been cancelled
CodeQL checks / Analyze (javascript) (push) Has been cancelled
2025-11-11 16:54:46 +02:00
Victor Marin
c6f4a268f0 CR mods
Some checks failed
CodeQL checks / Detect whether code changed (push) Has been cancelled
CodeQL checks / Analyze (actions) (push) Has been cancelled
CodeQL checks / Analyze (go) (push) Has been cancelled
CodeQL checks / Analyze (javascript) (push) Has been cancelled
2025-11-10 19:35:24 +02:00
Victor Marin
e5edf8d0df fix
Some checks failed
CodeQL checks / Detect whether code changed (push) Has been cancelled
CodeQL checks / Analyze (actions) (push) Has been cancelled
CodeQL checks / Analyze (go) (push) Has been cancelled
CodeQL checks / Analyze (javascript) (push) Has been cancelled
2025-11-05 17:23:49 +02:00
Victor Marin
d1a2e705b6 fix 2025-11-05 17:19:25 +02:00
Victor Marin
a3fa0f07f9 fix 2025-11-05 16:17:26 +02:00
Victor Marin
f116bb783f fix 2025-11-05 15:45:08 +02:00
Victor Marin
da84148dc5 groupBy per panel action tests 2025-11-05 15:24:14 +02:00
Victor Marin
361fa3582b wip groupBy per panel 2025-11-05 13:00:01 +02:00
Victor Marin
e4289dedc6 wip groupBy per panel 2025-11-03 18:18:26 +02:00
Victor Marin
55a4361f17 Merge branch 'main' into mdvictor/chart-group-by 2025-11-03 15:40:22 +02:00
Victor Marin
da26ce43f5 wip per panel group by 2025-11-03 15:28:37 +02:00
14 changed files with 732 additions and 375 deletions

View File

@@ -377,10 +377,14 @@ export interface FeatureToggles {
*/
perPanelNonApplicableDrilldowns?: boolean;
/**
* Enabled a group by action per panel
* Enables a group by action per panel
*/
panelGroupBy?: boolean;
/**
* Enables filtering by grouping labels on the panel level through legend or tooltip
*/
perPanelFiltering?: boolean;
/**
* Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard
*/
panelFilterVariable?: boolean;

View File

@@ -55,6 +55,15 @@ export interface PanelContext {
*/
onAddAdHocFilter?: (item: AdHocFilterItem) => void;
/**
* Returns filters based on existing grouping or an empty array
*/
getFiltersBasedOnGrouping?: (items: AdHocFilterItem[]) => AdHocFilterItem[];
/**
*
* Used to apply multiple filters at once
*/
onBulkAddAdHocFilters?: (items: AdHocFilterItem[]) => void;
/**
* Enables modifying thresholds directly from the panel
*

View File

@@ -89,4 +89,65 @@ describe('VizTooltipFooter', () => {
expect(screen.queryByRole('button', { name: /filter for 'testValue'/i })).not.toBeInTheDocument();
});
it('should render filter by grouping buttons and fire onclick', async () => {
const onForClick = jest.fn();
const onOutClick = jest.fn();
const filterByGroupedLabels = {
onFilterForGroupedLabels: onForClick,
onFilterOutGroupedLabels: onOutClick,
};
render(
<MemoryRouter>
<VizTooltipFooter dataLinks={[]} filterByGroupedLabels={filterByGroupedLabels} />
</MemoryRouter>
);
const onForButton = screen.getByRole('button', { name: /Apply as filter/i });
expect(onForButton).toBeInTheDocument();
const onOutButton = screen.getByRole('button', { name: /Apply as inverse filter/i });
expect(onOutButton).toBeInTheDocument();
await userEvent.click(onForButton);
expect(onForClick).toHaveBeenCalled();
await userEvent.click(onOutButton);
expect(onOutClick).toHaveBeenCalled();
});
it('should not render filter by grouping buttons when there are one-click links', () => {
const filterByGroupedLabels = {
onFilterForGroupedLabels: jest.fn(),
onFilterOutGroupedLabels: jest.fn(),
};
const onClick = jest.fn();
const field: Field = {
name: '',
type: FieldType.string,
values: [],
config: {},
};
const oneClickLink: LinkModel<Field> = {
href: '#',
onClick,
title: 'One Click Link',
origin: field,
target: undefined,
oneClick: true,
};
render(
<MemoryRouter>
<VizTooltipFooter dataLinks={[oneClickLink]} filterByGroupedLabels={filterByGroupedLabels} />
</MemoryRouter>
);
expect(screen.queryByRole('button', { name: /Apply as filter/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /Apply as inverse filter/i })).not.toBeInTheDocument();
});
});

View File

@@ -17,10 +17,16 @@ export interface AdHocFilterModel extends AdHocFilterItem {
onClick: () => void;
}
export interface FilterByGroupedLabelsModel {
onFilterForGroupedLabels?: () => void;
onFilterOutGroupedLabels?: () => void;
}
interface VizTooltipFooterProps {
dataLinks: Array<LinkModel<Field>>;
actions?: Array<ActionModel<Field>>;
adHocFilters?: AdHocFilterModel[];
filterByGroupedLabels?: FilterByGroupedLabelsModel;
annotate?: () => void;
}
@@ -85,7 +91,13 @@ const renderActions = makeRenderLinksOrActions<ActionModel>(
(item, i) => <ActionButton key={i} action={item} variant="secondary" />
);
export const VizTooltipFooter = ({ dataLinks, actions = [], annotate, adHocFilters = [] }: VizTooltipFooterProps) => {
export const VizTooltipFooter = ({
dataLinks,
actions = [],
annotate,
adHocFilters = [],
filterByGroupedLabels,
}: VizTooltipFooterProps) => {
const styles = useStyles2(getStyles);
const hasOneClickLink = useMemo(() => dataLinks.some((link) => link.oneClick === true), [dataLinks]);
const hasOneClickAction = useMemo(() => actions.some((action) => action.oneClick === true), [actions]);
@@ -105,6 +117,31 @@ export const VizTooltipFooter = ({ dataLinks, actions = [], annotate, adHocFilte
))}
</div>
)}
{!hasOneClickLink && !hasOneClickAction && filterByGroupedLabels && (
<div className={styles.footerSection}>
<Stack direction="column" gap={0.5} width="fit-content">
<Button
icon="filter"
variant="secondary"
size="sm"
onClick={filterByGroupedLabels.onFilterForGroupedLabels}
>
<Trans i18nKey="grafana-ui.viz-tooltip.footer-apply-series-as-filter">Apply as filter</Trans>
</Button>
<Button
icon="filter"
variant="secondary"
size="sm"
onClick={filterByGroupedLabels.onFilterOutGroupedLabels}
>
<Trans i18nKey="grafana-ui.viz-tooltip.footer-apply-series-as-inverse-filter">
Apply as inverse filter
</Trans>
</Button>
</Stack>
</div>
)}
{!hasOneClickLink && !hasOneClickAction && annotate != null && (
<div className={styles.footerSection}>
<Button icon="comment-alt" variant="secondary" size="sm" id={ADD_ANNOTATION_ID} onClick={annotate}>

View File

@@ -84,7 +84,11 @@ export { EmotionPerfTest } from '../components/ThemeDemos/EmotionPerfTest';
export { ThemeDemo } from '../components/ThemeDemos/ThemeDemo';
export { VizTooltipContent } from '../components/VizTooltip/VizTooltipContent';
export { VizTooltipFooter, type AdHocFilterModel } from '../components/VizTooltip/VizTooltipFooter';
export {
VizTooltipFooter,
type AdHocFilterModel,
type FilterByGroupedLabelsModel,
} from '../components/VizTooltip/VizTooltipFooter';
export { VizTooltipHeader } from '../components/VizTooltip/VizTooltipHeader';
export { VizTooltipWrapper } from '../components/VizTooltip/VizTooltipWrapper';
export { VizTooltipRow } from '../components/VizTooltip/VizTooltipRow';

View File

@@ -609,7 +609,14 @@ var (
},
{
Name: "panelGroupBy",
Description: "Enabled a group by action per panel",
Description: "Enables a group by action per panel",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "perPanelFiltering",
Description: "Enables filtering by grouping labels on the panel level through legend or tooltip",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,

View File

@@ -85,6 +85,7 @@ dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
perPanelNonApplicableDrilldowns,experimental,@grafana/dashboards-squad,false,false,true
panelGroupBy,experimental,@grafana/dashboards-squad,false,false,true
perPanelFiltering,experimental,@grafana/dashboards-squad,false,false,true
panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true
pdfTables,preview,@grafana/grafana-operator-experience-squad,false,false,false
canvasPanelPanZoom,preview,@grafana/dataviz-squad,false,false,true
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
85 unlimitedLayoutsNesting experimental @grafana/dashboards-squad false false true
86 perPanelNonApplicableDrilldowns experimental @grafana/dashboards-squad false false true
87 panelGroupBy experimental @grafana/dashboards-squad false false true
88 perPanelFiltering experimental @grafana/dashboards-squad false false true
89 panelFilterVariable experimental @grafana/dashboards-squad false false true
90 pdfTables preview @grafana/grafana-operator-experience-squad false false false
91 canvasPanelPanZoom preview @grafana/dataviz-squad false false true

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
import { EventBusSrv } from '@grafana/data';
import { BackendSrv, setBackendSrv } from '@grafana/runtime';
import { PanelContext } from '@grafana/ui';
import { AdHocVariableModel, EventBusSrv, GroupByVariableModel, VariableModel } from '@grafana/data';
import { BackendSrv, config, setBackendSrv } from '@grafana/runtime';
import { GroupByVariable, sceneGraph } from '@grafana/scenes';
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { findVizPanelByKey } from '../utils/utils';
@@ -159,6 +160,146 @@ describe('setDashboardPanelContext', () => {
expect(variable.state.filters[1].operator).toBe('!=');
});
});
describe('getFiltersBasedOnGrouping', () => {
beforeAll(() => {
config.featureToggles.groupByVariable = true;
});
afterAll(() => {
config.featureToggles.groupByVariable = false;
});
it('should return filters based on grouping', () => {
const { scene, context } = buildTestScene({ existingFilterVariable: true, existingGroupByVariable: true });
const groupBy = sceneGraph.getVariables(scene).state.variables.find((f) => f instanceof GroupByVariable);
groupBy?.changeValueTo(['container', 'cluster']);
const filters: AdHocFilterItem[] = [
{ key: 'container', value: 'container', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
];
const result = context.getFiltersBasedOnGrouping?.(filters);
expect(result).toEqual([
{ key: 'container', value: 'container', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
]);
});
it('should return empty filters if there is no groupBy selection', () => {
const { context } = buildTestScene({ existingFilterVariable: true, existingGroupByVariable: true });
const filters: AdHocFilterItem[] = [
{ key: 'container', value: 'container', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
];
const result = context.getFiltersBasedOnGrouping?.(filters);
expect(result).toEqual([]);
});
it('should return empty filters if there is no groupBy variable', () => {
const { context } = buildTestScene({ existingFilterVariable: true, existingGroupByVariable: false });
const filters: AdHocFilterItem[] = [
{ key: 'container', value: 'container', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
];
const result = context.getFiltersBasedOnGrouping?.(filters);
expect(result).toEqual([]);
});
it('should return empty filters if panel and groupBy ds differs', () => {
const { scene, context } = buildTestScene({
existingFilterVariable: true,
existingGroupByVariable: true,
groupByDatasourceUid: 'different-ds',
});
const groupBy = sceneGraph.getVariables(scene).state.variables.find((f) => f instanceof GroupByVariable);
groupBy?.changeValueTo(['container', 'cluster']);
const filters: AdHocFilterItem[] = [
{ key: 'container', value: 'container', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
];
const result = context.getFiltersBasedOnGrouping?.(filters);
expect(result).toEqual([]);
});
});
describe('onBulkAddAdHocFilters', () => {
it('should add adhoc filters', () => {
const { scene, context } = buildTestScene({
existingFilterVariable: true,
});
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
const filters: AdHocFilterItem[] = [
{ key: 'existing', value: 'val', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
];
context.onBulkAddAdHocFilters?.(filters);
expect(variable.state.filters).toEqual([
{ key: 'existing', value: 'val', operator: '=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
]);
});
it('should update and add adhoc filters', () => {
const { scene, context } = buildTestScene({
existingFilterVariable: true,
});
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
variable.setState({ filters: [{ key: 'existing', value: 'val', operator: '=' }] });
const filters: AdHocFilterItem[] = [
{ key: 'existing', value: 'val', operator: '!=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
];
context.onBulkAddAdHocFilters?.(filters);
expect(variable.state.filters).toEqual([
{ key: 'existing', value: 'val', operator: '!=' },
{ key: 'cluster', value: 'cluster', operator: '=' },
{ key: 'cpu', value: 'cpu', operator: '=' },
{ key: 'id', value: 'id', operator: '=' },
]);
});
it('should not do anything if filters empty', () => {
const { scene, context } = buildTestScene({
existingFilterVariable: true,
});
const variable = getAdHocFilterVariableFor(scene, { uid: 'my-ds-uid' });
const filters: AdHocFilterItem[] = [];
context.onBulkAddAdHocFilters?.(filters);
expect(variable.state.filters).toEqual([]);
});
});
});
interface SceneOptions {
@@ -169,9 +310,29 @@ interface SceneOptions {
canDelete?: boolean;
orgCanEdit?: boolean;
existingFilterVariable?: boolean;
existingGroupByVariable?: boolean;
groupByDatasourceUid?: string;
}
function buildTestScene(options: SceneOptions) {
const varList: VariableModel[] = [];
if (options.existingFilterVariable) {
varList.push({
type: 'adhoc',
name: 'Filters',
datasource: { uid: 'my-ds-uid' },
} as AdHocVariableModel);
}
if (options.existingGroupByVariable) {
varList.push({
type: 'groupby',
name: 'Group By',
datasource: { uid: options.groupByDatasourceUid ?? 'my-ds-uid', type: 'prometheus' },
} as GroupByVariableModel);
}
const scene = transformSaveModelToScene({
dashboard: {
title: 'hello',
@@ -203,15 +364,7 @@ function buildTestScene(options: SceneOptions) {
},
],
templating: {
list: options.existingFilterVariable
? [
{
type: 'adhoc',
name: 'Filters',
datasource: { uid: 'my-ds-uid' },
},
]
: [],
list: varList,
},
},
meta: {

View File

@@ -133,6 +133,43 @@ export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelConte
updateAdHocFilterVariable(filterVar, newFilter);
};
context.getFiltersBasedOnGrouping = (items: AdHocFilterItem[]) => {
const dashboard = getDashboardSceneFor(vizPanel);
const queryRunner = getQueryRunnerFor(vizPanel);
if (!queryRunner) {
return [];
}
const groupByVar = getGroupByVariableFor(dashboard, queryRunner.state.datasource);
if (!groupByVar) {
return [];
}
const currentValues = Array.isArray(groupByVar.state.value)
? groupByVar.state.value
: groupByVar.state.value
? [groupByVar.state.value]
: [];
return items
.map((item) => (currentValues.find((key) => key === item.key) ? item : undefined))
.filter((item) => item !== undefined);
};
context.onBulkAddAdHocFilters = (items: AdHocFilterItem[]) => {
const dashboard = getDashboardSceneFor(vizPanel);
const queryRunner = getQueryRunnerFor(vizPanel);
if (!queryRunner) {
return;
}
const filterVar = getAdHocFilterVariableFor(dashboard, queryRunner.state.datasource);
bulkUpdateAdHocFiltersVariable(filterVar, items);
};
context.canExecuteActions = () => {
const dashboard = getDashboardSceneFor(vizPanel);
return dashboard.canEditDashboard();
@@ -167,6 +204,21 @@ function reRunBuiltInAnnotationsLayer(scene: DashboardScene) {
}
}
function getGroupByVariableFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) {
const variables = sceneGraph.getVariables(scene);
for (const variable of variables.state.variables) {
if (sceneUtils.isGroupByVariable(variable)) {
const filtersDs = variable.state.datasource;
if (filtersDs === ds || filtersDs?.uid === ds?.uid) {
return variable;
}
}
}
return null;
}
export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceRef | null | undefined) {
const variables = sceneGraph.getVariables(scene);
@@ -195,6 +247,35 @@ export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceR
return newVariable;
}
function bulkUpdateAdHocFiltersVariable(filterVar: AdHocFiltersVariable, newFilters: AdHocFilterItem[]) {
if (!newFilters.length) {
return;
}
const updatedFilters = filterVar.state.filters.slice();
let hasChanges = false;
for (const newFilter of newFilters) {
const filterToReplaceIndex = updatedFilters.findIndex(
(filter) =>
filter.key === newFilter.key && filter.value === newFilter.value && filter.operator !== newFilter.operator
);
if (filterToReplaceIndex >= 0) {
updatedFilters.splice(filterToReplaceIndex, 1, newFilter);
hasChanges = true;
continue;
}
updatedFilters.push(newFilter);
hasChanges = true;
}
if (hasChanges) {
filterVar.updateFilters(updatedFilters);
}
}
function updateAdHocFilterVariable(filterVar: AdHocFiltersVariable, newFilter: AdHocFilterItem) {
// This function handles 'Filter for value' and 'Filter out value' from table cell
// We are allowing to add filters with the same key because elastic search ds supports that

View File

@@ -19,7 +19,7 @@ import {
XAxisInteractionAreaPlugin,
usePanelContext,
} from '@grafana/ui';
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
import { FILTER_OUT_OPERATOR, TimeRange2, TooltipHoverMode } from '@grafana/ui/internal';
import { TimeSeries } from 'app/core/components/TimeSeries/TimeSeries';
import { config } from 'app/core/config';
@@ -31,7 +31,7 @@ import { OutsideRangePlugin } from './plugins/OutsideRangePlugin';
import { ThresholdControlsPlugin } from './plugins/ThresholdControlsPlugin';
import { getXAnnotationFrames } from './plugins/utils';
import { getPrepareTimeseriesSuggestion } from './suggestions';
import { getTimezones, prepareGraphableFields } from './utils';
import { getGroupedFilters, getTimezones, prepareGraphableFields } from './utils';
interface TimeSeriesPanelProps extends PanelProps<Options> {}
@@ -56,6 +56,8 @@ export const TimeSeriesPanel = ({
showThresholds,
eventBus,
canExecuteActions,
getFiltersBasedOnGrouping,
onBulkAddAdHocFilters,
} = usePanelContext();
const { dataLinkPostProcessor } = useDataLinksContext();
@@ -175,6 +177,11 @@ export const TimeSeriesPanel = ({
dismiss();
};
const groupingFilters =
seriesIdx && config.featureToggles.perPanelFiltering && getFiltersBasedOnGrouping
? getGroupedFilters(alignedFrame, seriesIdx, getFiltersBasedOnGrouping)
: [];
return (
// not sure it header time here works for annotations, since it's taken from nearest datapoint index
<TimeSeriesTooltip
@@ -189,6 +196,17 @@ export const TimeSeriesPanel = ({
maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables}
dataLinks={dataLinks}
filterByGroupedLabels={
config.featureToggles.perPanelFiltering && groupingFilters.length && onBulkAddAdHocFilters
? {
onFilterForGroupedLabels: () => onBulkAddAdHocFilters(groupingFilters),
onFilterOutGroupedLabels: () =>
onBulkAddAdHocFilters(
groupingFilters.map((item) => ({ ...item, operator: FILTER_OUT_OPERATOR }))
),
}
: undefined
}
canExecuteActions={userCanExecuteActions}
compareDiffMs={compareDiffMs}
/>

View File

@@ -18,6 +18,7 @@ import {
getContentItems,
VizTooltipItem,
AdHocFilterModel,
FilterByGroupedLabelsModel,
} from '@grafana/ui/internal';
import { getFieldActions } from '../status-history/utils';
@@ -50,6 +51,7 @@ export interface TimeSeriesTooltipProps {
dataLinks: LinkModel[];
hideZeros?: boolean;
adHocFilters?: AdHocFilterModel[];
filterByGroupedLabels?: FilterByGroupedLabelsModel;
canExecuteActions?: boolean;
compareDiffMs?: number[];
}
@@ -70,8 +72,10 @@ export const TimeSeriesTooltip = ({
adHocFilters,
canExecuteActions,
compareDiffMs,
filterByGroupedLabels,
}: TimeSeriesTooltipProps) => {
const pluginContext = usePluginContext();
const xField = series.fields[0];
let xVal = xField.values[dataIdxs[0]!];
@@ -107,7 +111,13 @@ export const TimeSeriesTooltip = ({
: [];
footer = (
<VizTooltipFooter dataLinks={dataLinks} actions={actions} annotate={annotate} adHocFilters={adHocFilters} />
<VizTooltipFooter
dataLinks={dataLinks}
actions={actions}
annotate={annotate}
adHocFilters={adHocFilters}
filterByGroupedLabels={filterByGroupedLabels}
/>
);
}
}

View File

@@ -12,7 +12,8 @@ import {
} from '@grafana/data';
import { convertFieldType } from '@grafana/data/internal';
import { GraphFieldConfig, LineInterpolation, TooltipDisplayMode, VizTooltipOptions } from '@grafana/schema';
import { buildScaleKey } from '@grafana/ui/internal';
import { AdHocFilterItem } from '@grafana/ui';
import { buildScaleKey, FILTER_FOR_OPERATOR } from '@grafana/ui/internal';
import { HeatmapTooltip } from '../heatmap/panelcfg.gen';
@@ -329,3 +330,28 @@ export function getTimezones(timezones: string[] | undefined, defaultTimezone: s
export const isTooltipScrollable = (tooltipOptions: VizTooltipOptions | HeatmapTooltip) => {
return tooltipOptions.mode === TooltipDisplayMode.Multi && tooltipOptions.maxHeight != null;
};
export function getGroupedFilters(
frame: DataFrame,
seriesIdx: number,
getFiltersBasedOnGrouping: (filters: AdHocFilterItem[]) => AdHocFilterItem[]
) {
const groupingFilters: AdHocFilterItem[] = [];
const xField = frame.fields[seriesIdx];
if (xField && xField.labels && xField.config.filterable) {
const seriesFilters: AdHocFilterItem[] = [];
Object.entries(xField.labels).forEach(([key, value]) => {
seriesFilters.push({
key,
operator: FILTER_FOR_OPERATOR,
value,
});
});
groupingFilters.push(...getFiltersBasedOnGrouping(seriesFilters));
}
return groupingFilters;
}

View File

@@ -9250,6 +9250,8 @@
"actions-confirmation-label": "Confirmation message",
"actions-confirmation-message": "Provide a descriptive prompt to confirm or cancel the action.",
"footer-add-annotation": "Add annotation",
"footer-apply-series-as-filter": "Apply as filter",
"footer-apply-series-as-inverse-filter": "Apply as inverse filter",
"footer-click-to-action": "Click to {{actionTitle}}",
"footer-click-to-navigate": "Click to open {{linkTitle}}",
"footer-filter-for-value": "Filter for '{{value}}'",