Dashboards: Fix edit button visibility to respect editable flag in new layouts (#115372)

Dashboard in `editable: false` mode
This commit is contained in:
Kristina Demeshchik
2025-12-15 14:25:21 -05:00
committed by GitHub
parent eec4722372
commit 9aa8fb183d
3 changed files with 95 additions and 4 deletions
@@ -1,11 +1,21 @@
import { render } from '@testing-library/react';
import { render, screen } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { SceneVariableSet, ScopesVariable, TextBoxVariable } from '@grafana/scenes';
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
import { DashboardControls, DashboardControlsState } from './DashboardControls';
import { DashboardScene } from './DashboardScene';
jest.mock('app/features/playlist/PlaylistSrv', () => ({
playlistSrv: {
useState: jest.fn().mockReturnValue({ isPlaying: false }),
state: { isPlaying: false },
stop: jest.fn(),
},
}));
describe('DashboardControls', () => {
describe('Given a standard scene', () => {
it('should initialize with default values', () => {
@@ -221,8 +231,85 @@ describe('DashboardControls', () => {
expect(setState).toHaveBeenCalledTimes(0);
});
});
describe('DashboardControlActions editable flag', () => {
const originalFeatureToggles = { ...config.featureToggles };
beforeEach(() => {
config.featureToggles.dashboardNewLayouts = true;
jest.mocked(playlistSrv.useState).mockReturnValue({ isPlaying: false });
});
afterEach(() => {
config.featureToggles = originalFeatureToggles;
jest.clearAllMocks();
});
it('should show EditDashboardSwitch when editable is true', async () => {
const controls = buildTestSceneWithEditable({ editable: true, canEdit: true });
render(<controls.Component model={controls} />);
expect(await screen.findByRole('button', { name: /edit/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /make editable/i })).not.toBeInTheDocument();
});
it('should show MakeDashboardEditableButton when editable is false', async () => {
const controls = buildTestSceneWithEditable({ editable: false, canEdit: false, canMakeEditable: true });
render(<controls.Component model={controls} />);
expect(await screen.findByRole('button', { name: /make editable/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /^edit$/i })).not.toBeInTheDocument();
});
it('should not show edit buttons when canEditDashboard returns false', async () => {
const controls = buildTestSceneWithEditable({
editable: true,
canEdit: false,
canMakeEditable: false,
isSnapshot: true,
});
render(<controls.Component model={controls} />);
expect(screen.queryByRole('button', { name: /edit/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /make editable/i })).not.toBeInTheDocument();
});
it('should not show edit buttons when playlist is playing', async () => {
jest.mocked(playlistSrv.useState).mockReturnValue({ isPlaying: true });
const controls = buildTestSceneWithEditable({ editable: true, canEdit: true });
render(<controls.Component model={controls} />);
expect(screen.queryByRole('button', { name: /^edit$/i })).not.toBeInTheDocument();
expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)).toBeInTheDocument();
});
});
});
function buildTestSceneWithEditable(options: {
editable: boolean;
canEdit?: boolean;
canMakeEditable?: boolean;
isSnapshot?: boolean;
}): DashboardControls {
const { editable, canEdit = true, canMakeEditable = false, isSnapshot = false } = options;
const dashboard = new DashboardScene({
uid: 'test-uid',
editable,
meta: {
canEdit,
canMakeEditable,
isSnapshot,
},
controls: new DashboardControls({}),
});
dashboard.activate();
return dashboard.state.controls as DashboardControls;
}
function buildTestScene(state?: Partial<DashboardControlsState>): DashboardControls {
const variable = new TextBoxVariable({
name: 'A',
@@ -31,6 +31,7 @@ import { VariableControls } from './VariableControls';
import { DashboardControlsButton } from './dashboard-controls-menu/DashboardControlsMenuButton';
import { hasDashboardControls, useHasDashboardControls } from './dashboard-controls-menu/utils';
import { EditDashboardSwitch } from './new-toolbar/actions/EditDashboardSwitch';
import { MakeDashboardEditableButton } from './new-toolbar/actions/MakeDashboardEditableButton';
import { SaveDashboard } from './new-toolbar/actions/SaveDashboard';
import { ShareDashboardButton } from './new-toolbar/actions/ShareDashboardButton';
@@ -191,7 +192,7 @@ function DashboardControlsRenderer({ model }: SceneComponentProps<DashboardContr
}
function DashboardControlActions({ dashboard }: { dashboard: DashboardScene }) {
const { isEditing, editPanel, uid, meta } = dashboard.useState();
const { isEditing, editPanel, uid, meta, editable } = dashboard.useState();
const { isPlaying } = playlistSrv.useState();
if (editPanel) {
@@ -201,13 +202,17 @@ function DashboardControlActions({ dashboard }: { dashboard: DashboardScene }) {
const canEditDashboard = dashboard.canEditDashboard();
const hasUid = Boolean(uid);
const isSnapshot = Boolean(meta.isSnapshot);
const isEditable = Boolean(editable);
const showShareButton = hasUid && !isSnapshot && !isPlaying;
return (
<>
{showShareButton && <ShareDashboardButton dashboard={dashboard} />}
{isEditing && <SaveDashboard dashboard={dashboard} />}
{!isPlaying && canEditDashboard && <EditDashboardSwitch dashboard={dashboard} />}
{!isPlaying && canEditDashboard && isEditable && <EditDashboardSwitch dashboard={dashboard} />}
{!isPlaying && canEditDashboard && !isEditable && !isEditing && (
<MakeDashboardEditableButton dashboard={dashboard} />
)}
{isPlaying && (
<Button
variant="secondary"
@@ -17,7 +17,6 @@ export const MakeDashboardEditableButton = ({ dashboard }: ToolbarActionProps) =
}}
tooltip={t('dashboard.toolbar.new.enter-edit-mode.tooltip', 'This dashboard was marked as read only')}
variant="secondary"
size="sm"
data-testid={selectors.components.NavToolbar.editDashboard.editButton}
>
<Trans i18nKey="dashboard.toolbar.new.enter-edit-mode.label">Make editable</Trans>