Dashboards: Per panel filtering for timeseries (#114499)

* wip per panel group by

* wip groupBy per panel

* wip groupBy per panel

* groupBy per panel action tests

* fix

* fix

* fix

* fix

* CR mods

* switch to dropdown

* adjust apply

* optimise action logic to avoid unnecessary triggers

* canary scenes

* wip

(cherry picked from commit 51a00db93d0805f481a9e48213382468f1eb2986)

* optimise action logic to avoid unnecessary triggers

(cherry picked from commit c4de2dfff8)

* refactor

* refactor

* memoize values/ refactor

* refactor

* refactor components - do not make async call unless queries/groupByOptions change

* canary scenes

* fix test

* Optimise handlers

* Reset options if they are not applied

* refactor subscriptions

* refactor

* scenes bump

* fixes

* properly deactivate header actions on panel edit

* list

* refactor showing menu using css, remove header deactivation code from panel-edit

* cleanup

* cleanup

* cleanup + action redesign

* i18n

* wip

* wip

* wip

* wip

* wip

* tests

* pr mods

* translations

* fix

* fix

* fixes

* translations

* translations

* extra ff check

* CR mods

---------

Co-authored-by: Sergej-Vlasov <sergej.s.vlasov@gmail.com>
Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
Victor Marin
2025-12-08 16:18:04 +00:00
committed by GitHub
co-authored by Sergej-Vlasov Dominik Prokop
parent fef6196195
commit 7ea009c7f8
16 changed files with 834 additions and 376 deletions
+5 -1
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;
@@ -1490,6 +1490,16 @@ export const versionedComponents = {
},
},
},
VizTooltipFooter: {
buttons: {
apply: {
['12.1.0']: 'data-testid viz-tooltip-footer-apply-filters-button',
},
applyInverse: {
['12.1.0']: 'data-testid viz-tooltip-footer-apply-inverse-filters-button',
},
},
},
} satisfies VersionedSelectorGroup;
export type VersionedComponents = typeof versionedComponents;
@@ -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
*/
onAddAdHocFilters?: (items: AdHocFilterItem[]) => void;
/**
* Enables modifying thresholds directly from the panel
*
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
import { MemoryRouter } from 'react-router-dom-v5-compat';
import { Field, FieldType, LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VizTooltipFooter, AdHocFilterModel } from './VizTooltipFooter';
@@ -89,4 +90,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.queryByTestId(selectors.components.VizTooltipFooter.buttons.apply)).not.toBeInTheDocument();
expect(screen.queryByTestId(selectors.components.VizTooltipFooter.buttons.applyInverse)).not.toBeInTheDocument();
});
});
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import { useMemo } from 'react';
import { ActionModel, Field, GrafanaTheme2, LinkModel, ThemeSpacingTokens } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { useStyles2 } from '../../themes/ThemeContext';
@@ -17,10 +18,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 +92,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 +118,39 @@ 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"
data-testid={selectors.components.VizTooltipFooter.buttons.apply}
>
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"
data-testid={selectors.components.VizTooltipFooter.buttons.applyInverse}
>
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}>
+5 -1
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';