Dashboard: Hide sidebar in kiosk mode (#115387)

* Dashboard: Hide sidebar in kiosk mode

* Fix kiosk url sync issues

* Fixes

* fixes

* Update
This commit is contained in:
Torkel Ödegaard
2025-12-16 11:43:59 +01:00
committed by GitHub
parent a4eb98b4ed
commit 6ffb805d45
7 changed files with 93 additions and 75 deletions
+1 -1
View File
@@ -1817,7 +1817,7 @@
},
"public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": {
"react-hooks/rules-of-hooks": {
"count": 4
"count": 5
}
},
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": {
@@ -97,7 +97,13 @@ export const Dropdown = React.memo(({ children, overlay, placement, offset, root
see https://github.com/jsx-eslint/eslint-plugin-jsx-a11y/blob/main/docs/rules/no-static-element-interactions.md#case-the-event-handler-is-only-being-used-to-capture-bubbled-events
*/}
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */}
<div ref={refs.setFloating} style={floatingStyles} onClick={onOverlayClicked} onKeyDown={handleKeys}>
<div
ref={refs.setFloating}
style={floatingStyles}
onClick={onOverlayClicked}
onKeyDown={handleKeys}
{...getFloatingProps()}
>
<CSSTransition
nodeRef={transitionRef}
appear={true}
@@ -1,14 +1,15 @@
import { css, cx } from '@emotion/css';
import React, { useEffect } from 'react';
import React, { useEffect, useLayoutEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config, useChromeHeaderHeight } from '@grafana/runtime';
import { useSceneObjectState } from '@grafana/scenes';
import { ElementSelectionContext, useSidebar, useStyles2, Sidebar } from '@grafana/ui';
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
import NativeScrollbar, { DivScrollElement } from 'app/core/components/NativeScrollbar';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { KioskMode } from 'app/types/dashboard';
import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions';
@@ -29,10 +30,9 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
const headerHeight = useChromeHeaderHeight();
const { editPane } = dashboard.state;
const styles = useStyles2(getStyles, headerHeight ?? 0);
const hasUid = Boolean(dashboard.state.uid);
const canStar = Boolean(dashboard.state.meta.canStar);
//const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed();
const { chrome } = useGrafana();
const { kioskMode } = chrome.useState();
const isInKioskMode = kioskMode === KioskMode.Full;
if (!config.featureToggles.dashboardNewLayouts) {
return (
@@ -46,6 +46,11 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
);
}
/**
* Adds star button and left side actions to app chrome breadcrumb area
*/
useUpdateAppChromeActions(dashboard);
/**
* Enable / disable selection based on dashboard isEditing state
*/
@@ -59,12 +64,6 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
const { selectionContext, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const onBodyRef = (ref: HTMLDivElement | null) => {
if (ref) {
dashboard.onSetScrollRef(new DivScrollElement(ref));
}
};
const sidebarContext = useSidebar({
hasOpenPane: Boolean(openPane),
contentMargin: 1,
@@ -88,39 +87,77 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
editPane.clearSelection();
};
const onBodyRef = (ref: HTMLDivElement | null) => {
if (ref) {
dashboard.onSetScrollRef(new DivScrollElement(ref));
}
};
function renderBody() {
// In kiosk mode the full document body scrolls so we don't need to wrap in our own scrollbar
if (isInKioskMode) {
return (
<div
className={cx(styles.bodyWrapper, styles.bodyWrapperKiosk)}
data-testid={selectors.components.DashboardEditPaneSplitter.primaryBody}
>
<NativeScrollbar onSetScrollRef={dashboard.onSetScrollRef}>{body}</NativeScrollbar>
</div>
);
}
return (
<div
className={styles.bodyWrapper}
data-testid={selectors.components.DashboardEditPaneSplitter.primaryBody}
{...sidebarContext.outerWrapperProps}
>
<div className={styles.scrollContainer} ref={onBodyRef} onPointerDown={onClearSelection}>
{body}
</div>
<Sidebar contextValue={sidebarContext}>
<DashboardEditPaneRenderer editPane={editPane} dashboard={dashboard} isDocked={sidebarContext.isDocked} />
</Sidebar>
</div>
);
}
return (
<div className={styles.container}>
<ElementSelectionContext.Provider value={selectionContext}>
<AppChromeUpdate
breadcrumbActions={
<>
{hasUid && canStar && <StarButton dashboard={dashboard} />}
{hasUid && canStar && <PublicDashboardBadge dashboard={dashboard} />}
{renderDynamicNavActions()}
</>
}
/>
<div className={cx(styles.controlsWrapperSticky)} onPointerDown={onClearSelection}>
<div className={styles.controlsWrapperSticky} onPointerDown={onClearSelection}>
{controls}
</div>
<div className={styles.bodyWrapper} {...sidebarContext.outerWrapperProps}>
<div
className={styles.bodyWithToolbar}
data-testid={selectors.components.DashboardEditPaneSplitter.primaryBody}
ref={onBodyRef}
onPointerDown={onClearSelection}
>
{body}
</div>
<Sidebar contextValue={sidebarContext}>
<DashboardEditPaneRenderer editPane={editPane} dashboard={dashboard} isDocked={sidebarContext.isDocked} />
</Sidebar>
</div>
{renderBody()}
</ElementSelectionContext.Provider>
</div>
);
}
function useUpdateAppChromeActions(dashboard: DashboardScene) {
const { chrome } = useGrafana();
useLayoutEffect(() => {
const hasUid = Boolean(dashboard.state.uid);
const canStar = Boolean(dashboard.state.meta.canStar);
const breadcrumbActions = (
<>
{hasUid && canStar && <StarButton dashboard={dashboard} />}
{hasUid && canStar && <PublicDashboardBadge dashboard={dashboard} />}
{renderDynamicNavActions()}
</>
);
chrome.update({ breadcrumbActions });
return () => {
chrome.update({ breadcrumbActions: undefined });
};
}, [chrome, dashboard]);
}
function renderDynamicNavActions() {
const dashboard = getDashboardSrv().getCurrent()!;
const showProps = { dashboard };
@@ -152,13 +189,17 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) {
bodyWrapper: css({
label: 'body-wrapper',
display: 'flex',
flexDirection: 'row',
flexDirection: 'column',
flexGrow: 1,
position: 'relative',
flex: '1 1 0',
overflow: 'hidden',
}),
bodyWithToolbar: css({
bodyWrapperKiosk: css({
padding: theme.spacing(0, 2, 2, 2),
overflow: 'unset',
}),
scrollContainer: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
@@ -32,7 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DashboardJson } from 'app/features/manage-dashboards/types';
import { VariablesChanged } from 'app/features/variables/types';
import { DashboardDTO, DashboardMeta, KioskMode, SaveDashboardResponseDTO } from 'app/types/dashboard';
import { DashboardDTO, DashboardMeta, SaveDashboardResponseDTO } from 'app/types/dashboard';
import { ShowConfirmModalEvent } from 'app/types/events';
import {
@@ -140,8 +140,6 @@ export interface DashboardSceneState extends SceneObjectState {
editPanel?: PanelEditor;
/** Scene object that handles the current drawer or modal */
overlay?: SceneObject;
/** Kiosk mode */
kioskMode?: KioskMode;
/** Share view */
shareView?: string;
/** Renders panels in grid and filtered */
@@ -1,5 +1,4 @@
import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { KioskMode } from 'app/types/dashboard';
import { DashboardScene } from './DashboardScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
@@ -22,25 +21,6 @@ describe('DashboardSceneUrlSync', () => {
layout.state.grid.setState({ UNSAFE_fitPanels: true });
expect(scene.urlSync?.getUrlState().autofitpanels).toBe('true');
});
it('Should set kiosk mode when url has kiosk', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ kiosk: 'invalid' });
expect(scene.state.kioskMode).toBe(undefined);
scene.urlSync?.updateFromUrl({ kiosk: '' });
expect(scene.state.kioskMode).toBe(KioskMode.Full);
scene.urlSync?.updateFromUrl({ kiosk: 'true' });
expect(scene.state.kioskMode).toBe(KioskMode.Full);
});
it('Should get the kiosk mode from the scene state', () => {
const scene = buildTestScene();
expect(scene.urlSync?.getUrlState().kiosk).toBe(undefined);
scene.setState({ kioskMode: KioskMode.Full });
expect(scene.urlSync?.getUrlState().kiosk).toBe('true');
});
});
describe('entering edit mode', () => {
@@ -1,6 +1,5 @@
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes';
import { contextSrv } from 'app/core/services/context_srv';
import { KioskMode } from 'app/types/dashboard';
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createDashboardEditViewFor } from '../settings/utils';
@@ -15,7 +14,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
constructor(private _scene: DashboardScene) {}
getKeys(): string[] {
return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'kiosk', 'shareView'];
return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'shareView'];
}
getUrlState(): SceneObjectUrlValues {
@@ -26,7 +25,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
viewPanel: state.viewPanel,
editview: state.editview?.getUrlKey(),
editPanel: state.editPanel?.getUrlKey() || undefined,
kiosk: state.kioskMode === KioskMode.Full ? 'true' : undefined,
shareView: state.shareView,
orgId: contextSrv.user.orgId.toString(),
};
@@ -117,12 +115,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
}
}
if (typeof values.kiosk === 'string') {
if (values.kiosk === 'true' || values.kiosk === '') {
update.kioskMode = KioskMode.Full;
}
}
if (Object.keys(update).length > 0) {
this._scene.setState(update);
}
@@ -1,6 +1,6 @@
import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n';
import { Button } from '@grafana/ui';
import { ToolbarButton } from '@grafana/ui';
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { trackDashboardSceneEditButtonClicked } from 'app/features/dashboard-scene/utils/tracking';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
@@ -17,10 +17,11 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
}
return (
<Button
<ToolbarButton
tooltip={tooltip}
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
variant="secondary"
icon={dashboard.state.isEditing ? 'edit' : 'pen'}
variant="canvas"
onClick={(evt) => {
evt.preventDefault();
evt.stopPropagation();
@@ -37,6 +38,6 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
{dashboard.state.isEditing
? t('dashboard.toolbar.edit-button.exit', 'Exit edit')
: t('dashboard.toolbar.edit-button.enter', 'Edit')}
</Button>
</ToolbarButton>
);
};