Skip to content

Commit

Permalink
VariableDependencyConfig: Support * to extract dependencies from ev…
Browse files Browse the repository at this point in the history
…ery state path (#599)
  • Loading branch information
ivanortegaalba authored Feb 14, 2024
1 parent f624fe9 commit 25d5e08
Show file tree
Hide file tree
Showing 3 changed files with 31 additions and 22 deletions.
18 changes: 8 additions & 10 deletions docusaurus/docs/advanced-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ class TextInterpolator extends SceneObjectBase<TextInterpolatorState> {

`VariableDependencyConfig` accepts an object with the following configuration options:

- `statePaths` - Configures which properties of the object state can contain variables.
- `statePaths` - Configures which properties of the object state can contain variables. Use `['*']` to refer to any property of the object state.
- `onReferencedVariableValueChanged` - Configures a callback that will be executed when variable(s) that the object depends on are changed.

:::note
Expand Down Expand Up @@ -154,8 +154,8 @@ The preceding code will render a scene with a template variable, text input, and
You can register a custom variable macro using `sceneUtils.registerVariableMacro`. A variable macro is useful for variable expressions you want to be evaluted dynamically based on some context. Examples of core variables
that are implemented as macros.

* ${__url.params:include:var-from,var-to}
* ${__user.login}
- ${\_\_url.params:include:var-from,var-to}
- ${\_\_user.login}

Example:

Expand Down Expand Up @@ -221,14 +221,12 @@ Variables: A, B, C (B depends on A, C depends on B). A depends on time range so

SceneQueryRunner with a query that depends on variable C

* 1. Time range changes value
* 2. Variable A starts loading
* 3. SceneQueryRunner responds to time range change tries to start new query, but before new query is issued calls `variableDependency.hasDependencyInLoadingState`. This checks if variable C is loading wich it is not, so then checks if variable B is loading (since it's a dependency of C), which it is not so then checks A, A is loading so it returns true and SceneQueryRunner will skip issuing a new query. When this happens the VariableDependencyConfig will set an internal flag that it is waiting for a variable dependency, this makes sure that the moment a next variable completes onVariableUpdateCompleted is called (no matter if the variable that was completed is a direct dependency or if it has changed value or not, we just care that it completed loading).
* 4. Variable A completes loading. The options (possible values) are the same so no change value.
* 5. SceneQueryRunner's VariableDependencyConfig receives the notification that variable A has completed it's loading phase, since it is in a waiting for variables state it will call the onVariableUpdateCompleted callback even though A is not a direct dependency and it has not changed value.
- 1. Time range changes value
- 2. Variable A starts loading
- 3. SceneQueryRunner responds to time range change tries to start new query, but before new query is issued calls `variableDependency.hasDependencyInLoadingState`. This checks if variable C is loading wich it is not, so then checks if variable B is loading (since it's a dependency of C), which it is not so then checks A, A is loading so it returns true and SceneQueryRunner will skip issuing a new query. When this happens the VariableDependencyConfig will set an internal flag that it is waiting for a variable dependency, this makes sure that the moment a next variable completes onVariableUpdateCompleted is called (no matter if the variable that was completed is a direct dependency or if it has changed value or not, we just care that it completed loading).
- 4. Variable A completes loading. The options (possible values) are the same so no change value.
- 5. SceneQueryRunner's VariableDependencyConfig receives the notification that variable A has completed it's loading phase, since it is in a waiting for variables state it will call the onVariableUpdateCompleted callback even though A is not a direct dependency and it has not changed value.

## Source code

[View the example source code](https://github.com/grafana/scenes/tree/main/docusaurus/docs/advanced-variables.tsx)


14 changes: 11 additions & 3 deletions packages/scenes/src/variables/VariableDependencyConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,19 @@ class TestObj extends SceneObjectBase<TestState> {
describe('VariableDependencyConfig', () => {
it('Should be able to extract dependencies from all state', () => {
const sceneObj = new TestObj();
const deps = new VariableDependencyConfig(sceneObj, {});
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['*'] });

expect(deps.getNames()).toEqual(new Set(['queryVarA', 'queryVarB', 'nestedVarA', 'otherPropA']));
});

it('Should not extract dependencies from all state if no statePaths or variableName is defined', () => {
const sceneObj = new TestObj();
const deps = new VariableDependencyConfig(sceneObj, {});

expect(deps.scanCount).toBe(0);
expect(deps.getNames()).toEqual(new Set([]));
});

it('Should be able to extract dependencies from statePaths', () => {
const sceneObj = new TestObj();
const deps = new VariableDependencyConfig(sceneObj, { statePaths: ['query', 'nested'] });
Expand Down Expand Up @@ -90,7 +98,7 @@ describe('VariableDependencyConfig', () => {
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 });
const deps = new VariableDependencyConfig(sceneObj, { onReferencedVariableValueChanged: fn, statePaths: ['*'] });

deps.variableUpdateCompleted(new ConstantVariable({ name: 'not-dep', value: '1' }), true);
expect(fn.mock.calls.length).toBe(0);
Expand All @@ -102,7 +110,7 @@ describe('VariableDependencyConfig', () => {
it('Can update explicit depenendencies', () => {
const sceneObj = new TestObj();
const fn = jest.fn();
const deps = new VariableDependencyConfig(sceneObj, { onReferencedVariableValueChanged: fn });
const deps = new VariableDependencyConfig(sceneObj, { onReferencedVariableValueChanged: fn, statePaths: ['*'] });

deps.variableUpdateCompleted(new ConstantVariable({ name: 'not-dep', value: '1' }), true);
expect(fn.mock.calls.length).toBe(0);
Expand Down
21 changes: 12 additions & 9 deletions packages/scenes/src/variables/VariableDependencyConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ interface VariableDependencyConfigOptions<TState extends SceneObjectState> {
/**
* State paths to scan / extract variable dependencies from. Leave empty to scan all paths.
*/
statePaths?: Array<keyof TState>;
statePaths?: Array<keyof TState | '*'>;

/**
* Explicit list of variable names to depend on. Leave empty to scan state for dependencies.
Expand Down Expand Up @@ -40,7 +40,7 @@ interface VariableDependencyConfigOptions<TState extends SceneObjectState> {
export class VariableDependencyConfig<TState extends SceneObjectState> implements SceneVariableDependencyConfigLike {
private _state: TState | undefined;
private _dependencies = new Set<string>();
private _statePaths?: Array<keyof TState>;
private _statePaths?: Array<keyof TState | '*'>;
private _isWaitingForVariables = false;

public scanCount = 0;
Expand Down Expand Up @@ -123,7 +123,7 @@ export class VariableDependencyConfig<TState extends SceneObjectState> implement
if (newState !== prevState) {
if (this._statePaths) {
for (const path of this._statePaths) {
if (newState[path] !== prevState[path]) {
if (path === '*' || newState[path] !== prevState[path]) {
this.scanStateForDependencies(newState);
break;
}
Expand All @@ -144,7 +144,7 @@ export class VariableDependencyConfig<TState extends SceneObjectState> implement
this.scanStateForDependencies(this._state!);
}

public setPaths(paths: Array<keyof TState>) {
public setPaths(paths: Array<keyof TState | '*'>) {
this._statePaths = paths;
}

Expand All @@ -159,13 +159,16 @@ export class VariableDependencyConfig<TState extends SceneObjectState> implement
} else {
if (this._statePaths) {
for (const path of this._statePaths) {
const value = state[path];
if (value) {
this.extractVariablesFrom(value);
if (path === '*') {
this.extractVariablesFrom(state);
break;
} else {
const value = state[path];
if (value) {
this.extractVariablesFrom(value);
}
}
}
} else {
this.extractVariablesFrom(state);
}
}
}
Expand Down

0 comments on commit 25d5e08

Please sign in to comment.