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.                                                                                                                                                                                    | | ||||
| | `extractFieldsNameDeduplication`            | Make sure extracted field names are unique in the dataframe                                                                                                                                                                                                                       | | ||||
| | `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                                                                                                                                                                                                                            | | ||||
| | `ssoSettingsApi`                            | Enables the SSO settings API                                                                                                                                                                                                                                                      | | ||||
| | `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); | ||||
|   }); | ||||
| 
 | ||||
|   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; | ||||
|   extractFieldsNameDeduplication?: boolean; | ||||
|   dashboardSceneForViewers?: boolean; | ||||
|   dashboardSceneSolo?: boolean; | ||||
|   dashboardScene?: boolean; | ||||
|   panelFilterVariable?: boolean; | ||||
|   pdfTables?: boolean; | ||||
|  |  | |||
|  | @ -948,6 +948,13 @@ var ( | |||
| 			FrontendOnly: true, | ||||
| 			Owner:        grafanaDashboardsSquad, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:         "dashboardSceneSolo", | ||||
| 			Description:  "Enables rendering dashboards using scenes for solo panels", | ||||
| 			Stage:        FeatureStageExperimental, | ||||
| 			FrontendOnly: true, | ||||
| 			Owner:        grafanaDashboardsSquad, | ||||
| 		}, | ||||
| 		{ | ||||
| 			Name:         "dashboardScene", | ||||
| 			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 | ||||
| extractFieldsNameDeduplication,experimental,@grafana/dataviz-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 | ||||
| panelFilterVariable,experimental,@grafana/dashboards-squad,false,false,true | ||||
| pdfTables,preview,@grafana/sharing-squad,false,false,false | ||||
|  |  | |||
| 
 | 
|  | @ -519,6 +519,10 @@ const ( | |||
| 	// Enables dashboard rendering using Scenes for viewer roles
 | ||||
| 	FlagDashboardSceneForViewers = "dashboardSceneForViewers" | ||||
| 
 | ||||
| 	// FlagDashboardSceneSolo
 | ||||
| 	// Enables rendering dashboards using scenes for solo panels
 | ||||
| 	FlagDashboardSceneSolo = "dashboardSceneSolo" | ||||
| 
 | ||||
| 	// FlagDashboardScene
 | ||||
| 	// Enables dashboard rendering using scenes for all roles
 | ||||
| 	FlagDashboardScene = "dashboardScene" | ||||
|  |  | |||
|  | @ -2050,6 +2050,19 @@ | |||
|         "codeowner": "@grafana/dataviz-squad", | ||||
|         "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() { | ||||
|     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) { | ||||
|  |  | |||
|  | @ -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', | ||||
|       routeName: DashboardRoutes.Normal, | ||||
|       chromeless: true, | ||||
|       component: SafeDynamicImport( | ||||
|         () => import(/* webpackChunkName: "SoloPanelPage" */ '../features/dashboard/containers/SoloPanelPage') | ||||
|       component: SafeDynamicImport(() => | ||||
|         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
 | ||||
|  | @ -99,15 +101,6 @@ export function getAppRoutes(): RouteDescriptor[] { | |||
|         () => 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', | ||||
|       component: SafeDynamicImport( | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue