mirror of
https://github.com/grafana/grafana.git
synced 2025-02-10 23:55:47 -06:00
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>
This commit is contained in:
parent
c093a471e6
commit
0c4aa6d0d8
@ -338,6 +338,16 @@ export const DashNav = React.memo<Props>((props) => {
|
|||||||
|
|
||||||
buttons.push(renderTimeControls());
|
buttons.push(renderTimeControls());
|
||||||
buttons.push(tvButton);
|
buttons.push(tvButton);
|
||||||
|
|
||||||
|
if (config.featureToggles.scenes) {
|
||||||
|
buttons.push(
|
||||||
|
<ToolbarButton
|
||||||
|
tooltip={'View as Scene'}
|
||||||
|
icon="apps"
|
||||||
|
onClick={() => locationService.push(`/scenes/dashboard/${dashboard.uid}`)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
return buttons;
|
return buttons;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,7 @@ export class DashboardLoaderSrv {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
loadDashboard(type: UrlQueryValue, slug: any, uid: any) {
|
loadDashboard(type: UrlQueryValue, slug: any, uid: any): Promise<DashboardDTO> {
|
||||||
let promise;
|
let promise;
|
||||||
|
|
||||||
if (type === 'script') {
|
if (type === 'script') {
|
||||||
|
@ -1,27 +1,48 @@
|
|||||||
// Libraries
|
// Libraries
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
|
import { useAsync } from 'react-use';
|
||||||
|
|
||||||
import { Stack } from '@grafana/experimental';
|
import { Stack } from '@grafana/experimental';
|
||||||
import { Card } from '@grafana/ui';
|
import { Card } from '@grafana/ui';
|
||||||
import { Page } from 'app/core/components/Page/Page';
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
// Types
|
// Types
|
||||||
|
import { getGrafanaSearcher } from '../search/service';
|
||||||
|
|
||||||
import { getScenes } from './scenes';
|
import { getScenes } from './scenes';
|
||||||
|
|
||||||
export interface Props {}
|
export interface Props {}
|
||||||
|
|
||||||
export const SceneListPage: FC<Props> = ({}) => {
|
export const SceneListPage: FC<Props> = ({}) => {
|
||||||
const scenes = getScenes();
|
const scenes = getScenes();
|
||||||
|
const results = useAsync(() => {
|
||||||
|
return getGrafanaSearcher().starred({ starred: true });
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navId="scenes">
|
<Page navId="scenes" subTitle="Experimental new runtime and state model for dashboards">
|
||||||
<Page.Contents>
|
<Page.Contents>
|
||||||
<Stack direction="column">
|
<Stack direction="column" gap={1}>
|
||||||
{scenes.map((scene) => (
|
<h5>Test scenes</h5>
|
||||||
<Card href={`/scenes/${scene.state.title}`} key={scene.state.title}>
|
<Stack direction="column" gap={0}>
|
||||||
<Card.Heading>{scene.state.title}</Card.Heading>
|
{scenes.map((scene) => (
|
||||||
</Card>
|
<Card href={`/scenes/${scene.state.title}`} key={scene.state.title}>
|
||||||
))}
|
<Card.Heading>{scene.state.title}</Card.Heading>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
{results.value && (
|
||||||
|
<>
|
||||||
|
<h5>Starred dashboards</h5>
|
||||||
|
<Stack direction="column" gap={0}>
|
||||||
|
{results.value!.view.map((dash) => (
|
||||||
|
<Card href={`/scenes/dashboard/${dash.uid}`} key={dash.uid}>
|
||||||
|
<Card.Heading>{dash.name}</Card.Heading>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Page.Contents>
|
</Page.Contents>
|
||||||
</Page>
|
</Page>
|
||||||
|
44
public/app/features/scenes/dashboard/DashboardScene.tsx
Normal file
44
public/app/features/scenes/dashboard/DashboardScene.tsx
Normal file
@ -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>
|
||||||
|
);
|
||||||
|
}
|
32
public/app/features/scenes/dashboard/DashboardScenePage.tsx
Normal file
32
public/app/features/scenes/dashboard/DashboardScenePage.tsx
Normal file
@ -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;
|
181
public/app/features/scenes/dashboard/DashboardsLoader.ts
Normal file
181
public/app/features/scenes/dashboard/DashboardsLoader.ts
Normal file
@ -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;
|
||||||
|
}
|
@ -523,6 +523,12 @@ export function getDynamicDashboardRoutes(cfg = config): RouteDescriptor[] {
|
|||||||
path: '/scenes',
|
path: '/scenes',
|
||||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/SceneListPage')),
|
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/SceneListPage')),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/dashboard/:uid',
|
||||||
|
component: SafeDynamicImport(
|
||||||
|
() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/dashboard/DashboardScenePage')
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/scenes/:name',
|
path: '/scenes/:name',
|
||||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/ScenePage')),
|
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/ScenePage')),
|
||||||
|
Loading…
Reference in New Issue
Block a user