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": { "public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx": {
"react-hooks/rules-of-hooks": { "react-hooks/rules-of-hooks": {
"count": 4 "count": 5
} }
}, },
"public/app/features/dashboard-scene/inspect/HelpWizard/HelpWizard.tsx": { "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 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 */} {/* 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 <CSSTransition
nodeRef={transitionRef} nodeRef={transitionRef}
appear={true} appear={true}
@@ -1,14 +1,15 @@
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import React, { useEffect } from 'react'; import React, { useEffect, useLayoutEffect } from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { config, useChromeHeaderHeight } from '@grafana/runtime'; import { config, useChromeHeaderHeight } from '@grafana/runtime';
import { useSceneObjectState } from '@grafana/scenes'; import { useSceneObjectState } from '@grafana/scenes';
import { ElementSelectionContext, useSidebar, useStyles2, Sidebar } from '@grafana/ui'; 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 NativeScrollbar, { DivScrollElement } from 'app/core/components/NativeScrollbar';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { KioskMode } from 'app/types/dashboard';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions } from '../scene/NavToolbarActions';
@@ -29,10 +30,9 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
const headerHeight = useChromeHeaderHeight(); const headerHeight = useChromeHeaderHeight();
const { editPane } = dashboard.state; const { editPane } = dashboard.state;
const styles = useStyles2(getStyles, headerHeight ?? 0); const styles = useStyles2(getStyles, headerHeight ?? 0);
const hasUid = Boolean(dashboard.state.uid); const { chrome } = useGrafana();
const canStar = Boolean(dashboard.state.meta.canStar); const { kioskMode } = chrome.useState();
const isInKioskMode = kioskMode === KioskMode.Full;
//const [isCollapsed, setIsCollapsed] = useEditPaneCollapsed();
if (!config.featureToggles.dashboardNewLayouts) { if (!config.featureToggles.dashboardNewLayouts) {
return ( 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 * 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 { selectionContext, openPane } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const onBodyRef = (ref: HTMLDivElement | null) => {
if (ref) {
dashboard.onSetScrollRef(new DivScrollElement(ref));
}
};
const sidebarContext = useSidebar({ const sidebarContext = useSidebar({
hasOpenPane: Boolean(openPane), hasOpenPane: Boolean(openPane),
contentMargin: 1, contentMargin: 1,
@@ -88,39 +87,77 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
editPane.clearSelection(); 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 ( return (
<div className={styles.container}> <div className={styles.container}>
<ElementSelectionContext.Provider value={selectionContext}> <ElementSelectionContext.Provider value={selectionContext}>
<AppChromeUpdate <div className={styles.controlsWrapperSticky} onPointerDown={onClearSelection}>
breadcrumbActions={
<>
{hasUid && canStar && <StarButton dashboard={dashboard} />}
{hasUid && canStar && <PublicDashboardBadge dashboard={dashboard} />}
{renderDynamicNavActions()}
</>
}
/>
<div className={cx(styles.controlsWrapperSticky)} onPointerDown={onClearSelection}>
{controls} {controls}
</div> </div>
<div className={styles.bodyWrapper} {...sidebarContext.outerWrapperProps}> {renderBody()}
<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>
</ElementSelectionContext.Provider> </ElementSelectionContext.Provider>
</div> </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() { function renderDynamicNavActions() {
const dashboard = getDashboardSrv().getCurrent()!; const dashboard = getDashboardSrv().getCurrent()!;
const showProps = { dashboard }; const showProps = { dashboard };
@@ -152,13 +189,17 @@ function getStyles(theme: GrafanaTheme2, headerHeight: number) {
bodyWrapper: css({ bodyWrapper: css({
label: 'body-wrapper', label: 'body-wrapper',
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
position: 'relative', position: 'relative',
flex: '1 1 0', flex: '1 1 0',
overflow: 'hidden', overflow: 'hidden',
}), }),
bodyWithToolbar: css({ bodyWrapperKiosk: css({
padding: theme.spacing(0, 2, 2, 2),
overflow: 'unset',
}),
scrollContainer: css({
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
flexGrow: 1, flexGrow: 1,
@@ -32,7 +32,7 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher'; import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DashboardJson } from 'app/features/manage-dashboards/types'; import { DashboardJson } from 'app/features/manage-dashboards/types';
import { VariablesChanged } from 'app/features/variables/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 { ShowConfirmModalEvent } from 'app/types/events';
import { import {
@@ -140,8 +140,6 @@ export interface DashboardSceneState extends SceneObjectState {
editPanel?: PanelEditor; editPanel?: PanelEditor;
/** Scene object that handles the current drawer or modal */ /** Scene object that handles the current drawer or modal */
overlay?: SceneObject; overlay?: SceneObject;
/** Kiosk mode */
kioskMode?: KioskMode;
/** Share view */ /** Share view */
shareView?: string; shareView?: string;
/** Renders panels in grid and filtered */ /** Renders panels in grid and filtered */
@@ -1,5 +1,4 @@
import { SceneQueryRunner, VizPanel } from '@grafana/scenes'; import { SceneQueryRunner, VizPanel } from '@grafana/scenes';
import { KioskMode } from 'app/types/dashboard';
import { DashboardScene } from './DashboardScene'; import { DashboardScene } from './DashboardScene';
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
@@ -22,25 +21,6 @@ describe('DashboardSceneUrlSync', () => {
layout.state.grid.setState({ UNSAFE_fitPanels: true }); layout.state.grid.setState({ UNSAFE_fitPanels: true });
expect(scene.urlSync?.getUrlState().autofitpanels).toBe('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', () => { describe('entering edit mode', () => {
@@ -1,6 +1,5 @@
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes'; import { SceneObjectUrlSyncHandler, SceneObjectUrlValues, VizPanel } from '@grafana/scenes';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { KioskMode } from 'app/types/dashboard';
import { buildPanelEditScene } from '../panel-edit/PanelEditor'; import { buildPanelEditScene } from '../panel-edit/PanelEditor';
import { createDashboardEditViewFor } from '../settings/utils'; import { createDashboardEditViewFor } from '../settings/utils';
@@ -15,7 +14,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
constructor(private _scene: DashboardScene) {} constructor(private _scene: DashboardScene) {}
getKeys(): string[] { getKeys(): string[] {
return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'kiosk', 'shareView']; return ['inspect', 'viewPanel', 'editPanel', 'editview', 'autofitpanels', 'shareView'];
} }
getUrlState(): SceneObjectUrlValues { getUrlState(): SceneObjectUrlValues {
@@ -26,7 +25,6 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
viewPanel: state.viewPanel, viewPanel: state.viewPanel,
editview: state.editview?.getUrlKey(), editview: state.editview?.getUrlKey(),
editPanel: state.editPanel?.getUrlKey() || undefined, editPanel: state.editPanel?.getUrlKey() || undefined,
kiosk: state.kioskMode === KioskMode.Full ? 'true' : undefined,
shareView: state.shareView, shareView: state.shareView,
orgId: contextSrv.user.orgId.toString(), 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) { if (Object.keys(update).length > 0) {
this._scene.setState(update); this._scene.setState(update);
} }
@@ -1,6 +1,6 @@
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { t } from '@grafana/i18n'; 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 { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
import { trackDashboardSceneEditButtonClicked } from 'app/features/dashboard-scene/utils/tracking'; import { trackDashboardSceneEditButtonClicked } from 'app/features/dashboard-scene/utils/tracking';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
@@ -17,10 +17,11 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
} }
return ( return (
<Button <ToolbarButton
tooltip={tooltip} tooltip={tooltip}
data-testid={selectors.components.NavToolbar.editDashboard.editButton} data-testid={selectors.components.NavToolbar.editDashboard.editButton}
variant="secondary" icon={dashboard.state.isEditing ? 'edit' : 'pen'}
variant="canvas"
onClick={(evt) => { onClick={(evt) => {
evt.preventDefault(); evt.preventDefault();
evt.stopPropagation(); evt.stopPropagation();
@@ -37,6 +38,6 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
{dashboard.state.isEditing {dashboard.state.isEditing
? t('dashboard.toolbar.edit-button.exit', 'Exit edit') ? t('dashboard.toolbar.edit-button.exit', 'Exit edit')
: t('dashboard.toolbar.edit-button.enter', 'Edit')} : t('dashboard.toolbar.edit-button.enter', 'Edit')}
</Button> </ToolbarButton>
); );
}; };