Skip to content

Commit 0c4aa6d

Browse files
torkelodprokop
andauthored
DashboardScene: First step to loading the current dashboard model and rendering it as a scene (grafana#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>
1 parent c093a47 commit 0c4aa6d

File tree

7 files changed

+302
-8
lines changed

7 files changed

+302
-8
lines changed

public/app/features/dashboard/components/DashNav/DashNav.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,16 @@ export const DashNav = React.memo<Props>((props) => {
338338

339339
buttons.push(renderTimeControls());
340340
buttons.push(tvButton);
341+
342+
if (config.featureToggles.scenes) {
343+
buttons.push(
344+
<ToolbarButton
345+
tooltip={'View as Scene'}
346+
icon="apps"
347+
onClick={() => locationService.push(`/scenes/dashboard/${dashboard.uid}`)}
348+
/>
349+
);
350+
}
341351
return buttons;
342352
};
343353

public/app/features/dashboard/services/DashboardLoaderSrv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class DashboardLoaderSrv {
3232
};
3333
}
3434

35-
loadDashboard(type: UrlQueryValue, slug: any, uid: any) {
35+
loadDashboard(type: UrlQueryValue, slug: any, uid: any): Promise<DashboardDTO> {
3636
let promise;
3737

3838
if (type === 'script') {

public/app/features/scenes/SceneListPage.tsx

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,48 @@
11
// Libraries
22
import React, { FC } from 'react';
3+
import { useAsync } from 'react-use';
34

45
import { Stack } from '@grafana/experimental';
56
import { Card } from '@grafana/ui';
67
import { Page } from 'app/core/components/Page/Page';
78

89
// Types
10+
import { getGrafanaSearcher } from '../search/service';
11+
912
import { getScenes } from './scenes';
1013

1114
export interface Props {}
1215

1316
export const SceneListPage: FC<Props> = ({}) => {
1417
const scenes = getScenes();
18+
const results = useAsync(() => {
19+
return getGrafanaSearcher().starred({ starred: true });
20+
}, []);
1521

1622
return (
17-
<Page navId="scenes">
23+
<Page navId="scenes" subTitle="Experimental new runtime and state model for dashboards">
1824
<Page.Contents>
19-
<Stack direction="column">
20-
{scenes.map((scene) => (
21-
<Card href={`/scenes/${scene.state.title}`} key={scene.state.title}>
22-
<Card.Heading>{scene.state.title}</Card.Heading>
23-
</Card>
24-
))}
25+
<Stack direction="column" gap={1}>
26+
<h5>Test scenes</h5>
27+
<Stack direction="column" gap={0}>
28+
{scenes.map((scene) => (
29+
<Card href={`/scenes/${scene.state.title}`} key={scene.state.title}>
30+
<Card.Heading>{scene.state.title}</Card.Heading>
31+
</Card>
32+
))}
33+
</Stack>
34+
{results.value && (
35+
<>
36+
<h5>Starred dashboards</h5>
37+
<Stack direction="column" gap={0}>
38+
{results.value!.view.map((dash) => (
39+
<Card href={`/scenes/dashboard/${dash.uid}`} key={dash.uid}>
40+
<Card.Heading>{dash.name}</Card.Heading>
41+
</Card>
42+
))}
43+
</Stack>
44+
</>
45+
)}
2546
</Stack>
2647
</Page.Contents>
2748
</Page>
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
3+
import { PageLayoutType } from '@grafana/data';
4+
import { config, locationService } from '@grafana/runtime';
5+
import { PageToolbar, ToolbarButton } from '@grafana/ui';
6+
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
7+
import { Page } from 'app/core/components/Page/Page';
8+
9+
import { SceneObjectBase } from '../core/SceneObjectBase';
10+
import { SceneComponentProps, SceneLayout, SceneObject, SceneObjectStatePlain } from '../core/types';
11+
12+
interface DashboardSceneState extends SceneObjectStatePlain {
13+
title: string;
14+
uid: string;
15+
layout: SceneLayout;
16+
actions?: SceneObject[];
17+
}
18+
19+
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
20+
public static Component = DashboardSceneRenderer;
21+
}
22+
23+
function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
24+
const { title, layout, actions = [], uid } = model.useState();
25+
26+
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
27+
28+
toolbarActions.push(
29+
<ToolbarButton icon="apps" onClick={() => locationService.push(`/d/${uid}`)} tooltip="View as Dashboard" />
30+
);
31+
const pageToolbar = config.featureToggles.topnav ? (
32+
<AppChromeUpdate actions={toolbarActions} />
33+
) : (
34+
<PageToolbar title={title}>{toolbarActions}</PageToolbar>
35+
);
36+
37+
return (
38+
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
39+
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}>
40+
<layout.Component model={layout} />
41+
</div>
42+
</Page>
43+
);
44+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Libraries
2+
import React, { FC, useEffect } from 'react';
3+
4+
import { Page } from 'app/core/components/Page/Page';
5+
import PageLoader from 'app/core/components/PageLoader/PageLoader';
6+
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
7+
8+
import { getDashboardLoader } from './DashboardsLoader';
9+
10+
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
11+
12+
export const DashboardScenePage: FC<Props> = ({ match }) => {
13+
const loader = getDashboardLoader();
14+
const { dashboard, isLoading } = loader.useState();
15+
16+
useEffect(() => {
17+
loader.load(match.params.uid);
18+
}, [loader, match.params.uid]);
19+
20+
if (!dashboard) {
21+
return (
22+
<Page navId="dashboards/browse">
23+
{isLoading && <PageLoader />}
24+
{!isLoading && <h2>Dashboard not found</h2>}
25+
</Page>
26+
);
27+
}
28+
29+
return <dashboard.Component model={dashboard} />;
30+
};
31+
32+
export default DashboardScenePage;
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { getDefaultTimeRange } from '@grafana/data';
2+
import { StateManagerBase } from 'app/core/services/StateManagerBase';
3+
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
4+
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
5+
import { DashboardDTO } from 'app/types';
6+
7+
import { SceneTimePicker } from '../components/SceneTimePicker';
8+
import { VizPanel } from '../components/VizPanel';
9+
import { SceneGridLayout, SceneGridRow } from '../components/layout/SceneGridLayout';
10+
import { SceneTimeRange } from '../core/SceneTimeRange';
11+
import { SceneObject } from '../core/types';
12+
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
13+
14+
import { DashboardScene } from './DashboardScene';
15+
16+
export interface DashboardLoaderState {
17+
dashboard?: DashboardScene;
18+
isLoading?: boolean;
19+
loadError?: string;
20+
}
21+
22+
export class DashboardLoader extends StateManagerBase<DashboardLoaderState> {
23+
private cache: Record<string, DashboardScene> = {};
24+
25+
public async load(uid: string) {
26+
const fromCache = this.cache[uid];
27+
if (fromCache) {
28+
this.setState({ dashboard: fromCache });
29+
return;
30+
}
31+
32+
this.setState({ isLoading: true });
33+
34+
try {
35+
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
36+
37+
if (rsp.dashboard) {
38+
this.initDashboard(rsp);
39+
} else {
40+
throw new Error('No dashboard returned');
41+
}
42+
} catch (err) {
43+
this.setState({ isLoading: false, loadError: String(err) });
44+
}
45+
}
46+
47+
private initDashboard(rsp: DashboardDTO) {
48+
// Just to have migrations run
49+
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta);
50+
51+
const dashboard = new DashboardScene({
52+
title: oldModel.title,
53+
uid: oldModel.uid,
54+
layout: new SceneGridLayout({
55+
children: this.buildSceneObjectsFromDashboard(oldModel),
56+
}),
57+
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
58+
actions: [new SceneTimePicker({})],
59+
});
60+
61+
this.cache[rsp.dashboard.uid] = dashboard;
62+
this.setState({ dashboard, isLoading: false });
63+
}
64+
65+
private buildSceneObjectsFromDashboard(dashboard: DashboardModel) {
66+
// collects all panels and rows
67+
const panels: SceneObject[] = [];
68+
69+
// indicates expanded row that's currently processed
70+
let currentRow: PanelModel | null = null;
71+
// collects panels in the currently processed, expanded row
72+
let currentRowPanels: SceneObject[] = [];
73+
74+
for (const panel of dashboard.panels) {
75+
if (panel.type === 'row') {
76+
if (!currentRow) {
77+
if (Boolean(panel.collapsed)) {
78+
// collapsed rows contain their panels within the row model
79+
panels.push(
80+
new SceneGridRow({
81+
title: panel.title,
82+
isCollapsed: true,
83+
size: {
84+
y: panel.gridPos.y,
85+
},
86+
children: panel.panels
87+
? panel.panels.map(
88+
(p) =>
89+
new VizPanel({
90+
title: p.title,
91+
pluginId: p.type,
92+
size: {
93+
x: p.gridPos.x,
94+
y: p.gridPos.y,
95+
width: p.gridPos.w,
96+
height: p.gridPos.h,
97+
},
98+
options: p.options,
99+
fieldConfig: p.fieldConfig,
100+
$data: new SceneQueryRunner({
101+
queries: p.targets,
102+
}),
103+
})
104+
)
105+
: [],
106+
})
107+
);
108+
} else {
109+
// indicate new row to be processed
110+
currentRow = panel;
111+
}
112+
} else {
113+
// when a row has been processed, and we hit a next one for processing
114+
if (currentRow.id !== panel.id) {
115+
// commit previous row panels
116+
panels.push(
117+
new SceneGridRow({
118+
title: currentRow!.title,
119+
size: {
120+
y: currentRow.gridPos.y,
121+
},
122+
children: currentRowPanels,
123+
})
124+
);
125+
126+
currentRow = panel;
127+
currentRowPanels = [];
128+
}
129+
}
130+
} else {
131+
const panelObject = new VizPanel({
132+
title: panel.title,
133+
pluginId: panel.type,
134+
size: {
135+
x: panel.gridPos.x,
136+
y: panel.gridPos.y,
137+
width: panel.gridPos.w,
138+
height: panel.gridPos.h,
139+
},
140+
options: panel.options,
141+
fieldConfig: panel.fieldConfig,
142+
$data: new SceneQueryRunner({
143+
queries: panel.targets,
144+
}),
145+
});
146+
147+
// when processing an expanded row, collect its panels
148+
if (currentRow) {
149+
currentRowPanels.push(panelObject);
150+
} else {
151+
panels.push(panelObject);
152+
}
153+
}
154+
}
155+
156+
// commit a row if it's the last one
157+
if (currentRow) {
158+
panels.push(
159+
new SceneGridRow({
160+
title: currentRow!.title,
161+
size: {
162+
y: currentRow.gridPos.y,
163+
},
164+
children: currentRowPanels,
165+
})
166+
);
167+
}
168+
169+
return panels;
170+
}
171+
}
172+
173+
let loader: DashboardLoader | null = null;
174+
175+
export function getDashboardLoader(): DashboardLoader {
176+
if (!loader) {
177+
loader = new DashboardLoader({});
178+
}
179+
180+
return loader;
181+
}

public/app/routes/routes.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,12 @@ export function getDynamicDashboardRoutes(cfg = config): RouteDescriptor[] {
523523
path: '/scenes',
524524
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/SceneListPage')),
525525
},
526+
{
527+
path: '/scenes/dashboard/:uid',
528+
component: SafeDynamicImport(
529+
() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/dashboard/DashboardScenePage')
530+
),
531+
},
526532
{
527533
path: '/scenes/:name',
528534
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/ScenePage')),

0 commit comments

Comments
 (0)