Files
grafana/public/app/features/explore/AddToDashboard/index.test.tsx
Giordano Ricci 09f48173fe Explore: allow users to save Explore state to a new panel in a new dashboard (#45148)
* Add Button & Form

* Save to new dashboard

* minor adjustements

* move modal to a separate component

* handle dashboard name related errors

* pick visualization based on query results

* lift state

* fix types

* Add Open & Close tests

* Add submit test

* add navigation tests

* add tests for API errors

* remove console log

* create wrapper component for AddToDashboardButton

* remove unused mapped prop

* add wrapper test

* rename isActive to isVisible

* invert control over save & redirect logic

* remove leftover commented code

* cleanup setup parameters

* reorganize code & improve tests

* Remove option to add to existing dashboard

* UI tweaks

* disable button if no queries

* Fix tests

* better accessible tests

* handle submission errors

* improve addToDashboard types

* use dashboardSrv' saveDashboard

* remove leftover test helper

* fix typo

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>

* Apply suggestions from code review

Co-authored-by: Kristina <kristina.durivage@grafana.com>

Co-authored-by: Piotr Jamróz <pm.jamroz@gmail.com>
Co-authored-by: Kristina <kristina.durivage@grafana.com>
2022-03-03 08:54:06 +00:00

263 lines
8.8 KiB
TypeScript

import React from 'react';
import { act, render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import { ExploreId, ExplorePanelData, ExploreState } from 'app/types';
import { Provider } from 'react-redux';
import { configureStore } from 'app/store/configureStore';
import userEvent from '@testing-library/user-event';
import { DataQuery, MutableDataFrame } from '@grafana/data';
import { createEmptyQueryResponse } from '../state/utils';
import { locationService } from '@grafana/runtime';
import { DashboardSearchHit, DashboardSearchItemType } from 'app/features/search/types';
import * as api from './addToDashboard';
import * as dashboardApi from 'app/features/manage-dashboards/state/actions';
import { AddToDashboard } from '.';
const setup = (
children: JSX.Element,
queries: DataQuery[] = [],
queryResponse: ExplorePanelData = createEmptyQueryResponse()
) => {
const store = configureStore({ explore: { left: { queries, queryResponse } } as ExploreState });
return render(<Provider store={store}>{children}</Provider>);
};
const createFolder = (title: string, id: number): DashboardSearchHit => ({
title,
id,
isStarred: false,
type: DashboardSearchItemType.DashFolder,
items: [],
url: '',
uri: '',
tags: [],
});
const openModal = async () => {
userEvent.click(screen.getByRole('button', { name: /add to dashboard/i }));
expect(await screen.findByRole('dialog', { name: 'Add panel to dashboard' })).toBeInTheDocument();
};
describe('Add to Dashboard Button', () => {
const searchFoldersResponse = Promise.resolve([createFolder('Folder 1', 1), createFolder('Folder 2', 2)]);
const redirectURL = '/some/redirect/url';
let addToDashboardMock: jest.SpyInstance<
ReturnType<typeof api.addToDashboard>,
Parameters<typeof api.addToDashboard>
>;
const waitForSearchFolderResponse = async () => {
return act(async () => {
await searchFoldersResponse;
});
};
beforeEach(() => {
jest.spyOn(dashboardApi, 'searchFolders').mockReturnValue(searchFoldersResponse);
addToDashboardMock = jest.spyOn(api, 'addToDashboard').mockResolvedValue('/some/redirect/url');
});
afterEach(() => {
jest.restoreAllMocks();
});
it('Opens and closes the modal correctly', async () => {
setup(<AddToDashboard exploreId={ExploreId.left} />, [{ refId: 'A' }]);
await openModal();
userEvent.click(screen.getByRole('button', { name: /cancel/i }));
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
});
describe('navigation', () => {
it('Navigates to dashboard when clicking on "Save and go to dashboard"', async () => {
locationService.push = jest.fn();
setup(<AddToDashboard exploreId={ExploreId.left} />, [{ refId: 'A' }]);
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and go to dashboard/i }));
await waitForSearchFolderResponse();
expect(locationService.push).toHaveBeenCalledWith(redirectURL);
});
it('Does NOT navigate to dashboard when clicking on "Save and keep exploring"', async () => {
locationService.push = jest.fn();
setup(<AddToDashboard exploreId={ExploreId.left} />, [{ refId: 'A' }]);
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForSearchFolderResponse();
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
expect(locationService.push).not.toHaveBeenCalled();
});
});
it('All queries are correctly passed through', async () => {
const queries: DataQuery[] = [{ refId: 'A' }, { refId: 'B', hide: true }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries);
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
queries: queries,
})
);
});
it('Defaults to table if no response is available', async () => {
const queries: DataQuery[] = [{ refId: 'A' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, createEmptyQueryResponse());
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'table',
})
);
});
it('Defaults to table if no query is active', async () => {
const queries: DataQuery[] = [{ refId: 'A', hide: true }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries);
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'table',
})
);
});
it('Filters out hidden queries when selecting visualization', async () => {
const queries: DataQuery[] = [{ refId: 'A', hide: true }, { refId: 'B' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, {
...createEmptyQueryResponse(),
graphFrames: [new MutableDataFrame({ refId: 'B', fields: [] })],
logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })],
});
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
// Query A comes before B, but it's hidden. visualization will be picked according to frames generated by B
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
queries: queries,
visualization: 'timeseries',
})
);
});
it('Sets visualization to logs if there are log frames', async () => {
const queries: DataQuery[] = [{ refId: 'A' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, {
...createEmptyQueryResponse(),
logsFrames: [new MutableDataFrame({ refId: 'A', fields: [] })],
});
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
// Query A comes before B, but it's hidden. visualization will be picked according to frames generated by B
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'logs',
})
);
});
it('Sets visualization to timeseries if there are graph frames', async () => {
const queries: DataQuery[] = [{ refId: 'A' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, {
...createEmptyQueryResponse(),
graphFrames: [new MutableDataFrame({ refId: 'A', fields: [] })],
});
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'timeseries',
})
);
});
it('Sets visualization to nodeGraph if there are node graph frames', async () => {
const queries: DataQuery[] = [{ refId: 'A' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, {
...createEmptyQueryResponse(),
nodeGraphFrames: [new MutableDataFrame({ refId: 'A', fields: [] })],
});
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'nodeGraph',
})
);
});
// trace view is not supported in dashboards, defaulting to table
it('Sets visualization to table if there are trace frames', async () => {
const queries: DataQuery[] = [{ refId: 'A' }];
setup(<AddToDashboard exploreId={ExploreId.left} />, queries, {
...createEmptyQueryResponse(),
traceFrames: [new MutableDataFrame({ refId: 'A', fields: [] })],
});
await openModal();
userEvent.click(screen.getByRole('button', { name: /save and keep exploring/i }));
await waitForElementToBeRemoved(() => screen.queryByRole('dialog', { name: 'Add panel to dashboard' }));
expect(addToDashboardMock).toHaveBeenCalledWith(
expect.objectContaining({
visualization: 'table',
})
);
});
});