mirror of https://github.com/grafana/grafana.git
				
				
				
			DashboardScene: Adds solo page that uses dasboarde scene to render single panel (#77940)
* DashboardScene: Adds solo page that uses dasboarde scene to render single panel * Update * Panel and row repeats working * Update * added e2e tests * Refactor * Fixes * Fix e2e * fix * fix * fix
This commit is contained in:
		
							parent
							
								
									02c0f5929c
								
							
						
					
					
						commit
						fe6d1460b0
					
				|  | @ -159,6 +159,7 @@ Experimental features might be changed or removed without prior notice. | ||||||
| | `annotationPermissionUpdate`                | Separate annotation permissions from dashboard permissions to allow for more granular control.                                                                                                                                                                                    | | | `annotationPermissionUpdate`                | Separate annotation permissions from dashboard permissions to allow for more granular control.                                                                                                                                                                                    | | ||||||
| | `extractFieldsNameDeduplication`            | Make sure extracted field names are unique in the dataframe                                                                                                                                                                                                                       | | | `extractFieldsNameDeduplication`            | Make sure extracted field names are unique in the dataframe                                                                                                                                                                                                                       | | ||||||
| | `dashboardSceneForViewers`                  | Enables dashboard rendering using Scenes for viewer roles                                                                                                                                                                                                                         | | | `dashboardSceneForViewers`                  | Enables dashboard rendering using Scenes for viewer roles                                                                                                                                                                                                                         | | ||||||
|  | | `dashboardSceneSolo`                        | Enables rendering dashboards using scenes for solo panels                                                                                                                                                                                                                         | | ||||||
| | `dashboardScene`                            | Enables dashboard rendering using scenes for all roles                                                                                                                                                                                                                            | | | `dashboardScene`                            | Enables dashboard rendering using scenes for all roles                                                                                                                                                                                                                            | | ||||||
| | `ssoSettingsApi`                            | Enables the SSO settings API                                                                                                                                                                                                                                                      | | | `ssoSettingsApi`                            | Enables the SSO settings API                                                                                                                                                                                                                                                      | | ||||||
| | `logsInfiniteScrolling`                     | Enables infinite scrolling for the Logs panel in Explore and Dashboards                                                                                                                                                                                                           | | | `logsInfiniteScrolling`                     | Enables infinite scrolling for the Logs panel in Explore and Dashboards                                                                                                                                                                                                           | | ||||||
|  |  | ||||||
|  | @ -11,4 +11,34 @@ describe('Solo Route', () => { | ||||||
| 
 | 
 | ||||||
|     cy.get('canvas').should('have.length', 6); |     cy.get('canvas').should('have.length', 6); | ||||||
|   }); |   }); | ||||||
|  | 
 | ||||||
|  |   it('Can view solo panel in scenes', () => { | ||||||
|  |     // open Panel Tests - Graph NG
 | ||||||
|  |     e2e.pages.SoloPanel.visit( | ||||||
|  |       'TkZXxlNG3/panel-tests-graph-ng?orgId=1&from=1699954597665&to=1699956397665&panelId=54&__feature.dashboardSceneSolo=true' | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     e2e.components.Panels.Panel.title('Interpolation: Step before').should('exist'); | ||||||
|  |     cy.contains('uplot-main-div').should('not.exist'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Can view solo repeated panel in scenes', () => { | ||||||
|  |     // open Panel Tests - Graph NG
 | ||||||
|  |     e2e.pages.SoloPanel.visit( | ||||||
|  |       'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-1&__feature.dashboardSceneSolo=true' | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     e2e.components.Panels.Panel.title('server=B').should('exist'); | ||||||
|  |     cy.contains('uplot-main-div').should('not.exist'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Can view solo in repeaterd row and panel in scenes', () => { | ||||||
|  |     // open Panel Tests - Graph NG
 | ||||||
|  |     e2e.pages.SoloPanel.visit( | ||||||
|  |       'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-2-row-2-clone-2&__feature.dashboardSceneSolo=true' | ||||||
|  |     ); | ||||||
|  | 
 | ||||||
|  |     e2e.components.Panels.Panel.title('server = D, pod = Sod').should('exist'); | ||||||
|  |     cy.contains('uplot-main-div').should('not.exist'); | ||||||
|  |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|  | @ -146,6 +146,7 @@ export interface FeatureToggles { | ||||||
|   annotationPermissionUpdate?: boolean; |   annotationPermissionUpdate?: boolean; | ||||||
|   extractFieldsNameDeduplication?: boolean; |   extractFieldsNameDeduplication?: boolean; | ||||||
|   dashboardSceneForViewers?: boolean; |   dashboardSceneForViewers?: boolean; | ||||||
|  |   dashboardSceneSolo?: boolean; | ||||||
|   dashboardScene?: boolean; |   dashboardScene?: boolean; | ||||||
|   panelFilterVariable?: boolean; |   panelFilterVariable?: boolean; | ||||||
|   pdfTables?: boolean; |   pdfTables?: boolean; | ||||||
|  |  | ||||||
|  | @ -948,6 +948,13 @@ var ( | ||||||
| 			FrontendOnly: true, | 			FrontendOnly: true, | ||||||
| 			Owner:        grafanaDashboardsSquad, | 			Owner:        grafanaDashboardsSquad, | ||||||
| 		}, | 		}, | ||||||
|  | 		{ | ||||||
|  | 			Name:         "dashboardSceneSolo", | ||||||
|  | 			Description:  "Enables rendering dashboards using scenes for solo panels", | ||||||
|  | 			Stage:        FeatureStageExperimental, | ||||||
|  | 			FrontendOnly: true, | ||||||
|  | 			Owner:        grafanaDashboardsSquad, | ||||||
|  | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Name:         "dashboardScene", | 			Name:         "dashboardScene", | ||||||
| 			Description:  "Enables dashboard rendering using scenes for all roles", | 			Description:  "Enables dashboard rendering using scenes for all roles", | ||||||
|  |  | ||||||
|  | @ -127,6 +127,7 @@ alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false | ||||||
| annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false | annotationPermissionUpdate,experimental,@grafana/identity-access-team,false,false,false | ||||||
| extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true | extractFieldsNameDeduplication,experimental,@grafana/dataviz-squad,false,false,true | ||||||
| dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true | dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,true | ||||||
|  | dashboardSceneSolo,experimental,@grafana/dashboards-squad,false,false,true | ||||||
| dashboardScene,experimental,@grafana/dashboards-squad,false,false,true | dashboardScene,experimental,@grafana/dashboards-squad,false,false,true | ||||||
| panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true | panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true | ||||||
| pdfTables,preview,@grafana/sharing-squad,false,false,false | pdfTables,preview,@grafana/sharing-squad,false,false,false | ||||||
|  |  | ||||||
| 
 | 
|  | @ -519,6 +519,10 @@ const ( | ||||||
| 	// Enables dashboard rendering using Scenes for viewer roles
 | 	// Enables dashboard rendering using Scenes for viewer roles
 | ||||||
| 	FlagDashboardSceneForViewers = "dashboardSceneForViewers" | 	FlagDashboardSceneForViewers = "dashboardSceneForViewers" | ||||||
| 
 | 
 | ||||||
|  | 	// FlagDashboardSceneSolo
 | ||||||
|  | 	// Enables rendering dashboards using scenes for solo panels
 | ||||||
|  | 	FlagDashboardSceneSolo = "dashboardSceneSolo" | ||||||
|  | 
 | ||||||
| 	// FlagDashboardScene
 | 	// FlagDashboardScene
 | ||||||
| 	// Enables dashboard rendering using scenes for all roles
 | 	// Enables dashboard rendering using scenes for all roles
 | ||||||
| 	FlagDashboardScene = "dashboardScene" | 	FlagDashboardScene = "dashboardScene" | ||||||
|  |  | ||||||
|  | @ -2050,6 +2050,19 @@ | ||||||
|         "codeowner": "@grafana/dataviz-squad", |         "codeowner": "@grafana/dataviz-squad", | ||||||
|         "frontend": true |         "frontend": true | ||||||
|       } |       } | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "metadata": { | ||||||
|  |         "name": "dashboardSceneSolo", | ||||||
|  |         "resourceVersion": "1707577534071", | ||||||
|  |         "creationTimestamp": "2024-02-10T15:05:34Z" | ||||||
|  |       }, | ||||||
|  |       "spec": { | ||||||
|  |         "description": "Enables rendering dashboards using scenes for solo panels", | ||||||
|  |         "stage": "experimental", | ||||||
|  |         "codeowner": "@grafana/dashboards-squad", | ||||||
|  |         "frontend": true | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|  | @ -202,7 +202,13 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc | ||||||
| 
 | 
 | ||||||
|   public clearState() { |   public clearState() { | ||||||
|     getDashboardSrv().setCurrent(undefined); |     getDashboardSrv().setCurrent(undefined); | ||||||
|     this.setState({ dashboard: undefined, loadError: undefined, isLoading: false, panelEditor: undefined }); | 
 | ||||||
|  |     this.setState({ | ||||||
|  |       dashboard: undefined, | ||||||
|  |       loadError: undefined, | ||||||
|  |       isLoading: false, | ||||||
|  |       panelEditor: undefined, | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) { |   public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) { | ||||||
|  |  | ||||||
|  | @ -0,0 +1,63 @@ | ||||||
|  | // Libraries
 | ||||||
|  | import React, { useEffect } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { Alert, Spinner } from '@grafana/ui'; | ||||||
|  | import PageLoader from 'app/core/components/PageLoader/PageLoader'; | ||||||
|  | import { EntityNotFound } from 'app/core/components/PageNotFound/EntityNotFound'; | ||||||
|  | import { GrafanaRouteComponentProps } from 'app/core/navigation/types'; | ||||||
|  | import { DashboardPageRouteParams } from 'app/features/dashboard/containers/types'; | ||||||
|  | import { DashboardRoutes } from 'app/types'; | ||||||
|  | 
 | ||||||
|  | import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager'; | ||||||
|  | import { DashboardScene } from '../scene/DashboardScene'; | ||||||
|  | 
 | ||||||
|  | import { useSoloPanel } from './useSoloPanel'; | ||||||
|  | 
 | ||||||
|  | export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams, { panelId: string }> {} | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Used for iframe embedding and image rendering of single panels | ||||||
|  |  */ | ||||||
|  | export function SoloPanelPage({ match, queryParams }: Props) { | ||||||
|  |   const stateManager = getDashboardScenePageStateManager(); | ||||||
|  |   const { dashboard } = stateManager.useState(); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     stateManager.loadDashboard({ uid: match.params.uid!, route: DashboardRoutes.Embedded }); | ||||||
|  |     return () => stateManager.clearState(); | ||||||
|  |   }, [stateManager, match, queryParams]); | ||||||
|  | 
 | ||||||
|  |   if (!queryParams.panelId) { | ||||||
|  |     return <EntityNotFound entity="Panel" />; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!dashboard) { | ||||||
|  |     return <PageLoader />; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return <SoloPanelRenderer dashboard={dashboard} panelId={queryParams.panelId} />; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export default SoloPanelPage; | ||||||
|  | 
 | ||||||
|  | export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: DashboardScene; panelId: string }) { | ||||||
|  |   const [panel, error] = useSoloPanel(dashboard, panelId); | ||||||
|  | 
 | ||||||
|  |   if (error) { | ||||||
|  |     return <Alert title={error} />; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   if (!panel) { | ||||||
|  |     return ( | ||||||
|  |       <span> | ||||||
|  |         Loading <Spinner /> | ||||||
|  |       </span> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   return ( | ||||||
|  |     <div className="panel-solo"> | ||||||
|  |       <panel.Component model={panel} /> | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
|  | @ -0,0 +1,84 @@ | ||||||
|  | import { useState, useEffect } from 'react'; | ||||||
|  | 
 | ||||||
|  | import { VizPanel, SceneObject, SceneGridRow, getUrlSyncManager } from '@grafana/scenes'; | ||||||
|  | 
 | ||||||
|  | import { DashboardScene } from '../scene/DashboardScene'; | ||||||
|  | import { PanelRepeaterGridItem } from '../scene/PanelRepeaterGridItem'; | ||||||
|  | import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior'; | ||||||
|  | import { DashboardRepeatsProcessedEvent } from '../scene/types'; | ||||||
|  | import { findVizPanelByKey, isPanelClone } from '../utils/utils'; | ||||||
|  | 
 | ||||||
|  | export function useSoloPanel(dashboard: DashboardScene, panelId: string): [VizPanel | undefined, string | undefined] { | ||||||
|  |   const [panel, setPanel] = useState<VizPanel>(); | ||||||
|  |   const [error, setError] = useState<string | undefined>(); | ||||||
|  | 
 | ||||||
|  |   useEffect(() => { | ||||||
|  |     getUrlSyncManager().initSync(dashboard); | ||||||
|  | 
 | ||||||
|  |     const cleanUp = dashboard.activate(); | ||||||
|  | 
 | ||||||
|  |     const panel = findVizPanelByKey(dashboard, panelId); | ||||||
|  |     if (panel) { | ||||||
|  |       activateParents(panel); | ||||||
|  |       setPanel(panel); | ||||||
|  |     } else if (isPanelClone(panelId)) { | ||||||
|  |       findRepeatClone(dashboard, panelId).then((panel) => { | ||||||
|  |         if (panel) { | ||||||
|  |           setPanel(panel); | ||||||
|  |         } else { | ||||||
|  |           setError('Panel not found'); | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return cleanUp; | ||||||
|  |   }, [dashboard, panelId]); | ||||||
|  | 
 | ||||||
|  |   return [panel, error]; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function activateParents(panel: VizPanel) { | ||||||
|  |   let parent = panel.parent; | ||||||
|  | 
 | ||||||
|  |   while (parent && !parent.isActive) { | ||||||
|  |     parent.activate(); | ||||||
|  |     parent = parent.parent; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function findRepeatClone(dashboard: DashboardScene, panelId: string): Promise<VizPanel | undefined> { | ||||||
|  |   return new Promise((resolve) => { | ||||||
|  |     dashboard.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { | ||||||
|  |       const panel = findVizPanelByKey(dashboard, panelId); | ||||||
|  |       if (panel) { | ||||||
|  |         resolve(panel); | ||||||
|  |       } else { | ||||||
|  |         // If rows are repeated they could add new panel repeaters that needs to be activated
 | ||||||
|  |         activateAllRepeaters(dashboard.state.body); | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     activateAllRepeaters(dashboard.state.body); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function activateAllRepeaters(layout: SceneObject) { | ||||||
|  |   layout.forEachChild((child) => { | ||||||
|  |     if (child instanceof PanelRepeaterGridItem && !child.isActive) { | ||||||
|  |       child.activate(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     if (child instanceof SceneGridRow && child.state.$behaviors) { | ||||||
|  |       for (const behavior of child.state.$behaviors) { | ||||||
|  |         if (behavior instanceof RowRepeaterBehavior && !child.isActive) { | ||||||
|  |           child.activate(); | ||||||
|  |           break; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       // Activate any panel PanelRepeaterGridItem inside the row
 | ||||||
|  |       activateAllRepeaters(child); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  | @ -85,8 +85,10 @@ export function getAppRoutes(): RouteDescriptor[] { | ||||||
|       pageClass: 'dashboard-solo', |       pageClass: 'dashboard-solo', | ||||||
|       routeName: DashboardRoutes.Normal, |       routeName: DashboardRoutes.Normal, | ||||||
|       chromeless: true, |       chromeless: true, | ||||||
|       component: SafeDynamicImport( |       component: SafeDynamicImport(() => | ||||||
|         () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') |         config.featureToggles.dashboardSceneSolo | ||||||
|  |           ? import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard-scene/solo/SoloPanelPage') | ||||||
|  |           : import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') | ||||||
|       ), |       ), | ||||||
|     }, |     }, | ||||||
|     // This route handles embedding of snapshot/scripted dashboard panels
 |     // This route handles embedding of snapshot/scripted dashboard panels
 | ||||||
|  | @ -99,15 +101,6 @@ export function getAppRoutes(): RouteDescriptor[] { | ||||||
|         () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') |         () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') | ||||||
|       ), |       ), | ||||||
|     }, |     }, | ||||||
|     { |  | ||||||
|       path: '/d-solo/:uid', |  | ||||||
|       pageClass: 'dashboard-solo', |  | ||||||
|       routeName: DashboardRoutes.Normal, |  | ||||||
|       chromeless: true, |  | ||||||
|       component: SafeDynamicImport( |  | ||||||
|         () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') |  | ||||||
|       ), |  | ||||||
|     }, |  | ||||||
|     { |     { | ||||||
|       path: '/dashboard/import', |       path: '/dashboard/import', | ||||||
|       component: SafeDynamicImport( |       component: SafeDynamicImport( | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue