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