diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 6cda1d14fe9..afc76be7de0 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 1cfba1b70b7..c508ace3aef 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -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'); + }); }); diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b87e766734a..59acae823fa 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -146,6 +146,7 @@ export interface FeatureToggles { annotationPermissionUpdate?: boolean; extractFieldsNameDeduplication?: boolean; dashboardSceneForViewers?: boolean; + dashboardSceneSolo?: boolean; dashboardScene?: boolean; panelFilterVariable?: boolean; pdfTables?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 83b7c1d919f..cb84847fa25 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b6414f26aa7..5d693934c4e 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 4707f9d8afb..7456a13c927 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 08f37910cda..e80566d3192 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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 + } } ] } \ No newline at end of file diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index eea3b3816bb..6fc3934e65e 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -202,7 +202,13 @@ export class DashboardScenePageStateManager extends StateManagerBase {} + +/** + * 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 ; + } + + if (!dashboard) { + return ; + } + + return ; +} + +export default SoloPanelPage; + +export function SoloPanelRenderer({ dashboard, panelId }: { dashboard: DashboardScene; panelId: string }) { + const [panel, error] = useSoloPanel(dashboard, panelId); + + if (error) { + return ; + } + + if (!panel) { + return ( + + Loading + + ); + } + + return ( +
+ +
+ ); +} diff --git a/public/app/features/dashboard-scene/solo/useSoloPanel.ts b/public/app/features/dashboard-scene/solo/useSoloPanel.ts new file mode 100644 index 00000000000..9ca7d2139bd --- /dev/null +++ b/public/app/features/dashboard-scene/solo/useSoloPanel.ts @@ -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(); + const [error, setError] = useState(); + + 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 { + 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); + } + }); +} diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx index 20c5d5e8fc8..846e354fb43 100644 --- a/public/app/routes/routes.tsx +++ b/public/app/routes/routes.tsx @@ -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(