mirror of https://github.com/grafana/grafana
Scenes: Add support for shared query results of other panel (#65413)
* Scene: Add support for shared query results of other panel * Update * Fixing dashboardpull/65324/head
parent
4ded937c79
commit
3af8f3246a
@ -0,0 +1,54 @@ |
|||||||
|
import { getDefaultTimeRange, LoadingState } from '@grafana/data'; |
||||||
|
import { |
||||||
|
SceneDataNode, |
||||||
|
SceneFlexItem, |
||||||
|
SceneFlexLayout, |
||||||
|
sceneGraph, |
||||||
|
SceneObjectBase, |
||||||
|
SceneObjectState, |
||||||
|
} from '@grafana/scenes'; |
||||||
|
|
||||||
|
import { ShareQueryDataProvider } from './ShareQueryDataProvider'; |
||||||
|
import { activateFullSceneTree, getVizPanelKeyForPanelId } from './utils'; |
||||||
|
|
||||||
|
export class SceneDummyPanel extends SceneObjectBase<SceneObjectState> {} |
||||||
|
|
||||||
|
describe('ShareQueryDataProvider', () => { |
||||||
|
it('Should find and subscribe to another VizPanels data provider', () => { |
||||||
|
const panel = new SceneDummyPanel({ |
||||||
|
key: getVizPanelKeyForPanelId(2), |
||||||
|
$data: new ShareQueryDataProvider({ |
||||||
|
query: { refId: 'A', panelId: 1 }, |
||||||
|
}), |
||||||
|
}); |
||||||
|
|
||||||
|
const sourceData = new SceneDataNode({ |
||||||
|
data: { |
||||||
|
series: [], |
||||||
|
state: LoadingState.Done, |
||||||
|
timeRange: getDefaultTimeRange(), |
||||||
|
structureRev: 11, |
||||||
|
}, |
||||||
|
}); |
||||||
|
|
||||||
|
const scene = new SceneFlexLayout({ |
||||||
|
children: [ |
||||||
|
new SceneFlexItem({ |
||||||
|
body: new SceneDummyPanel({ |
||||||
|
key: getVizPanelKeyForPanelId(1), |
||||||
|
$data: sourceData, |
||||||
|
}), |
||||||
|
}), |
||||||
|
new SceneFlexItem({ body: panel }), |
||||||
|
], |
||||||
|
}); |
||||||
|
|
||||||
|
activateFullSceneTree(scene); |
||||||
|
|
||||||
|
expect(sceneGraph.getData(panel).state.data?.structureRev).toBe(11); |
||||||
|
|
||||||
|
sourceData.setState({ data: { ...sourceData.state.data!, structureRev: 12 } }); |
||||||
|
|
||||||
|
expect(sceneGraph.getData(panel).state.data?.structureRev).toBe(12); |
||||||
|
}); |
||||||
|
}); |
||||||
@ -0,0 +1,103 @@ |
|||||||
|
import { Unsubscribable } from 'rxjs'; |
||||||
|
|
||||||
|
import { |
||||||
|
SceneDataProvider, |
||||||
|
SceneDataState, |
||||||
|
SceneDataTransformer, |
||||||
|
SceneDeactivationHandler, |
||||||
|
SceneObject, |
||||||
|
SceneObjectBase, |
||||||
|
} from '@grafana/scenes'; |
||||||
|
import { DashboardQuery } from 'app/plugins/datasource/dashboard/types'; |
||||||
|
|
||||||
|
import { getVizPanelKeyForPanelId } from './utils'; |
||||||
|
|
||||||
|
export interface ShareQueryDataProviderState extends SceneDataState { |
||||||
|
query: DashboardQuery; |
||||||
|
} |
||||||
|
|
||||||
|
export class ShareQueryDataProvider extends SceneObjectBase<ShareQueryDataProviderState> implements SceneDataProvider { |
||||||
|
private _querySub: Unsubscribable | undefined; |
||||||
|
private _sourceDataDeactivationHandler?: SceneDeactivationHandler; |
||||||
|
|
||||||
|
public constructor(state: ShareQueryDataProviderState) { |
||||||
|
super(state); |
||||||
|
|
||||||
|
this.addActivationHandler(() => { |
||||||
|
// TODO handle changes to query model (changed panelId / withTransforms)
|
||||||
|
//this.subscribeToState(this._onStateChanged);
|
||||||
|
|
||||||
|
this._subscribeToSource(); |
||||||
|
|
||||||
|
return () => { |
||||||
|
if (this._querySub) { |
||||||
|
this._querySub.unsubscribe(); |
||||||
|
} |
||||||
|
if (this._sourceDataDeactivationHandler) { |
||||||
|
this._sourceDataDeactivationHandler(); |
||||||
|
} |
||||||
|
}; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private _subscribeToSource() { |
||||||
|
const { query } = this.state; |
||||||
|
|
||||||
|
if (this._querySub) { |
||||||
|
this._querySub.unsubscribe(); |
||||||
|
} |
||||||
|
|
||||||
|
if (!query.panelId) { |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
const keyToFind = getVizPanelKeyForPanelId(query.panelId); |
||||||
|
const source = findObjectInScene(this.getRoot(), (scene: SceneObject) => scene.state.key === keyToFind); |
||||||
|
|
||||||
|
if (!source) { |
||||||
|
console.log('Shared dashboard query refers to a panel that does not exist in the scene'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
let sourceData = source.state.$data; |
||||||
|
if (!sourceData) { |
||||||
|
console.log('No source data found for shared dashboard query'); |
||||||
|
return; |
||||||
|
} |
||||||
|
|
||||||
|
// This will activate if sourceData is part of hidden panel
|
||||||
|
// Also make sure the sourceData is not deactivated if hidden later
|
||||||
|
this._sourceDataDeactivationHandler = sourceData.activate(); |
||||||
|
|
||||||
|
if (sourceData instanceof SceneDataTransformer) { |
||||||
|
if (!query.withTransforms) { |
||||||
|
if (!sourceData.state.$data) { |
||||||
|
throw new Error('No source inner query runner found in data transformer'); |
||||||
|
} |
||||||
|
sourceData = sourceData.state.$data; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
this._querySub = sourceData.subscribeToState((state) => this.setState({ data: state.data })); |
||||||
|
|
||||||
|
// Copy the initial state
|
||||||
|
this.setState({ data: sourceData.state.data }); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
export function findObjectInScene(scene: SceneObject, check: (scene: SceneObject) => boolean): SceneObject | null { |
||||||
|
if (check(scene)) { |
||||||
|
return scene; |
||||||
|
} |
||||||
|
|
||||||
|
let found: SceneObject | null = null; |
||||||
|
|
||||||
|
scene.forEachChild((child) => { |
||||||
|
let maybe = findObjectInScene(child, check); |
||||||
|
if (maybe) { |
||||||
|
found = maybe; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return found; |
||||||
|
} |
||||||
@ -0,0 +1,26 @@ |
|||||||
|
import { SceneDeactivationHandler, SceneObject } from '@grafana/scenes'; |
||||||
|
|
||||||
|
export function getVizPanelKeyForPanelId(panelId: number) { |
||||||
|
return `panel-${panelId}`; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order |
||||||
|
* of React mount order and useEffect ordering. |
||||||
|
* |
||||||
|
*/ |
||||||
|
export function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler { |
||||||
|
const deactivationHandlers: SceneDeactivationHandler[] = []; |
||||||
|
|
||||||
|
scene.forEachChild((child) => { |
||||||
|
deactivationHandlers.push(activateFullSceneTree(child)); |
||||||
|
}); |
||||||
|
|
||||||
|
deactivationHandlers.push(scene.activate()); |
||||||
|
|
||||||
|
return () => { |
||||||
|
for (const handler of deactivationHandlers) { |
||||||
|
handler(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
Loading…
Reference in new issue