Dashboards: Per panel GroupBy action (#113445)

* 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

* pr mods

* translations

* fix

* fix

* fix design

---------

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-11-28 15:03:11 +02:00
committed by GitHub
co-authored by Sergej-Vlasov Dominik Prokop
parent f0e9c2e8a3
commit d3d294c99e
14 changed files with 648 additions and 2 deletions
+4
View File
@@ -377,6 +377,10 @@ export interface FeatureToggles {
*/
perPanelNonApplicableDrilldowns?: boolean;
/**
* Enabled a group by action per panel
*/
panelGroupBy?: boolean;
/**
* Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard
*/
panelFilterVariable?: boolean;
@@ -443,6 +443,9 @@ export const versionedComponents = {
PanelDataErrorMessage: {
'10.4.0': 'data-testid Panel data error message',
},
PanelGroupByHeaderAction: {
'12.4.0': 'data-testid Panel group by header action',
},
},
Visualization: {
Graph: {
+7
View File
@@ -607,6 +607,13 @@ var (
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "panelGroupBy",
Description: "Enabled a group by action per panel",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaDashboardsSquad,
},
{
Name: "panelFilterVariable",
Description: "Enables use of the `systemPanelFilterVar` variable to filter panels in a dashboard",
+1
View File
@@ -84,6 +84,7 @@ kubernetesDashboardsV2,experimental,@grafana/dashboards-squad,false,false,false
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
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
84 dashboardUndoRedo experimental @grafana/dashboards-squad false false true
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 panelFilterVariable experimental @grafana/dashboards-squad false false true
89 pdfTables preview @grafana/grafana-operator-experience-squad false false false
90 canvasPanelPanZoom preview @grafana/dataviz-squad false false true
+13
View File
@@ -2512,6 +2512,19 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "panelGroupBy",
"resourceVersion": "1764257043719",
"creationTimestamp": "2025-11-27T15:24:03Z"
},
"spec": {
"description": "Enabled a group by action per panel",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"frontend": true
}
},
{
"metadata": {
"name": "panelTimeSettings",
@@ -0,0 +1,154 @@
import { of } from 'rxjs';
import { DataQueryRequest, DataSourceApi, LoadingState } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test';
import { setPluginImportUtils } from '@grafana/runtime';
import {
GroupByVariable,
SceneDataTransformer,
SceneQueryRunner,
SceneVariableSet,
VizPanel,
VizPanelState,
} from '@grafana/scenes';
import { activateFullSceneTree } from '../utils/test-utils';
import { DashboardScene } from './DashboardScene';
import { VizPanelHeaderActions } from './VizPanelHeaderActions';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
const runRequestMock = jest.fn().mockImplementation((ds: DataSourceApi, request: DataQueryRequest) => {
return of({
state: LoadingState.Loading,
series: [],
timeRange: request.range,
});
});
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
return runRequestMock(ds, request);
},
getDataSourceSrv: () => ({
get: jest.fn().mockResolvedValue({
getRef: () => ({ uid: 'ds-1', type: 'test' }),
}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds-1', type: 'test' }),
}),
getPluginImportUtils: () => ({
getPanelPluginFromCache: jest.fn(() => undefined),
}),
}));
setPluginImportUtils({
importPanelPlugin: () => Promise.resolve(getPanelPlugin({})),
getPanelPluginFromCache: () => undefined,
});
describe('VizPanelHeaderActions', () => {
it('renders PanelGroupByAction when the group by variable applies to the panel', async () => {
const { headerActions } = await buildScene();
expect(headerActions.state.supportsApplicability).toBe(true);
});
it('does not set when applicability is disabled', async () => {
const { headerActions } = await buildScene({ applicabilityEnabled: false });
expect(headerActions.state.supportsApplicability).toBe(false);
});
it('does not set when the datasource uid does not match', async () => {
const { headerActions } = await buildScene({
variableDatasourceUid: 'other-ds',
});
expect(headerActions.state.supportsApplicability).toBe(false);
});
it('does not set if variable ds changes to a different type', async () => {
const { headerActions, groupByVariable } = await buildScene();
expect(headerActions.state.supportsApplicability).toBe(true);
groupByVariable.setState({ datasource: { uid: 'ds-2' } });
expect(headerActions.state.supportsApplicability).toBe(false);
});
it('does not set if variable applicability becomes disabled', async () => {
const { headerActions, groupByVariable } = await buildScene();
expect(headerActions.state.supportsApplicability).toBe(true);
groupByVariable.setState({ applicabilityEnabled: false });
expect(headerActions.state.supportsApplicability).toBe(false);
});
it('sdoes not set if queryRunner changes datasource to different one than vars', async () => {
const { headerActions, queryRunner } = await buildScene();
expect(headerActions.state.supportsApplicability).toBe(true);
queryRunner.setState({ datasource: { uid: 'ds-2' } });
expect(headerActions.state.supportsApplicability).toBe(false);
});
});
interface BuildSceneOptions {
applicabilityEnabled?: boolean;
variableDatasourceUid?: string;
}
async function buildScene(options?: BuildSceneOptions) {
const headerActions = new VizPanelHeaderActions({});
const queryRunner = new SceneQueryRunner({
datasource: { uid: 'ds-1' },
queries: [{ refId: 'A', datasource: { uid: 'ds-1' } }],
});
const groupByVariable = new GroupByVariable({
name: 'group',
label: 'group',
value: [],
text: [],
options: [],
applicabilityEnabled: options?.applicabilityEnabled ?? true,
datasource: { uid: options?.variableDatasourceUid ?? 'ds-1' },
});
const dataProvider = new SceneDataTransformer({
$data: queryRunner,
transformations: [],
});
const panelState: VizPanelState = {
key: 'panel-1',
title: 'Panel A',
pluginId: 'timeseries',
headerActions,
$data: dataProvider,
options: {},
fieldConfig: { defaults: {}, overrides: [] },
};
const panel = new VizPanel(panelState);
const scene = new DashboardScene({
$variables: new SceneVariableSet({
variables: [groupByVariable],
}),
body: DefaultGridLayoutManager.fromVizPanels([panel]),
});
activateFullSceneTree(scene);
await new Promise((r) => setTimeout(r, 1));
return { headerActions, panel, groupByVariable, queryRunner };
}
@@ -0,0 +1,136 @@
import { Unsubscribable } from 'rxjs';
import {
GroupByVariable,
SceneComponentProps,
sceneGraph,
SceneObjectBase,
SceneObjectState,
SceneQueryRunner,
VizPanel,
} from '@grafana/scenes';
import { DataSourceRef } from '@grafana/schema';
import { verifyDrilldownApplicability } from '../utils/drilldownUtils';
import { PanelGroupByAction } from './panel-actions/PanelGroupByAction/PanelGroupByAction';
export interface VizPanelHeaderActionsState extends SceneObjectState {
hideGroupByAction?: boolean;
supportsApplicability?: boolean;
}
export class VizPanelHeaderActions extends SceneObjectBase<VizPanelHeaderActionsState> {
static Component = VizPanelHeaderActionsRenderer;
private _groupByVar?: GroupByVariable;
private _groupBySub?: Unsubscribable;
private _queryRunnerDatasource?: DataSourceRef;
constructor(state: Partial<VizPanelHeaderActionsState>) {
super({
hideGroupByAction: state.hideGroupByAction ?? false,
...state,
});
this.addActivationHandler(this._onActivate);
}
private _onActivate = () => {
if (!this.parent || !(this.parent instanceof VizPanel)) {
throw new Error('VizPanelHeaderActions must be a child of a VizPanel');
}
if (!this.state.hideGroupByAction) {
this.subscribeToGroupByChanges();
}
return () => {
this._groupBySub?.unsubscribe();
};
};
private setAplicabilitySupport(groupByDs?: DataSourceRef | null, groupByApplicability?: boolean) {
this.setState({
supportsApplicability: verifyDrilldownApplicability(
this,
this._queryRunnerDatasource,
groupByDs ?? this._groupByVar?.state.datasource ?? null,
groupByApplicability ?? this._groupByVar?.state.applicabilityEnabled ?? false
),
});
}
private subscribeToGroupByChanges() {
const vars = sceneGraph.getVariables(this);
const queryRunner = this.getQueryRunner();
this._groupByVar = vars.state.variables.find((variable) => variable instanceof GroupByVariable);
this._queryRunnerDatasource = queryRunner?.state.datasource;
this.setAplicabilitySupport();
// check when var set updates and search for groupBy var
this._subs.add(
vars.subscribeToState((n) => {
this._groupByVar = n.variables.find((variable) => variable instanceof GroupByVariable);
if (this._groupByVar) {
this._groupBySub?.unsubscribe();
this._groupBySub = this._groupByVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled);
}
});
}
})
);
// update query runner datasource changes
this._subs.add(
queryRunner?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource) {
this._queryRunnerDatasource = n.datasource;
this.setAplicabilitySupport();
}
})
);
this._groupBySub = this._groupByVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setAplicabilitySupport(n.datasource, n.applicabilityEnabled);
}
});
}
public getQueryRunner() {
const panel = this.parent;
const dataObject = panel ? sceneGraph.getData(panel) : undefined;
const queryRunner = dataObject?.state.$data;
if (!queryRunner || !(queryRunner instanceof SceneQueryRunner)) {
return null;
}
return queryRunner;
}
}
export function VizPanelHeaderActionsRenderer({ model }: SceneComponentProps<VizPanelHeaderActions>) {
const { hideGroupByAction, supportsApplicability } = model.useState();
const variables = sceneGraph.getVariables(model);
const groupByVariable = variables.state.variables.find((variable) => variable instanceof GroupByVariable);
const queryRunner = model.getQueryRunner();
const queries = queryRunner?.state.data?.request?.targets ?? [];
return (
<>
{!hideGroupByAction && supportsApplicability && (
<div className="show-on-hover">
<PanelGroupByAction groupByVariable={groupByVariable!} queries={queries} />
</div>
)}
</>
);
}
@@ -116,7 +116,7 @@ export class VizPanelSubHeader extends SceneObjectBase<VizPanelSubHeaderState> {
private refreshDrilldownVarsSubscriptions() {
if (this._groupByVar) {
this._groupBySub?.unsubscribe();
this._groupByVar?.subscribeToState((n, p) => {
this._groupBySub = this._groupByVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper(undefined, {
datasource: n.datasource,
@@ -127,7 +127,8 @@ export class VizPanelSubHeader extends SceneObjectBase<VizPanelSubHeaderState> {
}
if (this._adHocVar) {
this._adHocVar?.subscribeToState((n, p) => {
this._adHocSub?.unsubscribe();
this._adHocSub = this._adHocVar?.subscribeToState((n, p) => {
if (n.datasource !== p.datasource || n.applicabilityEnabled !== p.applicabilityEnabled) {
this.setDrilldownApplicabilitySupportHelper({
datasource: n.datasource,
@@ -0,0 +1,150 @@
import { useState, useCallback, useEffect, useMemo, useRef } from 'react';
import { lastValueFrom } from 'rxjs';
import { fuzzySearch } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Trans } from '@grafana/i18n';
import { GroupByVariable, SceneDataQuery, VariableValueOption, VariableValueSingle } from '@grafana/scenes';
import { Button, Icon, Popover } from '@grafana/ui';
import { PanelGroupByActionPopover } from './PanelGroupByActionPopover';
interface Props {
groupByVariable: GroupByVariable;
queries: SceneDataQuery[];
}
export function PanelGroupByAction({ groupByVariable, queries }: Props) {
const { options: groupByOptions } = groupByVariable.useState();
const [options, setOptions] = useState<VariableValueOption[]>([]);
const [selectedValues, setSelectedValues] = useState<VariableValueSingle[]>([]);
const [searchValue, setSearchValue] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [reloadOptions, setReloadOptions] = useState(false);
const [isPopoverVisible, setPopoverVisible] = useState<boolean>(false);
const ref = useRef<HTMLButtonElement>(null);
const fetchOptions = useCallback(async () => {
if (!groupByVariable || !reloadOptions) {
return;
}
setIsLoading(true);
try {
if (groupByOptions.length === 0) {
await lastValueFrom(groupByVariable.validateAndUpdate());
const options = await getApplicableGroupByOptions(groupByVariable, groupByVariable.state.options, queries);
setSelectedValues(getGroupByValue(groupByVariable));
setOptions(options);
return;
}
const options = await getApplicableGroupByOptions(groupByVariable, groupByOptions, queries);
setSelectedValues(getGroupByValue(groupByVariable));
setOptions(options);
} catch (error) {
setSelectedValues([]);
setOptions([]);
} finally {
setIsLoading(false);
setReloadOptions(false);
}
}, [groupByOptions, groupByVariable, queries, reloadOptions]);
useEffect(() => {
setReloadOptions(true);
}, [groupByOptions, queries]);
useEffect(() => {
if (isPopoverVisible) {
fetchOptions();
}
}, [fetchOptions, isPopoverVisible]);
const filteredOptions = useMemo(() => {
if (!searchValue) {
return options;
}
const haystack = options.map((option) => option.label);
const indices = fuzzySearch(haystack, searchValue);
return indices.map((idx) => options[idx]);
}, [options, searchValue]);
const onCancel = () => {
setSearchValue('');
setPopoverVisible(false);
};
const openPopover = () => {
// Reset checked state to match current variable value when opening
setSelectedValues(getGroupByValue(groupByVariable));
setPopoverVisible(true);
};
return (
<Button
variant="secondary"
size="sm"
fill="text"
data-testid={selectors.components.Panels.Panel.PanelGroupByHeaderAction}
ref={ref}
onClick={(ev) => {
openPopover();
ev.stopPropagation();
}}
>
<Trans i18nKey="panel-group-by.button">Group by</Trans>
<Icon name="angle-down" />
{isPopoverVisible && ref.current && (
<Popover
content={
<PanelGroupByActionPopover
groupByVariable={groupByVariable}
onCancel={onCancel}
isLoading={isLoading}
searchValue={searchValue}
setSearchValue={setSearchValue}
options={filteredOptions}
values={selectedValues}
onValuesChange={setSelectedValues}
/>
}
onKeyDown={(event) => {
if (event.key === ' ') {
event.stopPropagation();
}
}}
placement="bottom-start"
referenceElement={ref.current}
show
/>
)}
</Button>
);
}
function getGroupByValue(groupByVariable: GroupByVariable) {
return Array.isArray(groupByVariable.state.value)
? groupByVariable.state.value
: groupByVariable.state.value
? [groupByVariable.state.value]
: [];
}
async function getApplicableGroupByOptions(
groupByVariable: GroupByVariable,
options: VariableValueOption[],
queries: SceneDataQuery[]
) {
const values = options.map((option) => option.value);
const applicability = await groupByVariable.getGroupByApplicabilityForQueries(values, queries);
return applicability
? applicability.filter((item) => item.applicable).map((item) => ({ label: item.key, value: item.key }))
: options;
}
@@ -0,0 +1,156 @@
import { css, cx } from '@emotion/css';
import { useCallback } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { GroupByVariable, VariableValueOption, VariableValueSingle } from '@grafana/scenes';
import { Button, Checkbox, ClickOutsideWrapper, Icon, Input, Stack, useStyles2 } from '@grafana/ui';
interface Props {
groupByVariable: GroupByVariable;
onCancel: () => void;
isLoading: boolean;
searchValue: string;
setSearchValue: (value: string) => void;
options: VariableValueOption[];
values: VariableValueSingle[];
onValuesChange: (value: VariableValueSingle[]) => void;
}
export function PanelGroupByActionPopover({
groupByVariable,
onCancel,
isLoading,
searchValue,
setSearchValue,
options,
values,
onValuesChange,
}: Props) {
const styles = useStyles2(getStyles);
const onCheckedChanged = useCallback(
(option: VariableValueOption) => (event: React.FormEvent<HTMLInputElement>) => {
const newValues = event.currentTarget.checked
? values.concat(option.value)
: values.filter((c) => c !== option.value);
onValuesChange(newValues);
},
[onValuesChange, values]
);
const isChecked = (option: VariableValueOption) => {
return values.includes(option.value);
};
const handleApply = useCallback(() => {
if (!values.length) {
return;
}
groupByVariable.changeValueTo(values, values.map(String), true);
onCancel();
}, [groupByVariable, onCancel, values]);
const isAnyOptionChecked = () => {
if (!values.length) {
return false;
}
return values.some((value) => options.find((option) => option.value === value));
};
return (
<ClickOutsideWrapper onClick={onCancel} useCapture={true}>
{/* This is just blocking click events from bubbeling and should not have a keyboard interaction. */}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<div className={styles.menuContainer} onClick={(ev) => ev.stopPropagation()}>
<Stack direction="column">
<div className={styles.searchContainer}>
<Input
prefix={<Icon name="search" />}
placeholder={t('panel-group-by.search-placeholder', 'Search')}
value={searchValue}
onChange={(e) => setSearchValue(e.currentTarget.value)}
/>
</div>
<div className={styles.listContainer}>
{isLoading ? (
<div className={styles.emptyMessage}>
<Trans i18nKey="panel-group-by.loading">Loading options</Trans>
</div>
) : options.length === 0 ? (
<div className={styles.emptyMessage}>
<Trans i18nKey="panel-group-by.no-options">No options found</Trans>
</div>
) : (
options.map((option) => {
return (
<div key={String(option.value)} className={cx(styles.option)}>
<Checkbox value={isChecked(option)} label={option.label} onChange={onCheckedChanged(option)} />
</div>
);
})
)}
</div>
<Stack justifyContent="end" direction="row-reverse">
<Button size="sm" onClick={handleApply} disabled={!isAnyOptionChecked()}>
<Trans i18nKey="grafana-ui.table.filter-popup-apply">Ok</Trans>
</Button>
<Button size="sm" variant="secondary" onClick={onCancel}>
<Trans i18nKey="grafana-ui.table.filter-popup-cancel">Cancel</Trans>
</Button>
</Stack>
</Stack>
</div>
</ClickOutsideWrapper>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
menuContainer: css({
display: 'flex',
flexDirection: 'column',
background: theme.colors.background.elevated,
border: `1px solid ${theme.colors.border.weak}`,
borderRadius: theme.shape.radius.default,
boxShadow: theme.shadows.z3,
padding: theme.spacing(2),
}),
searchContainer: css({
width: '100%',
paddingBottom: theme.spacing(1),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
listContainer: css({
flex: 1,
overflow: 'auto',
minHeight: '100px',
maxHeight: '300px',
padding: theme.spacing(0.5),
borderBottom: `1px solid ${theme.colors.border.weak}`,
}),
option: css({
display: 'flex',
alignItems: 'center',
gap: theme.spacing(1),
padding: theme.spacing(1),
cursor: 'pointer',
borderRadius: theme.shape.radius.default,
'&:hover': {
background: theme.colors.background.secondary,
},
'&:focus-visible': {
outline: `2px solid ${theme.colors.primary.border}`,
outlineOffset: '-2px',
},
}),
emptyMessage: css({
padding: theme.spacing(2),
textAlign: 'center',
color: theme.colors.text.secondary,
}),
});
@@ -31,6 +31,7 @@ import { LibraryPanelBehavior } from '../../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../../scene/PanelMenuBehavior';
import { PanelNotices } from '../../scene/PanelNotices';
import { VizPanelHeaderActions } from '../../scene/VizPanelHeaderActions';
import { VizPanelSubHeader } from '../../scene/VizPanelSubHeader';
import { AutoGridItem } from '../../scene/layout-auto-grid/AutoGridItem';
import { DashboardGridItem } from '../../scene/layout-default/DashboardGridItem';
@@ -71,6 +72,9 @@ export function buildVizPanel(panel: PanelKind, id?: number): VizPanel {
seriesLimit: config.panelSeriesLimit,
$data: createPanelDataProvider(panel),
titleItems,
headerActions: new VizPanelHeaderActions({
hideGroupByAction: !config.featureToggles.panelGroupBy,
}),
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
@@ -121,6 +125,9 @@ export function buildLibraryPanel(panel: LibraryPanelKind, id?: number): VizPane
}),
],
extendPanelContext: setDashboardPanelContext,
headerActions: new VizPanelHeaderActions({
hideGroupByAction: !config.featureToggles.panelGroupBy,
}),
pluginId: LibraryPanelBehavior.LOADING_VIZ_PANEL_PLUGIN_ID,
title: panel.spec.title,
options: {},
@@ -46,6 +46,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelLinksBehavior, panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { PanelNotices } from '../scene/PanelNotices';
import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions';
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
import { DashboardGridItem, RepeatDirection } from '../scene/layout-default/DashboardGridItem';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
@@ -442,6 +443,9 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
hoverHeaderOffset: 0,
$data: createPanelDataProvider(panel),
titleItems,
headerActions: new VizPanelHeaderActions({
hideGroupByAction: !config.featureToggles.panelGroupBy,
}),
subHeader: new VizPanelSubHeader({
hideNonApplicableDrilldowns: !config.featureToggles.perPanelNonApplicableDrilldowns,
}),
@@ -24,6 +24,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { UNCONFIGURED_PANEL_PLUGIN_ID } from '../scene/UnconfiguredPanel';
import { VizPanelHeaderActions } from '../scene/VizPanelHeaderActions';
import { VizPanelSubHeader } from '../scene/VizPanelSubHeader';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
@@ -283,6 +284,9 @@ export function getDefaultVizPanel(): VizPanel {
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
headerActions: new VizPanelHeaderActions({
hideGroupByAction: !config.featureToggles.panelGroupBy,
}),
$data: new SceneDataTransformer({
$data: new SceneQueryRunner({
queries: [{ refId: 'A' }],
+6
View File
@@ -11094,6 +11094,12 @@
"could-anything-matching-query": "Could not find anything matching your query"
}
},
"panel-group-by": {
"button": "Group by",
"loading": "Loading options",
"no-options": "No options found",
"search-placeholder": "Search"
},
"panel-type-filter": {
"clear-button": "Clear types",
"select-aria-label": "Panel type filter",