mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
10
public/app/features/dashboard-scene/scene/types.ts
Normal file
10
public/app/features/dashboard-scene/scene/types.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user