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/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 };
}

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

Loading…
Cancel
Save