mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: View panel scene (#78718)
* DashboardScene: View panel fixes * Update * Update * Update * works and added tests * Update * Update
This commit is contained in:
@@ -255,7 +255,7 @@
|
||||
"@grafana/lezer-traceql": "0.0.11",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "1.24.6",
|
||||
"@grafana/scenes": "^1.24.6",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@kusto/monaco-kusto": "^7.4.0",
|
||||
|
||||
@@ -28,9 +28,10 @@ import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
||||
import { DashboardEditView } from '../settings/utils';
|
||||
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
|
||||
import { getDashboardUrl } from '../utils/urlBuilders';
|
||||
import { findVizPanelByKey, forceRenderChildren, getClosestVizPanel, getPanelIdForVizPanel } from '../utils/utils';
|
||||
import { forceRenderChildren, getClosestVizPanel, getPanelIdForVizPanel } from '../utils/utils';
|
||||
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { setupKeyboardShortcuts } from './keyboardShortcuts';
|
||||
|
||||
export interface DashboardSceneState extends SceneObjectState {
|
||||
@@ -58,8 +59,8 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||
meta: DashboardMeta;
|
||||
/** Panel to inspect */
|
||||
inspectPanelKey?: string;
|
||||
/** Panel to view in full screen */
|
||||
viewPanelKey?: string;
|
||||
/** Panel to view in fullscreen */
|
||||
viewPanelScene?: ViewPanelScene;
|
||||
/** Edit view */
|
||||
editview?: DashboardEditView;
|
||||
/** Scene object that handles the current drawer or modal */
|
||||
@@ -174,7 +175,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
};
|
||||
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
const { meta, viewPanelKey } = this.state;
|
||||
const { meta, viewPanelScene } = this.state;
|
||||
|
||||
let pageNav: NavModelItem = {
|
||||
text: this.state.title,
|
||||
@@ -200,7 +201,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
}
|
||||
|
||||
if (viewPanelKey) {
|
||||
if (viewPanelScene) {
|
||||
pageNav = {
|
||||
text: 'View panel',
|
||||
parentItem: pageNav,
|
||||
@@ -213,9 +214,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
/**
|
||||
* Returns the body (layout) or the full view panel
|
||||
*/
|
||||
public getBodyToRender(viewPanelKey?: string): SceneObject {
|
||||
const viewPanel = findVizPanelByKey(this, viewPanelKey);
|
||||
return viewPanel ?? this.state.body;
|
||||
public getBodyToRender(): SceneObject {
|
||||
return this.state.viewPanelScene ?? this.state.body;
|
||||
}
|
||||
|
||||
private startTrackingChanges() {
|
||||
|
||||
@@ -13,12 +13,12 @@ import { DashboardScene } from './DashboardScene';
|
||||
import { NavToolbarActions } from './NavToolbarActions';
|
||||
|
||||
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
|
||||
const { controls, viewPanelKey, overlay, editview } = model.useState();
|
||||
const { controls, overlay, editview } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const pageNav = model.getPageNav(location, navIndex);
|
||||
const bodyToRender = model.getBodyToRender(viewPanelKey);
|
||||
const bodyToRender = model.getBodyToRender();
|
||||
const navModel = getNavModel(navIndex, 'dashboards/browse');
|
||||
|
||||
if (editview) {
|
||||
|
||||
@@ -22,7 +22,7 @@ describe('DashboardSceneUrlSync', () => {
|
||||
it('Should set viewPanelKey when url has viewPanel', () => {
|
||||
const scene = buildTestScene();
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
|
||||
expect(scene.state.viewPanelKey).toBe('2');
|
||||
expect(scene.state.viewPanelScene!.getUrlKey()).toBe('panel-2');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,7 +34,7 @@ describe('DashboardSceneUrlSync', () => {
|
||||
|
||||
scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1-clone-1' });
|
||||
|
||||
expect(scene.state.viewPanelKey).toBeUndefined();
|
||||
expect(scene.state.viewPanelScene).toBeUndefined();
|
||||
// Verify no error notice was shown
|
||||
expect(errorNotice).toBe(0);
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('DashboardSceneUrlSync', () => {
|
||||
|
||||
// Verify it subscribes to DashboardRepeatsProcessedEvent
|
||||
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
|
||||
expect(scene.state.viewPanelKey).toBe('panel-1-clone-1');
|
||||
expect(scene.state.viewPanelScene?.getUrlKey()).toBe('panel-1-clone-1');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { createDashboardEditViewFor } from '../settings/utils';
|
||||
import { findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
import { DashboardRepeatsProcessedEvent } from './types';
|
||||
|
||||
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
@@ -25,13 +26,13 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
const state = this._scene.state;
|
||||
return {
|
||||
inspect: state.inspectPanelKey,
|
||||
viewPanel: state.viewPanelKey,
|
||||
viewPanel: state.viewPanelScene?.getUrlKey(),
|
||||
editview: state.editview?.getUrlKey(),
|
||||
};
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||
const { inspectPanelKey, viewPanelKey, meta, isEditing } = this._scene.state;
|
||||
const { inspectPanelKey, viewPanelScene, meta, isEditing } = this._scene.state;
|
||||
const update: Partial<DashboardSceneState> = {};
|
||||
|
||||
if (typeof values.editview === 'string' && meta.canEdit) {
|
||||
@@ -78,9 +79,9 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
update.viewPanelKey = values.viewPanel;
|
||||
} else if (viewPanelKey) {
|
||||
update.viewPanelKey = undefined;
|
||||
update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
|
||||
} else if (viewPanelScene) {
|
||||
update.viewPanelScene = undefined;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
@@ -94,7 +95,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
const panel = findVizPanelByKey(this._scene, viewPanel);
|
||||
if (panel) {
|
||||
this._eventSub?.unsubscribe();
|
||||
this._scene.setState({ viewPanelKey: viewPanel });
|
||||
this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
const { actions = [], isEditing, viewPanelKey, isDirty, uid, meta, editview } = dashboard.useState();
|
||||
const { actions = [], isEditing, viewPanelScene, isDirty, uid, meta, editview } = dashboard.useState();
|
||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||
|
||||
if (uid && !editview) {
|
||||
@@ -62,7 +62,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
|
||||
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />);
|
||||
|
||||
if (viewPanelKey) {
|
||||
if (viewPanelScene) {
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => locationService.partial({ viewPanel: null })}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
LocalValueVariable,
|
||||
SceneGridItem,
|
||||
SceneGridLayout,
|
||||
SceneGridRow,
|
||||
SceneVariableSet,
|
||||
VizPanel,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { ViewPanelScene } from './ViewPanelScene';
|
||||
|
||||
describe('ViewPanelScene', () => {
|
||||
it('Should build scene on activate', () => {
|
||||
const { viewPanelScene } = buildScene();
|
||||
viewPanelScene.activate();
|
||||
expect(viewPanelScene.state.body).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should look copy row variable scope', () => {
|
||||
const { viewPanelScene } = buildScene({ rowVariables: true, panelVariables: true });
|
||||
viewPanelScene.activate();
|
||||
|
||||
const variables = viewPanelScene.state.body?.state.$variables;
|
||||
expect(variables?.state.variables.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
interface SceneOptions {
|
||||
rowVariables?: boolean;
|
||||
panelVariables?: boolean;
|
||||
}
|
||||
|
||||
function buildScene(options?: SceneOptions) {
|
||||
// builds a scene how it looks like after row and panel repeats are processed
|
||||
const panel = new VizPanel({
|
||||
key: 'panel-22',
|
||||
$variables: options?.panelVariables
|
||||
? new SceneVariableSet({
|
||||
variables: [new LocalValueVariable({ value: 'panel-var-value' })],
|
||||
})
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const dashboard = new DashboardScene({
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridRow({
|
||||
x: 0,
|
||||
y: 10,
|
||||
width: 24,
|
||||
$variables: options?.rowVariables
|
||||
? new SceneVariableSet({
|
||||
variables: [new LocalValueVariable({ value: 'row-var-value' })],
|
||||
})
|
||||
: undefined,
|
||||
height: 1,
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
body: panel,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
|
||||
|
||||
return { viewPanelScene, dashboard };
|
||||
}
|
||||
95
public/app/features/dashboard-scene/scene/ViewPanelScene.tsx
Normal file
95
public/app/features/dashboard-scene/scene/ViewPanelScene.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
SceneComponentProps,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
VizPanel,
|
||||
sceneUtils,
|
||||
SceneVariables,
|
||||
SceneGridRow,
|
||||
sceneGraph,
|
||||
SceneVariableSet,
|
||||
SceneVariable,
|
||||
} from '@grafana/scenes';
|
||||
|
||||
interface ViewPanelSceneState extends SceneObjectState {
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
body?: VizPanel;
|
||||
}
|
||||
|
||||
export class ViewPanelScene extends SceneObjectBase<ViewPanelSceneState> {
|
||||
public constructor(state: ViewPanelSceneState) {
|
||||
super(state);
|
||||
|
||||
this.addActivationHandler(this._activationHandler.bind(this));
|
||||
}
|
||||
|
||||
public _activationHandler() {
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const panelState = sceneUtils.cloneSceneObjectState(panel.state, {
|
||||
key: panel.state.key + '-view',
|
||||
$variables: this.getScopedVariables(panel),
|
||||
});
|
||||
|
||||
const body = new VizPanel(panelState);
|
||||
|
||||
this.setState({ body });
|
||||
|
||||
return () => {
|
||||
// Make sure we preserve data state
|
||||
if (body.state.$data) {
|
||||
panel.setState({ $data: body.state.$data.clone() });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// In case the panel is inside a repeated row
|
||||
private getScopedVariables(panel: VizPanel): SceneVariables | undefined {
|
||||
const row = tryGetParentRow(panel);
|
||||
const variables: SceneVariable[] = [];
|
||||
|
||||
// Because we are rendering the panel outside it's potential row context we need to copy the row (scoped) varables
|
||||
if (row && row.state.$variables) {
|
||||
for (const variable of row.state.$variables.state.variables) {
|
||||
variables.push(variable.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// If we have local scoped panel variables we need to add the row variables to it
|
||||
if (panel.state.$variables) {
|
||||
for (const variable of panel.state.$variables.state.variables) {
|
||||
variables.push(variable.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if (variables.length > 0) {
|
||||
return new SceneVariableSet({ variables });
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public getUrlKey() {
|
||||
return this.state.panelRef.resolve().state.key;
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<ViewPanelScene>) => {
|
||||
const { body } = model.useState();
|
||||
|
||||
if (!body) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <body.Component model={body} />;
|
||||
};
|
||||
}
|
||||
|
||||
function tryGetParentRow(panel: VizPanel): SceneGridRow | undefined {
|
||||
try {
|
||||
return sceneGraph.getAncestor(panel, SceneGridRow);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ export function setupKeyboardShortcuts(scene: DashboardScene) {
|
||||
keybindings.addBinding({
|
||||
key: 'v',
|
||||
onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => {
|
||||
if (!scene.state.viewPanelKey) {
|
||||
if (!scene.state.viewPanelScene) {
|
||||
locationService.push(getViewPanelUrl(vizPanel));
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -3302,7 +3302,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes@npm:1.24.6":
|
||||
"@grafana/scenes@npm:^1.24.6":
|
||||
version: 1.24.6
|
||||
resolution: "@grafana/scenes@npm:1.24.6"
|
||||
dependencies:
|
||||
@@ -17314,7 +17314,7 @@ __metadata:
|
||||
"@grafana/lezer-traceql": "npm:0.0.11"
|
||||
"@grafana/monaco-logql": "npm:^0.0.7"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:1.24.6"
|
||||
"@grafana/scenes": "npm:^1.24.6"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/tsconfig": "npm:^1.3.0-rc1"
|
||||
"@grafana/ui": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user