diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 2ada1fcc28d..0ced7a34b85 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -338,6 +338,16 @@ export const DashNav = React.memo((props) => { buttons.push(renderTimeControls()); buttons.push(tvButton); + + if (config.featureToggles.scenes) { + buttons.push( + locationService.push(`/scenes/dashboard/${dashboard.uid}`)} + /> + ); + } return buttons; }; diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index 986cec7d14d..5927eb975e1 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -32,7 +32,7 @@ export class DashboardLoaderSrv { }; } - loadDashboard(type: UrlQueryValue, slug: any, uid: any) { + loadDashboard(type: UrlQueryValue, slug: any, uid: any): Promise { let promise; if (type === 'script') { diff --git a/public/app/features/scenes/SceneListPage.tsx b/public/app/features/scenes/SceneListPage.tsx index 2cf1e6ad489..59f48c9ab7c 100644 --- a/public/app/features/scenes/SceneListPage.tsx +++ b/public/app/features/scenes/SceneListPage.tsx @@ -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 = ({}) => { const scenes = getScenes(); + const results = useAsync(() => { + return getGrafanaSearcher().starred({ starred: true }); + }, []); return ( - + - - {scenes.map((scene) => ( - - {scene.state.title} - - ))} + +
Test scenes
+ + {scenes.map((scene) => ( + + {scene.state.title} + + ))} + + {results.value && ( + <> +
Starred dashboards
+ + {results.value!.view.map((dash) => ( + + {dash.name} + + ))} + + + )}
diff --git a/public/app/features/scenes/dashboard/DashboardScene.tsx b/public/app/features/scenes/dashboard/DashboardScene.tsx new file mode 100644 index 00000000000..46106a06a85 --- /dev/null +++ b/public/app/features/scenes/dashboard/DashboardScene.tsx @@ -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 { + public static Component = DashboardSceneRenderer; +} + +function DashboardSceneRenderer({ model }: SceneComponentProps) { + const { title, layout, actions = [], uid } = model.useState(); + + const toolbarActions = (actions ?? []).map((action) => ); + + toolbarActions.push( + locationService.push(`/d/${uid}`)} tooltip="View as Dashboard" /> + ); + const pageToolbar = config.featureToggles.topnav ? ( + + ) : ( + {toolbarActions} + ); + + return ( + +
+ +
+
+ ); +} diff --git a/public/app/features/scenes/dashboard/DashboardScenePage.tsx b/public/app/features/scenes/dashboard/DashboardScenePage.tsx new file mode 100644 index 00000000000..b806e608d4f --- /dev/null +++ b/public/app/features/scenes/dashboard/DashboardScenePage.tsx @@ -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 = ({ match }) => { + const loader = getDashboardLoader(); + const { dashboard, isLoading } = loader.useState(); + + useEffect(() => { + loader.load(match.params.uid); + }, [loader, match.params.uid]); + + if (!dashboard) { + return ( + + {isLoading && } + {!isLoading &&

Dashboard not found

} +
+ ); + } + + return ; +}; + +export default DashboardScenePage; diff --git a/public/app/features/scenes/dashboard/DashboardsLoader.ts b/public/app/features/scenes/dashboard/DashboardsLoader.ts new file mode 100644 index 00000000000..7d56b5a78d5 --- /dev/null +++ b/public/app/features/scenes/dashboard/DashboardsLoader.ts @@ -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 { + private cache: Record = {}; + + 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; +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 938a38b7e0d..17b5d38a063 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -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')),