diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index e2c1700633c..0401dd92c60 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -104,8 +104,7 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{folderDTO && }
{(canCreateDashboards || canCreateFolder) && (
diff --git a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
index a42c5ef9f97..28514bd6708 100644
--- a/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
+++ b/public/app/features/browse-dashboards/components/BrowseActions/BrowseActions.tsx
@@ -2,6 +2,7 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
+import { reportInteraction } from '@grafana/runtime';
import { Button, useStyles2 } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
@@ -73,6 +74,7 @@ export function BrowseActions() {
const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID);
parentsToRefresh.add(dashboard?.parentUID);
}
+ trackAction('delete', selectedDashboards, selectedFolders);
onActionComplete(parentsToRefresh);
};
@@ -97,6 +99,7 @@ export function BrowseActions() {
const dashboard = findItem(rootItems?.items ?? [], childrenByParentUID, dashboardUID);
parentsToRefresh.add(dashboard?.parentUID);
}
+ trackAction('move', selectedDashboards, selectedFolders);
onActionComplete(parentsToRefresh);
};
@@ -144,3 +147,19 @@ const getStyles = (theme: GrafanaTheme2) => ({
marginBottom: theme.spacing(2),
}),
});
+
+type actionType = 'move' | 'delete';
+const actionMap: Record = {
+ move: 'grafana_manage_dashboards_item_moved',
+ delete: 'grafana_manage_dashboards_item_deleted',
+};
+
+function trackAction(action: actionType, selectedDashboards: string[], selectedFolders: string[]) {
+ reportInteraction(actionMap[action], {
+ item_counts: {
+ folder: selectedFolders.length,
+ dashboard: selectedDashboards.length,
+ },
+ source: 'tree_actions',
+ });
+}
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
index d5811f9903d..2b6a1093aab 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.test.tsx
@@ -3,27 +3,36 @@ import userEvent from '@testing-library/user-event';
import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
+import { FolderDTO } from 'app/types';
+
+import { mockFolderDTO } from '../fixtures/folder.fixture';
+
import CreateNewButton from './CreateNewButton';
+const mockParentFolder = mockFolderDTO();
+
function render(...[ui, options]: Parameters) {
rtlRender({ui}, options);
}
-async function renderAndOpen(folderUID?: string) {
- render();
+async function renderAndOpen(folder?: FolderDTO) {
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
}
describe('NewActionsButton', () => {
- it('should display the correct urls with a given folderUID', async () => {
- await renderAndOpen('123');
+ it('should display the correct urls with a given parent folder', async () => {
+ await renderAndOpen(mockParentFolder);
- expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new?folderUid=123');
- expect(screen.getByText('Import')).toHaveAttribute('href', '/dashboard/import?folderUid=123');
+ expect(screen.getByText('New dashboard')).toHaveAttribute(
+ 'href',
+ `/dashboard/new?folderUid=${mockParentFolder.uid}`
+ );
+ expect(screen.getByText('Import')).toHaveAttribute('href', `/dashboard/import?folderUid=${mockParentFolder.uid}`);
});
- it('should display urls without params when there is no folderUID', async () => {
+ it('should display urls without params when there is no parent folder', async () => {
await renderAndOpen();
expect(screen.getByText('New dashboard')).toHaveAttribute('href', '/dashboard/new');
@@ -31,8 +40,7 @@ describe('NewActionsButton', () => {
});
it('clicking the "New folder" button opens the drawer', async () => {
- const mockParentFolderTitle = 'mockParentFolderTitle';
- render();
+ render();
const newButton = screen.getByText('New');
await userEvent.click(newButton);
@@ -41,7 +49,7 @@ describe('NewActionsButton', () => {
const drawer = screen.getByRole('dialog', { name: 'Drawer title New folder' });
expect(drawer).toBeInTheDocument();
expect(within(drawer).getByRole('heading', { name: 'New folder' })).toBeInTheDocument();
- expect(within(drawer).getByText(`Location: ${mockParentFolderTitle}`)).toBeInTheDocument();
+ expect(within(drawer).getByText(`Location: ${mockParentFolder.title}`)).toBeInTheDocument();
});
it('should only render dashboard items when folder creation is disabled', async () => {
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
index 6d9b1367313..cfc8f8d3822 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
@@ -1,6 +1,8 @@
import React, { useState } from 'react';
import { connect, ConnectedProps } from 'react-redux';
+import { useLocation } from 'react-router-dom';
+import { reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { createNewFolder } from 'app/features/folders/state/actions';
import {
@@ -9,6 +11,7 @@ import {
getImportPhrase,
getNewPhrase,
} from 'app/features/search/tempI18nPhrases';
+import { FolderDTO } from 'app/types';
import { NewFolderForm } from './NewFolderForm';
@@ -19,40 +22,54 @@ const mapDispatchToProps = {
const connector = connect(null, mapDispatchToProps);
interface OwnProps {
- parentFolderTitle?: string;
- /**
- * Pass a folder UID in which the dashboard or folder will be created
- */
- parentFolderUid?: string;
+ parentFolder?: FolderDTO;
canCreateFolder: boolean;
canCreateDashboard: boolean;
}
type Props = OwnProps & ConnectedProps;
-function CreateNewButton({
- parentFolderTitle,
- parentFolderUid,
- canCreateDashboard,
- canCreateFolder,
- createNewFolder,
-}: Props) {
+function CreateNewButton({ parentFolder, canCreateDashboard, canCreateFolder, createNewFolder }: Props) {
const [isOpen, setIsOpen] = useState(false);
+ const location = useLocation();
const [showNewFolderDrawer, setShowNewFolderDrawer] = useState(false);
const onCreateFolder = (folderName: string) => {
- createNewFolder(folderName, parentFolderUid);
+ createNewFolder(folderName, parentFolder?.uid);
+ const depth = parentFolder?.parents ? parentFolder.parents.length + 1 : 0;
+ reportInteraction('grafana_manage_dashboards_folder_created', {
+ is_subfolder: Boolean(parentFolder?.uid),
+ folder_depth: depth,
+ });
setShowNewFolderDrawer(false);
};
const newMenu = (
);
@@ -68,7 +85,7 @@ function CreateNewButton({
{showNewFolderDrawer && (
setShowNewFolderDrawer(false)}
size="sm"
diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
index 2551b3fa49a..6ad0f7ad5a0 100644
--- a/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
+++ b/public/app/features/browse-dashboards/components/FolderActionsButton.test.tsx
@@ -4,9 +4,11 @@ import React from 'react';
import { TestProvider } from 'test/helpers/TestProvider';
import { appEvents, contextSrv } from 'app/core/core';
-import { AccessControlAction, FolderDTO } from 'app/types';
+import { AccessControlAction } from 'app/types';
import { ShowModalReactEvent } from 'app/types/events';
+import { mockFolderDTO } from '../fixtures/folder.fixture';
+
import { DeleteModal } from './BrowseActions/DeleteModal';
import { MoveModal } from './BrowseActions/MoveModal';
import { FolderActionsButton } from './FolderActionsButton';
@@ -21,22 +23,7 @@ jest.mock('app/core/components/AccessControl', () => ({
}));
describe('browse-dashboards FolderActionsButton', () => {
- const mockFolder: FolderDTO = {
- canAdmin: true,
- canDelete: true,
- canEdit: true,
- canSave: true,
- created: '',
- createdBy: '',
- hasAcl: true,
- id: 1,
- title: 'myFolder',
- uid: '12345',
- updated: '',
- updatedBy: '',
- url: '',
- version: 1,
- };
+ const mockFolder = mockFolderDTO();
beforeEach(() => {
jest.spyOn(contextSrv, 'hasPermission').mockReturnValue(true);
diff --git a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
index 52c3c0b8ddf..c6188b4023c 100644
--- a/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
+++ b/public/app/features/browse-dashboards/components/FolderActionsButton.tsx
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
-import { locationService } from '@grafana/runtime';
+import { locationService, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import { Permissions } from 'app/core/components/AccessControl';
import { appEvents, contextSrv } from 'app/core/core';
@@ -30,6 +30,13 @@ export function FolderActionsButton({ folder }: Props) {
const onMove = async (destinationUID: string) => {
await moveFolder({ folderUID: folder.uid, destinationUID });
+ reportInteraction('grafana_manage_dashboards_item_moved', {
+ item_counts: {
+ folder: 1,
+ dashboard: 0,
+ },
+ source: 'folder_actions',
+ });
dispatch(refetchChildren({ parentUID: destinationUID, pageSize: destinationUID ? PAGE_SIZE : ROOT_PAGE_SIZE }));
if (folder.parentUid) {
@@ -41,6 +48,13 @@ export function FolderActionsButton({ folder }: Props) {
const onDelete = async () => {
await dispatch(deleteFolder(folder.uid));
+ reportInteraction('grafana_manage_dashboards_item_deleted', {
+ item_counts: {
+ folder: 1,
+ dashboard: 0,
+ },
+ source: 'folder_actions',
+ });
if (folder.parentUid) {
dispatch(
refetchChildren({ parentUID: folder.parentUid, pageSize: folder.parentUid ? PAGE_SIZE : ROOT_PAGE_SIZE })
diff --git a/public/app/features/browse-dashboards/components/NameCell.tsx b/public/app/features/browse-dashboards/components/NameCell.tsx
index 86f48ced71e..0d40a7ff574 100644
--- a/public/app/features/browse-dashboards/components/NameCell.tsx
+++ b/public/app/features/browse-dashboards/components/NameCell.tsx
@@ -4,6 +4,7 @@ import Skeleton from 'react-loading-skeleton';
import { CellProps } from 'react-table';
import { GrafanaTheme2 } from '@grafana/data';
+import { reportInteraction } from '@grafana/runtime';
import { IconButton, Link, Spinner, useStyles2 } from '@grafana/ui';
import { getSvgSize } from '@grafana/ui/src/components/Icon/utils';
import { Span } from '@grafana/ui/src/unstable';
@@ -82,7 +83,13 @@ export function NameCell({ row: { original: data }, onFolderClick }: NameCellPro
)}
{item.url ? (
-
+ {
+ reportInteraction('manage_dashboards_result_clicked');
+ }}
+ href={item.url}
+ className={styles.link}
+ >
{item.title}
) : (
diff --git a/public/app/features/browse-dashboards/fixtures/folder.fixture.ts b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts
new file mode 100644
index 00000000000..14cb3a83652
--- /dev/null
+++ b/public/app/features/browse-dashboards/fixtures/folder.fixture.ts
@@ -0,0 +1,25 @@
+import { Chance } from 'chance';
+
+import { FolderDTO } from 'app/types';
+
+export function mockFolderDTO(seed = 1, partial?: Partial): FolderDTO {
+ const random = Chance(seed);
+ const uid = random.guid();
+ return {
+ canAdmin: true,
+ canDelete: true,
+ canEdit: true,
+ canSave: true,
+ created: new Date(random.timestamp()).toISOString(),
+ createdBy: '',
+ hasAcl: true,
+ id: 1,
+ title: random.sentence({ words: 3 }),
+ uid,
+ updated: new Date(random.timestamp()).toISOString(),
+ updatedBy: '',
+ url: `/dashboards/f/${uid}`,
+ version: 1,
+ ...partial,
+ };
+}