DashboardScene: Fixes full page reload of fullscreen view of a repeated panel (#76326)

* Progress on view panel for repeats

* Good enough

* Update
This commit is contained in:
Torkel Ödegaard
2023-10-13 16:03:38 +02:00
committed by GitHub
parent 23fe8fcf5a
commit 0d55dad075
8 changed files with 165 additions and 41 deletions

View File

@@ -576,7 +576,7 @@ func TestDashAlertPermissionMigration(t *testing.T) {
name: "should ignore fixed roles even if they would affect access",
enterprise: true,
roles: map[accesscontrol.Role][]accesscontrol.Permission{
accesscontrol.Role{Name: "fixed:dashboards:writer"}: {
{Name: "fixed:dashboards:writer"}: {
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll},
@@ -602,7 +602,7 @@ func TestDashAlertPermissionMigration(t *testing.T) {
name: "should ignore custom roles even if they would affect access",
enterprise: true,
roles: map[accesscontrol.Role][]accesscontrol.Permission{
accesscontrol.Role{Name: "custom role"}: {
{Name: "custom role"}: {
{Action: dashboards.ActionDashboardsRead, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsWrite, Scope: dashboards.ScopeDashboardsAll},
{Action: dashboards.ActionDashboardsDelete, Scope: dashboards.ScopeDashboardsAll},

View File

@@ -4,26 +4,6 @@ import { sceneGraph, SceneGridItem, SceneGridLayout, SceneQueryRunner, VizPanel
import { DashboardScene } from './DashboardScene';
describe('DashboardScene', () => {
describe('Given a standard scene', () => {
it('Should set inspectPanelKey when url has inspect key', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '2' });
expect(scene.state.inspectPanelKey).toBe('2');
});
it('Should handle inspect key that is not found', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '12321' });
expect(scene.state.inspectPanelKey).toBe(undefined);
});
it('Should set viewPanelKey when url has viewPanel', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
expect(scene.state.viewPanelKey).toBe('2');
});
});
describe('Editing and discarding', () => {
describe('Given scene in edit mode', () => {
let scene: DashboardScene;

View File

@@ -0,0 +1,91 @@
import { AppEvents } from '@grafana/data';
import { SceneGridItem, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
import appEvents from 'app/core/app_events';
import { DashboardScene } from './DashboardScene';
import { DashboardRepeatsProcessedEvent } from './types';
describe('DashboardSceneUrlSync', () => {
describe('Given a standard scene', () => {
it('Should set inspectPanelKey when url has inspect key', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '2' });
expect(scene.state.inspectPanelKey).toBe('2');
});
it('Should handle inspect key that is not found', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ inspect: '12321' });
expect(scene.state.inspectPanelKey).toBe(undefined);
});
it('Should set viewPanelKey when url has viewPanel', () => {
const scene = buildTestScene();
scene.urlSync?.updateFromUrl({ viewPanel: '2' });
expect(scene.state.viewPanelKey).toBe('2');
});
});
describe('Given a viewPanelKey with clone that is not found', () => {
const scene = buildTestScene();
let errorNotice = 0;
appEvents.on(AppEvents.alertError, (evt) => errorNotice++);
scene.urlSync?.updateFromUrl({ viewPanel: 'panel-1-clone-1' });
expect(scene.state.viewPanelKey).toBeUndefined();
// Verify no error notice was shown
expect(errorNotice).toBe(0);
// fake adding clone panel
const layout = scene.state.body as SceneGridLayout;
layout.setState({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
body: new VizPanel({
title: 'Clone Panel A',
key: 'panel-1-clone-1',
pluginId: 'table',
}),
}),
],
});
// Verify it subscribes to DashboardRepeatsProcessedEvent
scene.publishEvent(new DashboardRepeatsProcessedEvent({ source: scene }));
expect(scene.state.viewPanelKey).toBe('panel-1-clone-1');
});
});
function buildTestScene() {
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
body: new SceneGridLayout({
children: [
new SceneGridItem({
key: 'griditem-1',
x: 0,
body: new VizPanel({
title: 'Panel A',
key: 'panel-1',
pluginId: 'table',
$data: new SceneQueryRunner({ key: 'data-query-runner', queries: [{ refId: 'A' }] }),
}),
}),
new SceneGridItem({
body: new VizPanel({
title: 'Panel B',
key: 'panel-2',
pluginId: 'table',
}),
}),
],
}),
});
return scene;
}

View File

@@ -1,3 +1,5 @@
import { Unsubscribable } from 'rxjs';
import { AppEvents } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
@@ -7,8 +9,11 @@ import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
import { findVizPanelByKey } from '../utils/utils';
import { DashboardScene, DashboardSceneState } from './DashboardScene';
import { DashboardRepeatsProcessedEvent } from './types';
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
private _eventSub?: Unsubscribable;
constructor(private _scene: DashboardScene) {}
getKeys(): string[] {
@@ -44,6 +49,12 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
if (typeof values.viewPanel === 'string') {
const panel = findVizPanelByKey(this._scene, values.viewPanel);
if (!panel) {
// // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet
if (values.viewPanel.indexOf('clone')) {
this._handleViewRepeatClone(values.viewPanel);
return;
}
appEvents.emit(AppEvents.alertError, ['Panel not found']);
locationService.partial({ viewPanel: null });
return;
@@ -58,4 +69,16 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
this._scene.setState(update);
}
}
private _handleViewRepeatClone(viewPanel: string) {
if (!this._eventSub) {
this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => {
const panel = findVizPanelByKey(this._scene, viewPanel);
if (panel) {
this._eventSub?.unsubscribe();
this._scene.setState({ viewPanelKey: viewPanel });
}
});
}
}
}

View File

@@ -108,6 +108,16 @@ describe('PanelRepeaterGridItem', () => {
// given 5 rows with total height 25 gives new itemHeight of 5
expect(repeater.state.itemHeight).toBe(5);
});
it('When updating variable should update repeats', async () => {
const { scene, repeater, variable } = buildScene({ variableQueryTime: 0 });
activateFullSceneTree(scene);
variable.changeValueTo(['1', '3'], ['A', 'C']);
expect(repeater.state.repeatedPanels?.length).toBe(2);
});
});
interface SceneOptions {
@@ -130,27 +140,27 @@ function buildScene(options: SceneOptions) {
}),
});
const variable = new TestVariable({
name: 'server',
query: 'A.*',
value: ALL_VARIABLE_VALUE,
text: ALL_VARIABLE_TEXT,
isMulti: true,
includeAll: true,
delayMs: options.variableQueryTime,
optionsToReturn: [
{ label: 'A', value: '1' },
{ label: 'B', value: '2' },
{ label: 'C', value: '3' },
{ label: 'D', value: '4' },
{ label: 'E', value: '5' },
],
});
const scene = new EmbeddedScene({
$timeRange: new SceneTimeRange({ from: 'now-6h', to: 'now' }),
$variables: new SceneVariableSet({
variables: [
new TestVariable({
name: 'server',
query: 'A.*',
value: ALL_VARIABLE_VALUE,
text: ALL_VARIABLE_TEXT,
isMulti: true,
includeAll: true,
delayMs: options.variableQueryTime,
optionsToReturn: [
{ label: 'A', value: '1' },
{ label: 'B', value: '2' },
{ label: 'C', value: '3' },
{ label: 'D', value: '4' },
{ label: 'E', value: '5' },
],
}),
],
variables: [variable],
}),
body: new SceneGridLayout({
children: [
@@ -161,5 +171,5 @@ function buildScene(options: SceneOptions) {
}),
});
return { scene, repeater };
return { scene, repeater, variable };
}

View File

@@ -20,6 +20,8 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants';
import { getMultiVariableValues } from '../utils/utils';
import { DashboardRepeatsProcessedEvent } from './types';
interface PanelRepeaterGridItemState extends SceneGridItemStateLike {
source: VizPanel;
repeatedPanels?: VizPanel[];
@@ -146,6 +148,9 @@ export class PanelRepeaterGridItem extends SceneObjectBase<PanelRepeaterGridItem
layout.forceRender();
}
}
// Used from dashboard url sync
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
}
private getMaxPerRow(): number {

View File

@@ -15,6 +15,8 @@ import {
import { getMultiVariableValues } from '../utils/utils';
import { DashboardRepeatsProcessedEvent } from './types';
interface RowRepeaterBehaviorState extends SceneObjectState {
variableName: string;
sources: SceneGridItemLike[];
@@ -120,6 +122,9 @@ export class RowRepeaterBehavior extends SceneObjectBase<RowRepeaterBehaviorStat
}
updateLayout(layout, rows, maxYOfRows, rowToRepeat);
// Used from dashboard url sync
this.publishEvent(new DashboardRepeatsProcessedEvent({ source: this }), true);
}
getRowClone(

View File

@@ -0,0 +1,10 @@
import { BusEventWithPayload } from '@grafana/data';
import { SceneObject } from '@grafana/scenes';
export interface DashboardRepeatsProcessedEventPayload {
source: SceneObject;
}
export class DashboardRepeatsProcessedEvent extends BusEventWithPayload<DashboardRepeatsProcessedEventPayload> {
public static type = 'dashboard-repeats-processed';
}