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