mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
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:
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user