mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Ref: datasource picker rudderstack events (#95074)
* ref: ds picker events added * ref: track opendropdown only on click * ref: update/update all payload added to the event * ref: configure more ds button event path to from path * ref: result count fix, enable/disable track * ref: debounce search tracks * ref: track connections_plugin_card_clicked * ref: call tracking from the child comp with result count * ref: change event names, add creator_team and schema version
This commit is contained in:
parent
59f5c1edfb
commit
2f965a07ab
@ -2,6 +2,7 @@ import { css } from '@emotion/css';
|
||||
import { useMemo, useState, FormEvent, MouseEvent } from 'react';
|
||||
|
||||
import { GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { useStyles2, LoadingPlaceholder, EmptyState } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
@ -65,8 +66,12 @@ export function AddNewConnection() {
|
||||
if (!canCreateDataSources) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
openModal(item);
|
||||
reportInteraction('connections_plugin_card_clicked', {
|
||||
plugin_id: item.id,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { debounce } from 'lodash';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
|
||||
@ -27,11 +28,23 @@ export interface Props {
|
||||
export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const trackSearch = useMemo(
|
||||
() =>
|
||||
debounce((q, count) => {
|
||||
if (q) {
|
||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||
item: INTERACTION_ITEM.SEARCH,
|
||||
query: q,
|
||||
result_count: count,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
const handleSearchChange = (value: string) => {
|
||||
if (value) {
|
||||
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.SEARCH, query: value });
|
||||
}
|
||||
setSearchQuery(value);
|
||||
};
|
||||
|
||||
@ -54,6 +67,8 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||
item: INTERACTION_ITEM.CHANGE_TAB,
|
||||
tab: VisualizationSelectPaneTab[value],
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
setListMode(value);
|
||||
};
|
||||
@ -93,10 +108,21 @@ export function PanelVizTypePicker({ panel, data, onChange, onClose }: Props) {
|
||||
</Field>
|
||||
<CustomScrollbar>
|
||||
{listMode === VisualizationSelectPaneTab.Visualizations && (
|
||||
<VizTypePicker pluginId={panel.state.pluginId} searchQuery={searchQuery} onChange={onChange} />
|
||||
<VizTypePicker
|
||||
pluginId={panel.state.pluginId}
|
||||
searchQuery={searchQuery}
|
||||
trackSearch={trackSearch}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)}
|
||||
{listMode === VisualizationSelectPaneTab.Suggestions && (
|
||||
<VisualizationSuggestions onChange={onChange} searchQuery={searchQuery} panel={panelModel} data={data} />
|
||||
<VisualizationSuggestions
|
||||
onChange={onChange}
|
||||
trackSearch={trackSearch}
|
||||
searchQuery={searchQuery}
|
||||
panel={panelModel}
|
||||
data={data}
|
||||
/>
|
||||
)}
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { LinkButton } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
@ -5,11 +7,16 @@ import { Trans } from 'app/core/internationalization';
|
||||
import { ROUTES } from 'app/features/connections/constants';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { trackAddNewDsClicked } from '../tracking';
|
||||
|
||||
export function DataSourceAddButton(): JSX.Element | null {
|
||||
const canCreateDataSource = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
|
||||
const handleClick = useCallback(() => {
|
||||
trackAddNewDsClicked({ path: location.pathname });
|
||||
}, []);
|
||||
|
||||
return canCreateDataSource ? (
|
||||
<LinkButton icon="plus" href={config.appSubUrl + ROUTES.DataSourcesNew}>
|
||||
<LinkButton icon="plus" href={config.appSubUrl + ROUTES.DataSourcesNew} onClick={handleClick}>
|
||||
<Trans i18nKey="data-sources.datasource-add-button.label">Add new data source</Trans>
|
||||
</LinkButton>
|
||||
) : null;
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { DataSourcePluginMeta, GrafanaTheme2 } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { LinkButton, useStyles2 } from '@grafana/ui';
|
||||
import { DataSourcePluginCategory } from 'app/types';
|
||||
|
||||
@ -20,6 +22,15 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
|
||||
const moreDataSourcesLink = `${ROUTES.AddNewConnection}?cat=data-source`;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
reportInteraction('connections_add_datasource_find_more_ds_plugins_clicked', {
|
||||
targetPath: moreDataSourcesLink,
|
||||
path: location.pathname,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}, [moreDataSourcesLink]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Categories */}
|
||||
@ -34,7 +45,7 @@ export function DataSourceCategories({ categories, onClickDataSourceType }: Prop
|
||||
|
||||
{/* Find more */}
|
||||
<div className={styles.more}>
|
||||
<LinkButton variant="secondary" href={moreDataSourcesLink} target="_self" rel="noopener">
|
||||
<LinkButton variant="secondary" href={moreDataSourcesLink} onClick={handleClick} target="_self" rel="noopener">
|
||||
Find more data source plugins
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useCallback } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
|
||||
import { StoreState, useSelector, useDispatch } from 'app/types';
|
||||
|
||||
import { getDataSourcesSearchQuery, getDataSourcesSort, setDataSourcesSearchQuery, setIsSortAscending } from '../state';
|
||||
import { trackDsSearched } from '../tracking';
|
||||
|
||||
const ascendingSortValue = 'alpha-asc';
|
||||
const descendingSortValue = 'alpha-desc';
|
||||
@ -19,7 +21,23 @@ const sortOptions = [
|
||||
|
||||
export function DataSourcesListHeader() {
|
||||
const dispatch = useDispatch();
|
||||
const setSearchQuery = useCallback((q: string) => dispatch(setDataSourcesSearchQuery(q)), [dispatch]);
|
||||
const debouncedTrackSearch = useMemo(
|
||||
() =>
|
||||
debounce((q) => {
|
||||
trackDsSearched({ query: q });
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
const setSearchQuery = useCallback(
|
||||
(q: string) => {
|
||||
dispatch(setDataSourcesSearchQuery(q));
|
||||
if (q) {
|
||||
debouncedTrackSearch(q);
|
||||
}
|
||||
},
|
||||
[dispatch, debouncedTrackSearch]
|
||||
);
|
||||
const searchQuery = useSelector(({ dataSources }: StoreState) => getDataSourcesSearchQuery(dataSources));
|
||||
|
||||
const setSort = useCallback(
|
||||
|
@ -3,7 +3,8 @@ import { autoUpdate, flip, offset, shift, size, useFloating } from '@floating-ui
|
||||
import { useDialog } from '@react-aria/dialog';
|
||||
import { FocusScope } from '@react-aria/focus';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { debounce } from 'lodash';
|
||||
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
|
||||
import * as React from 'react';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
@ -26,6 +27,7 @@ import { dataSourceLabel, matchDataSourceWithSearch } from './utils';
|
||||
|
||||
const INTERACTION_EVENT_NAME = 'dashboards_dspicker_clicked';
|
||||
const INTERACTION_ITEM = {
|
||||
SEARCH: 'search',
|
||||
OPEN_DROPDOWN: 'open_dspicker',
|
||||
SELECT_DS: 'select_ds',
|
||||
ADD_FILE: 'add_file',
|
||||
@ -78,6 +80,18 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
|
||||
const [filterTerm, setFilterTerm] = useState<string>('');
|
||||
const { onKeyDown, keyboardEvents } = useKeyNavigationListener();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const debouncedTrackSearch = useMemo(
|
||||
() =>
|
||||
debounce((q) => {
|
||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||
item: INTERACTION_ITEM.SEARCH,
|
||||
query: q,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}, 300),
|
||||
[]
|
||||
);
|
||||
|
||||
// Used to position the popper correctly and to bring back the focus when navigating from footer to input
|
||||
const [markerElement, setMarkerElement] = useState<HTMLInputElement | null>();
|
||||
@ -151,7 +165,6 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
|
||||
);
|
||||
|
||||
function openDropdown() {
|
||||
reportInteraction(INTERACTION_EVENT_NAME, { item: INTERACTION_ITEM.OPEN_DROPDOWN });
|
||||
setOpen(true);
|
||||
markerElement?.focus();
|
||||
}
|
||||
@ -214,7 +227,17 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
|
||||
<div className={styles.container} data-testid={selectors.components.DataSourcePicker.container}>
|
||||
{/* This clickable div is just extending the clickable area on the input element to include the prefix and suffix. */}
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div className={styles.trigger} onClick={openDropdown}>
|
||||
<div
|
||||
className={styles.trigger}
|
||||
onClick={() => {
|
||||
openDropdown();
|
||||
reportInteraction(INTERACTION_EVENT_NAME, {
|
||||
item: INTERACTION_ITEM.OPEN_DROPDOWN,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
id={inputId || 'data-source-picker'}
|
||||
className={inputHasFocus ? undefined : styles.input}
|
||||
@ -235,6 +258,9 @@ export function DataSourcePicker(props: DataSourcePickerProps) {
|
||||
onChange={(e) => {
|
||||
openDropdown();
|
||||
setFilterTerm(e.currentTarget.value);
|
||||
if (e.currentTarget.value) {
|
||||
debouncedTrackSearch(e.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
ref={handleReference}
|
||||
disabled={disabled}
|
||||
|
@ -394,7 +394,7 @@ describe('addDataSource', () => {
|
||||
plugin_version: '1.2.3',
|
||||
datasource_uid: 'azure23',
|
||||
grafana_version: '1.0',
|
||||
path: DATASOURCES_ROUTES.Edit.replace(':uid', 'azure23'),
|
||||
path: location.pathname,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -249,7 +249,7 @@ export function addDataSource(
|
||||
plugin_id: plugin.id,
|
||||
datasource_uid: result.datasource.uid,
|
||||
plugin_version: result.meta?.info?.version,
|
||||
path: editLink,
|
||||
path: location.pathname,
|
||||
});
|
||||
|
||||
locationService.push(editLink);
|
||||
|
@ -86,3 +86,19 @@ export const trackDsConfigClicked = (item: string) => {
|
||||
export const trackDsConfigUpdated = (props: { item: string; error?: unknown }) => {
|
||||
reportInteraction('connections_datasources_ds_configured', props);
|
||||
};
|
||||
|
||||
export const trackDsSearched = (props: { query: string }) => {
|
||||
reportInteraction('connections_datasource_list_searched', {
|
||||
...props,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
};
|
||||
|
||||
export const trackAddNewDsClicked = (props: { path: string }) => {
|
||||
reportInteraction('connections_datasource_list_add_datasource_clicked', {
|
||||
...props,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
};
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
@ -15,12 +16,19 @@ export interface Props {
|
||||
onChange: (options: VizTypeChangeDetails) => void;
|
||||
data?: PanelData;
|
||||
panel?: PanelModel;
|
||||
trackSearch?: (q: string, count: number) => void;
|
||||
}
|
||||
|
||||
export function VisualizationSuggestions({ searchQuery, onChange, data, panel }: Props) {
|
||||
export function VisualizationSuggestions({ searchQuery, onChange, data, panel, trackSearch }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const { value: suggestions } = useAsync(() => getAllSuggestions(data, panel), [data, panel]);
|
||||
const filteredSuggestions = filterSuggestionsBySearch(searchQuery, suggestions);
|
||||
const filteredSuggestions = useMemo(() => {
|
||||
const result = filterSuggestionsBySearch(searchQuery, suggestions);
|
||||
if (trackSearch) {
|
||||
trackSearch(searchQuery, result.length);
|
||||
}
|
||||
return result;
|
||||
}, [searchQuery, suggestions, trackSearch]);
|
||||
|
||||
return (
|
||||
// This div is needed in some places to make AutoSizer work
|
||||
|
@ -15,9 +15,10 @@ export interface Props {
|
||||
searchQuery: string;
|
||||
onChange: (options: VizTypeChangeDetails) => void;
|
||||
isWidget?: boolean;
|
||||
trackSearch?: (q: string, count: number) => void;
|
||||
}
|
||||
|
||||
export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = false }: Props) {
|
||||
export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = false, trackSearch }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
const pluginsList = useMemo(() => {
|
||||
if (config.featureToggles.vizAndWidgetSplit) {
|
||||
@ -26,10 +27,13 @@ export function VizTypePicker({ pluginId, searchQuery, onChange, isWidget = fals
|
||||
return getAllPanelPluginMeta();
|
||||
}, [isWidget]);
|
||||
|
||||
const filteredPluginTypes = useMemo(
|
||||
() => filterPluginList(pluginsList, searchQuery, pluginId),
|
||||
[pluginsList, searchQuery, pluginId]
|
||||
);
|
||||
const filteredPluginTypes = useMemo(() => {
|
||||
const result = filterPluginList(pluginsList, searchQuery, pluginId);
|
||||
if (trackSearch) {
|
||||
trackSearch(searchQuery, result.length);
|
||||
}
|
||||
return result;
|
||||
}, [pluginsList, searchQuery, pluginId, trackSearch]);
|
||||
|
||||
if (filteredPluginTypes.length === 0) {
|
||||
return <EmptySearchResult>Could not find anything matching your query</EmptySearchResult>;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { PluginMeta } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
@ -26,14 +27,27 @@ export function GetStartedWithApp({ plugin }: Props): React.ReactElement | null
|
||||
|
||||
const { enabled, jsonData } = pluginConfig?.meta;
|
||||
|
||||
const enable = () =>
|
||||
const enable = () => {
|
||||
reportInteraction('plugins_detail_enable_clicked', {
|
||||
path: location.pathname,
|
||||
plugin_id: plugin.id,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
updatePluginSettingsAndReload(plugin.id, {
|
||||
enabled: true,
|
||||
pinned: true,
|
||||
jsonData,
|
||||
});
|
||||
};
|
||||
|
||||
const disable = () => {
|
||||
reportInteraction('plugins_detail_disable_clicked', {
|
||||
path: location.pathname,
|
||||
plugin_id: plugin.id,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
updatePluginSettingsAndReload(plugin.id, {
|
||||
enabled: false,
|
||||
pinned: false,
|
||||
|
@ -55,6 +55,8 @@ export function InstallControlsButton({
|
||||
plugin_id: plugin.id,
|
||||
plugin_type: plugin.type,
|
||||
path: location.pathname,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@ -109,7 +111,7 @@ export function InstallControlsButton({
|
||||
};
|
||||
|
||||
const onUpdate = async () => {
|
||||
reportInteraction(PLUGIN_UPDATE_INTERACTION_EVENT_NAME);
|
||||
reportInteraction(PLUGIN_UPDATE_INTERACTION_EVENT_NAME, trackingProps);
|
||||
|
||||
await install(plugin.id, latestCompatibleVersion?.version, true);
|
||||
if (!errorInstalling) {
|
||||
|
@ -23,7 +23,11 @@ function PluginListItemComponent({ plugin, pathName }: Props) {
|
||||
|
||||
const reportUserClickInteraction = () => {
|
||||
if (locationService.getSearchObject()?.q) {
|
||||
reportInteraction('plugins_search_user_click', {});
|
||||
reportInteraction('plugins_search_user_click', {
|
||||
plugin_id: plugin.id,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
@ -87,7 +87,12 @@ export const UpdateAllModal = ({ isOpen, onDismiss, isLoading, plugins }: Props)
|
||||
|
||||
const onConfirm = async () => {
|
||||
if (!inProgress) {
|
||||
reportInteraction(PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME);
|
||||
reportInteraction(PLUGINS_UPDATE_ALL_INTERACTION_EVENT_NAME, {
|
||||
path: location.pathname,
|
||||
count: selectedPlugins?.size,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
|
||||
setInProgress(true);
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
import { PluginError, PluginType, unEscapeStringFromRegex } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
@ -14,6 +15,14 @@ export const selectItems = createSelector(selectRoot, ({ items }) => items);
|
||||
|
||||
export const { selectAll, selectById } = pluginsAdapter.getSelectors(selectItems);
|
||||
|
||||
const debouncedTrackSearch = debounce((count) => {
|
||||
reportInteraction('plugins_search', {
|
||||
resultsCount: count,
|
||||
creator_team: 'grafana_plugins_catalog',
|
||||
schema_version: '1.0.0',
|
||||
});
|
||||
}, 300);
|
||||
|
||||
export type PluginFilters = {
|
||||
// Searches for a string in certain fields (e.g. "name" or "orgName")
|
||||
// (Note: this will be an escaped regex string as it comes from `FilterInput`)
|
||||
@ -35,12 +44,11 @@ export type PluginFilters = {
|
||||
export const selectPlugins = (filters: PluginFilters) =>
|
||||
createSelector(selectAll, (plugins) => {
|
||||
const keyword = filters.keyword ? unEscapeStringFromRegex(filters.keyword.toLowerCase()) : '';
|
||||
// Fuzzy search does not consider plugin type filter
|
||||
const filteredPluginIds = keyword !== '' ? filterByKeyword(plugins, keyword) : null;
|
||||
|
||||
if (keyword) {
|
||||
reportInteraction('plugins_search', { resultsCount: filteredPluginIds?.length });
|
||||
}
|
||||
return plugins.filter((plugin) => {
|
||||
// Filters are applied here
|
||||
const filteredPlugins = plugins.filter((plugin) => {
|
||||
if (keyword && filteredPluginIds == null) {
|
||||
return false;
|
||||
}
|
||||
@ -67,6 +75,12 @@ export const selectPlugins = (filters: PluginFilters) =>
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
if (keyword) {
|
||||
debouncedTrackSearch(filteredPlugins.length);
|
||||
}
|
||||
|
||||
return filteredPlugins;
|
||||
});
|
||||
|
||||
export const selectPluginErrors = (filterByPluginType?: PluginType) =>
|
||||
|
@ -17,6 +17,7 @@ export function useKeyNavigationListener() {
|
||||
case 'ArrowLeft':
|
||||
case 'ArrowRight':
|
||||
case 'Enter':
|
||||
case 'Escape':
|
||||
eventsRef.current.next(e);
|
||||
default:
|
||||
// ignore
|
||||
|
Loading…
Reference in New Issue
Block a user