Dynamic Dashboards: Add new tracking events for dashboard interactions (#111022)
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneTimeRange, SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
|
||||
import { DashboardEditPaneRenderer } from './DashboardEditPaneRenderer';
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
jest.mock('../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
dashboardOutlineClicked: jest.fn(),
|
||||
outlineItemClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('react-router-dom-v5-compat', () => ({
|
||||
...jest.requireActual('react-router-dom-v5-compat'),
|
||||
useLocation: () => ({
|
||||
pathname: '/dashboard/test',
|
||||
search: '',
|
||||
hash: '',
|
||||
state: null,
|
||||
}),
|
||||
}));
|
||||
|
||||
export function buildTestScene() {
|
||||
const testScene = new DashboardScene({
|
||||
$variables: new SceneVariableSet({ variables: [] }),
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [new DashboardGridItem({ body: new VizPanel({ key: 'panel-1', pluginId: 'text' }) })],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
activateFullSceneTree(testScene);
|
||||
return testScene;
|
||||
}
|
||||
|
||||
describe('DashboardEditPaneRenderer', () => {
|
||||
describe('outline interactions tracking', () => {
|
||||
it('should call DashboardInteractions.outlineClicked when clicking on dashboard outline', async () => {
|
||||
const user = userEvent.setup();
|
||||
const scene = buildTestScene();
|
||||
render(
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={scene.state.editPane}
|
||||
isEditPaneCollapsed={false}
|
||||
onToggleCollapse={() => {}}
|
||||
/>
|
||||
);
|
||||
const outlineButton = screen.getByTestId(selectors.components.PanelEditor.Outline.section);
|
||||
await user.click(outlineButton);
|
||||
|
||||
expect(DashboardInteractions.dashboardOutlineClicked).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,8 @@ import { Trans, t } from '@grafana/i18n';
|
||||
import { useSceneObjectState } from '@grafana/scenes';
|
||||
import { useStyles2, useSplitter, ToolbarButton, ScrollContainer, Text, Icon, clearButtonStyles } from '@grafana/ui';
|
||||
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
import { DashboardOutline } from './DashboardOutline';
|
||||
import { ElementEditPane } from './ElementEditPane';
|
||||
@@ -15,7 +17,7 @@ import { useEditableElement } from './useEditableElement';
|
||||
|
||||
export interface Props {
|
||||
editPane: DashboardEditPane;
|
||||
isCollapsed: boolean;
|
||||
isEditPaneCollapsed: boolean;
|
||||
openOverlay?: boolean;
|
||||
onToggleCollapse: () => void;
|
||||
}
|
||||
@@ -23,7 +25,7 @@ export interface Props {
|
||||
/**
|
||||
* Making the EditPane rendering completely standalone (not using editPane.Component) in order to pass custom react props
|
||||
*/
|
||||
export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse, openOverlay }: Props) {
|
||||
export function DashboardEditPaneRenderer({ editPane, isEditPaneCollapsed, onToggleCollapse, openOverlay }: Props) {
|
||||
const { selection } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
|
||||
const styles = useStyles2(getStyles);
|
||||
const clearButton = useStyles2(clearButtonStyles);
|
||||
@@ -53,7 +55,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isCollapsed) {
|
||||
if (isEditPaneCollapsed) {
|
||||
return (
|
||||
<>
|
||||
<div className={styles.expandOptionsWrapper}>
|
||||
@@ -111,7 +113,10 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
|
||||
<div {...splitter.secondaryProps} className={cx(splitter.secondaryProps.className, styles.paneContent)}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOutlineCollapsed(!outlineCollapsed)}
|
||||
onClick={() => {
|
||||
DashboardInteractions.dashboardOutlineClicked();
|
||||
setOutlineCollapsed(!outlineCollapsed);
|
||||
}}
|
||||
className={cx(clearButton, styles.outlineCollapseButton)}
|
||||
data-testid={selectors.components.PanelEditor.Outline.section}
|
||||
>
|
||||
|
||||
@@ -118,7 +118,7 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
|
||||
<div {...secondaryProps} className={cx(secondaryProps.className, styles.editPane)}>
|
||||
<DashboardEditPaneRenderer
|
||||
editPane={editPane}
|
||||
isCollapsed={isCollapsed}
|
||||
isEditPaneCollapsed={isCollapsed}
|
||||
onToggleCollapse={onToggleCollapse}
|
||||
openOverlay={selectionContext.selected.length > 0}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneVariableSet, VizPanel } from '@grafana/scenes';
|
||||
import { ElementSelectionContext } from '@grafana/ui';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { AutoGridItem } from '../scene/layout-auto-grid/AutoGridItem';
|
||||
import { AutoGridLayout } from '../scene/layout-auto-grid/AutoGridLayout';
|
||||
import { AutoGridLayoutManager } from '../scene/layout-auto-grid/AutoGridLayoutManager';
|
||||
import { RowItem } from '../scene/layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../scene/layout-rows/RowsLayoutManager';
|
||||
import { TabItem } from '../scene/layout-tabs/TabItem';
|
||||
import { TabsLayoutManager } from '../scene/layout-tabs/TabsLayoutManager';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { activateFullSceneTree } from '../utils/test-utils';
|
||||
|
||||
import { DashboardOutline } from './DashboardOutline';
|
||||
|
||||
jest.mock('../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
outlineItemClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
const testScene = new DashboardScene({
|
||||
title: 'Test Dashboard',
|
||||
$variables: new SceneVariableSet({ variables: [] }),
|
||||
body: new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
title: 'Row level 1',
|
||||
layout: new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
title: 'Row level 2',
|
||||
layout: new TabsLayoutManager({
|
||||
tabs: [
|
||||
new TabItem({
|
||||
title: 'Tab level 3 - A',
|
||||
layout: new AutoGridLayoutManager({
|
||||
layout: new AutoGridLayout({
|
||||
children: [
|
||||
new AutoGridItem({
|
||||
body: new VizPanel({
|
||||
title: 'Panel level 4 - A',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
new TabItem({
|
||||
title: 'Tab level 3 - B',
|
||||
layout: new AutoGridLayoutManager({
|
||||
layout: new AutoGridLayout({
|
||||
children: [
|
||||
new AutoGridItem({
|
||||
body: new VizPanel({
|
||||
title: 'Panel level 4 - A',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
activateFullSceneTree(testScene);
|
||||
return testScene;
|
||||
}
|
||||
|
||||
describe('DashboardOutline', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('outline item interactions tracking', () => {
|
||||
it('should call DashboardInteractions.outlineItemClicked with correct parameters when clicking on items', async () => {
|
||||
const user = userEvent.setup();
|
||||
const scene = buildTestScene();
|
||||
|
||||
// enable selection on the edit pane to activate real selection behavior
|
||||
scene.state.editPane.enableSelection();
|
||||
|
||||
render(
|
||||
<ElementSelectionContext.Provider value={scene.state.editPane.state.selectionContext}>
|
||||
<DashboardOutline editPane={scene.state.editPane} />
|
||||
</ElementSelectionContext.Provider>
|
||||
);
|
||||
// select Row lvl 1
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.Outline.item('Row level 1')));
|
||||
expect(DashboardInteractions.outlineItemClicked).toHaveBeenNthCalledWith(1, {
|
||||
index: 1,
|
||||
depth: 1,
|
||||
});
|
||||
// click on caret to expand Row lvl 1
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.Outline.node('Row level 1')));
|
||||
|
||||
// select Row lvl 2
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.Outline.item('Row level 2')));
|
||||
expect(DashboardInteractions.outlineItemClicked).toHaveBeenNthCalledWith(2, {
|
||||
index: 0,
|
||||
depth: 2,
|
||||
});
|
||||
|
||||
// click on caret to expand Row lvl 2
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.Outline.node('Row level 2')));
|
||||
|
||||
// select Tab lvl 3 - B
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.Outline.item('Tab level 3 - B')));
|
||||
expect(DashboardInteractions.outlineItemClicked).toHaveBeenNthCalledWith(3, {
|
||||
index: 1,
|
||||
depth: 3,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import { SceneObject } from '@grafana/scenes';
|
||||
import { Box, Icon, Stack, Text, useElementSelection, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { isRepeatCloneOrChildOf } from '../utils/clone';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
import { getDashboardSceneFor } from '../utils/utils';
|
||||
|
||||
import { DashboardEditPane } from './DashboardEditPane';
|
||||
@@ -23,7 +24,7 @@ export function DashboardOutline({ editPane }: Props) {
|
||||
|
||||
return (
|
||||
<Box padding={1} gap={0} display="flex" direction="column" element="ul" role="tree" position="relative">
|
||||
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} />
|
||||
<DashboardOutlineNode sceneObject={dashboard} editPane={editPane} depth={0} index={0} />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -32,9 +33,10 @@ interface DashboardOutlineNodeProps {
|
||||
sceneObject: SceneObject;
|
||||
editPane: DashboardEditPane;
|
||||
depth: number;
|
||||
index: number;
|
||||
}
|
||||
|
||||
function DashboardOutlineNode({ sceneObject, editPane, depth }: DashboardOutlineNodeProps) {
|
||||
function DashboardOutlineNode({ sceneObject, editPane, depth, index }: DashboardOutlineNodeProps) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { key } = sceneObject.useState();
|
||||
const [isCollapsed, setIsCollapsed] = useState(depth > 0);
|
||||
@@ -59,6 +61,7 @@ function DashboardOutlineNode({ sceneObject, editPane, depth }: DashboardOutline
|
||||
}
|
||||
|
||||
editableElement.scrollIntoView?.();
|
||||
DashboardInteractions.outlineItemClicked({ index, depth });
|
||||
};
|
||||
|
||||
const onToggleCollapse = (evt: React.MouseEvent) => {
|
||||
@@ -122,8 +125,14 @@ function DashboardOutlineNode({ sceneObject, editPane, depth }: DashboardOutline
|
||||
{isContainer && !isCollapsed && (
|
||||
<ul className={styles.nodeChildren} role="group">
|
||||
{children.length > 0 ? (
|
||||
children.map((child) => (
|
||||
<DashboardOutlineNode key={child.state.key} sceneObject={child} editPane={editPane} depth={depth + 1} />
|
||||
children.map((child, i) => (
|
||||
<DashboardOutlineNode
|
||||
key={child.state.key}
|
||||
sceneObject={child}
|
||||
editPane={editPane}
|
||||
depth={depth + 1}
|
||||
index={i}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<Text color="secondary" element="li">
|
||||
|
||||
@@ -6,15 +6,29 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { LocationServiceProvider, locationService } from '@grafana/runtime';
|
||||
import { SceneQueryRunner, SceneTimeRange, UrlSyncContextProvider, VizPanel } from '@grafana/scenes';
|
||||
import { mockLocalStorage } from 'app/features/alerting/unified/mocks';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { DashboardMeta } from 'app/types/dashboard';
|
||||
|
||||
import { buildPanelEditScene } from '../panel-edit/PanelEditor';
|
||||
import { DashboardInteractions } from '../utils/interactions';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { ToolbarActions } from './NavToolbarActions';
|
||||
import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager';
|
||||
|
||||
jest.mock('../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
editButtonClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const localStorageMock = mockLocalStorage();
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
jest.mock('app/features/playlist/PlaylistSrv', () => ({
|
||||
playlistSrv: {
|
||||
useState: jest.fn().mockReturnValue({ isPlaying: false }),
|
||||
@@ -142,6 +156,27 @@ describe('NavToolbarActions', () => {
|
||||
expect(await screen.findByText('Discard panel changes')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Back to dashboard')).toBeInTheDocument();
|
||||
});
|
||||
describe('edit dashboard button tracking', () => {
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => {
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'false');
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:false if grafana.dashboard.edit-pane.outline.collapsed is true', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'true');
|
||||
setup();
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Given new sharing button', () => {
|
||||
|
||||
@@ -21,6 +21,7 @@ import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/Na
|
||||
import { LS_PANEL_COPY_KEY } from 'app/core/constants';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { trackDashboardSceneEditButtonClicked } from 'app/features/dashboard-scene/utils/tracking';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { useGetResourceRepositoryView } from 'app/features/provisioning/hooks/useGetResourceRepositoryView';
|
||||
import { getReadOnlyTooltipText } from 'app/features/provisioning/utils/repository';
|
||||
@@ -328,6 +329,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
dashboard.onEnterEditMode();
|
||||
}}
|
||||
tooltip={
|
||||
@@ -353,6 +355,7 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
dashboard.onEnterEditMode();
|
||||
dashboard.setState({ editable: true, meta: { ...meta, canEdit: true } });
|
||||
}}
|
||||
@@ -405,7 +408,10 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
condition: isEditing && !isNew && isShowingDashboard,
|
||||
render: () => (
|
||||
<Button
|
||||
onClick={() => dashboard.exitEditMode({ skipConfirm: false })}
|
||||
onClick={() => {
|
||||
DashboardInteractions.exitEditButtonClicked();
|
||||
dashboard.exitEditMode({ skipConfirm: false });
|
||||
}}
|
||||
tooltip={t('dashboard.toolbar.exit-edit-mode.tooltip', 'Exits edit mode and discards unsaved changes')}
|
||||
size="sm"
|
||||
key="discard"
|
||||
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { mockLocalStorage } from 'app/features/alerting/unified/mocks';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { activateFullSceneTree } from 'app/features/dashboard-scene/utils/test-utils';
|
||||
|
||||
import { DashboardScene } from '../../DashboardScene';
|
||||
import { DashboardGridItem } from '../../layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../../layout-default/DefaultGridLayoutManager';
|
||||
|
||||
import { EditDashboardSwitch } from './EditDashboardSwitch';
|
||||
|
||||
jest.mock('app/features/dashboard-scene/utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
editButtonClicked: jest.fn(),
|
||||
exitEditButtonClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const localStorageMock = mockLocalStorage();
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
export function buildTestScene(isEditing = false) {
|
||||
const testScene = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: isEditing,
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [new DashboardGridItem({ body: new VizPanel({ key: 'panel-1', pluginId: 'text' }) })],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
testScene.exitEditMode = jest.fn();
|
||||
activateFullSceneTree(testScene);
|
||||
return testScene;
|
||||
}
|
||||
|
||||
describe('EditDashboardSwitch', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
describe('edit dashboard switch tracking', () => {
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => {
|
||||
render(<EditDashboardSwitch dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'false');
|
||||
render(<EditDashboardSwitch dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:false if grafana.dashboard.edit-pane.outline.collapsed is true', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'true');
|
||||
render(<EditDashboardSwitch dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.exitEditButtonClicked when exiting edit mode', async () => {
|
||||
const scene = buildTestScene(true);
|
||||
render(<EditDashboardSwitch dashboard={scene} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.exitEditButtonClicked).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
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';
|
||||
|
||||
import { ToolbarActionProps } from '../types';
|
||||
@@ -20,8 +22,10 @@ export const EditDashboardSwitch = ({ dashboard }: ToolbarActionProps) => {
|
||||
evt.stopPropagation();
|
||||
|
||||
if (!dashboard.state.isEditing) {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
dashboard.onEnterEditMode();
|
||||
} else {
|
||||
DashboardInteractions.exitEditButtonClicked();
|
||||
dashboard.exitEditMode({ skipConfirm: false });
|
||||
}
|
||||
}}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { getPanelPlugin } from '@grafana/data/test';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { setPluginImportUtils } from '@grafana/runtime';
|
||||
import { SceneGridLayout, SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
import { mockLocalStorage } from 'app/features/alerting/unified/mocks';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { activateFullSceneTree } from 'app/features/dashboard-scene/utils/test-utils';
|
||||
|
||||
import { DashboardScene } from '../../DashboardScene';
|
||||
import { DashboardGridItem } from '../../layout-default/DashboardGridItem';
|
||||
import { DefaultGridLayoutManager } from '../../layout-default/DefaultGridLayoutManager';
|
||||
|
||||
import { MakeDashboardEditableButton } from './MakeDashboardEditableButton';
|
||||
|
||||
// Mock the DashboardInteractions module
|
||||
jest.mock('app/features/dashboard-scene/utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
editButtonClicked: jest.fn(),
|
||||
exitEditButtonClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const localStorageMock = mockLocalStorage();
|
||||
Object.defineProperty(window, 'localStorage', {
|
||||
value: localStorageMock,
|
||||
writable: true,
|
||||
});
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
export function buildTestScene(isEditing = false) {
|
||||
const testScene = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: isEditing,
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [new DashboardGridItem({ body: new VizPanel({ key: 'panel-1', pluginId: 'text' }) })],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
testScene.exitEditMode = jest.fn();
|
||||
activateFullSceneTree(testScene);
|
||||
return testScene;
|
||||
}
|
||||
|
||||
describe('MakeDashboardEditableButton', () => {
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
localStorageMock.clear();
|
||||
});
|
||||
|
||||
describe('edit dashboard button tracking', () => {
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is undefined', async () => {
|
||||
render(<MakeDashboardEditableButton dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:true if grafana.dashboard.edit-pane.outline.collapsed is false', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'false');
|
||||
render(<MakeDashboardEditableButton dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: true });
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.editButtonClicked with outlineExpanded:false if grafana.dashboard.edit-pane.outline.collapsed is true', async () => {
|
||||
localStorageMock.setItem('grafana.dashboard.edit-pane.outline.collapsed', 'true');
|
||||
render(<MakeDashboardEditableButton dashboard={buildTestScene()} />);
|
||||
await userEvent.click(await screen.findByTestId(selectors.components.NavToolbar.editDashboard.editButton));
|
||||
expect(DashboardInteractions.editButtonClicked).toHaveBeenCalledWith({ outlineExpanded: false });
|
||||
});
|
||||
});
|
||||
});
|
||||
+2
@@ -1,6 +1,7 @@
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { trackDashboardSceneEditButtonClicked } from 'app/features/dashboard-scene/utils/tracking';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
|
||||
import { ToolbarActionProps } from '../types';
|
||||
@@ -10,6 +11,7 @@ export const MakeDashboardEditableButton = ({ dashboard }: ToolbarActionProps) =
|
||||
<Button
|
||||
disabled={playlistSrv.state.isPlaying}
|
||||
onClick={() => {
|
||||
trackDashboardSceneEditButtonClicked();
|
||||
dashboard.onEnterEditMode();
|
||||
dashboard.setState({ editable: true, meta: { ...dashboard.state.meta, canEdit: true } });
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneVariable, SceneVariableState, TestVariable } from '@grafana/scenes';
|
||||
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
|
||||
import { VariableEditorList } from './VariableEditorList';
|
||||
|
||||
jest.mock('../../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
addVariableButtonClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const setup = (variables: Array<SceneVariable<SceneVariableState>> = []) => {
|
||||
return render(
|
||||
<VariableEditorList
|
||||
variables={variables}
|
||||
usages={[]}
|
||||
usagesNetwork={[]}
|
||||
onAdd={jest.fn()}
|
||||
onChangeOrder={jest.fn()}
|
||||
onDuplicate={jest.fn()}
|
||||
onDelete={jest.fn()}
|
||||
onEdit={jest.fn()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
describe('VariableEditorList', () => {
|
||||
describe('tracking add variable button', () => {
|
||||
it('should call DashboardInteractions.trackAddVariableButtonClicked with source settings_pane when onAdd is clicked in empty variable list', async () => {
|
||||
const user = userEvent.setup();
|
||||
setup();
|
||||
await user.click(screen.getByTestId(selectors.components.CallToActionCard.buttonV2('Add variable')));
|
||||
expect(DashboardInteractions.addVariableButtonClicked).toHaveBeenCalledWith({ source: 'settings_pane' });
|
||||
});
|
||||
it('should call DashboardInteractions.trackAddVariableButtonClicked with source settings_pane when onAdd is clicked in variable list', async () => {
|
||||
const user = userEvent.setup();
|
||||
const variables = [
|
||||
new TestVariable({ name: 'Renamed Variable', query: 'A.*', value: '', text: '', options: [] }),
|
||||
];
|
||||
setup(variables);
|
||||
await user.click(screen.getByTestId(selectors.pages.Dashboard.Settings.Variables.List.newButton));
|
||||
expect(DashboardInteractions.addVariableButtonClicked).toHaveBeenCalledWith({ source: 'settings_pane' });
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,7 @@ import { SceneVariable, SceneVariableState } from '@grafana/scenes';
|
||||
import { useStyles2, Stack, Button, EmptyState, TextLink } from '@grafana/ui';
|
||||
|
||||
import { isVariableEditable } from '../../serialization/sceneVariablesSetToVariables';
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { VariablesDependenciesButton } from '../../variables/VariablesDependenciesButton';
|
||||
import { UsagesToNetwork, VariableUsageTree } from '../../variables/utils';
|
||||
|
||||
@@ -47,10 +48,15 @@ export function VariableEditorList({
|
||||
onChangeOrder(result.source.index, result.destination.index);
|
||||
};
|
||||
|
||||
const onVariableAdd = () => {
|
||||
onAdd();
|
||||
DashboardInteractions.addVariableButtonClicked({ source: 'settings_pane' });
|
||||
};
|
||||
|
||||
const editableVariables = variables.filter(isVariableEditable);
|
||||
|
||||
return editableVariables.length <= 0 ? (
|
||||
<EmptyVariablesList onAdd={onAdd} />
|
||||
<EmptyVariablesList onAdd={onVariableAdd} />
|
||||
) : (
|
||||
<Stack direction="column" gap={3}>
|
||||
<table
|
||||
@@ -100,7 +106,11 @@ export function VariableEditorList({
|
||||
</table>
|
||||
<Stack>
|
||||
<VariablesDependenciesButton variables={variables} />
|
||||
<Button data-testid={selectors.pages.Dashboard.Settings.Variables.List.newButton} onClick={onAdd} icon="plus">
|
||||
<Button
|
||||
data-testid={selectors.pages.Dashboard.Settings.Variables.List.newButton}
|
||||
onClick={onVariableAdd}
|
||||
icon="plus"
|
||||
>
|
||||
<Trans i18nKey="dashboard-scene.variable-editor-list.new-variable">New variable</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneGridLayout, SceneTimeRange, SceneVariableSet } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { DefaultGridLayoutManager } from '../../scene/layout-default/DefaultGridLayoutManager';
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { activateFullSceneTree } from '../../utils/test-utils';
|
||||
|
||||
import { VariableList } from './VariableSetEditableElement';
|
||||
|
||||
jest.mock('../../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
addVariableButtonClicked: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const variables = new SceneVariableSet({ variables: [] });
|
||||
|
||||
export function buildTestScene() {
|
||||
const testScene = new DashboardScene({
|
||||
$variables: variables,
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new DefaultGridLayoutManager({
|
||||
grid: new SceneGridLayout({
|
||||
children: [],
|
||||
}),
|
||||
}),
|
||||
});
|
||||
activateFullSceneTree(testScene);
|
||||
return testScene;
|
||||
}
|
||||
|
||||
describe('VariableList', () => {
|
||||
describe('tracking add variable button', () => {
|
||||
it('should call DashboardInteractions.trackAddVariableButtonClicked with source edit_pane when onAdd is clicked in edit pane variable list', async () => {
|
||||
const user = userEvent.setup();
|
||||
buildTestScene();
|
||||
render(<VariableList set={variables} />);
|
||||
await user.click(screen.getByTestId(selectors.components.PanelEditor.ElementEditPane.addVariableButton));
|
||||
expect(DashboardInteractions.addVariableButtonClicked).toHaveBeenCalledWith({ source: 'edit_pane' });
|
||||
});
|
||||
});
|
||||
});
|
||||
+6
-2
@@ -13,6 +13,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
|
||||
import { dashboardEditActions } from '../../edit-pane/shared';
|
||||
import { DashboardScene } from '../../scene/DashboardScene';
|
||||
import { EditableDashboardElement, EditableDashboardElementInfo } from '../../scene/types/EditableDashboardElement';
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { getDashboardSceneFor } from '../../utils/utils';
|
||||
|
||||
import { EditableVariableType, getNextAvailableId, getVariableScene, getVariableTypeSelectOptions } from './utils';
|
||||
@@ -53,7 +54,7 @@ export class VariableSetEditableElement implements EditableDashboardElement {
|
||||
public useEditPaneOptions = useEditPaneOptions.bind(this, this.set);
|
||||
}
|
||||
|
||||
function VariableList({ set }: { set: SceneVariableSet }) {
|
||||
export function VariableList({ set }: { set: SceneVariableSet }) {
|
||||
const { variables } = set.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const [isAdding, setIsAdding] = useToggle(false);
|
||||
@@ -102,7 +103,10 @@ function VariableList({ set }: { set: SceneVariableSet }) {
|
||||
icon="plus"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={setIsAdding}
|
||||
onClick={() => {
|
||||
DashboardInteractions.addVariableButtonClicked({ source: 'edit_pane' });
|
||||
setIsAdding();
|
||||
}}
|
||||
data-testid={selectors.components.PanelEditor.ElementEditPane.addVariableButton}
|
||||
>
|
||||
<Trans i18nKey="dashboard.edit-pane.variables.add-variable">Add variable</Trans>
|
||||
|
||||
@@ -8,6 +8,36 @@ export const DashboardInteractions = {
|
||||
reportDashboardInteraction('init_dashboard_completed', { ...properties });
|
||||
},
|
||||
|
||||
// grafana_dashboards_edit_button_clicked
|
||||
// when a user clicks the ‘edit’ or ‘make editable’ button in a dashboard view mode
|
||||
editButtonClicked: (properties: { outlineExpanded: boolean }) => {
|
||||
reportDashboardInteraction('edit_button_clicked', properties);
|
||||
},
|
||||
|
||||
// grafana_dashboards_exit_edit_button_clicked
|
||||
// when a user clicks the ‘Exit edit’ or ‘Exit Edit mode’ button in a dashboard edit mode
|
||||
exitEditButtonClicked: () => {
|
||||
reportDashboardInteraction('exit_edit_button_clicked');
|
||||
},
|
||||
|
||||
// grafana_dashboards_outline_clicked
|
||||
// when a user opens the outline view
|
||||
dashboardOutlineClicked: () => {
|
||||
reportDashboardInteraction('outline_clicked');
|
||||
},
|
||||
|
||||
// grafana_dashboards_outline_item_clicked
|
||||
// when a user clicks on an element of the outline
|
||||
outlineItemClicked: (properties: { index: number; depth: number }) => {
|
||||
reportDashboardInteraction('outline_item_clicked', properties);
|
||||
},
|
||||
|
||||
// dashboards_add_variable_button_clicked
|
||||
// when a user clicks on ‘Add Variable’ or ‘New Variable’
|
||||
addVariableButtonClicked: (properties: { source: 'edit_pane' | 'settings_pane' }) => {
|
||||
reportDashboardInteraction('add_variable_button_clicked', properties);
|
||||
},
|
||||
|
||||
// Dashboard edit item actions
|
||||
// dashboards_edit_action_clicked: when user adds or removes an item in edit mode
|
||||
// props: { item: string } - item is one of: add_panel, group_row, group_tab, ungroup, paste_panel, remove_row, remove_tab
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { store } from '@grafana/data';
|
||||
|
||||
import { DashboardInteractions } from './interactions';
|
||||
|
||||
export const trackDashboardSceneEditButtonClicked = () => {
|
||||
const outlineExpandedByDefault = !store.getBool('grafana.dashboard.edit-pane.outline.collapsed', true);
|
||||
DashboardInteractions.editButtonClicked({
|
||||
outlineExpanded: outlineExpandedByDefault,
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user