Scenes: Add new vizualisation functionality (#83145)

* wip

* tests + refactor ad panel func

* PR mods
pull/83397/head
Victor Marin 1 year ago committed by GitHub
parent 445aeca04b
commit 09c8e7ccc0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 58
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  2. 51
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  3. 2
      public/app/features/dashboard-scene/scene/NavToolbarActions.test.tsx
  4. 21
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  5. 57
      public/app/features/dashboard-scene/utils/utils.ts
  6. 8
      public/app/features/dashboard/dashgrid/DashboardEmpty.tsx

@ -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,
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);

Loading…
Cancel
Save