mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 17:43:35 -06:00
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) {
|
||||
|
63
public/app/features/dashboard-scene/solo/SoloPanelPage.tsx
Normal file
63
public/app/features/dashboard-scene/solo/SoloPanelPage.tsx
Normal 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>
|
||||
);
|
||||
}
|
84
public/app/features/dashboard-scene/solo/useSoloPanel.ts
Normal file
84
public/app/features/dashboard-scene/solo/useSoloPanel.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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
Block a user