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:
Torkel Ödegaard 2024-02-11 09:08:47 +01:00 committed by GitHub
parent 02c0f5929c
commit fe6d1460b0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 215 additions and 12 deletions

View File

@ -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 |

View File

@ -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');
});
});

View File

@ -146,6 +146,7 @@ export interface FeatureToggles {
annotationPermissionUpdate?: boolean;
extractFieldsNameDeduplication?: boolean;
dashboardSceneForViewers?: boolean;
dashboardSceneSolo?: boolean;
dashboardScene?: boolean;
panelFilterVariable?: boolean;
pdfTables?: boolean;

View File

@ -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",

View File

@ -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

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
127 annotationPermissionUpdate experimental @grafana/identity-access-team false false false
128 extractFieldsNameDeduplication experimental @grafana/dataviz-squad false false true
129 dashboardSceneForViewers experimental @grafana/dashboards-squad false false true
130 dashboardSceneSolo experimental @grafana/dashboards-squad false false true
131 dashboardScene experimental @grafana/dashboards-squad false false true
132 panelFilterVariable experimental @grafana/dashboards-squad false false true
133 pdfTables preview @grafana/sharing-squad false false false

View File

@ -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"

View File

@ -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
}
}
]
}

View File

@ -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) {

View File

@ -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>
);
}

View File

@ -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);
}
});
}

View File

@ -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(