mirror of https://github.com/grafana/grafana
DashboardScene: First step to loading the current dashboard model and rendering it as a scene (#57012)
* Initial dashboard loading start * loading dashboard works and shows something * loading dashboard works and shows something * Minor tweaks * Add starred dashboards to scene list page * Use new SceneGridLayout * Allow switching directly from dashboard to a scene * Migrate basic dashboard rows to scene based dashboard * Review nit Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>pull/58782/head
parent
c093a471e6
commit
0c4aa6d0d8
@ -0,0 +1,44 @@ |
||||
import React from 'react'; |
||||
|
||||
import { PageLayoutType } from '@grafana/data'; |
||||
import { config, locationService } from '@grafana/runtime'; |
||||
import { PageToolbar, ToolbarButton } from '@grafana/ui'; |
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate'; |
||||
import { Page } from 'app/core/components/Page/Page'; |
||||
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase'; |
||||
import { SceneComponentProps, SceneLayout, SceneObject, SceneObjectStatePlain } from '../core/types'; |
||||
|
||||
interface DashboardSceneState extends SceneObjectStatePlain { |
||||
title: string; |
||||
uid: string; |
||||
layout: SceneLayout; |
||||
actions?: SceneObject[]; |
||||
} |
||||
|
||||
export class DashboardScene extends SceneObjectBase<DashboardSceneState> { |
||||
public static Component = DashboardSceneRenderer; |
||||
} |
||||
|
||||
function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) { |
||||
const { title, layout, actions = [], uid } = model.useState(); |
||||
|
||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />); |
||||
|
||||
toolbarActions.push( |
||||
<ToolbarButton icon="apps" onClick={() => locationService.push(`/d/${uid}`)} tooltip="View as Dashboard" /> |
||||
); |
||||
const pageToolbar = config.featureToggles.topnav ? ( |
||||
<AppChromeUpdate actions={toolbarActions} /> |
||||
) : ( |
||||
<PageToolbar title={title}>{toolbarActions}</PageToolbar> |
||||
); |
||||
|
||||
return ( |
||||
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}> |
||||
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}> |
||||
<layout.Component model={layout} /> |
||||
</div> |
||||
</Page> |
||||
); |
||||
} |
||||
@ -0,0 +1,32 @@ |
||||
// Libraries
|
||||
import React, { FC, useEffect } from 'react'; |
||||
|
||||
import { Page } from 'app/core/components/Page/Page'; |
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader'; |
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; |
||||
|
||||
import { getDashboardLoader } from './DashboardsLoader'; |
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {} |
||||
|
||||
export const DashboardScenePage: FC<Props> = ({ match }) => { |
||||
const loader = getDashboardLoader(); |
||||
const { dashboard, isLoading } = loader.useState(); |
||||
|
||||
useEffect(() => { |
||||
loader.load(match.params.uid); |
||||
}, [loader, match.params.uid]); |
||||
|
||||
if (!dashboard) { |
||||
return ( |
||||
<Page navId="dashboards/browse"> |
||||
{isLoading && <PageLoader />} |
||||
{!isLoading && <h2>Dashboard not found</h2>} |
||||
</Page> |
||||
); |
||||
} |
||||
|
||||
return <dashboard.Component model={dashboard} />; |
||||
}; |
||||
|
||||
export default DashboardScenePage; |
||||
@ -0,0 +1,181 @@ |
||||
import { getDefaultTimeRange } from '@grafana/data'; |
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase'; |
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; |
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; |
||||
import { DashboardDTO } from 'app/types'; |
||||
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker'; |
||||
import { VizPanel } from '../components/VizPanel'; |
||||
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout'; |
||||
import { SceneTimeRange } from '../core/SceneTimeRange'; |
||||
import { SceneObject } from '../core/types'; |
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner'; |
||||
|
||||
import { DashboardScene } from './DashboardScene'; |
||||
|
||||
export interface DashboardLoaderState { |
||||
dashboard?: DashboardScene; |
||||
isLoading?: boolean; |
||||
loadError?: string; |
||||
} |
||||
|
||||
export class DashboardLoader extends StateManagerBase<DashboardLoaderState> { |
||||
private cache: Record<string, DashboardScene> = {}; |
||||
|
||||
public async load(uid: string) { |
||||
const fromCache = this.cache[uid]; |
||||
if (fromCache) { |
||||
this.setState({ dashboard: fromCache }); |
||||
return; |
||||
} |
||||
|
||||
this.setState({ isLoading: true }); |
||||
|
||||
try { |
||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid); |
||||
|
||||
if (rsp.dashboard) { |
||||
this.initDashboard(rsp); |
||||
} else { |
||||
throw new Error('No dashboard returned'); |
||||
} |
||||
} catch (err) { |
||||
this.setState({ isLoading: false, loadError: String(err) }); |
||||
} |
||||
} |
||||
|
||||
private initDashboard(rsp: DashboardDTO) { |
||||
// Just to have migrations run
|
||||
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta); |
||||
|
||||
const dashboard = new DashboardScene({ |
||||
title: oldModel.title, |
||||
uid: oldModel.uid, |
||||
layout: new SceneGridLayout({ |
||||
children: this.buildSceneObjectsFromDashboard(oldModel), |
||||
}), |
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()), |
||||
actions: [new SceneTimePicker({})], |
||||
}); |
||||
|
||||
this.cache[rsp.dashboard.uid] = dashboard; |
||||
this.setState({ dashboard, isLoading: false }); |
||||
} |
||||
|
||||
private buildSceneObjectsFromDashboard(dashboard: DashboardModel) { |
||||
// collects all panels and rows
|
||||
const panels: SceneObject[] = []; |
||||
|
||||
// indicates expanded row that's currently processed
|
||||
let currentRow: PanelModel | null = null; |
||||
// collects panels in the currently processed, expanded row
|
||||
let currentRowPanels: SceneObject[] = []; |
||||
|
||||
for (const panel of dashboard.panels) { |
||||
if (panel.type === 'row') { |
||||
if (!currentRow) { |
||||
if (Boolean(panel.collapsed)) { |
||||
// collapsed rows contain their panels within the row model
|
||||
panels.push( |
||||
new SceneGridRow({ |
||||
title: panel.title, |
||||
isCollapsed: true, |
||||
size: { |
||||
y: panel.gridPos.y, |
||||
}, |
||||
children: panel.panels |
||||
? panel.panels.map( |
||||
(p) => |
||||
new VizPanel({ |
||||
title: p.title, |
||||
pluginId: p.type, |
||||
size: { |
||||
x: p.gridPos.x, |
||||
y: p.gridPos.y, |
||||
width: p.gridPos.w, |
||||
height: p.gridPos.h, |
||||
}, |
||||
options: p.options, |
||||
fieldConfig: p.fieldConfig, |
||||
$data: new SceneQueryRunner({ |
||||
queries: p.targets, |
||||
}), |
||||
}) |
||||
) |
||||
: [], |
||||
}) |
||||
); |
||||
} else { |
||||
// indicate new row to be processed
|
||||
currentRow = panel; |
||||
} |
||||
} else { |
||||
// when a row has been processed, and we hit a next one for processing
|
||||
if (currentRow.id !== panel.id) { |
||||
// commit previous row panels
|
||||
panels.push( |
||||
new SceneGridRow({ |
||||
title: currentRow!.title, |
||||
size: { |
||||
y: currentRow.gridPos.y, |
||||
}, |
||||
children: currentRowPanels, |
||||
}) |
||||
); |
||||
|
||||
currentRow = panel; |
||||
currentRowPanels = []; |
||||
} |
||||
} |
||||
} else { |
||||
const panelObject = new VizPanel({ |
||||
title: panel.title, |
||||
pluginId: panel.type, |
||||
size: { |
||||
x: panel.gridPos.x, |
||||
y: panel.gridPos.y, |
||||
width: panel.gridPos.w, |
||||
height: panel.gridPos.h, |
||||
}, |
||||
options: panel.options, |
||||
fieldConfig: panel.fieldConfig, |
||||
$data: new SceneQueryRunner({ |
||||
queries: panel.targets, |
||||
}), |
||||
}); |
||||
|
||||
// when processing an expanded row, collect its panels
|
||||
if (currentRow) { |
||||
currentRowPanels.push(panelObject); |
||||
} else { |
||||
panels.push(panelObject); |
||||
} |
||||
} |
||||
} |
||||
|
||||
// commit a row if it's the last one
|
||||
if (currentRow) { |
||||
panels.push( |
||||
new SceneGridRow({ |
||||
title: currentRow!.title, |
||||
size: { |
||||
y: currentRow.gridPos.y, |
||||
}, |
||||
children: currentRowPanels, |
||||
}) |
||||
); |
||||
} |
||||
|
||||
return panels; |
||||
} |
||||
} |
||||
|
||||
let loader: DashboardLoader | null = null; |
||||
|
||||
export function getDashboardLoader(): DashboardLoader { |
||||
if (!loader) { |
||||
loader = new DashboardLoader({}); |
||||
} |
||||
|
||||
return loader; |
||||
} |
||||
Loading…
Reference in new issue