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(tvButton);
|
||||
|
||||
if (config.featureToggles.scenes) {
|
||||
buttons.push(
|
||||
<ToolbarButton
|
||||
tooltip={'View as Scene'}
|
||||
icon="apps"
|
||||
onClick={() => locationService.push(`/scenes/dashboard/${dashboard.uid}`)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
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;
|
||||
|
||||
if (type === 'script') {
|
||||
|
@ -1,27 +1,48 @@
|
||||
// Libraries
|
||||
import React, { FC } from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { Stack } from '@grafana/experimental';
|
||||
import { Card } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
// Types
|
||||
import { getGrafanaSearcher } from '../search/service';
|
||||
|
||||
import { getScenes } from './scenes';
|
||||
|
||||
export interface Props {}
|
||||
|
||||
export const SceneListPage: FC<Props> = ({}) => {
|
||||
const scenes = getScenes();
|
||||
const results = useAsync(() => {
|
||||
return getGrafanaSearcher().starred({ starred: true });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Page navId="scenes">
|
||||
<Page navId="scenes" subTitle="Experimental new runtime and state model for dashboards">
|
||||
<Page.Contents>
|
||||
<Stack direction="column">
|
||||
{scenes.map((scene) => (
|
||||
<Card href={`/scenes/${scene.state.title}`} key={scene.state.title}>
|
||||
<Card.Heading>{scene.state.title}</Card.Heading>
|
||||
</Card>
|
||||
))}
|
||||
<Stack direction="column" gap={1}>
|
||||
<h5>Test scenes</h5>
|
||||
<Stack direction="column" gap={0}>
|
||||
{scenes.map((scene) => (
|
||||
<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>
|
||||
</Page.Contents>
|
||||
</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',
|
||||
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',
|
||||
component: SafeDynamicImport(() => import(/* webpackChunkName: "scenes"*/ 'app/features/scenes/ScenePage')),
|
||||
|
Loading…
Reference in New Issue
Block a user