DashboardScene: View panel scene (#78718)

* DashboardScene: View panel fixes

* Update

* Update

* Update

* works and added tests

* Update

* Update
pull/78902/head
Torkel Ödegaard 2 years ago committed by GitHub
parent 8e01932166
commit d33a624ba6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      package.json
  2. 16
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  3. 4
      public/app/features/dashboard-scene/scene/DashboardSceneRenderer.tsx
  4. 6
      public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.test.ts
  5. 13
      public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts
  6. 4
      public/app/features/dashboard-scene/scene/NavToolbarActions.tsx
  7. 71
      public/app/features/dashboard-scene/scene/ViewPanelScene.test.tsx
  8. 95
      public/app/features/dashboard-scene/scene/ViewPanelScene.tsx
  9. 2
      public/app/features/dashboard-scene/scene/keyboardShortcuts.ts
  10. 4
      yarn.lock

@ -255,7 +255,7 @@
"@grafana/lezer-traceql": "0.0.11", "@grafana/lezer-traceql": "0.0.11",
"@grafana/monaco-logql": "^0.0.7", "@grafana/monaco-logql": "^0.0.7",
"@grafana/runtime": "workspace:*", "@grafana/runtime": "workspace:*",
"@grafana/scenes": "1.24.6", "@grafana/scenes": "^1.24.6",
"@grafana/schema": "workspace:*", "@grafana/schema": "workspace:*",
"@grafana/ui": "workspace:*", "@grafana/ui": "workspace:*",
"@kusto/monaco-kusto": "^7.4.0", "@kusto/monaco-kusto": "^7.4.0",

@ -28,9 +28,10 @@ import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
import { DashboardEditView } from '../settings/utils'; import { DashboardEditView } from '../settings/utils';
import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper'; import { DashboardModelCompatibilityWrapper } from '../utils/DashboardModelCompatibilityWrapper';
import { getDashboardUrl } from '../utils/urlBuilders'; 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 { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
import { ViewPanelScene } from './ViewPanelScene';
import { setupKeyboardShortcuts } from './keyboardShortcuts'; import { setupKeyboardShortcuts } from './keyboardShortcuts';
export interface DashboardSceneState extends SceneObjectState { export interface DashboardSceneState extends SceneObjectState {
@ -58,8 +59,8 @@ export interface DashboardSceneState extends SceneObjectState {
meta: DashboardMeta; meta: DashboardMeta;
/** Panel to inspect */ /** Panel to inspect */
inspectPanelKey?: string; inspectPanelKey?: string;
/** Panel to view in full screen */ /** Panel to view in fullscreen */
viewPanelKey?: string; viewPanelScene?: ViewPanelScene;
/** Edit view */ /** Edit view */
editview?: DashboardEditView; editview?: DashboardEditView;
/** Scene object that handles the current drawer or modal */ /** 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) { public getPageNav(location: H.Location, navIndex: NavIndex) {
const { meta, viewPanelKey } = this.state; const { meta, viewPanelScene } = this.state;
let pageNav: NavModelItem = { let pageNav: NavModelItem = {
text: this.state.title, text: this.state.title,
@ -200,7 +201,7 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
} }
} }
if (viewPanelKey) { if (viewPanelScene) {
pageNav = { pageNav = {
text: 'View panel', text: 'View panel',
parentItem: pageNav, parentItem: pageNav,
@ -213,9 +214,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
/** /**
* Returns the body (layout) or the full view panel * Returns the body (layout) or the full view panel
*/ */
public getBodyToRender(viewPanelKey?: string): SceneObject { public getBodyToRender(): SceneObject {
const viewPanel = findVizPanelByKey(this, viewPanelKey); return this.state.viewPanelScene ?? this.state.body;
return viewPanel ?? this.state.body;
} }
private startTrackingChanges() { private startTrackingChanges() {

@ -13,12 +13,12 @@ import { DashboardScene } from './DashboardScene';
import { NavToolbarActions } from './NavToolbarActions'; import { NavToolbarActions } from './NavToolbarActions';
export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) { export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
const { controls, viewPanelKey, overlay, editview } = model.useState(); const { controls, overlay, editview } = model.useState();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
const location = useLocation(); const location = useLocation();
const navIndex = useSelector((state) => state.navIndex); const navIndex = useSelector((state) => state.navIndex);
const pageNav = model.getPageNav(location, navIndex); const pageNav = model.getPageNav(location, navIndex);
const bodyToRender = model.getBodyToRender(viewPanelKey); const bodyToRender = model.getBodyToRender();
const navModel = getNavModel(navIndex, 'dashboards/browse'); const navModel = getNavModel(navIndex, 'dashboards/browse');
if (editview) { if (editview) {

@ -22,7 +22,7 @@ describe('DashboardSceneUrlSync', () => {
it('Should set viewPanelKey when url has viewPanel', () => { it('Should set viewPanelKey when url has viewPanel', () => {
const scene = buildTestScene(); const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ viewPanel: '2' }); 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' }); scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1-clone-1' });
expect(scene.state.viewPanelKey).toBeUndefined(); expect(scene.state.viewPanelScene).toBeUndefined();
// Verify no error notice was shown // Verify no error notice was shown
expect(errorNotice).toBe(0); expect(errorNotice).toBe(0);
@ -56,7 +56,7 @@ describe('DashboardSceneUrlSync', () => {
// Verify it subscribes to DashboardRepeatsProcessedEvent // Verify it subscribes to DashboardRepeatsProcessedEvent
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene })); 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 { findVizPanelByKey } from '../utils/utils';
import { DashboardScene, DashboardSceneState } from './DashboardScene'; import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { ViewPanelScene } from './ViewPanelScene';
import { DashboardRepeatsProcessedEvent } from './types'; import { DashboardRepeatsProcessedEvent } from './types';
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
@ -25,13 +26,13 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
const state = this._scene.state; const state = this._scene.state;
return { return {
inspect: state.inspectPanelKey, inspect: state.inspectPanelKey,
viewPanel: state.viewPanelKey, viewPanel: state.viewPanelScene?.getUrlKey(),
editview: state.editview?.getUrlKey(), editview: state.editview?.getUrlKey(),
}; };
} }
updateFromUrl(values: SceneObjectUrlValues): void { updateFromUrl(values: SceneObjectUrlValues): void {
const { inspectPanelKey, viewPanelKey, meta, isEditing } = this._scene.state; const { inspectPanelKey, viewPanelScene, meta, isEditing } = this._scene.state;
const update: Partial<DashboardSceneState> = {}; const update: Partial<DashboardSceneState> = {};
if (typeof values.editview === 'string' && meta.canEdit) { if (typeof values.editview === 'string' && meta.canEdit) {
@ -78,9 +79,9 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
return; return;
} }
update.viewPanelKey = values.viewPanel; update.viewPanelScene = new ViewPanelScene({ panelRef: panel.getRef() });
} else if (viewPanelKey) { } else if (viewPanelScene) {
update.viewPanelKey = undefined; update.viewPanelScene = undefined;
} }
if (Object.keys(update).length > 0) { if (Object.keys(update).length > 0) {
@ -94,7 +95,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
const panel = findVizPanelByKey(this._scene, viewPanel); const panel = findVizPanelByKey(this._scene, viewPanel);
if (panel) { if (panel) {
this._eventSub?.unsubscribe(); 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 }) => { 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} />); const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
if (uid && !editview) { if (uid && !editview) {
@ -62,7 +62,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />); toolbarActions.push(<NavToolbarSeparator leftActionsSeparator key="separator" />);
if (viewPanelKey) { if (viewPanelScene) {
toolbarActions.push( toolbarActions.push(
<Button <Button
onClick={() => locationService.partial({ viewPanel: null })} 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 };
}

@ -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({ keybindings.addBinding({
key: 'v', key: 'v',
onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => { onTrigger: withFocusedPanel(scene, (vizPanel: VizPanel) => {
if (!scene.state.viewPanelKey) { if (!scene.state.viewPanelScene) {
locationService.push(getViewPanelUrl(vizPanel)); locationService.push(getViewPanelUrl(vizPanel));
} }
}), }),

@ -3302,7 +3302,7 @@ __metadata:
languageName: unknown languageName: unknown
linkType: soft linkType: soft
"@grafana/scenes@npm:1.24.6": "@grafana/scenes@npm:^1.24.6":
version: 1.24.6 version: 1.24.6
resolution: "@grafana/scenes@npm:1.24.6" resolution: "@grafana/scenes@npm:1.24.6"
dependencies: dependencies:
@ -17314,7 +17314,7 @@ __metadata:
"@grafana/lezer-traceql": "npm:0.0.11" "@grafana/lezer-traceql": "npm:0.0.11"
"@grafana/monaco-logql": "npm:^0.0.7" "@grafana/monaco-logql": "npm:^0.0.7"
"@grafana/runtime": "workspace:*" "@grafana/runtime": "workspace:*"
"@grafana/scenes": "npm:1.24.6" "@grafana/scenes": "npm:^1.24.6"
"@grafana/schema": "workspace:*" "@grafana/schema": "workspace:*"
"@grafana/tsconfig": "npm:^1.3.0-rc1" "@grafana/tsconfig": "npm:^1.3.0-rc1"
"@grafana/ui": "workspace:*" "@grafana/ui": "workspace:*"

Loading…
Cancel
Save