diff --git a/public/app/features/correlations/CorrelationsPage.test.tsx b/public/app/features/correlations/CorrelationsPage.test.tsx index 81827aee913..2de67a10d60 100644 --- a/public/app/features/correlations/CorrelationsPage.test.tsx +++ b/public/app/features/correlations/CorrelationsPage.test.tsx @@ -8,7 +8,14 @@ import { MockDataSourceApi } from 'test/mocks/datasource_srv'; import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock'; import { DataSourcePluginMeta } from '@grafana/data'; -import { BackendSrv, FetchError, FetchResponse, setDataSourceSrv, BackendSrvRequest } from '@grafana/runtime'; +import { + BackendSrv, + FetchError, + FetchResponse, + setDataSourceSrv, + BackendSrvRequest, + reportInteraction, +} from '@grafana/runtime'; import { GrafanaContext } from 'app/core/context/GrafanaContext'; import { contextSrv } from 'app/core/services/context_srv'; import { configureStore } from 'app/store/configureStore'; @@ -47,12 +54,6 @@ function createFetchError(overrides?: DeepPartial): FetchError { ); } -jest.mock('app/core/services/context_srv'); - -const mocks = { - contextSrv: jest.mocked(contextSrv), -}; - const renderWithContext = async ( datasources: ConstructorParameters[0] = {}, correlations: Correlation[] = [] @@ -217,6 +218,20 @@ const renderWithContext = async ( return renderResult; }; +jest.mock('app/core/services/context_srv'); + +const mocks = { + contextSrv: jest.mocked(contextSrv), + reportInteraction: jest.fn(), +}; + +jest.mock('@grafana/runtime', () => ({ + ...jest.requireActual('@grafana/runtime'), + reportInteraction: (...args: Parameters) => { + mocks.reportInteraction(...args); + }, +})); + beforeAll(() => { mocks.contextSrv.hasPermission.mockImplementation(() => true); }); @@ -254,6 +269,10 @@ describe('CorrelationsPage', () => { }); }); + afterEach(() => { + mocks.reportInteraction.mockClear(); + }); + it('shows CTA', async () => { // insert form should not be present expect(screen.queryByRole('button', { name: /add$/i })).not.toBeInTheDocument(); @@ -305,12 +324,18 @@ describe('CorrelationsPage', () => { // Waits for the form to be removed, meaning the correlation got successfully saved await waitForElementToBeRemoved(() => screen.queryByRole('button', { name: /add$/i })); + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added'); + // the table showing correlations should have appeared expect(screen.getByRole('table')).toBeInTheDocument(); }); }); describe('With correlations', () => { + afterEach(() => { + mocks.reportInteraction.mockClear(); + }); + let queryRowsByCellValue: (columnName: Matcher, textValue: Matcher) => HTMLTableRowElement[]; let getHeaderByName: (columnName: Matcher) => HTMLTableCellElement; let queryCellsByColumnName: (columnName: Matcher) => HTMLTableCellElement[]; @@ -430,6 +455,8 @@ describe('CorrelationsPage', () => { // the form should get removed after successful submissions await waitForElementToBeRemoved(() => screen.queryByRole('button', { name: /add$/i })); + + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_added'); }); it('correctly closes the form when clicking on the close icon', async () => { @@ -460,6 +487,8 @@ describe('CorrelationsPage', () => { fireEvent.click(confirmButton); await waitForElementToBeRemoved(() => screen.queryByRole('cell', { name: /some label$/i })); + + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_deleted'); }); it('correctly edits correlations', async () => { @@ -468,6 +497,8 @@ describe('CorrelationsPage', () => { const rowExpanderButton = within(tableRows[0]).getByRole('button', { name: /toggle row expanded/i }); fireEvent.click(rowExpanderButton); + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded'); + await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i)); fireEvent.change(screen.getByRole('textbox', { name: /label/i }), { target: { value: 'edited label' } }); @@ -482,6 +513,8 @@ describe('CorrelationsPage', () => { await waitFor(() => { expect(screen.queryByRole('cell', { name: /edited label$/i })).toBeInTheDocument(); }); + + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_edited'); }); }); @@ -526,6 +559,8 @@ describe('CorrelationsPage', () => { fireEvent.click(rowExpanderButton); + expect(mocks.reportInteraction).toHaveBeenLastCalledWith('grafana_correlations_details_expanded'); + // wait for the form to be rendered and query editor to be mounted await waitForElementToBeRemoved(() => screen.queryByText(/loading query editor/i)); diff --git a/public/app/features/correlations/CorrelationsPage.tsx b/public/app/features/correlations/CorrelationsPage.tsx index 5ee331be916..730b32c9952 100644 --- a/public/app/features/correlations/CorrelationsPage.tsx +++ b/public/app/features/correlations/CorrelationsPage.tsx @@ -4,7 +4,7 @@ import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; import { CellProps, SortByFn } from 'react-table'; import { GrafanaTheme2 } from '@grafana/data'; -import { isFetchError } from '@grafana/runtime'; +import { isFetchError, reportInteraction } from '@grafana/runtime'; import { Badge, Button, DeleteButton, HorizontalGroup, LoadingPlaceholder, useStyles2, Alert } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; @@ -15,6 +15,7 @@ import { AddCorrelationForm } from './Forms/AddCorrelationForm'; import { EditCorrelationForm } from './Forms/EditCorrelationForm'; import { EmptyCorrelationsCTA } from './components/EmptyCorrelationsCTA'; import { Column, Table } from './components/Table'; +import type { RemoveCorrelationParams } from './types'; import { CorrelationData, useCorrelations } from './useCorrelations'; const sortDatasource: SortByFn = (a, b, column) => @@ -43,11 +44,31 @@ export default function CorrelationsPage() { const canWriteCorrelations = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite); - const handleAdd = useCallback(() => { + const handleAdded = useCallback(() => { + reportInteraction('grafana_correlations_added'); fetchCorrelations(); setIsAdding(false); }, [fetchCorrelations]); + const handleUpdated = useCallback(() => { + reportInteraction('grafana_correlations_edited'); + fetchCorrelations(); + }, [fetchCorrelations]); + + const handleDelete = useCallback( + (params: RemoveCorrelationParams) => { + remove.execute(params); + }, + [remove] + ); + + // onDelete - triggers when deleting a correlation + useEffect(() => { + if (remove.value) { + reportInteraction('grafana_correlations_deleted'); + } + }, [remove.value]); + useEffect(() => { if (!remove.error && !remove.loading && remove.value) { fetchCorrelations(); @@ -66,11 +87,11 @@ export default function CorrelationsPage() { !readOnly && ( remove.execute({ sourceUID, uid })} + onConfirm={() => handleDelete({ sourceUID, uid })} closeOnConfirm /> ), - [remove] + [handleDelete] ); const columns = useMemo>>( @@ -142,15 +163,15 @@ export default function CorrelationsPage() { ) } - {isAdding && setIsAdding(false)} onCreated={handleAdd} />} + {isAdding && setIsAdding(false)} onCreated={handleAdded} />} {data && data.length >= 1 && ( ( - ( + )} columns={columns} @@ -164,6 +185,28 @@ export default function CorrelationsPage() { ); } +interface ExpandedRowProps { + correlation: CorrelationData; + readOnly: boolean; + onUpdated: () => void; +} +function ExpendedRow({ correlation: { source, target, ...correlation }, readOnly, onUpdated }: ExpandedRowProps) { + useEffect( + () => reportInteraction('grafana_correlations_details_expanded'), + // we only want to fire this on first render + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + return ( + + ); +} + const getDatasourceCellStyles = (theme: GrafanaTheme2) => ({ root: css` display: flex; diff --git a/public/app/features/correlations/useCorrelations.ts b/public/app/features/correlations/useCorrelations.ts index 88aad32fb06..b013a0d99d6 100644 --- a/public/app/features/correlations/useCorrelations.ts +++ b/public/app/features/correlations/useCorrelations.ts @@ -48,7 +48,7 @@ export const useCorrelations = () => { [backend] ); - const [removeInfo, remove] = useAsyncFn<(params: RemoveCorrelationParams) => Promise>( + const [removeInfo, remove] = useAsyncFn<(params: RemoveCorrelationParams) => Promise<{ message: string }>>( ({ sourceUID, uid }) => backend.delete(`/api/datasources/uid/${sourceUID}/correlations/${uid}`), [backend] );