diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 3fb85a71772..ae54d0ce941 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -57,6 +57,11 @@ export const Pages = { navV2: 'data-testid Dashboard navigation', publicDashboardTag: 'data-testid public dashboard tag', shareButton: 'data-testid share-button', + playlistControls: { + prev: 'data-testid playlist previous dashboard button', + stop: 'data-testid playlist stop dashboard button', + next: 'data-testid playlist next dashboard button', + }, }, SubMenu: { submenu: 'Dashboard submenu', diff --git a/public/app/core/components/Signup/SignupPage.test.tsx b/public/app/core/components/Signup/SignupPage.test.tsx index 9d3c8675632..5c2a0ffb468 100644 --- a/public/app/core/components/Signup/SignupPage.test.tsx +++ b/public/app/core/components/Signup/SignupPage.test.tsx @@ -14,6 +14,7 @@ jest.mock('@grafana/runtime', () => ({ post: postMock, }), config: { + ...jest.requireActual('@grafana/runtime').config, loginError: false, buildInfo: { version: 'v1.0', diff --git a/public/app/core/components/Signup/VerifyEmailPage.test.tsx b/public/app/core/components/Signup/VerifyEmailPage.test.tsx index 00dbb378a4d..ebdf201e9cd 100644 --- a/public/app/core/components/Signup/VerifyEmailPage.test.tsx +++ b/public/app/core/components/Signup/VerifyEmailPage.test.tsx @@ -12,6 +12,7 @@ jest.mock('@grafana/runtime', () => ({ post: postMock, }), config: { + ...jest.requireActual('@grafana/runtime').config, buildInfo: { version: 'v1.0', commit: '1', diff --git a/public/app/core/reducers/root.test.ts b/public/app/core/reducers/root.test.ts index 6bb8712b231..3396fc0d18c 100644 --- a/public/app/core/reducers/root.test.ts +++ b/public/app/core/reducers/root.test.ts @@ -9,6 +9,7 @@ import { createRootReducer } from './root'; jest.mock('@grafana/runtime', () => ({ ...jest.requireActual('@grafana/runtime'), config: { + ...jest.requireActual('@grafana/runtime').config, bootData: { navTree: [], user: {}, diff --git a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts index d2f1261a4e6..fe93e589fb2 100644 --- a/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts +++ b/public/app/features/dashboard-scene/panel-edit/PanelEditor.test.ts @@ -13,6 +13,7 @@ jest.mock('@grafana/runtime', () => ({ getPanelPluginFromCache: jest.fn(() => pluginToLoad), }), config: { + ...jest.requireActual('@grafana/runtime').config, panels: { text: { skipDataQuery: true, diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 7dc9c6339fd..807c7f7bce1 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -57,6 +57,15 @@ jest.mock('@grafana/runtime', () => ({ }, })); +jest.mock('app/features/playlist/PlaylistSrv', () => ({ + ...jest.requireActual('app/features/playlist/PlaylistSrv'), + playlistSrv: { + isPlaying: false, + next: jest.fn(), + prev: jest.fn(), + stop: jest.fn(), + }, +})); const worker = createWorker(); mockResultsOfDetectChangesWorker({ hasChanges: true, hasTimeChanges: false, hasVariableValueChanges: false }); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index 41bdb767c81..387071fc1d9 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -109,6 +109,7 @@ export interface DashboardSceneState extends SceneObjectState { overlay?: SceneObject; /** True when a user copies a panel in the dashboard */ hasCopiedPanel?: boolean; + /** The dashboard doesn't have panels */ isEmpty?: boolean; } diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx index da1c138fb79..ce001bb53bb 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx @@ -4,11 +4,26 @@ import React from 'react'; import { TestProvider } from 'test/helpers/TestProvider'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; +import { selectors } from '@grafana/e2e-selectors'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; + import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene'; import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel'; import { ToolbarActions } from './NavToolbarActions'; +jest.mock('app/features/playlist/PlaylistSrv', () => ({ + playlistSrv: { + useState: jest.fn().mockReturnValue({ isPlaying: false }), + setState: jest.fn(), + isPlaying: true, + start: jest.fn(), + next: jest.fn(), + prev: jest.fn(), + stop: jest.fn(), + }, +})); + describe('NavToolbarActions', () => { describe('Give an already saved dashboard', () => { it('Should show correct buttons when not in editing', async () => { @@ -23,6 +38,44 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Share')).toBeInTheDocument(); }); + it('Should the correct buttons when playing a playlist', async () => { + jest.mocked(playlistSrv).useState.mockReturnValueOnce({ isPlaying: true }); + setup(); + + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)).toBeInTheDocument(); + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)).toBeInTheDocument(); + expect(screen.queryByText('Edit')).not.toBeInTheDocument(); + expect(screen.queryByText('Share')).not.toBeInTheDocument(); + }); + + it('Should call the playlist srv when using playlist controls', async () => { + jest.mocked(playlistSrv).useState.mockReturnValueOnce({ isPlaying: true }); + setup(); + + // Previous dashboard + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.prev)); + expect(playlistSrv.prev).toHaveBeenCalledTimes(1); + + // Next dashboard + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.next)); + expect(playlistSrv.next).toHaveBeenCalledTimes(1); + + // Stop playlist + expect(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)).toBeInTheDocument(); + await userEvent.click(await screen.findByTestId(selectors.pages.Dashboard.DashNav.playlistControls.stop)); + expect(playlistSrv.stop).toHaveBeenCalledTimes(1); + }); + + it('Should hide the playlist controls when it is not playing', async () => { + setup(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); + }); + it('Should show correct buttons when editing', async () => { setup(); @@ -36,6 +89,9 @@ describe('NavToolbarActions', () => { expect(await screen.findByLabelText('Add library panel')).toBeInTheDocument(); expect(screen.queryByText('Edit')).not.toBeInTheDocument(); expect(screen.queryByText('Share')).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); }); it('Should show correct buttons when in settings menu', async () => { @@ -46,6 +102,9 @@ describe('NavToolbarActions', () => { expect(await screen.findByText('Save dashboard')).toBeInTheDocument(); expect(await screen.findByText('Back to dashboard')).toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.prev)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.stop)).not.toBeInTheDocument(); + expect(screen.queryByText(selectors.pages.Dashboard.DashNav.playlistControls.next)).not.toBeInTheDocument(); }); }); }); diff --git a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx index 232189129fc..31a30316fde 100644 --- a/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx +++ b/public/app/features/dashboard-scene/scene/NavToolbarActions.tsx @@ -2,13 +2,15 @@ import { css } from '@emotion/css'; import React from 'react'; import { GrafanaTheme2 } from '@grafana/data'; +import { selectors } from '@grafana/e2e-selectors'; import { locationService } from '@grafana/runtime'; import { Button, ButtonGroup, Dropdown, Icon, Menu, ToolbarButton, ToolbarButtonRow, useStyles2 } from '@grafana/ui'; import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator'; import { contextSrv } from 'app/core/core'; -import { t } from 'app/core/internationalization'; +import { t, Trans } from 'app/core/internationalization'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { playlistSrv } from 'app/features/playlist/PlaylistSrv'; import { ShareModal } from '../sharing/ShareModal'; import { DashboardInteractions } from '../utils/interactions'; @@ -47,6 +49,8 @@ export function ToolbarActions({ dashboard }: Props) { editPanel, hasCopiedPanel: copiedPanel, } = dashboard.useState(); + const { isPlaying } = playlistSrv.useState(); + const canSaveAs = contextSrv.hasEditPermissionInFolders; const toolbarActions: ToolbarAction[] = []; const buttonWithExtraMargin = useStyles2(getStyles); @@ -170,6 +174,49 @@ export function ToolbarActions({ dashboard }: Props) { ), }); + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.prev()} + /> + ), + }); + + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.stop()} + data-testid={selectors.pages.Dashboard.DashNav.playlistControls.stop} + > + Stop playlist + + ), + }); + + toolbarActions.push({ + group: 'playlist-actions', + condition: isPlaying && !editview && !isEditingPanel && !isEditing, + render: () => ( + playlistSrv.next()} + narrow + /> + ), + }); + if (dynamicDashNavActions.left.length > 0 && !isEditingPanel) { dynamicDashNavActions.left.map((action, index) => { const props = { dashboard: getDashboardSrv().getCurrent()! }; @@ -225,7 +272,7 @@ export function ToolbarActions({ dashboard }: Props) { toolbarActions.push({ group: 'main-buttons', - condition: uid && !isEditing && !meta.isSnapshot, + condition: uid && !isEditing && !meta.isSnapshot && !isPlaying, render: () => (