Nested folders: add analytics tracking for some features (#69901)

* add analytics for folder creation

* add interaction tracking for move/delete

* add tracking for item clicked in the new browse view

* review comments

* emit counts instead
This commit is contained in:
Ashley Harrison
2023-06-12 16:52:08 +01:00
committed by GitHub
parent b1da3dd202
commit 9799a28fad
8 changed files with 123 additions and 47 deletions
@@ -104,8 +104,7 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{folderDTO && <FolderActionsButton folder={folderDTO} />}
{(canCreateDashboards || canCreateFolder) && (
<CreateNewButton
parentFolderTitle={folderDTO?.title}
parentFolderUid={folderUID}
parentFolder={folderDTO}
canCreateDashboard={canCreateDashboards}
canCreateFolder={canCreateFolder}
/>
@@ -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<actionType, string> = {
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',
});
}
@@ -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<typeof rtlRender>) {
rtlRender(<TestProvider>{ui}</TestProvider>, options);
}
async function renderAndOpen(folderUID?: string) {
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolderUid={folderUID} />);
async function renderAndOpen(folder?: FolderDTO) {
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolder={folder} />);
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(<CreateNewButton canCreateDashboard canCreateFolder parentFolderTitle={mockParentFolderTitle} />);
render(<CreateNewButton canCreateDashboard canCreateFolder parentFolder={mockParentFolder} />);
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 () => {
@@ -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<typeof connector>;
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 = (
<Menu>
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/new', parentFolderUid)} label={getNewDashboardPhrase()} />
<MenuItem
label={getNewDashboardPhrase()}
onClick={() =>
reportInteraction('grafana_menu_item_clicked', {
url: addFolderUidToUrl('/dashboard/new', parentFolder?.uid),
from: location.pathname,
})
}
url={addFolderUidToUrl('/dashboard/new', parentFolder?.uid)}
/>
)}
{canCreateFolder && <MenuItem onClick={() => setShowNewFolderDrawer(true)} label={getNewFolderPhrase()} />}
{canCreateDashboard && (
<MenuItem url={addFolderUidToUrl('/dashboard/import', parentFolderUid)} label={getImportPhrase()} />
<MenuItem
label={getImportPhrase()}
onClick={() =>
reportInteraction('grafana_menu_item_clicked', {
url: addFolderUidToUrl('/dashboard/import', parentFolder?.uid),
from: location.pathname,
})
}
url={addFolderUidToUrl('/dashboard/import', parentFolder?.uid)}
/>
)}
</Menu>
);
@@ -68,7 +85,7 @@ function CreateNewButton({
{showNewFolderDrawer && (
<Drawer
title={getNewFolderPhrase()}
subtitle={parentFolderTitle ? `Location: ${parentFolderTitle}` : undefined}
subtitle={parentFolder?.title ? `Location: ${parentFolder.title}` : undefined}
scrollableContent
onClose={() => setShowNewFolderDrawer(false)}
size="sm"
@@ -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);
@@ -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 })
@@ -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
)}
<Span variant="body" truncate>
{item.url ? (
<Link href={item.url} className={styles.link}>
<Link
onClick={() => {
reportInteraction('manage_dashboards_result_clicked');
}}
href={item.url}
className={styles.link}
>
{item.title}
</Link>
) : (
@@ -0,0 +1,25 @@
import { Chance } from 'chance';
import { FolderDTO } from 'app/types';
export function mockFolderDTO(seed = 1, partial?: Partial<FolderDTO>): 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,
};
}