mirror of https://github.com/grafana/grafana
Dashboard: New EmbeddedDashboard runtime component (#78916)
* Embedding dashboards exploratino * Update * Update * Added e2e test * Update * initial state, and onStateChange, only explore panel menu action and other fixes and tests * fix e2e spec * Fix url * fixing testpull/81332/head
parent
9da3db1ddf
commit
e08700c1b5
@ -0,0 +1,24 @@ |
|||||||
|
import { selectors } from '@grafana/e2e-selectors'; |
||||||
|
|
||||||
|
import { e2e } from '../utils'; |
||||||
|
import { fromBaseUrl } from '../utils/support/url'; |
||||||
|
|
||||||
|
describe('Embedded dashboard', function () { |
||||||
|
beforeEach(() => { |
||||||
|
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD')); |
||||||
|
}); |
||||||
|
|
||||||
|
it('open test page', function () { |
||||||
|
cy.visit(fromBaseUrl('/dashboards/embedding-test')); |
||||||
|
|
||||||
|
// Verify pie charts are rendered
|
||||||
|
cy.get( |
||||||
|
`[data-viz-panel-key="panel-11"] [data-testid^="${selectors.components.Panels.Visualization.PieChart.svgSlice}"]` |
||||||
|
).should('have.length', 5); |
||||||
|
|
||||||
|
// Verify no url sync
|
||||||
|
e2e.components.TimePicker.openButton().click(); |
||||||
|
cy.get('label:contains("Last 1 hour")').click(); |
||||||
|
cy.url().should('eq', fromBaseUrl('/dashboards/embedding-test')); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,32 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
export interface EmbeddedDashboardProps { |
||||||
|
uid?: string; |
||||||
|
/** |
||||||
|
* Use this property to override initial time and variable state. |
||||||
|
* Example: ?from=now-5m&to=now&var-varname=value1 |
||||||
|
*/ |
||||||
|
initialState?: string; |
||||||
|
/** |
||||||
|
* Is called when ever the internal embedded dashboards url state changes. |
||||||
|
* Can be used to sync the internal url state (Which is not synced to URL) with the external context, or to |
||||||
|
* preserve some of the state when moving to other embedded dashboards. |
||||||
|
*/ |
||||||
|
onStateChange?: (state: string) => void; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Returns a React component that renders an embedded dashboard. |
||||||
|
* @alpha |
||||||
|
*/ |
||||||
|
export let EmbeddedDashboard: React.ComponentType<EmbeddedDashboardProps> = () => { |
||||||
|
throw new Error('EmbeddedDashboard requires runtime initialization'); |
||||||
|
}; |
||||||
|
|
||||||
|
/** |
||||||
|
* |
||||||
|
* @internal |
||||||
|
*/ |
||||||
|
export function setEmbeddedDashboard(component: React.ComponentType<EmbeddedDashboardProps>) { |
||||||
|
EmbeddedDashboard = component; |
||||||
|
} |
||||||
@ -0,0 +1,130 @@ |
|||||||
|
import { css } from '@emotion/css'; |
||||||
|
import React, { useEffect, useState } from 'react'; |
||||||
|
|
||||||
|
import { GrafanaTheme2, urlUtil } from '@grafana/data'; |
||||||
|
import { EmbeddedDashboardProps } from '@grafana/runtime'; |
||||||
|
import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes'; |
||||||
|
import { Spinner, Alert, useStyles2 } from '@grafana/ui'; |
||||||
|
|
||||||
|
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; |
||||||
|
import { DashboardScene } from '../scene/DashboardScene'; |
||||||
|
|
||||||
|
export function EmbeddedDashboard(props: EmbeddedDashboardProps) { |
||||||
|
const stateManager = getDashboardScenePageStateManager(); |
||||||
|
const { dashboard, loadError } = stateManager.useState(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
stateManager.loadDashboard({ uid: props.uid!, isEmbedded: true }); |
||||||
|
return () => { |
||||||
|
stateManager.clearState(); |
||||||
|
}; |
||||||
|
}, [stateManager, props.uid]); |
||||||
|
|
||||||
|
if (loadError) { |
||||||
|
return ( |
||||||
|
<Alert severity="error" title="Failed to load dashboard"> |
||||||
|
{loadError} |
||||||
|
</Alert> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
if (!dashboard) { |
||||||
|
return <Spinner />; |
||||||
|
} |
||||||
|
|
||||||
|
return <EmbeddedDashboardRenderer model={dashboard} {...props} />; |
||||||
|
} |
||||||
|
|
||||||
|
interface RendererProps extends EmbeddedDashboardProps { |
||||||
|
model: DashboardScene; |
||||||
|
} |
||||||
|
|
||||||
|
function EmbeddedDashboardRenderer({ model, initialState, onStateChange }: RendererProps) { |
||||||
|
const [isActive, setIsActive] = useState(false); |
||||||
|
const { controls, body } = model.useState(); |
||||||
|
const styles = useStyles2(getStyles); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
setIsActive(true); |
||||||
|
|
||||||
|
if (initialState) { |
||||||
|
const searchParms = new URLSearchParams(initialState); |
||||||
|
sceneUtils.syncStateFromSearchParams(model, searchParms); |
||||||
|
} |
||||||
|
|
||||||
|
return model.activate(); |
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [model]); |
||||||
|
|
||||||
|
useSubscribeToEmbeddedUrlState(onStateChange, model); |
||||||
|
|
||||||
|
if (!isActive) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<div className={styles.canvas}> |
||||||
|
{controls && ( |
||||||
|
<div className={styles.controls}> |
||||||
|
{controls.map((control) => ( |
||||||
|
<control.Component key={control.state.key} model={control} /> |
||||||
|
))} |
||||||
|
</div> |
||||||
|
)} |
||||||
|
<div className={styles.body}> |
||||||
|
<body.Component model={body} /> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function useSubscribeToEmbeddedUrlState(onStateChange: ((state: string) => void) | undefined, model: DashboardScene) { |
||||||
|
useEffect(() => { |
||||||
|
if (!onStateChange) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let lastState = ''; |
||||||
|
const sub = model.subscribeToEvent(SceneObjectStateChangedEvent, (evt) => { |
||||||
|
if (evt.payload.changedObject.urlSync) { |
||||||
|
const state = sceneUtils.getUrlState(model); |
||||||
|
const stateAsString = urlUtil.renderUrl('', state); |
||||||
|
|
||||||
|
if (lastState !== stateAsString) { |
||||||
|
lastState = stateAsString; |
||||||
|
onStateChange(stateAsString); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return () => sub.unsubscribe(); |
||||||
|
}, [model, onStateChange]); |
||||||
|
} |
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) { |
||||||
|
return { |
||||||
|
canvas: css({ |
||||||
|
label: 'canvas-content', |
||||||
|
display: 'flex', |
||||||
|
flexDirection: 'column', |
||||||
|
flexBasis: '100%', |
||||||
|
flexGrow: 1, |
||||||
|
}), |
||||||
|
body: css({ |
||||||
|
label: 'body', |
||||||
|
flexGrow: 1, |
||||||
|
display: 'flex', |
||||||
|
gap: '8px', |
||||||
|
marginBottom: theme.spacing(2), |
||||||
|
}), |
||||||
|
controls: css({ |
||||||
|
display: 'flex', |
||||||
|
flexWrap: 'wrap', |
||||||
|
alignItems: 'center', |
||||||
|
gap: theme.spacing(1), |
||||||
|
top: 0, |
||||||
|
zIndex: theme.zIndex.navbarFixed, |
||||||
|
padding: theme.spacing(0, 0, 2, 0), |
||||||
|
}), |
||||||
|
}; |
||||||
|
} |
||||||
@ -0,0 +1,12 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
import { EmbeddedDashboardProps } from '@grafana/runtime'; |
||||||
|
|
||||||
|
export function EmbeddedDashboardLazy(props: EmbeddedDashboardProps) { |
||||||
|
return <Component {...props} />; |
||||||
|
} |
||||||
|
|
||||||
|
const Component = React.lazy(async () => { |
||||||
|
const { EmbeddedDashboard } = await import(/* webpackChunkName: "EmbeddedDashboard" */ './EmbeddedDashboard'); |
||||||
|
return { default: EmbeddedDashboard }; |
||||||
|
}); |
||||||
@ -0,0 +1,22 @@ |
|||||||
|
import React, { useState } from 'react'; |
||||||
|
|
||||||
|
import { Box } from '@grafana/ui'; |
||||||
|
import { Page } from 'app/core/components/Page/Page'; |
||||||
|
|
||||||
|
import { EmbeddedDashboard } from './EmbeddedDashboard'; |
||||||
|
|
||||||
|
export function EmbeddedDashboardTestPage() { |
||||||
|
const [state, setState] = useState('?from=now-5m&to=now'); |
||||||
|
|
||||||
|
return ( |
||||||
|
<Page |
||||||
|
navId="dashboards/browse" |
||||||
|
pageNav={{ text: 'Embedding dashboard', subTitle: 'Showing dashboard: Panel Tests - Pie chart' }} |
||||||
|
> |
||||||
|
<Box paddingY={2}>Internal url state: {state}</Box> |
||||||
|
<EmbeddedDashboard uid="lVE-2YFMz" initialState={state} onStateChange={setState} /> |
||||||
|
</Page> |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
export default EmbeddedDashboardTestPage; |
||||||
Loading…
Reference in new issue