mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Scene: Variables and support for declaring variable dependencies and getting notified or re-rendered when they change (#58299)
* Component that can cache and extract variable dependencies * Component that can cache and extract variable dependencies * Updates * Refactoring * Lots of refactoring and iterations of supporting both re-rendering and query re-execution * Updated SceneCanvasText * Updated name of file * Updated * Refactoring a bit * Added back getName * Added comment * minor fix * Minor fix * Merge fixes * Merge fixes * Some review fixes * Updated comment * Added forceRender function * Add back fail on console log
This commit is contained in:
parent
93b4b9154e
commit
84a69135a7
@ -4570,10 +4570,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"]
|
||||
],
|
||||
"public/app/features/scenes/core/SceneTimeRange.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
@ -4582,6 +4579,11 @@ exports[`better eslint`] = {
|
||||
"public/app/features/scenes/core/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/scenes/core/utils.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"public/app/features/scenes/editor/SceneObjectTree.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
|
||||
@ -4593,6 +4595,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "4"]
|
||||
],
|
||||
"public/app/features/scenes/variables/sets/SceneVariableSet.test.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
],
|
||||
"public/app/features/scenes/variables/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -49,9 +49,9 @@ type MockQuotaChecker_CheckQuotaReached_Call struct {
|
||||
}
|
||||
|
||||
// CheckQuotaReached is a helper method to define mock.On call
|
||||
// - ctx context.Context
|
||||
// - target quota.TargetSrv
|
||||
// - scopeParams *quota.ScopeParameters
|
||||
// - ctx context.Context
|
||||
// - target quota.TargetSrv
|
||||
// - scopeParams *quota.ScopeParameters
|
||||
func (_e *MockQuotaChecker_Expecter) CheckQuotaReached(ctx interface{}, target interface{}, scopeParams interface{}) *MockQuotaChecker_CheckQuotaReached_Call {
|
||||
return &MockQuotaChecker_CheckQuotaReached_Call{Call: _e.mock.On("CheckQuotaReached", ctx, target, scopeParams)}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { Field, Input } from '@grafana/ui';
|
||||
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
|
||||
import { sceneTemplateInterpolator } from '../variables/sceneTemplateInterpolator';
|
||||
import { VariableDependencyConfig } from '../variables/VariableDependencyConfig';
|
||||
|
||||
export interface SceneCanvasTextState extends SceneLayoutChildState {
|
||||
text: string;
|
||||
@ -15,9 +15,10 @@ export interface SceneCanvasTextState extends SceneLayoutChildState {
|
||||
export class SceneCanvasText extends SceneObjectBase<SceneCanvasTextState> {
|
||||
public static Editor = Editor;
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, { statePaths: ['text'] });
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<SceneCanvasText>) => {
|
||||
const { text, fontSize = 20, align = 'left' } = model.useState();
|
||||
const textInterpolated = sceneTemplateInterpolator(text, model);
|
||||
const { text, fontSize = 20, align = 'left', key } = model.useState();
|
||||
|
||||
const style: CSSProperties = {
|
||||
fontSize: fontSize,
|
||||
@ -28,7 +29,11 @@ export class SceneCanvasText extends SceneObjectBase<SceneCanvasTextState> {
|
||||
justifyContent: align,
|
||||
};
|
||||
|
||||
return <div style={style}>{textInterpolated}</div>;
|
||||
return (
|
||||
<div style={style} data-testid={key}>
|
||||
{model.interpolate(text)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -7,6 +7,7 @@ import { Field, PanelChrome, Input } from '@grafana/ui';
|
||||
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
|
||||
import { VariableDependencyConfig } from '../variables/VariableDependencyConfig';
|
||||
|
||||
import { SceneDragHandle } from './SceneDragHandle';
|
||||
|
||||
@ -21,6 +22,10 @@ export class VizPanel extends SceneObjectBase<VizPanelState> {
|
||||
public static Component = ScenePanelRenderer;
|
||||
public static Editor = VizPanelEditor;
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
statePaths: ['title'],
|
||||
});
|
||||
|
||||
public onSetTimeRange = (timeRange: AbsoluteTimeRange) => {
|
||||
const sceneTimeRange = this.getTimeRange();
|
||||
sceneTimeRange.setState({
|
||||
@ -41,6 +46,8 @@ function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
|
||||
const isDraggable = layout.state.isDraggable ? state.isDraggable : false;
|
||||
const dragHandle = <SceneDragHandle layoutKey={layout.state.key!} />;
|
||||
|
||||
const titleInterpolated = model.interpolate(title);
|
||||
|
||||
return (
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
@ -49,7 +56,12 @@ function ScenePanelRenderer({ model }: SceneComponentProps<VizPanel>) {
|
||||
}
|
||||
|
||||
return (
|
||||
<PanelChrome title={title} width={width} height={height} leftItems={isDraggable ? [dragHandle] : undefined}>
|
||||
<PanelChrome
|
||||
title={titleInterpolated}
|
||||
width={width}
|
||||
height={height}
|
||||
leftItems={isDraggable ? [dragHandle] : undefined}
|
||||
>
|
||||
{(innerWidth, innerHeight) => (
|
||||
<>
|
||||
<PanelRenderer
|
||||
|
@ -24,6 +24,10 @@ export function SceneComponentWrapper<T extends SceneObject>({
|
||||
};
|
||||
}, [model]);
|
||||
|
||||
/** Useful for tests and evaluating efficiency in reducing renderings */
|
||||
// @ts-ignore
|
||||
model._renderCount += 1;
|
||||
|
||||
if (!isEditing) {
|
||||
return inner;
|
||||
}
|
||||
|
@ -5,6 +5,9 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
import { BusEvent, BusEventHandler, BusEventType, EventBusSrv } from '@grafana/data';
|
||||
import { useForceUpdate } from '@grafana/ui';
|
||||
|
||||
import { sceneTemplateInterpolator } from '../variables/sceneTemplateInterpolator';
|
||||
import { SceneVariables, SceneVariableDependencyConfigLike } from '../variables/types';
|
||||
|
||||
import { SceneComponentWrapper } from './SceneComponentWrapper';
|
||||
import { SceneObjectStateChangedEvent } from './events';
|
||||
import {
|
||||
@ -16,6 +19,7 @@ import {
|
||||
SceneObjectState,
|
||||
SceneLayoutState,
|
||||
} from './types';
|
||||
import { cloneSceneObject, forEachSceneObjectInState } from './utils';
|
||||
|
||||
export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObjectState>
|
||||
implements SceneObject<TState>
|
||||
@ -25,9 +29,13 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
private _state: TState;
|
||||
private _events = new EventBusSrv();
|
||||
|
||||
/** Incremented in SceneComponentWrapper, useful for tests and rendering optimizations */
|
||||
protected _renderCount = 0;
|
||||
protected _parent?: SceneObject;
|
||||
protected _subs = new Subscription();
|
||||
|
||||
protected _variableDependency: SceneVariableDependencyConfigLike | undefined;
|
||||
|
||||
public constructor(state: TState) {
|
||||
if (!state.key) {
|
||||
state.key = uuidv4();
|
||||
@ -53,6 +61,11 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
return this._parent;
|
||||
}
|
||||
|
||||
/** Returns variable dependency config */
|
||||
public get variableDependency(): SceneVariableDependencyConfigLike | undefined {
|
||||
return this._variableDependency;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used in render functions when rendering a SceneObject.
|
||||
* Wraps the component in an EditWrapper that handles edit mode
|
||||
@ -69,19 +82,7 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
}
|
||||
|
||||
private setParent() {
|
||||
for (const propValue of Object.values(this._state)) {
|
||||
if (propValue instanceof SceneObjectBase) {
|
||||
propValue._parent = this;
|
||||
}
|
||||
|
||||
if (Array.isArray(propValue)) {
|
||||
for (const child of propValue) {
|
||||
if (child instanceof SceneObjectBase) {
|
||||
child._parent = this;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
forEachSceneObjectInState(this._state, (child) => (child._parent = this));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -104,6 +105,7 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
...this._state,
|
||||
...update,
|
||||
};
|
||||
|
||||
this.setParent();
|
||||
this._subject.next(this._state);
|
||||
|
||||
@ -118,7 +120,6 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Publish an event and optionally bubble it up the scene
|
||||
**/
|
||||
@ -216,6 +217,18 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
throw new Error('No data found in scene tree');
|
||||
}
|
||||
|
||||
public getVariables(): SceneVariables | undefined {
|
||||
if (this.state.$variables) {
|
||||
return this.state.$variables;
|
||||
}
|
||||
|
||||
if (this.parent) {
|
||||
return this.parent.getVariables();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will walk up the scene object graph to the closest $layout scene object
|
||||
*/
|
||||
@ -247,36 +260,29 @@ export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObj
|
||||
throw new Error('No editor found in scene tree');
|
||||
}
|
||||
|
||||
/** Force a re-render, should only be needed when variable values change */
|
||||
public forceRender(): void {
|
||||
this.setState({});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will create new SceneItem with shalled cloned state, but all states items of type SceneObject are deep cloned
|
||||
* Will create new SceneObject with shallow-cloned state, but all state items of type SceneObject are deep cloned
|
||||
*/
|
||||
public clone(withState?: Partial<TState>): this {
|
||||
const clonedState = { ...this.state };
|
||||
return cloneSceneObject(this, withState);
|
||||
}
|
||||
|
||||
// Clone any SceneItems in state
|
||||
for (const key in clonedState) {
|
||||
const propValue = clonedState[key];
|
||||
if (propValue instanceof SceneObjectBase) {
|
||||
clonedState[key] = propValue.clone();
|
||||
}
|
||||
|
||||
// Clone scene objects in arrays
|
||||
if (Array.isArray(propValue)) {
|
||||
const newArray: any = [];
|
||||
for (const child of propValue) {
|
||||
if (child instanceof SceneObjectBase) {
|
||||
newArray.push(child.clone());
|
||||
} else {
|
||||
newArray.push(child);
|
||||
}
|
||||
}
|
||||
clonedState[key] = newArray;
|
||||
}
|
||||
/**
|
||||
* Interpolates the given string using the current scene object as context.
|
||||
* TODO: Cache interpolatinos?
|
||||
*/
|
||||
public interpolate(value: string | undefined) {
|
||||
// Skip interpolation if there are no variable depdendencies
|
||||
if (!value || !this._variableDependency || this._variableDependency.getNames().size === 0) {
|
||||
return value;
|
||||
}
|
||||
|
||||
Object.assign(clonedState, withState);
|
||||
|
||||
return new (this.constructor as any)(clonedState);
|
||||
return sceneTemplateInterpolator(value, this);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@ import { Observer, Subscription, Unsubscribable } from 'rxjs';
|
||||
|
||||
import { BusEvent, BusEventHandler, BusEventType, PanelData, TimeRange, UrlQueryMap } from '@grafana/data';
|
||||
|
||||
import { SceneVariables } from '../variables/types';
|
||||
import { SceneVariableDependencyConfigLike, SceneVariables } from '../variables/types';
|
||||
|
||||
export interface SceneObjectStatePlain {
|
||||
key?: string;
|
||||
@ -62,6 +62,9 @@ export interface SceneObject<TState extends SceneObjectState = SceneObjectState>
|
||||
/** SceneObject parent */
|
||||
readonly parent?: SceneObject;
|
||||
|
||||
/** This abtractions declares what variables the scene object depends on and how to handle when they change value. **/
|
||||
readonly variableDependency?: SceneVariableDependencyConfigLike;
|
||||
|
||||
/** Subscribe to state changes */
|
||||
subscribeToState(observer?: Partial<Observer<TState>>): Subscription;
|
||||
|
||||
@ -92,6 +95,9 @@ export interface SceneObject<TState extends SceneObjectState = SceneObjectState>
|
||||
/** Get the closest node with data */
|
||||
getData(): SceneObject<SceneDataState>;
|
||||
|
||||
/** Get the closest node with variables */
|
||||
getVariables(): SceneVariables | undefined;
|
||||
|
||||
/** Get the closest node with time range */
|
||||
getTimeRange(): SceneTimeRange;
|
||||
|
||||
@ -106,6 +112,9 @@ export interface SceneObject<TState extends SceneObjectState = SceneObjectState>
|
||||
|
||||
/** To be replaced by declarative method */
|
||||
Editor(props: SceneComponentProps<SceneObject<TState>>): React.ReactElement | null;
|
||||
|
||||
/** Force a re-render, should only be needed when variable values change */
|
||||
forceRender(): void;
|
||||
}
|
||||
|
||||
export type SceneLayoutChild = SceneObject<SceneLayoutChildState | SceneLayoutState>;
|
||||
|
56
public/app/features/scenes/core/utils.ts
Normal file
56
public/app/features/scenes/core/utils.ts
Normal file
@ -0,0 +1,56 @@
|
||||
import { SceneObjectBase } from './SceneObjectBase';
|
||||
import { SceneObjectState, SceneObjectStatePlain } from './types';
|
||||
|
||||
/**
|
||||
* Will call callback for all first level child scene objects and scene objects inside arrays
|
||||
*/
|
||||
export function forEachSceneObjectInState(state: SceneObjectStatePlain, callback: (scene: SceneObjectBase) => void) {
|
||||
for (const propValue of Object.values(state)) {
|
||||
if (propValue instanceof SceneObjectBase) {
|
||||
callback(propValue);
|
||||
}
|
||||
|
||||
if (Array.isArray(propValue)) {
|
||||
for (const child of propValue) {
|
||||
if (child instanceof SceneObjectBase) {
|
||||
callback(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will create new SceneItem with shalled cloned state, but all states items of type SceneObject are deep cloned
|
||||
*/
|
||||
export function cloneSceneObject<T extends SceneObjectBase<TState>, TState extends SceneObjectState>(
|
||||
sceneObject: SceneObjectBase<TState>,
|
||||
withState?: Partial<TState>
|
||||
): T {
|
||||
const clonedState = { ...sceneObject.state };
|
||||
|
||||
// Clone any SceneItems in state
|
||||
for (const key in clonedState) {
|
||||
const propValue = clonedState[key];
|
||||
if (propValue instanceof SceneObjectBase) {
|
||||
clonedState[key] = propValue.clone();
|
||||
}
|
||||
|
||||
// Clone scene objects in arrays
|
||||
if (Array.isArray(propValue)) {
|
||||
const newArray: any = [];
|
||||
for (const child of propValue) {
|
||||
if (child instanceof SceneObjectBase) {
|
||||
newArray.push(child.clone());
|
||||
} else {
|
||||
newArray.push(child);
|
||||
}
|
||||
}
|
||||
clonedState[key] = newArray;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(clonedState, withState);
|
||||
|
||||
return new (sceneObject.constructor as any)(clonedState);
|
||||
}
|
@ -18,6 +18,7 @@ import { runRequest } from 'app/features/query/state/runRequest';
|
||||
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneObjectStatePlain } from '../core/types';
|
||||
import { VariableDependencyConfig } from '../variables/VariableDependencyConfig';
|
||||
|
||||
export interface QueryRunnerState extends SceneObjectStatePlain {
|
||||
data?: PanelData;
|
||||
@ -31,6 +32,11 @@ export interface DataQueryExtended extends DataQuery {
|
||||
export class SceneQueryRunner extends SceneObjectBase<QueryRunnerState> {
|
||||
private querySub?: Unsubscribable;
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
statePaths: ['queries'],
|
||||
onReferencedVariableValueChanged: () => this.runQueries(),
|
||||
});
|
||||
|
||||
public activate() {
|
||||
super.activate();
|
||||
|
||||
|
@ -9,7 +9,8 @@ import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { SceneEditManager } from '../editor/SceneEditManager';
|
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
|
||||
|
||||
import { getQueryRunnerWithRandomWalkQuery } from './queries';
|
||||
|
||||
export function getFlexLayoutTest(): Scene {
|
||||
const scene = new Scene({
|
||||
@ -51,18 +52,7 @@ export function getFlexLayoutTest(): Scene {
|
||||
}),
|
||||
$editor: new SceneEditManager({}),
|
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
|
||||
$data: new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
uid: 'gdev-testdata',
|
||||
type: 'testdata',
|
||||
},
|
||||
scenarioId: 'random_walk',
|
||||
},
|
||||
],
|
||||
}),
|
||||
$data: getQueryRunnerWithRandomWalkQuery(),
|
||||
actions: [new SceneTimePicker({})],
|
||||
});
|
||||
|
||||
@ -70,19 +60,10 @@ export function getFlexLayoutTest(): Scene {
|
||||
}
|
||||
|
||||
export function getScenePanelRepeaterTest(): Scene {
|
||||
const queryRunner = new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
refId: 'A',
|
||||
datasource: {
|
||||
uid: 'gdev-testdata',
|
||||
type: 'testdata',
|
||||
},
|
||||
seriesCount: 2,
|
||||
alias: '__server_names',
|
||||
scenarioId: 'random_walk',
|
||||
},
|
||||
],
|
||||
const queryRunner = getQueryRunnerWithRandomWalkQuery({
|
||||
seriesCount: 2,
|
||||
alias: '__server_names',
|
||||
scenarioId: 'random_walk',
|
||||
});
|
||||
|
||||
const scene = new Scene({
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { TestDataQuery } from 'app/plugins/datasource/testdata/types';
|
||||
|
||||
import { SceneQueryRunner } from '../querying/SceneQueryRunner';
|
||||
|
||||
export function getQueryRunnerWithRandomWalkQuery() {
|
||||
export function getQueryRunnerWithRandomWalkQuery(overrides?: Partial<TestDataQuery>) {
|
||||
return new SceneQueryRunner({
|
||||
queries: [
|
||||
{
|
||||
@ -10,6 +12,7 @@ export function getQueryRunnerWithRandomWalkQuery() {
|
||||
type: 'testdata',
|
||||
},
|
||||
scenarioId: 'random_walk',
|
||||
...overrides,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
@ -4,25 +4,18 @@ import { Scene } from '../components/Scene';
|
||||
import { SceneCanvasText } from '../components/SceneCanvasText';
|
||||
import { SceneSubMenu } from '../components/SceneSubMenu';
|
||||
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||
import { VizPanel } from '../components/VizPanel';
|
||||
import { SceneFlexLayout } from '../components/layout/SceneFlexLayout';
|
||||
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
|
||||
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||
import { TestVariable } from '../variables/variants/TestVariable';
|
||||
|
||||
import { getQueryRunnerWithRandomWalkQuery } from './queries';
|
||||
|
||||
export function getVariablesDemo(): Scene {
|
||||
const scene = new Scene({
|
||||
title: 'Variables',
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
children: [
|
||||
new SceneCanvasText({
|
||||
text: 'Some text with a variable: ${server} - ${pod}',
|
||||
fontSize: 40,
|
||||
align: 'center',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
$variables: new SceneVariableSet({
|
||||
variables: [
|
||||
new TestVariable({
|
||||
@ -46,12 +39,34 @@ export function getVariablesDemo(): Scene {
|
||||
query: 'A.$server.$pod.*',
|
||||
value: 'handler',
|
||||
delayMs: 1000,
|
||||
isMulti: true,
|
||||
//isMulti: true,
|
||||
text: '',
|
||||
options: [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
layout: new SceneFlexLayout({
|
||||
direction: 'row',
|
||||
children: [
|
||||
new SceneFlexLayout({
|
||||
children: [
|
||||
new VizPanel({
|
||||
pluginId: 'timeseries',
|
||||
title: 'handler: $handler',
|
||||
$data: getQueryRunnerWithRandomWalkQuery({
|
||||
alias: 'handler: $handler',
|
||||
}),
|
||||
}),
|
||||
new SceneCanvasText({
|
||||
size: { width: '40%' },
|
||||
text: 'server - pod: ${server} - ${pod}',
|
||||
fontSize: 20,
|
||||
align: 'center',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
|
||||
actions: [new SceneTimePicker({})],
|
||||
subMenu: new SceneSubMenu({
|
||||
|
@ -0,0 +1,86 @@
|
||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||
import { SceneObjectStatePlain } from '../core/types';
|
||||
|
||||
import { VariableDependencyConfig } from './VariableDependencyConfig';
|
||||
import { ConstantVariable } from './variants/ConstantVariable';
|
||||
|
||||
interface TestState extends SceneObjectStatePlain {
|
||||
query: string;
|
||||
otherProp: string;
|
||||
nested: {
|
||||
query: string;
|
||||
};
|
||||
}
|
||||
|
||||
class TestObj extends SceneObjectBase<TestState> {
|
||||
public constructor() {
|
||||
super({
|
||||
query: 'query with ${queryVarA} ${queryVarB}',
|
||||
otherProp: 'string with ${otherPropA}',
|
||||
nested: {
|
||||
query: 'nested object with ${nestedVarA}',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('VariableDependencySet', () => {
|
||||
it('Should be able to extract dependencies from all state', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const deps = new VariableDependencyConfig(sceneObj, {});
|
||||
|
||||
expect(deps.getNames()).toEqual(new Set(['queryVarA', 'queryVarB', 'nestedVarA', 'otherPropA']));
|
||||
});
|
||||
|
||||
it('Should be able to extract dependencies from statePaths', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['query', 'nested'] });
|
||||
|
||||
expect(deps.getNames()).toEqual(new Set(['queryVarA', 'queryVarB', 'nestedVarA']));
|
||||
expect(deps.hasDependencyOn('queryVarA')).toBe(true);
|
||||
});
|
||||
|
||||
it('Should cache variable extraction', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['query', 'nested'] });
|
||||
|
||||
deps.getNames();
|
||||
deps.getNames();
|
||||
|
||||
expect(deps.scanCount).toBe(1);
|
||||
});
|
||||
|
||||
it('Should not rescan if state changes but not any of the state paths to scan', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['query', 'nested'] });
|
||||
deps.getNames();
|
||||
|
||||
sceneObj.setState({ otherProp: 'new value' });
|
||||
|
||||
deps.getNames();
|
||||
expect(deps.scanCount).toBe(1);
|
||||
});
|
||||
|
||||
it('Should re-scan when both state and specific state path change', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['query', 'nested'] });
|
||||
deps.getNames();
|
||||
|
||||
sceneObj.setState({ query: 'new query with ${newVar}' });
|
||||
|
||||
expect(deps.getNames()).toEqual(new Set(['newVar', 'nestedVarA']));
|
||||
expect(deps.scanCount).toBe(2);
|
||||
});
|
||||
|
||||
it('variableValuesChanged should only call onReferencedVariableValueChanged if dependent variable has changed', () => {
|
||||
const sceneObj = new TestObj();
|
||||
const fn = jest.fn();
|
||||
const deps = new VariableDependencyConfig(sceneObj, { onReferencedVariableValueChanged: fn });
|
||||
|
||||
deps.variableValuesChanged(new Set([new ConstantVariable({ name: 'not-dep', value: '1' })]));
|
||||
expect(fn.mock.calls.length).toBe(0);
|
||||
|
||||
deps.variableValuesChanged(new Set([new ConstantVariable({ name: 'queryVarA', value: '1' })]));
|
||||
expect(fn.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
130
public/app/features/scenes/variables/VariableDependencyConfig.ts
Normal file
130
public/app/features/scenes/variables/VariableDependencyConfig.ts
Normal file
@ -0,0 +1,130 @@
|
||||
import { variableRegex } from 'app/features/variables/utils';
|
||||
|
||||
import { SceneObject, SceneObjectState } from '../core/types';
|
||||
|
||||
import { SceneVariable, SceneVariableDependencyConfigLike } from './types';
|
||||
|
||||
interface VariableDependencyConfigOptions<TState extends SceneObjectState> {
|
||||
/**
|
||||
* State paths to scan / extract variable dependencies from. Leave empty to scan all paths.
|
||||
*/
|
||||
statePaths?: Array<keyof TState>;
|
||||
/**
|
||||
* Optional way to customize how to handle when a dependent variable changes
|
||||
* If not specified the default behavior is to trigger a re-render
|
||||
*/
|
||||
onReferencedVariableValueChanged?: () => void;
|
||||
}
|
||||
|
||||
export class VariableDependencyConfig<TState extends SceneObjectState> implements SceneVariableDependencyConfigLike {
|
||||
private _state: TState | undefined;
|
||||
private _dependencies = new Set<string>();
|
||||
private _statePaths?: Array<keyof TState>;
|
||||
private _onReferencedVariableValueChanged: () => void;
|
||||
|
||||
public scanCount = 0;
|
||||
|
||||
public constructor(private _sceneObject: SceneObject<TState>, options: VariableDependencyConfigOptions<TState>) {
|
||||
this._statePaths = options.statePaths;
|
||||
this._onReferencedVariableValueChanged =
|
||||
options.onReferencedVariableValueChanged ?? this.defaultHandlerReferencedVariableValueChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check for dependency on a specific variable
|
||||
*/
|
||||
public hasDependencyOn(name: string): boolean {
|
||||
return this.getNames().has(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is called whenever any set of variables have new values. It up to this implementation to check if it's relevant given the current dependencies.
|
||||
*/
|
||||
public variableValuesChanged(variables: Set<SceneVariable>) {
|
||||
const deps = this.getNames();
|
||||
|
||||
for (const variable of variables) {
|
||||
if (deps.has(variable.state.name)) {
|
||||
this._onReferencedVariableValueChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Only way to force a re-render is to update state right now
|
||||
*/
|
||||
private defaultHandlerReferencedVariableValueChanged = () => {
|
||||
this._sceneObject.forceRender();
|
||||
};
|
||||
|
||||
public getNames(): Set<string> {
|
||||
const prevState = this._state;
|
||||
const newState = (this._state = this._sceneObject.state);
|
||||
|
||||
if (!prevState) {
|
||||
// First time we always scan for dependencies
|
||||
this.scanStateForDependencies(this._state);
|
||||
return this._dependencies;
|
||||
}
|
||||
|
||||
// Second time we only scan if state is a different and if any specific state path has changed
|
||||
if (newState !== prevState) {
|
||||
if (this._statePaths) {
|
||||
for (const path of this._statePaths) {
|
||||
if (newState[path] !== prevState[path]) {
|
||||
this.scanStateForDependencies(newState);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.scanStateForDependencies(newState);
|
||||
}
|
||||
}
|
||||
|
||||
return this._dependencies;
|
||||
}
|
||||
|
||||
private scanStateForDependencies(state: TState) {
|
||||
this._dependencies.clear();
|
||||
this.scanCount += 1;
|
||||
|
||||
if (this._statePaths) {
|
||||
for (const path of this._statePaths) {
|
||||
const value = state[path];
|
||||
if (value) {
|
||||
this.extractVariablesFrom(value);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.extractVariablesFrom(state);
|
||||
}
|
||||
}
|
||||
|
||||
private extractVariablesFrom(value: unknown) {
|
||||
variableRegex.lastIndex = 0;
|
||||
|
||||
const stringToCheck = typeof value !== 'string' ? safeStringifyValue(value) : value;
|
||||
|
||||
const matches = stringToCheck.matchAll(variableRegex);
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
const [, var1, var2, , var3] = match;
|
||||
const variableName = var1 || var2 || var3;
|
||||
this._dependencies.add(variableName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const safeStringifyValue = (value: unknown) => {
|
||||
try {
|
||||
return JSON.stringify(value, null);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
@ -6,14 +6,14 @@ import { Tooltip } from '@grafana/ui';
|
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import { SceneComponentProps, SceneObject, SceneObjectStatePlain } from '../../core/types';
|
||||
import { SceneVariables, SceneVariableState } from '../types';
|
||||
import { SceneVariableState } from '../types';
|
||||
|
||||
export class VariableValueSelectors extends SceneObjectBase<SceneObjectStatePlain> {
|
||||
public static Component = VariableValueSelectorsRenderer;
|
||||
}
|
||||
|
||||
function VariableValueSelectorsRenderer({ model }: SceneComponentProps<VariableValueSelectors>) {
|
||||
const variables = getVariables(model).useState();
|
||||
const variables = model.getVariables()!.useState();
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -24,18 +24,6 @@ function VariableValueSelectorsRenderer({ model }: SceneComponentProps<VariableV
|
||||
);
|
||||
}
|
||||
|
||||
function getVariables(model: SceneObject): SceneVariables {
|
||||
if (model.state.$variables) {
|
||||
return model.state.$variables;
|
||||
}
|
||||
|
||||
if (model.parent) {
|
||||
return getVariables(model.parent);
|
||||
}
|
||||
|
||||
throw new Error('No variables found');
|
||||
}
|
||||
|
||||
function VariableValueSelectWrapper({ variable }: { variable: SceneObject<SceneVariableState> }) {
|
||||
const state = variable.useState();
|
||||
|
||||
|
@ -7,6 +7,11 @@ import { SceneObject } from '../core/types';
|
||||
import { SceneVariable } from './types';
|
||||
|
||||
export function sceneTemplateInterpolator(target: string, sceneObject: SceneObject) {
|
||||
// Skip any interpolation if there are no variables in the scene object graph
|
||||
if (!sceneObject.getVariables()) {
|
||||
return target;
|
||||
}
|
||||
|
||||
variableRegex.lastIndex = 0;
|
||||
|
||||
return target.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
||||
|
@ -1,3 +1,9 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
|
||||
import { SceneCanvasText } from '../../components/SceneCanvasText';
|
||||
import { SceneFlexLayout } from '../../components/layout/SceneFlexLayout';
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import { SceneObjectStatePlain } from '../../core/types';
|
||||
import { TestVariable } from '../variants/TestVariable';
|
||||
@ -91,5 +97,41 @@ describe('SceneVariableList', () => {
|
||||
scene.deactivate();
|
||||
expect(A.isGettingValues).toBe(false);
|
||||
});
|
||||
|
||||
describe('When update process completed and variables have changed values', () => {
|
||||
it('Should trigger re-renders of dependent scene objects', async () => {
|
||||
const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] });
|
||||
const B = new TestVariable({ name: 'B', query: 'A.$A.*', value: '', text: '', options: [] });
|
||||
|
||||
const helloText = new SceneCanvasText({ text: 'Hello' });
|
||||
const sceneObjectWithVariable = new SceneCanvasText({ text: '$A - $B' });
|
||||
|
||||
const scene = new SceneFlexLayout({
|
||||
$variables: new SceneVariableSet({ variables: [B, A] }),
|
||||
children: [helloText, sceneObjectWithVariable],
|
||||
});
|
||||
|
||||
render(<scene.Component model={scene} />);
|
||||
|
||||
expect(screen.getByText('Hello')).toBeInTheDocument();
|
||||
|
||||
act(() => {
|
||||
A.signalUpdateCompleted();
|
||||
B.signalUpdateCompleted();
|
||||
});
|
||||
|
||||
expect(screen.getByText('AA - AAA')).toBeInTheDocument();
|
||||
expect((helloText as any)._renderCount).toBe(1);
|
||||
expect((sceneObjectWithVariable as any)._renderCount).toBe(2);
|
||||
|
||||
act(() => {
|
||||
B.onSingleValueChange({ value: 'B', text: 'B' });
|
||||
});
|
||||
|
||||
expect(screen.getByText('AA - B')).toBeInTheDocument();
|
||||
expect((helloText as any)._renderCount).toBe(1);
|
||||
expect((sceneObjectWithVariable as any)._renderCount).toBe(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,20 +1,19 @@
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||
import { SceneObject } from '../../core/types';
|
||||
import { forEachSceneObjectInState } from '../../core/utils';
|
||||
import { SceneVariable, SceneVariables, SceneVariableSetState, SceneVariableValueChangedEvent } from '../types';
|
||||
|
||||
export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> implements SceneVariables {
|
||||
/** Variables that have changed in since the activation or since the first manual value change */
|
||||
// private variablesThatHaveChanged = new Map<string, SceneVariable>();
|
||||
private variablesThatHaveChanged = new Set<SceneVariable>();
|
||||
|
||||
/** Variables that are scheduled to be validated and updated */
|
||||
private variablesToUpdate = new Map<string, SceneVariable>();
|
||||
|
||||
/** Cached variable dependencies */
|
||||
private dependencies = new Map<string, string[]>();
|
||||
private variablesToUpdate = new Set<SceneVariable>();
|
||||
|
||||
/** Variables currently updating */
|
||||
private updating = new Map<string, VariableUpdateInProgress>();
|
||||
private updating = new Map<SceneVariable, VariableUpdateInProgress>();
|
||||
|
||||
public getByName(name: string): SceneVariable | undefined {
|
||||
// TODO: Replace with index
|
||||
@ -50,7 +49,13 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
* If one has a dependency that is currently in variablesToUpdate it will be skipped for now.
|
||||
*/
|
||||
private updateNextBatch() {
|
||||
for (const [name, variable] of this.variablesToUpdate) {
|
||||
// If we have nothing more to update and variable values changed we need to update scene objects that depend on these variables
|
||||
if (this.variablesToUpdate.size === 0 && this.variablesThatHaveChanged.size > 0) {
|
||||
this.notifyDependentSceneObjects();
|
||||
return;
|
||||
}
|
||||
|
||||
for (const variable of this.variablesToUpdate) {
|
||||
if (!variable.validateAndUpdate) {
|
||||
throw new Error('Variable added to variablesToUpdate but does not have validateAndUpdate');
|
||||
}
|
||||
@ -60,7 +65,7 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
continue;
|
||||
}
|
||||
|
||||
this.updating.set(name, {
|
||||
this.updating.set(variable, {
|
||||
variable,
|
||||
subscription: variable.validateAndUpdate().subscribe({
|
||||
next: () => this.validateAndUpdateCompleted(variable),
|
||||
@ -74,11 +79,11 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
* A variable has completed it's update process. This could mean that variables that depend on it can now be updated in turn.
|
||||
*/
|
||||
private validateAndUpdateCompleted(variable: SceneVariable) {
|
||||
const update = this.updating.get(variable.state.name);
|
||||
const update = this.updating.get(variable);
|
||||
update?.subscription.unsubscribe();
|
||||
|
||||
this.updating.delete(variable.state.name);
|
||||
this.variablesToUpdate.delete(variable.state.name);
|
||||
this.updating.delete(variable);
|
||||
this.variablesToUpdate.delete(variable);
|
||||
this.updateNextBatch();
|
||||
}
|
||||
|
||||
@ -94,15 +99,13 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
* Checks if the variable has any dependencies that is currently in variablesToUpdate
|
||||
*/
|
||||
private hasDependendencyInUpdateQueue(variable: SceneVariable) {
|
||||
const dependencies = this.dependencies.get(variable.state.name);
|
||||
if (!variable.variableDependency) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (dependencies) {
|
||||
for (const dep of dependencies) {
|
||||
for (const otherVariable of this.variablesToUpdate.values()) {
|
||||
if (otherVariable.state.name === dep) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (const otherVariable of this.variablesToUpdate.values()) {
|
||||
if (variable.variableDependency?.hasDependencyOn(otherVariable.state.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,11 +119,7 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
private validateAndUpdateAll() {
|
||||
for (const variable of this.state.variables) {
|
||||
if (variable.validateAndUpdate) {
|
||||
this.variablesToUpdate.set(variable.state.name, variable);
|
||||
}
|
||||
|
||||
if (variable.getDependencies) {
|
||||
this.dependencies.set(variable.state.name, variable.getDependencies());
|
||||
this.variablesToUpdate.add(variable);
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,24 +130,54 @@ export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> imp
|
||||
* This will trigger an update of all variables that depend on it.
|
||||
* */
|
||||
private onVariableValueChanged = (event: SceneVariableValueChangedEvent) => {
|
||||
const variable = event.payload;
|
||||
const variableThatChanged = event.payload;
|
||||
|
||||
this.variablesThatHaveChanged.add(variableThatChanged);
|
||||
|
||||
// Ignore this change if it is currently updating
|
||||
if (this.updating.has(variable.state.name)) {
|
||||
if (this.updating.has(variableThatChanged)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [name, deps] of this.dependencies) {
|
||||
if (deps.includes(variable.state.name)) {
|
||||
const otherVariable = this.getByName(name);
|
||||
if (otherVariable) {
|
||||
this.variablesToUpdate.set(name, otherVariable);
|
||||
// Add variables that depend on the changed variable to the update queue
|
||||
for (const otherVariable of this.state.variables) {
|
||||
if (otherVariable.variableDependency) {
|
||||
if (otherVariable.variableDependency.hasDependencyOn(variableThatChanged.state.name)) {
|
||||
this.variablesToUpdate.add(otherVariable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.updateNextBatch();
|
||||
};
|
||||
|
||||
/**
|
||||
* Walk scene object graph and update all objects that depend on variables that have changed
|
||||
*/
|
||||
private notifyDependentSceneObjects() {
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.traverseSceneAndNotify(this.parent);
|
||||
this.variablesThatHaveChanged.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursivly walk the full scene object graph and notify all objects with dependencies that include any of changed variables
|
||||
*/
|
||||
private traverseSceneAndNotify(sceneObject: SceneObject) {
|
||||
// No need to notify variables under this SceneVariableSet
|
||||
if (this === sceneObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sceneObject.variableDependency) {
|
||||
sceneObject.variableDependency.variableValuesChanged(this.variablesThatHaveChanged);
|
||||
}
|
||||
|
||||
forEachSceneObjectInState(sceneObject.state, (child) => this.traverseSceneAndNotify(child));
|
||||
}
|
||||
}
|
||||
|
||||
export interface VariableUpdateInProgress {
|
||||
|
@ -13,16 +13,9 @@ export interface SceneVariableState extends SceneObjectStatePlain {
|
||||
loading?: boolean;
|
||||
error?: any | null;
|
||||
description?: string | null;
|
||||
//text: string | string[];
|
||||
//value: string | string[]; // old current.value
|
||||
}
|
||||
|
||||
export interface SceneVariable<TState extends SceneVariableState = SceneVariableState> extends SceneObject<TState> {
|
||||
/**
|
||||
* Should return a string array of other variables this variable is using in it's definition.
|
||||
*/
|
||||
getDependencies?(): string[];
|
||||
|
||||
/**
|
||||
* This function is called on activation or when a dependency changes.
|
||||
*/
|
||||
@ -60,3 +53,17 @@ export interface SceneVariables extends SceneObject<SceneVariableSetState> {
|
||||
export class SceneVariableValueChangedEvent extends BusEventWithPayload<SceneVariable> {
|
||||
public static type = 'scene-variable-changed-value';
|
||||
}
|
||||
|
||||
export interface SceneVariableDependencyConfigLike {
|
||||
/** Return all variable names this object depend on */
|
||||
getNames(): Set<string>;
|
||||
|
||||
/** Used to check for dependency on a specific variable */
|
||||
hasDependencyOn(name: string): boolean;
|
||||
|
||||
/**
|
||||
* Will be called when any variable value has changed, not just variable names returned by getNames().
|
||||
* It is up the implementation of this interface to filter it by actual dependencies.
|
||||
**/
|
||||
variableValuesChanged(variables: Set<SceneVariable>): void;
|
||||
}
|
||||
|
@ -4,15 +4,14 @@ import { Observable, Subject } from 'rxjs';
|
||||
import { queryMetricTree } from 'app/plugins/datasource/testdata/metricTree';
|
||||
|
||||
import { SceneComponentProps } from '../../core/types';
|
||||
import { VariableDependencyConfig } from '../VariableDependencyConfig';
|
||||
import { VariableValueSelect } from '../components/VariableValueSelect';
|
||||
import { getVariableDependencies } from '../getVariableDependencies';
|
||||
import { sceneTemplateInterpolator } from '../sceneTemplateInterpolator';
|
||||
import { VariableValueOption } from '../types';
|
||||
|
||||
import { MultiValueVariable, MultiValueVariableState, VariableGetOptionsArgs } from './MultiValueVariable';
|
||||
|
||||
export interface TestVariableState extends MultiValueVariableState {
|
||||
//query: DataQuery;
|
||||
query: string;
|
||||
delayMs?: number;
|
||||
issuedQuery?: string;
|
||||
@ -25,6 +24,10 @@ export class TestVariable extends MultiValueVariable<TestVariableState> {
|
||||
private completeUpdate = new Subject<number>();
|
||||
public isGettingValues = true;
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
statePaths: ['query'],
|
||||
});
|
||||
|
||||
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
|
||||
const { delayMs } = this.state;
|
||||
|
||||
@ -69,10 +72,6 @@ export class TestVariable extends MultiValueVariable<TestVariableState> {
|
||||
this.completeUpdate.next(1);
|
||||
}
|
||||
|
||||
public getDependencies() {
|
||||
return getVariableDependencies(this.state.query);
|
||||
}
|
||||
|
||||
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => {
|
||||
return <VariableValueSelect model={model} />;
|
||||
};
|
||||
|
@ -24,6 +24,7 @@ export interface TestDataQuery extends DataQuery {
|
||||
csvFileName?: string;
|
||||
csvContent?: string;
|
||||
rawFrameContent?: string;
|
||||
seriesCount?: number;
|
||||
usa?: USAQuery;
|
||||
errorType?: 'server_panic' | 'frontend_exception' | 'frontend_observable';
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user