diff --git a/packages/grafana-ui/src/components/Card/Card.tsx b/packages/grafana-ui/src/components/Card/Card.tsx index 91ce47f71d7..8b8df2ad1e0 100644 --- a/packages/grafana-ui/src/components/Card/Card.tsx +++ b/packages/grafana-ui/src/components/Card/Card.tsx @@ -17,7 +17,7 @@ export interface Props extends Omit void; + onClick?: (e: React.MouseEvent) => void; /** @deprecated Use `Card.Heading` instead */ heading?: ReactNode; /** @deprecated Use `Card.Description` instead */ @@ -37,7 +37,7 @@ export interface CardInterface extends FC { const CardContext = React.createContext<{ href?: string; - onClick?: () => void; + onClick?: (e: React.MouseEvent) => void; disabled?: boolean; isSelected?: boolean; } | null>(null); @@ -93,7 +93,7 @@ const Heading = ({ children, className, 'aria-label': ariaLabel }: ChildProps & return (

{href ? ( - + {children} ) : onClick ? ( diff --git a/public/app/features/search/components/SearchCard.tsx b/public/app/features/search/components/SearchCard.tsx index a095f31cd5d..d99ddd7b318 100644 --- a/public/app/features/search/components/SearchCard.tsx +++ b/public/app/features/search/components/SearchCard.tsx @@ -20,13 +20,14 @@ export interface Props { item: DashboardSectionItem; onTagSelected?: (name: string) => any; onToggleChecked?: OnToggleChecked; + onClick?: (event: React.MouseEvent) => void; } export function getThumbnailURL(uid: string, isLight?: boolean) { return `/api/dashboards/uid/${uid}/img/thumb/${isLight ? 'light' : 'dark'}`; } -export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: Props) { +export function SearchCard({ editable, item, onTagSelected, onToggleChecked, onClick }: Props) { const [hasImage, setHasImage] = useState(true); const [lastUpdated, setLastUpdated] = useState(null); const [showExpandedView, setShowExpandedView] = useState(false); @@ -118,6 +119,7 @@ export function SearchCard({ editable, item, onTagSelected, onToggleChecked }: P onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave} onMouseMove={onMouseMove} + onClick={onClick} >
diff --git a/public/app/features/search/components/SearchCardExpanded.tsx b/public/app/features/search/components/SearchCardExpanded.tsx index 4700fdd916d..c369df0f889 100644 --- a/public/app/features/search/components/SearchCardExpanded.tsx +++ b/public/app/features/search/components/SearchCardExpanded.tsx @@ -16,9 +16,10 @@ export interface Props { imageWidth: number; item: DashboardSectionItem; lastUpdated?: string | null; + onClick?: (event: React.MouseEvent) => void; } -export function SearchCardExpanded({ className, imageHeight, imageWidth, item, lastUpdated }: Props) { +export function SearchCardExpanded({ className, imageHeight, imageWidth, item, lastUpdated, onClick }: Props) { const theme = useTheme2(); const [hasImage, setHasImage] = useState(true); const imageSrc = getThumbnailURL(item.uid!, theme.isLight); @@ -27,7 +28,7 @@ export function SearchCardExpanded({ className, imageHeight, imageWidth, item, l const folderTitle = item.folderTitle || 'General'; return ( - +
{hasImage ? ( any; onToggleChecked?: OnToggleChecked; + onClickItem?: (event: React.MouseEvent) => void; } const selectors = e2eSelectors.components.Search; @@ -29,7 +30,7 @@ const getIconFromMeta = (meta = ''): IconName => { }; /** @deprecated */ -export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSelected }) => { +export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSelected, onClickItem }) => { const styles = useStyles2(getStyles); const tagSelected = useCallback( (tag: string, event: React.MouseEvent) => { @@ -59,6 +60,7 @@ export const SearchItem: FC = ({ item, editable, onToggleChecked, onTagSe href={item.url} style={{ minHeight: SEARCH_ITEM_HEIGHT }} className={styles.container} + onClick={onClickItem} > {item.title} diff --git a/public/app/features/search/page/components/FolderSection.tsx b/public/app/features/search/page/components/FolderSection.tsx index 69f8de0be3f..51e3653ee36 100644 --- a/public/app/features/search/page/components/FolderSection.tsx +++ b/public/app/features/search/page/components/FolderSection.tsx @@ -25,6 +25,7 @@ export interface DashboardSection { interface SectionHeaderProps { selection?: SelectionChecker; selectionToggle?: SelectionToggle; + onClickItem?: (e: React.MouseEvent) => void; onTagSelected: (tag: string) => void; section: DashboardSection; renderStandaloneBody?: boolean; // render the body on its own @@ -34,6 +35,7 @@ interface SectionHeaderProps { export const FolderSection: FC = ({ section, selectionToggle, + onClickItem, onTagSelected, selection, renderStandaloneBody, @@ -137,6 +139,7 @@ export const FolderSection: FC = ({ } }} editable={Boolean(selection != null)} + onClickItem={onClickItem} /> ); }); diff --git a/public/app/features/search/page/components/FolderView.tsx b/public/app/features/search/page/components/FolderView.tsx index 7ca5fb21b09..71000ceaa38 100644 --- a/public/app/features/search/page/components/FolderView.tsx +++ b/public/app/features/search/page/components/FolderView.tsx @@ -15,11 +15,18 @@ import { SearchResultsProps } from '../components/SearchResultsTable'; import { DashboardSection, FolderSection } from './FolderSection'; -type Props = Pick & { +type Props = Pick & { tags?: string[]; hidePseudoFolders?: boolean; }; -export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hidePseudoFolders }: Props) => { +export const FolderView = ({ + selection, + selectionToggle, + onTagSelected, + tags, + hidePseudoFolders, + onClickItem, +}: Props) => { const styles = useStyles2(getStyles); const results = useAsync(async () => { @@ -73,6 +80,7 @@ export const FolderView = ({ selection, selectionToggle, onTagSelected, tags, hi onTagSelected={onTagSelected} section={section} tags={tags} + onClickItem={onClickItem} /> )}
diff --git a/public/app/features/search/page/components/SearchResultsCards.tsx b/public/app/features/search/page/components/SearchResultsCards.tsx index b70310e5c34..3f93ca1fa21 100644 --- a/public/app/features/search/page/components/SearchResultsCards.tsx +++ b/public/app/features/search/page/components/SearchResultsCards.tsx @@ -21,10 +21,9 @@ export const SearchResultsCards = React.memo( height, selection, selectionToggle, - clearSelection, onTagSelected, - onDatasourceChange, keyboardEvents, + onClickItem, }: SearchResultsProps) => { const styles = useStyles2(getStyles); const infiniteLoaderRef = useRef(null); @@ -89,11 +88,12 @@ export const SearchResultsCards = React.memo( } }} editable={Boolean(selection != null)} + onClickItem={onClickItem} /> ); }, - [response.view, highlightIndex, styles, onTagSelected, selection, selectionToggle] + [response.view, highlightIndex, styles, onTagSelected, selection, selectionToggle, onClickItem] ); if (!response.totalRows) { diff --git a/public/app/features/search/page/components/SearchResultsGrid.tsx b/public/app/features/search/page/components/SearchResultsGrid.tsx index 8d691b5d782..d063789c960 100644 --- a/public/app/features/search/page/components/SearchResultsGrid.tsx +++ b/public/app/features/search/page/components/SearchResultsGrid.tsx @@ -20,6 +20,7 @@ export const SearchResultsGrid = ({ selection, selectionToggle, onTagSelected, + onClickItem, keyboardEvents, }: SearchResultsProps) => { const styles = useStyles2(getStyles); @@ -35,6 +36,7 @@ export const SearchResultsGrid = ({ } }, onTagSelected, + onClick: onClickItem, }; const itemCount = response.totalRows ?? response.view.length; diff --git a/public/app/features/search/page/components/SearchResultsTable.tsx b/public/app/features/search/page/components/SearchResultsTable.tsx index 8f30e9c1671..1e495e9e256 100644 --- a/public/app/features/search/page/components/SearchResultsTable.tsx +++ b/public/app/features/search/page/components/SearchResultsTable.tsx @@ -26,6 +26,7 @@ export type SearchResultsProps = { clearSelection: () => void; onTagSelected: (tag: string) => void; onDatasourceChange?: (datasource?: string) => void; + onClickItem?: (event: React.MouseEvent) => void; keyboardEvents: Observable; }; @@ -45,6 +46,7 @@ export const SearchResultsTable = React.memo( clearSelection, onTagSelected, onDatasourceChange, + onClickItem, keyboardEvents, }: SearchResultsProps) => { const styles = useStyles2(getStyles); @@ -121,14 +123,14 @@ export const SearchResultsTable = React.memo( cell={cell} columnIndex={index} columnCount={row.cells.length} - userProps={{ href: url }} + userProps={{ href: url, onClick: onClickItem }} /> ); })} ); }, - [rows, prepareRow, response.view.fields.url?.values, highlightIndex, styles, tableStyles] + [rows, prepareRow, response.view.fields.url?.values, highlightIndex, styles, tableStyles, onClickItem] ); if (!rows.length) { diff --git a/public/app/features/search/page/components/SearchView.tsx b/public/app/features/search/page/components/SearchView.tsx index 549fc81294e..198e3e5b15e 100644 --- a/public/app/features/search/page/components/SearchView.tsx +++ b/public/app/features/search/page/components/SearchView.tsx @@ -14,7 +14,12 @@ import { PreviewsSystemRequirements } from '../../components/PreviewsSystemRequi import { useSearchQuery } from '../../hooks/useSearchQuery'; import { getGrafanaSearcher, SearchQuery } from '../../service'; import { SearchLayout } from '../../types'; -import { reportDashboardListViewed } from '../reporting'; +import { + reportDashboardListViewed, + reportSearchResultInteraction, + reportSearchQueryInteraction, + reportSearchFailedQueryInteraction, +} from '../reporting'; import { newSearchSelection, updateSearchSelection } from '../selection'; import { ActionRow, getValidQueryLayout } from './ActionRow'; @@ -65,6 +70,7 @@ export const SearchView = ({ const isFolders = layout === SearchLayout.Folders; const [listKey, setListKey] = useState(Date.now()); + const eventTrackingNamespace = folderDTO ? 'manage_dashboards' : 'dashboard_search'; const searchQuery = useMemo(() => { const q: SearchQuery = { @@ -103,23 +109,55 @@ export const SearchView = ({ // Search usage reporting useDebounce( () => { - reportDashboardListViewed(folderDTO ? 'manage_dashboards' : 'dashboard_search', { + reportDashboardListViewed(eventTrackingNamespace, { layout: query.layout, starred: query.starred, sortValue: query.sort?.value, query: query.query, tagCount: query.tag?.length, + includePanels, }); }, 1000, - [folderDTO, query.layout, query.starred, query.sort?.value, query.query?.length, query.tag?.length] + [] ); + const onClickItem = () => { + reportSearchResultInteraction(eventTrackingNamespace, { + layout: query.layout, + starred: query.starred, + sortValue: query.sort?.value, + query: query.query, + tagCount: query.tag?.length, + includePanels, + }); + }; + const results = useAsync(() => { + const trackingInfo = { + layout: query.layout, + starred: query.starred, + sortValue: query.sort?.value, + query: query.query, + tagCount: query.tag?.length, + includePanels, + }; + + reportSearchQueryInteraction(eventTrackingNamespace, trackingInfo); + if (searchQuery.starred) { - return getGrafanaSearcher().starred(searchQuery); + return getGrafanaSearcher() + .starred(searchQuery) + .catch((error) => + reportSearchFailedQueryInteraction(eventTrackingNamespace, { ...trackingInfo, error: error?.message }) + ); } - return getGrafanaSearcher().search(searchQuery); + + return getGrafanaSearcher() + .search(searchQuery) + .catch((error) => + reportSearchFailedQueryInteraction(eventTrackingNamespace, { ...trackingInfo, error: error?.message }) + ); }, [searchQuery]); const clearSelection = useCallback(() => { @@ -200,6 +238,7 @@ export const SearchView = ({ renderStandaloneBody={true} tags={query.tag} key={listKey} + onClickItem={onClickItem} /> ); } @@ -211,6 +250,7 @@ export const SearchView = ({ tags={query.tag} onTagSelected={onTagAdd} hidePseudoFolders={hidePseudoFolders} + onClickItem={onClickItem} /> ); } @@ -229,6 +269,7 @@ export const SearchView = ({ onTagSelected: onTagAdd, keyboardEvents, onDatasourceChange: query.datasource ? onDatasourceChange : undefined, + onClickItem: onClickItem, }; if (layout === SearchLayout.Grid) { diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx index d99e0df6308..89dcdf6472f 100644 --- a/public/app/features/search/page/components/columns.tsx +++ b/public/app/features/search/page/components/columns.tsx @@ -126,7 +126,7 @@ export const generateColumns = ( classNames += ' ' + styles.missingTitleText; } return ( -
+ {name} ); diff --git a/public/app/features/search/page/reporting.ts b/public/app/features/search/page/reporting.ts index 34e9d51c12c..48276400421 100644 --- a/public/app/features/search/page/reporting.ts +++ b/public/app/features/search/page/reporting.ts @@ -2,25 +2,48 @@ import { config, reportInteraction } from '@grafana/runtime'; import { SearchLayout } from '../types'; -export const reportDashboardListViewed = ( - dashboardListType: 'manage_dashboards' | 'dashboard_search', - query: { - layout?: SearchLayout; - starred?: boolean; - sortValue?: string; - query?: string; - tagCount?: number; - } +interface QueryProps { + layout: SearchLayout; + starred: boolean; + sortValue: string; + query: string; + tagCount: number; + includePanels: boolean; +} + +type DashboardListType = 'manage_dashboards' | 'dashboard_search'; + +export const reportDashboardListViewed = (dashboardListType: DashboardListType, query: QueryProps) => { + reportInteraction(`${dashboardListType}_viewed`, getQuerySearchContext(query)); +}; + +export const reportSearchResultInteraction = (dashboardListType: DashboardListType, query: QueryProps) => { + reportInteraction(`${dashboardListType}_result_clicked`, getQuerySearchContext(query)); +}; + +export const reportSearchQueryInteraction = (dashboardListType: DashboardListType, query: QueryProps) => { + reportInteraction(`${dashboardListType}_query_submitted`, getQuerySearchContext(query)); +}; + +export const reportSearchFailedQueryInteraction = ( + dashboardListType: DashboardListType, + { error, ...query }: QueryProps & { error?: string } ) => { + reportInteraction(`${dashboardListType}_query_failed`, { ...getQuerySearchContext(query), error }); +}; + +const getQuerySearchContext = (query: QueryProps) => { const showPreviews = query.layout === SearchLayout.Grid; const previewsEnabled = Boolean(config.featureToggles.panelTitleSearch); const previews = previewsEnabled ? (showPreviews ? 'on' : 'off') : 'feature_disabled'; - reportInteraction(`${dashboardListType}_viewed`, { + + return { previews, layout: query.layout, starredFilter: query.starred ?? false, sort: query.sortValue ?? '', tagCount: query.tagCount ?? 0, queryLength: query.query?.length ?? 0, - }); + includePanels: query.includePanels ?? false, + }; };