mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scenes: Add new vizualisation functionality (#83145)
* wip * tests + refactor ad panel func * PR mods
This commit is contained in:
@@ -8,6 +8,7 @@ import {
|
||||
SceneVariableSet,
|
||||
TestVariable,
|
||||
VizPanel,
|
||||
SceneGridRow,
|
||||
} from '@grafana/scenes';
|
||||
import { Dashboard } from '@grafana/schema';
|
||||
import appEvents from 'app/core/app_events';
|
||||
@@ -25,6 +26,14 @@ import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
|
||||
jest.mock('../settings/version-history/HistorySrv');
|
||||
jest.mock('../serialization/transformSaveModelToScene');
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: () => {
|
||||
return {
|
||||
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DashboardScene', () => {
|
||||
describe('DashboardSrv.getCurrent compatibility', () => {
|
||||
@@ -112,6 +121,43 @@ describe('DashboardScene', () => {
|
||||
scene.exitEditMode({ skipConfirm: true });
|
||||
expect(sceneGraph.getTimeRange(scene)!.state.timeZone).toBe(prevState);
|
||||
});
|
||||
|
||||
it('Should throw an error when adding a panel to a layout that is not SceneGridLayout', () => {
|
||||
const scene = buildTestScene({ body: undefined });
|
||||
|
||||
expect(() => {
|
||||
scene.addPanel(new VizPanel({ title: 'Panel Title', key: 'panel-4', pluginId: 'timeseries' }));
|
||||
}).toThrow('Trying to add a panel in a layout that is not SceneGridLayout');
|
||||
});
|
||||
|
||||
it('Should add a new panel to the dashboard', () => {
|
||||
const vizPanel = new VizPanel({
|
||||
title: 'Panel Title',
|
||||
key: 'panel-4',
|
||||
pluginId: 'timeseries',
|
||||
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
|
||||
});
|
||||
|
||||
scene.addPanel(vizPanel);
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridItem = body.state.children[0] as SceneGridItem;
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
expect(body.state.children.length).toBe(5);
|
||||
expect(gridItem.state.body!.state.key).toBe('panel-4');
|
||||
});
|
||||
|
||||
it('Should create and add a new panel to the dashboard', () => {
|
||||
scene.onCreateNewPanel();
|
||||
|
||||
const body = scene.state.body as SceneGridLayout;
|
||||
const gridItem = body.state.children[0] as SceneGridItem;
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
expect(body.state.children.length).toBe(5);
|
||||
expect(gridItem.state.body!.state.key).toBe('panel-4');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,6 +281,18 @@ function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||
pluginId: 'table',
|
||||
}),
|
||||
}),
|
||||
new SceneGridRow({
|
||||
key: 'gridrow-1',
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
body: new VizPanel({
|
||||
title: 'Panel C',
|
||||
key: 'panel-3',
|
||||
pluginId: 'table',
|
||||
}),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
new SceneGridItem({
|
||||
body: new VizPanel({
|
||||
title: 'Panel B',
|
||||
|
@@ -45,7 +45,15 @@ import { historySrv } from '../settings/version-history';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { djb2Hash } from '../utils/djb2Hash';
|
||||
import { getDashboardUrl } from '../utils/urlBuilders';
|
||||
import { forceRenderChildren, getClosestVizPanel, getPanelIdForVizPanel, isPanelClone } from '../utils/utils';
|
||||
import {
|
||||
NEW_PANEL_HEIGHT,
|
||||
NEW_PANEL_WIDTH,
|
||||
forceRenderChildren,
|
||||
getClosestVizPanel,
|
||||
getDefaultVizPanel,
|
||||
getPanelIdForVizPanel,
|
||||
isPanelClone,
|
||||
} from '../utils/utils';
|
||||
|
||||
import { DashboardControls } from './DashboardControls';
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
@@ -416,20 +424,29 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
|
||||
public addPanel(vizPanel: VizPanel): void {
|
||||
// TODO: need logic for adding a panel when other panels exist
|
||||
// This is the logic when dashboard is empty
|
||||
this.setState({
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
height: 10,
|
||||
width: 10,
|
||||
x: 0.2,
|
||||
y: 0,
|
||||
body: vizPanel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
if (!(this.state.body instanceof SceneGridLayout)) {
|
||||
throw new Error('Trying to add a panel in a layout that is not SceneGridLayout');
|
||||
}
|
||||
|
||||
const sceneGridLayout = this.state.body;
|
||||
|
||||
// move all gridItems below the new one
|
||||
for (const child of sceneGridLayout.state.children) {
|
||||
child.setState({
|
||||
y: NEW_PANEL_HEIGHT + (child.state.y ?? 0),
|
||||
});
|
||||
}
|
||||
|
||||
const newGridItem = new SceneGridItem({
|
||||
height: NEW_PANEL_HEIGHT,
|
||||
width: NEW_PANEL_WIDTH,
|
||||
x: 0,
|
||||
y: 0,
|
||||
body: vizPanel,
|
||||
});
|
||||
|
||||
sceneGridLayout.setState({
|
||||
children: [newGridItem, ...sceneGridLayout.state.children],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -523,6 +540,14 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
locationService.partial({ editview: 'settings' });
|
||||
};
|
||||
|
||||
public onCreateNewPanel(): number {
|
||||
const vizPanel = getDefaultVizPanel(this);
|
||||
|
||||
this.addPanel(vizPanel);
|
||||
|
||||
return getPanelIdForVizPanel(vizPanel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request
|
||||
*/
|
||||
|
@@ -15,6 +15,7 @@ describe('NavToolbarActions', () => {
|
||||
setup();
|
||||
|
||||
expect(screen.queryByText('Save dashboard')).not.toBeInTheDocument();
|
||||
expect(screen.queryByLabelText('Add visualization')).not.toBeInTheDocument();
|
||||
expect(await screen.findByText('Edit')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Share')).toBeInTheDocument();
|
||||
});
|
||||
@@ -26,6 +27,7 @@ describe('NavToolbarActions', () => {
|
||||
|
||||
expect(await screen.findByText('Save dashboard')).toBeInTheDocument();
|
||||
expect(await screen.findByText('Exit edit')).toBeInTheDocument();
|
||||
expect(await screen.findByLabelText('Add visualization')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Edit')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Share')).not.toBeInTheDocument();
|
||||
});
|
||||
|
@@ -34,7 +34,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
NavToolbarActions.displayName = 'NavToolbarActions';
|
||||
|
||||
/**
|
||||
* This part is split into a separate componet to help test this
|
||||
* This part is split into a separate component to help test this
|
||||
*/
|
||||
export function ToolbarActions({ dashboard }: Props) {
|
||||
const { isEditing, viewPanelScene, isDirty, uid, meta, editview, editPanel } = dashboard.useState();
|
||||
@@ -46,7 +46,24 @@ export function ToolbarActions({ dashboard }: Props) {
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: uid && !editview && Boolean(meta.canStar) && !isEditingPanel,
|
||||
condition: isEditing && !editview && !isViewingPanel && !isEditingPanel,
|
||||
render: () => (
|
||||
<ToolbarButton
|
||||
key="add-visualization"
|
||||
tooltip={'Add visualization'}
|
||||
icon="graph-bar"
|
||||
onClick={() => {
|
||||
const id = dashboard.onCreateNewPanel();
|
||||
DashboardInteractions.toolbarAddButtonClicked({ item: 'add_visualization' });
|
||||
locationService.partial({ editPanel: id });
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
toolbarActions.push({
|
||||
group: 'icon-actions',
|
||||
condition: uid && !editview && Boolean(meta.canStar) && !isEditingPanel && !isEditing,
|
||||
render: () => {
|
||||
let desc = meta.isStarred
|
||||
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
|
||||
|
@@ -4,6 +4,9 @@ import {
|
||||
MultiValueVariable,
|
||||
SceneDataTransformer,
|
||||
sceneGraph,
|
||||
SceneGridItem,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneObject,
|
||||
SceneQueryRunner,
|
||||
VizPanel,
|
||||
@@ -15,6 +18,9 @@ import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
|
||||
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
|
||||
|
||||
export const NEW_PANEL_HEIGHT = 8;
|
||||
export const NEW_PANEL_WIDTH = 12;
|
||||
|
||||
export function getVizPanelKeyForPanelId(panelId: number) {
|
||||
return `panel-${panelId}`;
|
||||
}
|
||||
@@ -194,10 +200,51 @@ export function isPanelClone(key: string) {
|
||||
return key.includes('clone');
|
||||
}
|
||||
|
||||
export function onCreateNewPanel(dashboard: DashboardScene): number {
|
||||
const vizPanel = new VizPanel({
|
||||
export function getNextPanelId(dashboard: DashboardScene) {
|
||||
let max = 0;
|
||||
const body = dashboard.state.body;
|
||||
|
||||
if (body instanceof SceneGridLayout) {
|
||||
for (const child of body.state.children) {
|
||||
if (child instanceof SceneGridItem) {
|
||||
const vizPanel = child.state.body;
|
||||
|
||||
if (vizPanel instanceof VizPanel) {
|
||||
const panelId = getPanelIdForVizPanel(vizPanel);
|
||||
|
||||
if (panelId > max) {
|
||||
max = panelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (child instanceof SceneGridRow) {
|
||||
for (const rowChild of child.state.children) {
|
||||
if (rowChild instanceof SceneGridItem) {
|
||||
const vizPanel = rowChild.state.body;
|
||||
|
||||
if (vizPanel instanceof VizPanel) {
|
||||
const panelId = getPanelIdForVizPanel(vizPanel);
|
||||
|
||||
if (panelId > max) {
|
||||
max = panelId;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return max + 1;
|
||||
}
|
||||
|
||||
export function getDefaultVizPanel(dashboard: DashboardScene): VizPanel {
|
||||
const panelId = getNextPanelId(dashboard);
|
||||
|
||||
return new VizPanel({
|
||||
title: 'Panel Title',
|
||||
key: 'panel-1', // the first panel should always be panel-1
|
||||
key: getVizPanelKeyForPanelId(panelId),
|
||||
pluginId: 'timeseries',
|
||||
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
|
||||
menu: new VizPanelMenu({
|
||||
@@ -211,8 +258,4 @@ export function onCreateNewPanel(dashboard: DashboardScene): number {
|
||||
transformations: [],
|
||||
}),
|
||||
});
|
||||
dashboard.addPanel(vizPanel);
|
||||
const id = getPanelIdForVizPanel(vizPanel);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ import { DashboardModel } from 'app/features/dashboard/state';
|
||||
import { onAddLibraryPanel, onCreateNewPanel, onImportDashboard } from 'app/features/dashboard/utils/dashboard';
|
||||
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
||||
import { DashboardInteractions } from 'app/features/dashboard-scene/utils/interactions';
|
||||
import { onCreateNewPanel as onCreateNewPanelScene } from 'app/features/dashboard-scene/utils/utils';
|
||||
import { useDispatch, useSelector } from 'app/types';
|
||||
|
||||
import { setInitialDatasource } from '../state/reducers';
|
||||
@@ -24,12 +23,11 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const dispatch = useDispatch();
|
||||
const initialDatasource = useSelector((state) => state.dashboard.initialDatasource);
|
||||
const isDashboardScene = dashboard instanceof DashboardScene;
|
||||
|
||||
const onAddVisualization = () => {
|
||||
let id;
|
||||
if (isDashboardScene) {
|
||||
id = onCreateNewPanelScene(dashboard);
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
id = dashboard.onCreateNewPanel();
|
||||
} else {
|
||||
id = onCreateNewPanel(dashboard, initialDatasource);
|
||||
dispatch(setInitialDatasource(undefined));
|
||||
@@ -114,7 +112,7 @@ const DashboardEmpty = ({ dashboard, canCreate }: Props) => {
|
||||
data-testid={selectors.pages.AddDashboard.itemButton('Add a panel from the panel library button')}
|
||||
onClick={() => {
|
||||
DashboardInteractions.emptyDashboardButtonClicked({ item: 'import_from_library' });
|
||||
if (isDashboardScene) {
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
// TODO: dashboard scene logic for adding a library panel
|
||||
} else {
|
||||
onAddLibraryPanel(dashboard);
|
||||
|
Reference in New Issue
Block a user