grafana/public/app/features/scenes/variables/sets/SceneVariableSet.ts
Dominik Prokop 1758ddd457
Scenes: Add query variable support (#59553)
* WIP first attempt to query variable

* regex issue repro demo

* Refresh variable on time range change if refresh specified

* Instantiate variable runner when updating query variable options

* Simplify runners getTarget interface

* Fix issue with variable ot being updated correctly after other variable changed

* Add templateSrv.replace compatibility with query variable

* QueryVariable: use datasource variable as source

* use proper format

* Make sure variables set is correctly updated when query variable errors

* Do not destruct scopedVars when using sceneGraph.interpolate in templateSrv

* Add support for Legacy variables (metricFindQuery)

* Review

* Fix lint

* Test: Add unit for datasource by variable

* test: Add unit for datasource as var

* query: delegate interpolation to datasourceSrv

* Cleanup

Co-authored-by: Ivan Ortega <ivanortegaalba@gmail.com>
2022-12-12 04:01:27 -08:00

199 lines
6.2 KiB
TypeScript

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 Set<SceneVariable>();
/** Variables that are scheduled to be validated and updated */
private variablesToUpdate = new Set<SceneVariable>();
/** Variables currently updating */
private updating = new Map<SceneVariable, VariableUpdateInProgress>();
public getByName(name: string): SceneVariable | undefined {
// TODO: Replace with index
return this.state.variables.find((x) => x.state.name === name);
}
/**
* Subscribes to child variable value changes
* And starts the variable value validation process
*/
public activate(): void {
super.activate();
// Subscribe to changes to child variables
this._subs.add(this.subscribeToEvent(SceneVariableValueChangedEvent, this.onVariableValueChanged));
this.validateAndUpdateAll();
}
/**
* Cancel all currently running updates
*/
public deactivate(): void {
super.deactivate();
this.variablesToUpdate.clear();
for (const update of this.updating.values()) {
update.subscription?.unsubscribe();
}
}
/**
* This loops through variablesToUpdate and update all that that can.
* If one has a dependency that is currently in variablesToUpdate it will be skipped for now.
*/
private updateNextBatch() {
// 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');
}
// Ignore it if it's already started
if (this.updating.has(variable)) {
continue;
}
// Wait for variables that has dependencies that also needs updates
if (this.hasDependendencyInUpdateQueue(variable)) {
continue;
}
const variableToUpdate: VariableUpdateInProgress = {
variable,
};
this.updating.set(variable, variableToUpdate);
variableToUpdate.subscription = variable.validateAndUpdate().subscribe({
next: () => this.validateAndUpdateCompleted(variable),
error: (err) => this.handleVariableError(variable, err),
});
}
}
/**
* 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);
update?.subscription?.unsubscribe();
this.updating.delete(variable);
this.variablesToUpdate.delete(variable);
this.updateNextBatch();
}
/**
* TODO handle this properly (and show error in UI).
* Not sure if this should be handled here on in MultiValueVariable
*/
private handleVariableError(variable: SceneVariable, err: Error) {
const update = this.updating.get(variable);
update?.subscription?.unsubscribe();
this.updating.delete(variable);
this.variablesToUpdate.delete(variable);
variable.setState({ loading: false, error: err });
}
/**
* Checks if the variable has any dependencies that is currently in variablesToUpdate
*/
private hasDependendencyInUpdateQueue(variable: SceneVariable) {
if (!variable.variableDependency) {
return false;
}
for (const otherVariable of this.variablesToUpdate.values()) {
if (variable.variableDependency?.hasDependencyOn(otherVariable.state.name)) {
return true;
}
}
return false;
}
/**
* Extract dependencies from all variables and add those that needs update to the variablesToUpdate map
* Then it will start the update process.
*/
private validateAndUpdateAll() {
for (const variable of this.state.variables) {
if (variable.validateAndUpdate) {
this.variablesToUpdate.add(variable);
}
}
this.updateNextBatch();
}
/**
* This will trigger an update of all variables that depend on it.
* */
private onVariableValueChanged = (event: SceneVariableValueChangedEvent) => {
const variableThatChanged = event.payload;
this.variablesThatHaveChanged.add(variableThatChanged);
// Ignore this change if it is currently updating
if (this.updating.has(variableThatChanged)) {
return;
}
// 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 {
variable: SceneVariable;
subscription?: Unsubscribable;
}