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:
co-authored by
Sergej-Vlasov
Dominik Prokop
parent
fef6196195
commit
7ea009c7f8
+5
-1
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user