Dashboard Controls: Make it possible to hide dashboard controls from the URL (#111001)

* feat: make it possible to hide dashboard-controls from the URL

* tests: update tests with the new url query param
This commit is contained in:
Levente Balogh
2025-09-15 14:17:35 +02:00
committed by GitHub
parent fd8c7fbc22
commit 4989c12695
2 changed files with 110 additions and 22 deletions
@@ -14,17 +14,56 @@ describe('DashboardControls', () => {
expect(scene.state.refreshPicker).toBeDefined();
});
it('should return if time controls are hidden', () => {
const scene = buildTestScene({
hideTimeControls: false,
hideVariableControls: false,
hideLinksControls: false,
describe('.hasControls()', () => {
it('should return TRUE if any of the controls are available', () => {
const scene = buildTestScene({
hideTimeControls: false,
hideVariableControls: false,
hideLinksControls: false,
hideDashboardControls: false,
});
// All controls visible
expect(scene.hasControls()).toBeTruthy();
// Hiding time controls
scene.setState({
hideTimeControls: true,
hideVariableControls: false,
hideLinksControls: false,
hideDashboardControls: false,
});
expect(scene.hasControls()).toBeTruthy();
// Hide variable controls as well
scene.setState({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: false,
hideDashboardControls: false,
});
expect(scene.hasControls()).toBeTruthy();
// Hide link controls as well
scene.setState({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: false,
});
expect(scene.hasControls()).toBeTruthy();
});
it('should return FALSE if no controls are available', () => {
const scene = buildTestScene({
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: true,
});
expect(scene.hasControls()).toBeFalsy();
});
expect(scene.hasControls()).toBeTruthy();
scene.setState({ hideTimeControls: true });
expect(scene.hasControls()).toBeTruthy();
scene.setState({ hideVariableControls: true, hideLinksControls: true });
expect(scene.hasControls()).toBeFalsy();
});
});
@@ -52,10 +91,11 @@ describe('DashboardControls', () => {
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: true,
});
const renderer = render(<scene.Component model={scene} />);
expect(await renderer.queryByTestId(selectors.pages.Dashboard.Controls)).not.toBeInTheDocument();
expect(renderer.queryByTestId(selectors.pages.Dashboard.Controls)).not.toBeInTheDocument();
});
});
@@ -63,7 +103,12 @@ describe('DashboardControls', () => {
it('should return keys', () => {
const scene = buildTestScene();
// @ts-expect-error
expect(scene._urlSync.getKeys()).toEqual(['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks']);
expect(scene._urlSync.getKeys()).toEqual([
'_dash.hideTimePicker',
'_dash.hideVariables',
'_dash.hideLinks',
'_dash.hideDashboardControls',
]);
});
it('should not return url state for hide flags', () => {
@@ -73,6 +118,7 @@ describe('DashboardControls', () => {
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: true,
});
expect(scene.getUrlState()).toEqual({});
});
@@ -83,18 +129,22 @@ describe('DashboardControls', () => {
'_dash.hideTimePicker': 'true',
'_dash.hideVariables': 'true',
'_dash.hideLinks': 'true',
'_dash.hideDashboardControls': 'true',
});
expect(scene.state.hideTimeControls).toBeTruthy();
expect(scene.state.hideVariableControls).toBeTruthy();
expect(scene.state.hideLinksControls).toBeTruthy();
expect(scene.state.hideDashboardControls).toBeTruthy();
scene.updateFromUrl({
'_dash.hideTimePicker': '',
'_dash.hideVariables': '',
'_dash.hideLinks': '',
'_dash.hideDashboardControls': '',
});
expect(scene.state.hideTimeControls).toBeTruthy();
expect(scene.state.hideVariableControls).toBeTruthy();
expect(scene.state.hideLinksControls).toBeTruthy();
expect(scene.state.hideDashboardControls).toBeTruthy();
});
it('should not override state if no new state comes from url', () => {
@@ -102,11 +152,13 @@ describe('DashboardControls', () => {
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: true,
});
scene.updateFromUrl({});
expect(scene.state.hideTimeControls).toBeTruthy();
expect(scene.state.hideVariableControls).toBeTruthy();
expect(scene.state.hideLinksControls).toBeTruthy();
expect(scene.state.hideDashboardControls).toBeTruthy();
});
it('should not call setState if no changes', () => {
@@ -114,6 +166,7 @@ describe('DashboardControls', () => {
hideTimeControls: true,
hideVariableControls: true,
hideLinksControls: true,
hideDashboardControls: true,
});
const setState = jest.spyOn(scene, 'setState');
@@ -121,6 +174,7 @@ describe('DashboardControls', () => {
'_dash.hideTimePicker': 'true',
'_dash.hideVariables': 'true',
'_dash.hideLinks': 'true',
'_dash.hideDashboardControls': 'true',
});
expect(setState).toHaveBeenCalledTimes(0);
@@ -151,6 +205,19 @@ function buildTestScene(state?: Partial<DashboardControlsState>): DashboardContr
targetBlank: false,
tooltip: 'Link',
},
{
title: 'Link (dashboard controls)',
url: 'http://localhost:3000/$A',
type: 'link',
asDropdown: false,
icon: '',
includeVars: true,
keepTime: true,
tags: [],
targetBlank: false,
tooltip: 'Link',
placement: 'inControlsMenu',
},
],
$variables: new SceneVariableSet({
variables: [variable],
@@ -31,6 +31,8 @@ export interface DashboardControlsState extends SceneObjectState {
hideTimeControls?: boolean;
hideVariableControls?: boolean;
hideLinksControls?: boolean;
// Hides the dashbaord-controls dropdown menu
hideDashboardControls?: boolean;
}
export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
@@ -41,7 +43,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
});
protected _urlSync = new SceneObjectUrlSyncConfig(this, {
keys: ['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks'],
keys: ['_dash.hideTimePicker', '_dash.hideVariables', '_dash.hideLinks', '_dash.hideDashboardControls'],
});
/**
@@ -53,7 +55,7 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
}
updateFromUrl(values: SceneObjectUrlValues) {
const { hideTimeControls, hideVariableControls, hideLinksControls } = this.state;
const { hideTimeControls, hideVariableControls, hideLinksControls, hideDashboardControls } = this.state;
const isEnabledViaUrl = (key: string) => values[key] === 'true' || values[key] === '';
// Only allow hiding, never "unhiding" from url
@@ -70,6 +72,10 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
if (!hideLinksControls && isEnabledViaUrl('_dash.hideLinks')) {
this.setState({ hideLinksControls: true });
}
if (!hideDashboardControls && isEnabledViaUrl('_dash.hideDashboardControls')) {
this.setState({ hideDashboardControls: true });
}
}
public constructor(state: Partial<DashboardControlsState>) {
@@ -104,6 +110,18 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
}
}
// Dashboard controls is a separate dropdown menu at the top-right of the controls
public hasDashboardControls(): boolean {
const dashboard = getDashboardSceneFor(this);
const { links } = dashboard.state;
const hasControlMenuVariables = sceneGraph
.getVariables(dashboard)
?.state.variables.some((v) => v.state.showInControlsMenu === true);
const hasControlMenuLinks = links.some((link) => link.placement === 'inControlsMenu');
return hasControlMenuVariables || hasControlMenuLinks;
}
public hasControls(): boolean {
const hasVariables = sceneGraph
.getVariables(this)
@@ -113,22 +131,25 @@ export class DashboardControls extends SceneObjectBase<DashboardControlsState> {
const hideLinks = this.state.hideLinksControls || !hasLinks;
const hideVariables = this.state.hideVariableControls || (!hasAnnotations && !hasVariables);
const hideTimePicker = this.state.hideTimeControls;
const hideDashboardControls = this.state.hideDashboardControls || !this.hasDashboardControls();
return !(hideVariables && hideLinks && hideTimePicker);
return !(hideVariables && hideLinks && hideTimePicker && hideDashboardControls);
}
}
function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardControls>) {
const { refreshPicker, timePicker, hideTimeControls, hideVariableControls, hideLinksControls } = model.useState();
const {
refreshPicker,
timePicker,
hideTimeControls,
hideVariableControls,
hideLinksControls,
hideDashboardControls,
} = model.useState();
const dashboard = getDashboardSceneFor(model);
const { links, editPanel } = dashboard.useState();
const styles = useStyles2(getStyles);
const showDebugger = window.location.search.includes('scene-debugger');
const hasControlMenuVariables = sceneGraph
.getVariables(dashboard)
.useState()
.variables.some((v) => v.state.showInControlsMenu === true);
const hasControlMenuLinks = links.some((link) => link.placement === 'inControlsMenu');
if (!model.hasControls()) {
// To still have spacing when no controls are rendered
@@ -157,7 +178,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
<refreshPicker.Component model={refreshPicker} />
</div>
)}
{(hasControlMenuVariables || hasControlMenuLinks) && (
{!hideDashboardControls && model.hasDashboardControls() && (
<Stack>
<DashboardControlsButton dashboard={dashboard} />
</Stack>