mirror of https://github.com/grafana/grafana.git
Dynamic Dashboards: Add tracking for item actions (#111059)
This commit is contained in:
parent
4583402ba9
commit
544582e495
|
@ -45,6 +45,9 @@ export const versionedComponents = {
|
|||
pasteTab: {
|
||||
'12.1.0': 'data-testid CanvasGridAddActions paste-tab',
|
||||
},
|
||||
pastePanel: {
|
||||
'12.1.0': 'data-testid CanvasGridAddActions paste-panel',
|
||||
},
|
||||
},
|
||||
DashboardEditPaneSplitter: {
|
||||
primaryBody: {
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { SceneTimeRange } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
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 { DashboardEditPane } from './DashboardEditPane';
|
||||
import { EditPaneHeader } from './EditPaneHeader';
|
||||
import { ElementSelection } from './ElementSelection';
|
||||
|
||||
// Mock DashboardInteractions
|
||||
jest.mock('../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
trackRemoveRowClick: jest.fn(),
|
||||
trackRemoveTabClick: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
const sceneWithTab = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new TabsLayoutManager({
|
||||
tabs: [
|
||||
new TabItem({
|
||||
title: 'test tab',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const sceneWithRow = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
title: 'test row',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const buildTestScene = (scene: DashboardScene) => {
|
||||
activateFullSceneTree(scene);
|
||||
return scene;
|
||||
};
|
||||
|
||||
describe('EditPaneHeader', () => {
|
||||
const mockEditPane = {
|
||||
state: { selection: null },
|
||||
clearSelection: jest.fn(),
|
||||
} as unknown as DashboardEditPane;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('tracking item deletion', () => {
|
||||
it('should call DashboardActions.trackDeleteRow when deleting a row', async () => {
|
||||
const user = userEvent.setup();
|
||||
const scene = buildTestScene(sceneWithRow);
|
||||
const row = (scene.state.body as RowsLayoutManager).state.rows[0];
|
||||
const elementSelection = new ElementSelection([['row-test', row.getRef()]]);
|
||||
const editableElement = elementSelection.createSelectionElement()!;
|
||||
|
||||
render(<EditPaneHeader element={editableElement} editPane={mockEditPane} />);
|
||||
|
||||
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
||||
expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardActions.trackDeleteTab when deleting a tab', async () => {
|
||||
const user = userEvent.setup();
|
||||
const scene = buildTestScene(sceneWithTab);
|
||||
const tab = (scene.state.body as TabsLayoutManager).state.tabs[0];
|
||||
const elementSelection = new ElementSelection([['tab-test', tab.getRef()]]);
|
||||
const editableElement = elementSelection.createSelectionElement()!;
|
||||
|
||||
render(<EditPaneHeader element={editableElement} editPane={mockEditPane} />);
|
||||
|
||||
await user.click(screen.getByTestId(selectors.components.EditPaneHeader.deleteButton));
|
||||
expect(DashboardInteractions.trackRemoveTabClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -4,6 +4,7 @@ import { GrafanaTheme2 } from '@grafana/data';
|
|||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { Button, Menu, Stack, Text, useStyles2, Dropdown, Icon, IconButton } from '@grafana/ui';
|
||||
import { trackDeleteDashboardElement } from 'app/features/dashboard/utils/tracking';
|
||||
|
||||
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
|
||||
|
||||
|
@ -26,6 +27,15 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
|||
const onGoBack = () => editPane.clearSelection();
|
||||
const canGoBack = editPane.state.selection;
|
||||
|
||||
const onDeleteElement = () => {
|
||||
if (onConfirmDelete) {
|
||||
onConfirmDelete();
|
||||
} else if (onDelete) {
|
||||
onDelete();
|
||||
}
|
||||
trackDeleteDashboardElement(elementInfo);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<Stack direction="row" gap={0.5}>
|
||||
|
@ -75,7 +85,7 @@ export function EditPaneHeader({ element, editPane }: EditPaneHeaderProps) {
|
|||
|
||||
{(onDelete || onConfirmDelete) && (
|
||||
<Button
|
||||
onClick={onConfirmDelete || onDelete}
|
||||
onClick={onDeleteElement}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
fill="outline"
|
||||
|
|
|
@ -0,0 +1,159 @@
|
|||
import { screen, render } 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 { SceneTimeRange, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { activateFullSceneTree } from '../../utils/test-utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { RowItem } from '../layout-rows/RowItem';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
import { TabItem } from '../layout-tabs/TabItem';
|
||||
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
|
||||
|
||||
import { CanvasGridAddActions } from './CanvasGridAddActions';
|
||||
|
||||
jest.mock('../../utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
trackAddPanelClick: jest.fn(),
|
||||
trackGroupRowClick: jest.fn(),
|
||||
trackGroupTabClick: jest.fn(),
|
||||
trackUngroupClick: jest.fn(),
|
||||
trackPastePanelClick: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// mock getDefaultVizPanel
|
||||
jest.mock('../../utils/utils', () => ({
|
||||
...jest.requireActual('../../utils/utils'),
|
||||
getDefaultVizPanel: () => new VizPanel({ key: 'panel-1', pluginId: 'text' }),
|
||||
}));
|
||||
|
||||
// mock addNew
|
||||
jest.mock('./addNew', () => ({
|
||||
...jest.requireActual('./addNew'),
|
||||
addNewRowTo: jest.fn(),
|
||||
addNewTabTo: jest.fn(),
|
||||
}));
|
||||
|
||||
// mock useClipboardState
|
||||
jest.mock('./useClipboardState', () => ({
|
||||
...jest.requireActual('./useClipboardState'),
|
||||
useClipboardState: () => ({
|
||||
hasCopiedPanel: true,
|
||||
}),
|
||||
}));
|
||||
|
||||
// mock ungroupLayout
|
||||
jest.mock('./utils', () => ({
|
||||
...jest.requireActual('./utils'),
|
||||
groupLayout: jest.fn(),
|
||||
}));
|
||||
|
||||
setPluginImportUtils({
|
||||
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
||||
getPanelPluginFromCache: (id: string) => undefined,
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
const sceneWithNestedLayout = new DashboardScene({
|
||||
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
|
||||
isEditing: true,
|
||||
body: new TabsLayoutManager({
|
||||
tabs: [
|
||||
new TabItem({
|
||||
title: 'test tab',
|
||||
layout: new RowsLayoutManager({
|
||||
rows: [
|
||||
new RowItem({
|
||||
title: 'Test Title',
|
||||
layout: new TabsLayoutManager({
|
||||
tabs: [new TabItem({ title: 'Subtab' })],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
activateFullSceneTree(sceneWithNestedLayout);
|
||||
return sceneWithNestedLayout;
|
||||
}
|
||||
|
||||
describe('CanvasGridAddActions', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('tracking scene actions', () => {
|
||||
it('should call DashboardInteractions.trackAddPanelClick when clicking on add panel button', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = scene.state.body;
|
||||
layoutManager.addPanel = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.addPanel));
|
||||
expect(DashboardInteractions.trackAddPanelClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackGroupRowClick when clicking on group into row button', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = scene.state.body;
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.groupPanels));
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.addRow));
|
||||
expect(DashboardInteractions.trackGroupRowClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackGroupTabClick when clicking on group into tab', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = scene.state.body;
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.groupPanels));
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.addTab));
|
||||
expect(DashboardInteractions.trackGroupTabClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackUngroupClick when clicking on ungroup panels in row layout', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = (scene.state.body as TabsLayoutManager).state.tabs[0].state.layout as RowsLayoutManager;
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.ungroup));
|
||||
expect(DashboardInteractions.trackUngroupClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackUngroupClick when clicking on ungroup panels in tab layout', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = (
|
||||
((scene.state.body as TabsLayoutManager).state.tabs[0].state.layout as RowsLayoutManager).state.rows[0].state
|
||||
.layout as TabsLayoutManager
|
||||
).state.tabs[0].state.layout;
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.ungroup));
|
||||
expect(DashboardInteractions.trackUngroupClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackPastePanel when clicking on the paste panel button', async () => {
|
||||
const scene = buildTestScene();
|
||||
const layoutManager = scene.state.body;
|
||||
layoutManager.pastePanel = jest.fn();
|
||||
const user = userEvent.setup();
|
||||
render(<CanvasGridAddActions layoutManager={layoutManager} />);
|
||||
|
||||
await user.click(await screen.findByTestId(selectors.components.CanvasGridAddActions.pastePanel));
|
||||
expect(DashboardInteractions.trackPastePanelClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -6,6 +6,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||
import { Button, Dropdown, Menu, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
|
||||
import { DashboardInteractions } from '../../utils/interactions';
|
||||
import { getDefaultVizPanel } from '../../utils/utils';
|
||||
import { DashboardScene } from '../DashboardScene';
|
||||
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
|
||||
|
@ -31,7 +32,10 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
|
|||
fill="text"
|
||||
icon="plus"
|
||||
data-testid={selectors.components.CanvasGridAddActions.addPanel}
|
||||
onClick={() => layoutManager.addPanel(getDefaultVizPanel())}
|
||||
onClick={() => {
|
||||
layoutManager.addPanel(getDefaultVizPanel());
|
||||
DashboardInteractions.trackAddPanelClick();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.add-panel">Add panel</Trans>
|
||||
</Button>
|
||||
|
@ -41,15 +45,19 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
|
|||
<Menu.Item
|
||||
icon="list-ul"
|
||||
label={t('dashboard.canvas-actions.group-into-row', 'Group into row')}
|
||||
testId={selectors.components.CanvasGridAddActions.addRow}
|
||||
onClick={() => {
|
||||
addNewRowTo(layoutManager);
|
||||
DashboardInteractions.trackGroupRowClick();
|
||||
}}
|
||||
></Menu.Item>
|
||||
<Menu.Item
|
||||
icon="layers"
|
||||
testId={selectors.components.CanvasGridAddActions.addTab}
|
||||
label={t('dashboard.canvas-actions.group-into-tab', 'Group into tab')}
|
||||
onClick={() => {
|
||||
addNewTabTo(layoutManager);
|
||||
DashboardInteractions.trackGroupTabClick();
|
||||
}}
|
||||
></Menu.Item>
|
||||
</Menu>
|
||||
|
@ -67,11 +75,13 @@ export function CanvasGridAddActions({ layoutManager }: Props) {
|
|||
{renderUngroupAction(layoutManager)}
|
||||
{hasCopiedPanel && layoutManager.pastePanel && (
|
||||
<Button
|
||||
data-testid={selectors.components.CanvasGridAddActions.pastePanel}
|
||||
variant="primary"
|
||||
fill="text"
|
||||
icon="clipboard-alt"
|
||||
onClick={() => {
|
||||
layoutManager.pastePanel?.();
|
||||
DashboardInteractions.trackPastePanelClick();
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="dashboard.canvas-actions.paste-panel">Paste panel</Trans>
|
||||
|
@ -91,6 +101,7 @@ function renderUngroupAction(layoutManager: DashboardLayoutManager) {
|
|||
const parentLayout = dashboardSceneGraph.getLayoutManagerFor(layoutManager.parent!);
|
||||
|
||||
const onUngroup = () => {
|
||||
DashboardInteractions.trackUngroupClick();
|
||||
ungroupLayout(parentLayout, layoutManager);
|
||||
};
|
||||
|
||||
|
|
|
@ -8,6 +8,31 @@ export const DashboardInteractions = {
|
|||
reportDashboardInteraction('init_dashboard_completed', { ...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
|
||||
trackAddPanelClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'add_panel' });
|
||||
},
|
||||
trackGroupRowClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'group_row' });
|
||||
},
|
||||
trackGroupTabClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'group_tab' });
|
||||
},
|
||||
trackUngroupClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'ungroup' });
|
||||
},
|
||||
trackPastePanelClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'paste_panel' });
|
||||
},
|
||||
trackRemoveRowClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'remove_row' });
|
||||
},
|
||||
trackRemoveTabClick() {
|
||||
reportDashboardInteraction('edit_action_clicked', { item: 'remove_tab' });
|
||||
},
|
||||
|
||||
panelLinkClicked: (properties?: Record<string, unknown>) => {
|
||||
reportDashboardInteraction('panelheader_datalink_clicked', properties);
|
||||
},
|
||||
|
|
|
@ -3,12 +3,20 @@ import userEvent from '@testing-library/user-event';
|
|||
|
||||
import { createTheme } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
||||
|
||||
import { PanelModel } from '../../state/PanelModel';
|
||||
|
||||
import { DashboardRow, UnthemedDashboardRow } from './DashboardRow';
|
||||
|
||||
// mock DashboardInteractions
|
||||
jest.mock('app/features/dashboard-scene/utils/interactions', () => ({
|
||||
DashboardInteractions: {
|
||||
trackRemoveRowClick: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DashboardRow', () => {
|
||||
let panel: PanelModel, dashboardMock: any;
|
||||
|
||||
|
@ -95,4 +103,11 @@ describe('DashboardRow', () => {
|
|||
const dashboardRow = new UnthemedDashboardRow({ panel: rowPanel, dashboard: dashboardMock, theme: createTheme() });
|
||||
expect(dashboardRow.getWarning()).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should call DashboardInteractions.trackRemoveRowClick when clicking on delete row button', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<DashboardRow panel={panel} dashboard={dashboardMock} />);
|
||||
await user.click(screen.getByRole('button', { name: 'Delete row' }));
|
||||
expect(DashboardInteractions.trackRemoveRowClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Trans, t } from '@grafana/i18n';
|
|||
import { getTemplateSrv, RefreshEvent } from '@grafana/runtime';
|
||||
import { Icon, TextLink, Themeable2, withTheme2 } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard/constants';
|
||||
import grabDarkSvg from 'img/grab_dark.svg';
|
||||
import grabLightSvg from 'img/grab_light.svg';
|
||||
|
@ -141,7 +142,10 @@ export class UnthemedDashboardRow extends Component<DashboardRowProps> {
|
|||
<button
|
||||
type="button"
|
||||
className="pointer"
|
||||
onClick={this.onDelete}
|
||||
onClick={() => {
|
||||
DashboardInteractions.trackRemoveRowClick();
|
||||
this.onDelete();
|
||||
}}
|
||||
aria-label={t('dashboard.unthemed-dashboard-row.aria-label-delete-row', 'Delete row')}
|
||||
>
|
||||
<Icon name="trash-alt" />
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { VariableModel } from '@grafana/schema/dist/esm/index';
|
||||
import { VariableKind } from '@grafana/schema/dist/esm/schema/dashboard/v2';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import { EditableDashboardElementInfo } from 'app/features/dashboard-scene/scene/types/EditableDashboardElement';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
|
||||
import { DashboardModel } from '../state/DashboardModel';
|
||||
|
@ -38,6 +39,19 @@ export function trackDashboardSceneLoaded(dashboard: DashboardScene, duration?:
|
|||
});
|
||||
}
|
||||
|
||||
export const trackDeleteDashboardElement = (element: EditableDashboardElementInfo) => {
|
||||
switch (element?.typeName) {
|
||||
case 'Row':
|
||||
DashboardInteractions.trackRemoveRowClick();
|
||||
break;
|
||||
case 'Tab':
|
||||
DashboardInteractions.trackRemoveTabClick();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
export function getPanelPluginCounts(panels: string[] | string[]) {
|
||||
return panels.reduce((r: Record<string, number>, p) => {
|
||||
r[panelName(p)] = 1 + r[panelName(p)] || 1;
|
||||
|
|
Loading…
Reference in New Issue