2025-03-17 20:26:59 +08:00
|
|
|
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
2023-10-09 22:40:46 +08:00
|
|
|
import { setPluginImportUtils } from '@grafana/runtime';
|
2024-09-03 22:12:55 +08:00
|
|
|
import { SceneGridLayout, SceneVariableSet, TestVariable, VizPanel } from '@grafana/scenes';
|
|
|
|
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
import { isInCloneChain } from '../../utils/clone';
|
2024-11-05 15:05:09 +08:00
|
|
|
import { activateFullSceneTree, buildPanelRepeaterScene } from '../../utils/test-utils';
|
|
|
|
import { DashboardScene } from '../DashboardScene';
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2024-05-15 18:29:46 +08:00
|
|
|
import { DashboardGridItem, DashboardGridItemState } from './DashboardGridItem';
|
2024-11-05 15:05:09 +08:00
|
|
|
import { DefaultGridLayoutManager } from './DefaultGridLayoutManager';
|
2024-09-03 22:12:55 +08:00
|
|
|
|
|
|
|
jest.mock('@grafana/runtime', () => ({
|
|
|
|
...jest.requireActual('@grafana/runtime'),
|
|
|
|
getPluginLinkExtensions: jest.fn().mockReturnValue({ extensions: [] }),
|
|
|
|
}));
|
2024-04-08 22:55:35 +08:00
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
setPluginImportUtils({
|
|
|
|
importPanelPlugin: (id: string) => Promise.resolve(getPanelPlugin({})),
|
|
|
|
getPanelPluginFromCache: (id: string) => undefined,
|
|
|
|
});
|
|
|
|
|
2024-08-30 20:50:09 +08:00
|
|
|
const mockGetQueryRunnerFor = jest.fn();
|
2024-11-05 15:05:09 +08:00
|
|
|
jest.mock('../../utils/utils', () => ({
|
|
|
|
...jest.requireActual('../../utils/utils'),
|
2024-08-30 20:50:09 +08:00
|
|
|
getQueryRunnerFor: jest.fn().mockImplementation(() => mockGetQueryRunnerFor()),
|
|
|
|
}));
|
|
|
|
|
2023-09-05 19:51:46 +08:00
|
|
|
describe('PanelRepeaterGridItem', () => {
|
|
|
|
it('Given scene with variable with 2 values', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(5);
|
|
|
|
|
|
|
|
const panel1 = repeater.state.repeatedPanels![0];
|
|
|
|
const panel2 = repeater.state.repeatedPanels![1];
|
|
|
|
|
|
|
|
// Panels should have scoped variables
|
|
|
|
expect(panel1.state.$variables?.state.variables[0].getValue()).toBe('1');
|
|
|
|
expect(panel1.state.$variables?.state.variables[0].getValueText?.()).toBe('A');
|
|
|
|
expect(panel2.state.$variables?.state.variables[0].getValue()).toBe('2');
|
2025-01-22 19:25:04 +08:00
|
|
|
|
2025-02-03 17:46:47 +08:00
|
|
|
expect(isInCloneChain(panel1.state.key!)).toBe(false);
|
|
|
|
expect(isInCloneChain(panel2.state.key!)).toBe(true);
|
2023-09-05 19:51:46 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Should wait for variable to load', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1 });
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(0);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(5);
|
|
|
|
});
|
|
|
|
|
2025-03-12 22:29:06 +08:00
|
|
|
it('Should pass isMulti/includeAll values if variable is multi variable and has them set', async () => {
|
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1 });
|
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(0);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(5);
|
|
|
|
|
|
|
|
// LocalValueVariableState is not exposed, so we build this type casting
|
|
|
|
const variableState = repeater.state.repeatedPanels![0].state.$variables?.state.variables[0].state as {
|
|
|
|
isMulti?: boolean;
|
|
|
|
includeAll?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
expect(variableState.isMulti).toBe(true);
|
|
|
|
expect(variableState.includeAll).toBe(true);
|
|
|
|
});
|
|
|
|
|
2024-07-31 22:08:29 +08:00
|
|
|
it('Should display a panel when there are no options', async () => {
|
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 1, numberOfOptions: 0 });
|
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(0);
|
|
|
|
|
2024-08-20 22:19:28 +08:00
|
|
|
await new Promise((r) => setTimeout(r, 100));
|
2024-07-31 22:08:29 +08:00
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(1);
|
|
|
|
});
|
|
|
|
|
2024-09-03 22:12:55 +08:00
|
|
|
it('Should redo the repeat when editing panel and then returning to dashboard', async () => {
|
|
|
|
const panel = new DashboardGridItem({
|
|
|
|
variableName: 'server',
|
|
|
|
repeatedPanels: [],
|
|
|
|
body: new VizPanel({
|
|
|
|
title: 'Panel $server',
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
const variable = new TestVariable({
|
|
|
|
name: 'server',
|
|
|
|
query: 'A.*',
|
|
|
|
value: ALL_VARIABLE_VALUE,
|
|
|
|
text: ALL_VARIABLE_TEXT,
|
|
|
|
isMulti: true,
|
|
|
|
includeAll: true,
|
|
|
|
delayMs: 0,
|
|
|
|
optionsToReturn: [
|
|
|
|
{ label: 'A', value: '1' },
|
|
|
|
{ label: 'B', value: '2' },
|
|
|
|
{ label: 'C', value: '3' },
|
|
|
|
{ label: 'D', value: '4' },
|
|
|
|
{ label: 'E', value: '5' },
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const scene = new DashboardScene({
|
|
|
|
$variables: new SceneVariableSet({
|
|
|
|
variables: [variable],
|
|
|
|
}),
|
2024-09-27 21:11:28 +08:00
|
|
|
body: new DefaultGridLayoutManager({
|
|
|
|
grid: new SceneGridLayout({ children: [panel] }),
|
2024-09-03 22:12:55 +08:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
const deactivate = activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(panel.state.repeatedPanels?.length).toBe(5);
|
|
|
|
|
|
|
|
const vizPanel = panel.state.body as VizPanel;
|
|
|
|
|
|
|
|
expect(vizPanel.state.title).toBe('Panel $server');
|
|
|
|
|
|
|
|
// mimic going to panel edit
|
|
|
|
deactivate();
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
vizPanel.setState({ title: 'Changed' });
|
2024-09-25 17:04:20 +08:00
|
|
|
|
|
|
|
panel.editingCompleted(true);
|
2024-09-03 22:12:55 +08:00
|
|
|
|
|
|
|
// mimic returning to dashboard
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(panel.state.repeatedPanels?.length).toBe(5);
|
|
|
|
expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Should only redo the repeat of an edited panel, not all panels in dashboard', async () => {
|
|
|
|
const panel = new DashboardGridItem({
|
|
|
|
variableName: 'server',
|
|
|
|
repeatedPanels: [],
|
|
|
|
body: new VizPanel({
|
|
|
|
title: 'Panel $server',
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
const panel2 = new DashboardGridItem({
|
|
|
|
variableName: 'server',
|
|
|
|
repeatedPanels: [],
|
|
|
|
body: new VizPanel({
|
|
|
|
title: 'Panel $server 2',
|
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
const variable = new TestVariable({
|
|
|
|
name: 'server',
|
|
|
|
query: 'A.*',
|
|
|
|
value: ALL_VARIABLE_VALUE,
|
|
|
|
text: ALL_VARIABLE_TEXT,
|
|
|
|
isMulti: true,
|
|
|
|
includeAll: true,
|
|
|
|
delayMs: 0,
|
|
|
|
optionsToReturn: [
|
|
|
|
{ label: 'A', value: '1' },
|
|
|
|
{ label: 'B', value: '2' },
|
|
|
|
{ label: 'C', value: '3' },
|
|
|
|
{ label: 'D', value: '4' },
|
|
|
|
{ label: 'E', value: '5' },
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
const scene = new DashboardScene({
|
|
|
|
$variables: new SceneVariableSet({
|
|
|
|
variables: [variable],
|
|
|
|
}),
|
2024-09-27 21:11:28 +08:00
|
|
|
body: new DefaultGridLayoutManager({
|
|
|
|
grid: new SceneGridLayout({
|
|
|
|
children: [panel, panel2],
|
|
|
|
}),
|
2024-09-03 22:12:55 +08:00
|
|
|
}),
|
|
|
|
});
|
|
|
|
|
|
|
|
const deactivate = activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(panel.state.repeatedPanels?.length).toBe(5);
|
|
|
|
|
|
|
|
const vizPanel = panel.state.body as VizPanel;
|
|
|
|
|
|
|
|
expect(vizPanel.state.title).toBe('Panel $server');
|
|
|
|
|
|
|
|
// mimic going to panel edit
|
|
|
|
deactivate();
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
vizPanel.setState({ title: 'Changed' });
|
2024-09-25 17:04:20 +08:00
|
|
|
|
|
|
|
panel.editingCompleted(true);
|
2024-09-03 22:12:55 +08:00
|
|
|
|
|
|
|
const performRepeatMock = jest.spyOn(panel, 'performRepeat');
|
2024-09-25 17:04:20 +08:00
|
|
|
|
2024-09-03 22:12:55 +08:00
|
|
|
// mimic returning to dashboard
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(performRepeatMock).toHaveBeenCalledTimes(1); // only for the edited panel
|
|
|
|
expect(panel.state.repeatedPanels?.length).toBe(5);
|
|
|
|
expect((panel.state.repeatedPanels![0] as VizPanel).state.title).toBe('Changed');
|
|
|
|
});
|
|
|
|
|
2024-07-31 22:08:29 +08:00
|
|
|
it('Should display a panel when there are variable errors', () => {
|
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({
|
|
|
|
variableQueryTime: 0,
|
|
|
|
numberOfOptions: 0,
|
|
|
|
throwError: 'Error',
|
|
|
|
});
|
|
|
|
|
|
|
|
// we expect console.error when variable encounters an error
|
|
|
|
const origError = console.error;
|
|
|
|
console.error = jest.fn();
|
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(1);
|
|
|
|
console.error = origError;
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Should display a panel when there are variable errors async query', async () => {
|
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({
|
|
|
|
variableQueryTime: 1,
|
|
|
|
numberOfOptions: 0,
|
|
|
|
throwError: 'Error',
|
|
|
|
});
|
|
|
|
|
|
|
|
// we expect console.error when variable encounters an error
|
|
|
|
const origError = console.error;
|
|
|
|
console.error = jest.fn();
|
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(1);
|
|
|
|
console.error = origError;
|
|
|
|
});
|
|
|
|
|
2024-07-16 23:19:51 +08:00
|
|
|
it('Should adjust container height to fit panels direction is horizontal', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, maxPerRow: 2, itemHeight: 10 });
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
const layoutForceRender = jest.fn();
|
2024-09-27 21:11:28 +08:00
|
|
|
const layout = scene.state.body as DefaultGridLayoutManager;
|
|
|
|
layout.state.grid.forceRender = layoutForceRender;
|
2023-10-09 22:40:46 +08:00
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
// panels require 3 rows so total height should be 30
|
|
|
|
expect(repeater.state.height).toBe(30);
|
2023-10-09 22:40:46 +08:00
|
|
|
// Should update layout state by force re-render
|
|
|
|
expect(layoutForceRender.mock.calls.length).toBe(1);
|
2023-09-05 19:51:46 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
it('Should adjust container height to fit panels when direction is vertical', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0, itemHeight: 10, repeatDirection: 'v' });
|
2023-09-05 19:51:46 +08:00
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
// In vertical direction height itemCount * itemHeight
|
|
|
|
expect(repeater.state.height).toBe(50);
|
|
|
|
});
|
|
|
|
|
2024-05-15 18:29:46 +08:00
|
|
|
it('Should skip repeat when variable values are the same ', async () => {
|
|
|
|
const { scene, repeater, variable } = buildPanelRepeaterScene({ variableQueryTime: 0, itemHeight: 10 });
|
|
|
|
const stateUpdates: DashboardGridItemState[] = [];
|
|
|
|
repeater.subscribeToState((state) => stateUpdates.push(state));
|
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
expect(stateUpdates.length).toBe(1);
|
|
|
|
repeater.variableDependency?.variableUpdateCompleted(variable, true);
|
|
|
|
expect(stateUpdates.length).toBe(1);
|
|
|
|
});
|
|
|
|
|
2023-09-05 19:51:46 +08:00
|
|
|
it('Should adjust itemHeight when container is resized, direction horizontal', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({
|
2023-09-05 19:51:46 +08:00
|
|
|
variableQueryTime: 0,
|
|
|
|
itemHeight: 10,
|
|
|
|
repeatDirection: 'h',
|
|
|
|
maxPerRow: 4,
|
|
|
|
});
|
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
// Sould be two rows (5 panels and maxPerRow 5)
|
|
|
|
expect(repeater.state.height).toBe(20);
|
|
|
|
|
|
|
|
// resize container
|
|
|
|
repeater.setState({ height: 10 });
|
|
|
|
// given 2 current rows, the itemHeight is halved
|
|
|
|
expect(repeater.state.itemHeight).toBe(5);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Should adjust itemHeight when container is resized, direction vertical', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({
|
2023-09-05 19:51:46 +08:00
|
|
|
variableQueryTime: 0,
|
|
|
|
itemHeight: 10,
|
|
|
|
repeatDirection: 'v',
|
|
|
|
});
|
|
|
|
|
2023-10-09 22:40:46 +08:00
|
|
|
activateFullSceneTree(scene);
|
2023-09-05 19:51:46 +08:00
|
|
|
|
|
|
|
// In vertical direction height itemCount * itemHeight
|
|
|
|
expect(repeater.state.height).toBe(50);
|
|
|
|
|
|
|
|
// resize container
|
|
|
|
repeater.setState({ height: 25 });
|
|
|
|
// given 5 rows with total height 25 gives new itemHeight of 5
|
|
|
|
expect(repeater.state.itemHeight).toBe(5);
|
|
|
|
});
|
2023-10-13 22:03:38 +08:00
|
|
|
|
2024-03-04 21:10:04 +08:00
|
|
|
it('Should update repeats when updating variable', async () => {
|
2023-10-23 17:46:35 +08:00
|
|
|
const { scene, repeater, variable } = buildPanelRepeaterScene({ variableQueryTime: 0 });
|
2023-10-13 22:03:38 +08:00
|
|
|
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
|
|
|
|
variable.changeValueTo(['1', '3'], ['A', 'C']);
|
|
|
|
|
|
|
|
expect(repeater.state.repeatedPanels?.length).toBe(2);
|
|
|
|
});
|
2024-03-04 21:10:04 +08:00
|
|
|
|
|
|
|
it('Should fall back to default variable if specified variable cannot be found', () => {
|
|
|
|
const { scene, repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
|
|
|
|
scene.setState({ $variables: undefined });
|
|
|
|
activateFullSceneTree(scene);
|
|
|
|
expect(repeater.state.repeatedPanels?.[0].state.$variables?.state.variables[0].state.name).toBe(
|
|
|
|
'_____default_sys_repeat_var_____'
|
|
|
|
);
|
|
|
|
});
|
2024-04-08 22:55:35 +08:00
|
|
|
|
|
|
|
it('Should return className when repeat variable is set', () => {
|
|
|
|
const { repeater } = buildPanelRepeaterScene({ variableQueryTime: 0 });
|
|
|
|
|
|
|
|
expect(repeater.getClassName()).toBe('panel-repeater-grid-item');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('Should not className variable is not set', () => {
|
|
|
|
const gridItem = new DashboardGridItem({
|
|
|
|
body: new VizPanel({ pluginId: 'text' }),
|
|
|
|
});
|
|
|
|
|
|
|
|
expect(gridItem.getClassName()).toBe('');
|
|
|
|
});
|
2023-09-05 19:51:46 +08:00
|
|
|
});
|