mirror of https://github.com/grafana/grafana
[v11.0.x] DashboardScene: Move add library panel view from grid item to drawer (#86409)
DashboardScene: Move add library panel view from grid item to drawer (#86257)
* DashboardScene: Add library panel in drawer
* DashboardScene: Move add libray panel view to a drawer instead of a grid item
* Update
* Update tests
(cherry picked from commit 5837def850
)
Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
pull/86480/head
parent
df2fee63a5
commit
6881eabedf
@ -0,0 +1,74 @@ |
||||
import { SceneGridLayout, SceneTimeRange } from '@grafana/scenes'; |
||||
import { LibraryPanel } from '@grafana/schema/dist/esm/index.gen'; |
||||
|
||||
import { activateFullSceneTree } from '../utils/test-utils'; |
||||
|
||||
import { AddLibraryPanelDrawer } from './AddLibraryPanelDrawer'; |
||||
import { DashboardGridItem } from './DashboardGridItem'; |
||||
import { DashboardScene } from './DashboardScene'; |
||||
import { LibraryVizPanel } from './LibraryVizPanel'; |
||||
|
||||
describe('AddLibraryPanelWidget', () => { |
||||
let dashboard: DashboardScene; |
||||
let addLibPanelDrawer: AddLibraryPanelDrawer; |
||||
|
||||
beforeEach(async () => { |
||||
const result = await buildTestScene(); |
||||
dashboard = result.dashboard; |
||||
addLibPanelDrawer = result.drawer; |
||||
}); |
||||
|
||||
it('should add library panel from menu', () => { |
||||
const panelInfo: LibraryPanel = { |
||||
uid: 'uid', |
||||
model: { |
||||
type: 'timeseries', |
||||
}, |
||||
name: 'name', |
||||
version: 1, |
||||
type: 'timeseries', |
||||
}; |
||||
|
||||
addLibPanelDrawer.onAddLibraryPanel(panelInfo); |
||||
|
||||
const layout = dashboard.state.body as SceneGridLayout; |
||||
const gridItem = layout.state.children[0] as DashboardGridItem; |
||||
|
||||
expect(layout.state.children.length).toBe(1); |
||||
expect(gridItem.state.body!).toBeInstanceOf(LibraryVizPanel); |
||||
expect((gridItem.state.body! as LibraryVizPanel).state.panelKey).toBe('panel-1'); |
||||
}); |
||||
|
||||
it('should throw error if adding lib panel in a layout that is not SceneGridLayout', () => { |
||||
dashboard.setState({ body: undefined }); |
||||
|
||||
expect(() => addLibPanelDrawer.onAddLibraryPanel({} as LibraryPanel)).toThrow( |
||||
'Trying to add a library panel in a layout that is not SceneGridLayout' |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
async function buildTestScene() { |
||||
const drawer = new AddLibraryPanelDrawer({}); |
||||
const dashboard = new DashboardScene({ |
||||
$timeRange: new SceneTimeRange({}), |
||||
title: 'hello', |
||||
uid: 'dash-1', |
||||
version: 4, |
||||
meta: { |
||||
canEdit: true, |
||||
}, |
||||
body: new SceneGridLayout({ |
||||
children: [], |
||||
}), |
||||
overlay: drawer, |
||||
}); |
||||
|
||||
activateFullSceneTree(dashboard); |
||||
|
||||
await new Promise((r) => setTimeout(r, 1)); |
||||
|
||||
dashboard.onEnterEditMode(); |
||||
|
||||
return { dashboard, drawer }; |
||||
} |
@ -0,0 +1,69 @@ |
||||
import React from 'react'; |
||||
|
||||
import { SceneComponentProps, SceneGridLayout, SceneObjectBase, SceneObjectState } from '@grafana/scenes'; |
||||
import { LibraryPanel } from '@grafana/schema'; |
||||
import { Drawer } from '@grafana/ui'; |
||||
import { t } from 'app/core/internationalization'; |
||||
import { |
||||
LibraryPanelsSearch, |
||||
LibraryPanelsSearchVariant, |
||||
} from 'app/features/library-panels/components/LibraryPanelsSearch/LibraryPanelsSearch'; |
||||
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph'; |
||||
import { NEW_PANEL_HEIGHT, NEW_PANEL_WIDTH, getDashboardSceneFor, getVizPanelKeyForPanelId } from '../utils/utils'; |
||||
|
||||
import { DashboardGridItem } from './DashboardGridItem'; |
||||
import { LibraryVizPanel } from './LibraryVizPanel'; |
||||
|
||||
export interface AddLibraryPanelDrawerState extends SceneObjectState {} |
||||
|
||||
export class AddLibraryPanelDrawer extends SceneObjectBase<AddLibraryPanelDrawerState> { |
||||
public onClose = () => { |
||||
getDashboardSceneFor(this).closeModal(); |
||||
}; |
||||
|
||||
public onAddLibraryPanel = (panelInfo: LibraryPanel) => { |
||||
const dashboard = getDashboardSceneFor(this); |
||||
const layout = dashboard.state.body; |
||||
|
||||
if (!(layout instanceof SceneGridLayout)) { |
||||
throw new Error('Trying to add a library panel in a layout that is not SceneGridLayout'); |
||||
} |
||||
|
||||
const panelId = dashboardSceneGraph.getNextPanelId(dashboard); |
||||
|
||||
const body = new LibraryVizPanel({ |
||||
title: 'Panel Title', |
||||
uid: panelInfo.uid, |
||||
name: panelInfo.name, |
||||
panelKey: getVizPanelKeyForPanelId(panelId), |
||||
}); |
||||
|
||||
const newGridItem = new DashboardGridItem({ |
||||
height: NEW_PANEL_HEIGHT, |
||||
width: NEW_PANEL_WIDTH, |
||||
x: 0, |
||||
y: 0, |
||||
body: body, |
||||
key: `grid-item-${panelId}`, |
||||
}); |
||||
|
||||
layout.setState({ children: [newGridItem, ...layout.state.children] }); |
||||
|
||||
this.onClose(); |
||||
}; |
||||
|
||||
static Component = ({ model }: SceneComponentProps<AddLibraryPanelDrawer>) => { |
||||
const title = t('library-panel.add-widget.title', 'Add panel from panel library'); |
||||
|
||||
return ( |
||||
<Drawer title={title} onClose={model.onClose}> |
||||
<LibraryPanelsSearch |
||||
onClick={model.onAddLibraryPanel} |
||||
variant={LibraryPanelsSearchVariant.Tight} |
||||
showPanelFilter |
||||
/> |
||||
</Drawer> |
||||
); |
||||
}; |
||||
} |
@ -1,253 +0,0 @@ |
||||
import { 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 { DashboardGridItem } from './DashboardGridItem'; |
||||
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 DashboardGridItem({ |
||||
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 DashboardGridItem; |
||||
|
||||
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 DashboardGridItem({ |
||||
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 DashboardGridItem; |
||||
|
||||
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 DashboardGridItem({ |
||||
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 DashboardGridItem; |
||||
const gridItemTwo = body.state.children[1] as DashboardGridItem; |
||||
|
||||
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 DashboardGridItem({ |
||||
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 DashboardGridItem; |
||||
|
||||
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 DashboardGridItem({ |
||||
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 }; |
||||
} |
@ -1,163 +0,0 @@ |
||||
import { css, cx, keyframes } from '@emotion/css'; |
||||
import React from 'react'; |
||||
import tinycolor from 'tinycolor2'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { SceneComponentProps, 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 { DashboardGridItem } from './DashboardGridItem'; |
||||
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 DashboardGridItem && 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 DashboardGridItem) { |
||||
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