Dynamic Dashboards: Add new tracking events for dashboard interactions (#111022)

This commit is contained in:
Ida Štambuk
2025-09-16 18:40:34 +02:00
committed by GitHub
parent 7e13882a1a
commit 0be2394372
17 changed files with 595 additions and 14 deletions
@@ -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"
@@ -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 });
}
}}
@@ -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 });
});
});
});
@@ -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>
@@ -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' });
});
});
});
@@ -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,
});
};