mirror of https://github.com/grafana/grafana
Scenes: Add 'Import from library' functionality (#83498)
* wip * tests + refactor ad panel func * Add row functionality * update row state only when there are children * Add new row + copy paste panels * Add library panel functionality * tests * PR mods * reafctor + tests * reafctor * fix test * refactor * fix bug on cancelling lib widget * dashboard now saves with lib panel widget * add lib panels widget works in rows as well * split add lib panel func to another PR * Add library panel functionality * refactor * take panelKey into account when getting next panel id in dashboard * fix testspull/83608/head
parent
393b12f49f
commit
04539ffccb
@ -0,0 +1,252 @@ |
|||||||
|
import { SceneGridItem, SceneGridLayout, SceneGridRow, SceneTimeRange } from '@grafana/scenes'; |
||||||
|
import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; |
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene'; |
||||||
|
import { activateFullSceneTree } from '../utils/test-utils'; |
||||||
|
|
||||||
|
import { AddLibraryPanelWidget } from './AddLibraryPanelWidget'; |
||||||
|
import { LibraryVizPanel } from './LibraryVizPanel'; |
||||||
|
|
||||||
|
describe('AddLibraryPanelWidget', () => { |
||||||
|
let dashboard: DashboardScene; |
||||||
|
let addLibPanelWidget: AddLibraryPanelWidget; |
||||||
|
const mockEvent = { |
||||||
|
preventDefault: jest.fn(), |
||||||
|
} as unknown as React.MouseEvent<HTMLButtonElement>; |
||||||
|
|
||||||
|
beforeEach(async () => { |
||||||
|
const result = await buildTestScene(); |
||||||
|
dashboard = result.dashboard; |
||||||
|
addLibPanelWidget = result.addLibPanelWidget; |
||||||
|
}); |
||||||
|
|
||||||
|
it('should return the dashboard', () => { |
||||||
|
expect(addLibPanelWidget.getDashboard()).toBe(dashboard); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should cancel adding a lib panel', () => { |
||||||
|
addLibPanelWidget.onCancelAddPanel(mockEvent); |
||||||
|
|
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(0); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should cancel lib panel at correct position', () => { |
||||||
|
const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); |
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
|
||||||
|
body.setState({ |
||||||
|
children: [ |
||||||
|
...body.state.children, |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-2', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: anotherLibPanelWidget, |
||||||
|
}), |
||||||
|
], |
||||||
|
}); |
||||||
|
dashboard.setState({ body }); |
||||||
|
|
||||||
|
anotherLibPanelWidget.onCancelAddPanel(mockEvent); |
||||||
|
|
||||||
|
const gridItem = body.state.children[0] as SceneGridItem; |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(1); |
||||||
|
expect(gridItem.state.body!.state.key).toBe(addLibPanelWidget.state.key); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should cancel lib panel inside a row child', () => { |
||||||
|
const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); |
||||||
|
dashboard.setState({ |
||||||
|
body: new SceneGridLayout({ |
||||||
|
children: [ |
||||||
|
new SceneGridRow({ |
||||||
|
key: 'panel-2', |
||||||
|
children: [ |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-2', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: anotherLibPanelWidget, |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
|
||||||
|
anotherLibPanelWidget.onCancelAddPanel(mockEvent); |
||||||
|
|
||||||
|
const gridRow = body.state.children[0] as SceneGridRow; |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(1); |
||||||
|
expect(gridRow.state.children.length).toBe(0); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add library panel from menu', () => { |
||||||
|
const panelInfo: LibraryPanel = { |
||||||
|
uid: 'uid', |
||||||
|
model: { |
||||||
|
type: 'timeseries', |
||||||
|
}, |
||||||
|
name: 'name', |
||||||
|
version: 1, |
||||||
|
type: 'timeseries', |
||||||
|
}; |
||||||
|
|
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
const gridItem = body.state.children[0] as SceneGridItem; |
||||||
|
|
||||||
|
expect(gridItem.state.body!).toBeInstanceOf(AddLibraryPanelWidget); |
||||||
|
|
||||||
|
addLibPanelWidget.onAddLibraryPanel(panelInfo); |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(1); |
||||||
|
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); |
||||||
|
expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(addLibPanelWidget.state.key); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add a lib panel at correct position', () => { |
||||||
|
const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); |
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
|
||||||
|
body.setState({ |
||||||
|
children: [ |
||||||
|
...body.state.children, |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-2', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: anotherLibPanelWidget, |
||||||
|
}), |
||||||
|
], |
||||||
|
}); |
||||||
|
dashboard.setState({ body }); |
||||||
|
|
||||||
|
const panelInfo: LibraryPanel = { |
||||||
|
uid: 'uid', |
||||||
|
model: { |
||||||
|
type: 'timeseries', |
||||||
|
}, |
||||||
|
name: 'name', |
||||||
|
version: 1, |
||||||
|
type: 'timeseries', |
||||||
|
}; |
||||||
|
|
||||||
|
anotherLibPanelWidget.onAddLibraryPanel(panelInfo); |
||||||
|
|
||||||
|
const gridItemOne = body.state.children[0] as SceneGridItem; |
||||||
|
const gridItemTwo = body.state.children[1] as SceneGridItem; |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(2); |
||||||
|
expect(gridItemOne.state.body!).toBeInstanceOf(AddLibraryPanelWidget); |
||||||
|
expect((gridItemTwo.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should add library panel from menu to a row child', () => { |
||||||
|
const anotherLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-2' }); |
||||||
|
dashboard.setState({ |
||||||
|
body: new SceneGridLayout({ |
||||||
|
children: [ |
||||||
|
new SceneGridRow({ |
||||||
|
key: 'panel-2', |
||||||
|
children: [ |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-2', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: anotherLibPanelWidget, |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
const panelInfo: LibraryPanel = { |
||||||
|
uid: 'uid', |
||||||
|
model: { |
||||||
|
type: 'timeseries', |
||||||
|
}, |
||||||
|
name: 'name', |
||||||
|
version: 1, |
||||||
|
type: 'timeseries', |
||||||
|
}; |
||||||
|
|
||||||
|
const body = dashboard.state.body as SceneGridLayout; |
||||||
|
|
||||||
|
anotherLibPanelWidget.onAddLibraryPanel(panelInfo); |
||||||
|
|
||||||
|
const gridRow = body.state.children[0] as SceneGridRow; |
||||||
|
const gridItem = gridRow.state.children[0] as SceneGridItem; |
||||||
|
|
||||||
|
expect(body.state.children.length).toBe(1); |
||||||
|
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); |
||||||
|
expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe(anotherLibPanelWidget.state.key); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { |
||||||
|
dashboard.setState({ |
||||||
|
body: undefined, |
||||||
|
}); |
||||||
|
|
||||||
|
expect(() => addLibPanelWidget.onAddLibraryPanel({} as LibraryPanel)).toThrow( |
||||||
|
'Trying to add a library panel in a layout that is not SceneGridLayout' |
||||||
|
); |
||||||
|
}); |
||||||
|
|
||||||
|
it('should throw error if removing the library panel widget in a layout that is not SceneGridLayout', () => { |
||||||
|
dashboard.setState({ |
||||||
|
body: undefined, |
||||||
|
}); |
||||||
|
|
||||||
|
expect(() => addLibPanelWidget.onCancelAddPanel(mockEvent)).toThrow( |
||||||
|
'Trying to remove the library panel widget in a layout that is not SceneGridLayout' |
||||||
|
); |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
async function buildTestScene() { |
||||||
|
const addLibPanelWidget = new AddLibraryPanelWidget({ key: 'panel-1' }); |
||||||
|
const dashboard = new DashboardScene({ |
||||||
|
$timeRange: new SceneTimeRange({}), |
||||||
|
title: 'hello', |
||||||
|
uid: 'dash-1', |
||||||
|
version: 4, |
||||||
|
meta: { |
||||||
|
canEdit: true, |
||||||
|
}, |
||||||
|
body: new SceneGridLayout({ |
||||||
|
children: [ |
||||||
|
new SceneGridItem({ |
||||||
|
key: 'griditem-1', |
||||||
|
x: 0, |
||||||
|
y: 0, |
||||||
|
width: 10, |
||||||
|
height: 12, |
||||||
|
body: addLibPanelWidget, |
||||||
|
}), |
||||||
|
], |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
activateFullSceneTree(dashboard); |
||||||
|
|
||||||
|
await new Promise((r) => setTimeout(r, 1)); |
||||||
|
|
||||||
|
dashboard.onEnterEditMode(); |
||||||
|
|
||||||
|
return { dashboard, addLibPanelWidget }; |
||||||
|
} |
||||||
@ -0,0 +1,169 @@ |
|||||||
|
import { css, cx, keyframes } from '@emotion/css'; |
||||||
|
import React from 'react'; |
||||||
|
import tinycolor from 'tinycolor2'; |
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data'; |
||||||
|
import { |
||||||
|
SceneComponentProps, |
||||||
|
SceneGridItem, |
||||||
|
SceneGridLayout, |
||||||
|
SceneGridRow, |
||||||
|
SceneObjectBase, |
||||||
|
SceneObjectState, |
||||||
|
} from '@grafana/scenes'; |
||||||
|
import { LibraryPanel } from '@grafana/schema'; |
||||||
|
import { IconButton, useStyles2 } from '@grafana/ui'; |
||||||
|
import { Trans } from 'app/core/internationalization'; |
||||||
|
import { |
||||||
|
LibraryPanelsSearch, |
||||||
|
LibraryPanelsSearchVariant, |
||||||
|
} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; |
||||||
|
|
||||||
|
import { getDashboardSceneFor } from '../utils/utils'; |
||||||
|
|
||||||
|
import { DashboardScene } from './DashboardScene'; |
||||||
|
import { LibraryVizPanel } from './LibraryVizPanel'; |
||||||
|
|
||||||
|
export interface AddLibraryPanelWidgetState extends SceneObjectState { |
||||||
|
key: string; |
||||||
|
} |
||||||
|
|
||||||
|
export class AddLibraryPanelWidget extends SceneObjectBase<AddLibraryPanelWidgetState> { |
||||||
|
public constructor(state: AddLibraryPanelWidgetState) { |
||||||
|
super({ |
||||||
|
...state, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private get _dashboard(): DashboardScene { |
||||||
|
return getDashboardSceneFor(this); |
||||||
|
} |
||||||
|
|
||||||
|
public getDashboard(): DashboardScene { |
||||||
|
return this._dashboard; |
||||||
|
} |
||||||
|
|
||||||
|
public onCancelAddPanel = (evt: React.MouseEvent<HTMLButtonElement>) => { |
||||||
|
evt.preventDefault(); |
||||||
|
|
||||||
|
if (!(this._dashboard.state.body instanceof SceneGridLayout)) { |
||||||
|
throw new Error('Trying to remove the library panel widget in a layout that is not SceneGridLayout'); |
||||||
|
} |
||||||
|
|
||||||
|
const sceneGridLayout = this._dashboard.state.body; |
||||||
|
const children = []; |
||||||
|
|
||||||
|
for (const child of sceneGridLayout.state.children) { |
||||||
|
if (child.state.key !== this.parent?.state.key) { |
||||||
|
children.push(child); |
||||||
|
} |
||||||
|
|
||||||
|
if (child instanceof SceneGridRow) { |
||||||
|
const rowChildren = []; |
||||||
|
|
||||||
|
for (const rowChild of child.state.children) { |
||||||
|
if (rowChild instanceof SceneGridItem && rowChild.state.key !== this.parent?.state.key) { |
||||||
|
rowChildren.push(rowChild); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
child.setState({ children: rowChildren }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
sceneGridLayout.setState({ children }); |
||||||
|
}; |
||||||
|
|
||||||
|
public onAddLibraryPanel = (panelInfo: LibraryPanel) => { |
||||||
|
if (!(this._dashboard.state.body instanceof SceneGridLayout)) { |
||||||
|
throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); |
||||||
|
} |
||||||
|
|
||||||
|
const body = new LibraryVizPanel({ |
||||||
|
title: 'Panel Title', |
||||||
|
uid: panelInfo.uid, |
||||||
|
name: panelInfo.name, |
||||||
|
panelKey: this.state.key, |
||||||
|
}); |
||||||
|
|
||||||
|
if (this.parent instanceof SceneGridItem) { |
||||||
|
this.parent.setState({ body }); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
static Component = ({ model }: SceneComponentProps<AddLibraryPanelWidget>) => { |
||||||
|
const dashboard = model.getDashboard(); |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.wrapper}> |
||||||
|
<div className={cx('panel-container', styles.callToAction)}> |
||||||
|
<div className={cx(styles.headerRow, `grid-drag-handle-${dashboard.state.body.state.key}`)}> |
||||||
|
<span> |
||||||
|
<Trans i18nKey="library-panel.add-widget.title">Add panel from panel library</Trans> |
||||||
|
</span> |
||||||
|
<div className="flex-grow-1" /> |
||||||
|
<IconButton |
||||||
|
aria-label="Close 'Add Panel' widget" |
||||||
|
name="times" |
||||||
|
onClick={model.onCancelAddPanel} |
||||||
|
tooltip="Close widget" |
||||||
|
/> |
||||||
|
</div> |
||||||
|
<LibraryPanelsSearch |
||||||
|
onClick={model.onAddLibraryPanel} |
||||||
|
variant={LibraryPanelsSearchVariant.Tight} |
||||||
|
showPanelFilter |
||||||
|
/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => { |
||||||
|
const pulsate = keyframes({ |
||||||
|
'0%': { |
||||||
|
boxShadow: `0 0 0 2px ${theme.colors.background.canvas}, 0 0 0px 4px ${theme.colors.primary.main}`, |
||||||
|
}, |
||||||
|
'50%': { |
||||||
|
boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${tinycolor(theme.colors.primary.main) |
||||||
|
.darken(20) |
||||||
|
.toHexString()}`,
|
||||||
|
}, |
||||||
|
'100%': { |
||||||
|
boxShadow: `0 0 0 2px ${theme.components.dashboard.background}, 0 0 0px 4px ${theme.colors.primary.main}`, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
return { |
||||||
|
// wrapper is used to make sure box-shadow animation isn't cut off in dashboard page
|
||||||
|
wrapper: css({ |
||||||
|
height: '100%', |
||||||
|
paddingTop: `${theme.spacing(0.5)}`, |
||||||
|
}), |
||||||
|
headerRow: css({ |
||||||
|
display: 'flex', |
||||||
|
alignItems: 'center', |
||||||
|
height: '38px', |
||||||
|
flexShrink: 0, |
||||||
|
width: '100%', |
||||||
|
fontSize: theme.typography.fontSize, |
||||||
|
fontWeight: theme.typography.fontWeightMedium, |
||||||
|
paddingLeft: `${theme.spacing(1)}`, |
||||||
|
transition: 'background-color 0.1s ease-in-out', |
||||||
|
cursor: 'move', |
||||||
|
|
||||||
|
'&:hover': { |
||||||
|
background: `${theme.colors.background.secondary}`, |
||||||
|
}, |
||||||
|
}), |
||||||
|
callToAction: css({ |
||||||
|
overflow: 'hidden', |
||||||
|
outline: '2px dotted transparent', |
||||||
|
outlineOffset: '2px', |
||||||
|
boxShadow: '0 0 0 2px black, 0 0 0px 4px #1f60c4', |
||||||
|
animation: `${pulsate} 2s ease infinite`, |
||||||
|
}), |
||||||
|
}; |
||||||
|
}; |
||||||
Loading…
Reference in new issue