grafana/public/app/features/dashboard-scene/saving/SaveDashboardDrawer.test.tsx

308 lines
10 KiB
TypeScript

import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TestProvider } from 'test/helpers/TestProvider';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { sceneGraph, SceneRefreshPicker } from '@grafana/scenes';
import { AnnoKeyManagerKind, ManagerKind } from 'app/features/apiserver/types';
import { SaveDashboardResponseDTO } from 'app/types';
import { DashboardSceneState } from '../scene/DashboardScene';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import { transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { SaveDashboardDrawer } from './SaveDashboardDrawer';
jest.mock('app/features/manage-dashboards/services/ValidationSrv', () => ({
validationSrv: {
validateNewDashboardName: () => true,
},
}));
const saveDashboardMutationMock = jest.fn();
jest.mock('app/features/browse-dashboards/api/browseDashboardsAPI', () => ({
...jest.requireActual('app/features/browse-dashboards/api/browseDashboardsAPI'),
useSaveDashboardMutation: () => [saveDashboardMutationMock],
}));
describe('SaveDashboardDrawer', () => {
describe('Given an already saved dashboard', () => {
it('should render save drawer with only message textarea', async () => {
setup().openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.pages.SaveDashboardModal.saveTimerange)).not.toBeInTheDocument();
expect(screen.getByText('No changes to save')).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
});
it('When there are no changes', async () => {
setup().openAndRender();
expect(screen.getByText('No changes to save')).toBeInTheDocument();
});
it('When time range changed show save time range option', async () => {
const { dashboard, openAndRender } = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.pages.SaveDashboardModal.saveTimerange)).toBeInTheDocument();
});
it('Should update diff when including time range is', async () => {
const { dashboard, openAndRender } = setup();
sceneGraph.getTimeRange(dashboard).setState({ from: 'now-1h', to: 'now' });
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.pages.SaveDashboardModal.saveTimerange)).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
await userEvent.click(screen.getByTestId(selectors.pages.SaveDashboardModal.saveTimerange));
expect(await screen.findByRole('tab', { name: /Changes/ })).toBeInTheDocument();
});
it('When refresh changed show save refresh option', async () => {
const { dashboard, openAndRender } = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.queryByTestId(selectors.pages.SaveDashboardModal.saveRefresh)).toBeInTheDocument();
});
it('Should update diff when including time range is', async () => {
const { dashboard, openAndRender } = setup();
const refreshPicker = sceneGraph.findObject(dashboard, (obj) => obj instanceof SceneRefreshPicker);
if (refreshPicker instanceof SceneRefreshPicker) {
refreshPicker.setState({ refresh: '5s' });
}
openAndRender();
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
expect(screen.getByTestId(selectors.pages.SaveDashboardModal.saveRefresh)).toBeInTheDocument();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
await userEvent.click(screen.getByTestId(selectors.pages.SaveDashboardModal.saveRefresh));
expect(await screen.findByRole('tab', { name: /Changes/ })).toBeInTheDocument();
});
it('Can show changes', async () => {
const { dashboard, openAndRender } = setup();
dashboard.setState({ title: 'New title' });
openAndRender();
await userEvent.click(await screen.findByRole('tab', { name: /Changes/ }));
expect(await screen.findByText('Full JSON diff')).toBeInTheDocument();
});
it('Can save', async () => {
const { dashboard, openAndRender } = setup();
dashboard.setState({ title: 'New title' });
openAndRender();
mockSaveDashboard();
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[0][0];
expect(dataSent.dashboard.title).toEqual('New title');
expect(dashboard.state.version).toEqual(11);
expect(dashboard.state.uid).toEqual('my-uid-from-resp');
expect(dashboard.state.isDirty).toEqual(false);
});
it('Can handle save errors and overwrite', async () => {
const { dashboard, openAndRender } = setup();
dashboard.setState({ title: 'New title' });
openAndRender();
mockSaveDashboard({ saveError: 'version-mismatch' });
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
expect(await screen.findByText('Someone else has updated this dashboard')).toBeInTheDocument();
expect(await screen.findByText('Save and overwrite')).toBeInTheDocument();
// Now save and overwrite
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[1][0];
expect(dataSent.overwrite).toEqual(true);
});
});
describe('When a dashboard is managed by an external system', () => {
beforeEach(() => {
config.featureToggles.provisioning = true;
});
afterEach(() => {
config.featureToggles.provisioning = false;
});
it('It should show the changes tab if the resource can be edited', async () => {
const { dashboard, openAndRender } = setup({
meta: {
k8s: {
annotations: {
[AnnoKeyManagerKind]: ManagerKind.Repo,
},
},
},
});
// just changing the title here, in real case scenario changes are reflected through migrations
// eg. panel version - same for other manager tests below
dashboard.setState({ title: 'updated title' });
openAndRender();
expect(screen.queryByRole('tab', { name: /Changes/ })).toBeInTheDocument();
});
it('It should not show the changes tab if the resource cannot be edited; kubectl', async () => {
const { dashboard, openAndRender } = setup({
meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Kubectl } } },
});
dashboard.setState({ title: 'updated title' });
openAndRender();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
});
it('It should not show the changes tab if the resource cannot be edited; terraform', async () => {
const { dashboard, openAndRender } = setup({
meta: { k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Terraform } } },
});
dashboard.setState({ title: 'updated title' });
openAndRender();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
});
it('It should not show the changes tab if the resource cannot be edited; plugin', async () => {
const { dashboard, openAndRender } = setup({
meta: {
k8s: { annotations: { [AnnoKeyManagerKind]: ManagerKind.Plugin } },
},
});
dashboard.setState({ title: 'updated title' });
openAndRender();
expect(screen.queryByRole('tab', { name: /Changes/ })).not.toBeInTheDocument();
});
});
describe('Save as copy', () => {
it('Should show save as form', async () => {
const { openAndRender } = setup();
openAndRender(true);
expect(await screen.findByText('Save dashboard copy')).toBeInTheDocument();
mockSaveDashboard();
await userEvent.click(await screen.findByTestId(selectors.components.Drawer.DashboardSaveDrawer.saveButton));
const dataSent = saveDashboardMutationMock.mock.calls[0][0];
expect(dataSent.dashboard.uid).toEqual('');
});
});
});
interface MockBackendApiOptions {
saveError: 'version-mismatch' | 'name-exists' | 'plugin-dashboard';
}
function mockSaveDashboard(options: Partial<MockBackendApiOptions> = {}) {
saveDashboardMutationMock.mockClear();
if (options.saveError) {
saveDashboardMutationMock.mockResolvedValue({
error: { status: 412, data: { status: 'version-mismatch', message: 'sad face' } },
});
return;
}
saveDashboardMutationMock.mockResolvedValue({
data: {
id: 10,
uid: 'my-uid-from-resp',
slug: 'my-slug-from-resp',
status: 'success',
url: 'my-url',
version: 11,
...options,
} as SaveDashboardResponseDTO,
});
}
let cleanUp = () => {};
function setup(overrides?: Partial<DashboardSceneState>) {
const dashboard = transformSaveModelToScene({
dashboard: {
title: 'hello',
uid: 'my-uid',
schemaVersion: 30,
panels: [],
version: 10,
},
meta: {},
...overrides,
});
// Clear any data layers
dashboard.setState({ $data: undefined });
const initialSaveModel = transformSceneToSaveModel(dashboard);
dashboard.setInitialSaveModel(initialSaveModel);
cleanUp();
cleanUp = dashboard.activate();
dashboard.onEnterEditMode();
const openAndRender = (saveAsCopy?: boolean) => {
dashboard.openSaveDrawer({ saveAsCopy });
const drawer = dashboard.state.overlay as SaveDashboardDrawer;
render(
<TestProvider>
<drawer.Component model={drawer} />
</TestProvider>
);
return drawer;
};
// await act(() => Promise.resolve());
return { dashboard, openAndRender };
}