mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: SceneVariable update process (#57784)
* First baby steps * First baby steps * No progress really * Updates * no progress * refactoring * Progress on sub menu and value selectors * Some more tweaks * Lots of progress * Progress * Updates * Progress * Tweaks * Updates * Updates to variable system * Cleaner tests * Update * Some cleanup * correct test name * Renames and moves * prop rename * Fixed scene template interpolator * More tests for SceneObjectBase and fixed issue in EventBus * Updates * More tweaks * More refinements * Fixed test * Added test to EventBus * Clone all scene object arrays * Simplify * tried to merge issue * Update * added more comments to interface * temp progress * Trying to simplify things, but struggling a bit * Updated * Tweaks * Progress on fixing the select componenet and typing, and sharing code in a base class * Updated * Multi select * Simpler loading state * Update * removed failOnConsole * Removed old funcs * Moved logic from update manage to MultiValueVariable * Added tests for MultiValueVariable logic * Made value a more abstract concept to support object values * renamed func to getValueText * Refactored and moved logic to VariableSet * Added test for deactivation and query cancelling * Tweaks * Fixed lint issues
This commit is contained in:
parent
c646ff0ce3
commit
6ed35292fe
@ -14,6 +14,7 @@ interface SceneState extends SceneObjectStatePlain {
|
|||||||
title: string;
|
title: string;
|
||||||
layout: SceneObject;
|
layout: SceneObject;
|
||||||
actions?: SceneObject[];
|
actions?: SceneObject[];
|
||||||
|
subMenu?: SceneObject;
|
||||||
isEditing?: boolean;
|
isEditing?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +34,7 @@ export class Scene extends SceneObjectBase<SceneState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function SceneRenderer({ model }: SceneComponentProps<Scene>) {
|
function SceneRenderer({ model }: SceneComponentProps<Scene>) {
|
||||||
const { title, layout, actions = [], isEditing, $editor } = model.useState();
|
const { title, layout, actions = [], isEditing, $editor, subMenu } = model.useState();
|
||||||
|
|
||||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||||
|
|
||||||
@ -55,9 +56,12 @@ function SceneRenderer({ model }: SceneComponentProps<Scene>) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
|
<Page navId="scenes" pageNav={{ text: title }} layout={PageLayoutType.Canvas} toolbar={pageToolbar}>
|
||||||
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}>
|
<div style={{ flexGrow: 1, display: 'flex', flexDirection: 'column', gap: '8px' }}>
|
||||||
<layout.Component model={layout} isEditing={isEditing} />
|
{subMenu && <subMenu.Component model={subMenu} />}
|
||||||
{$editor && <$editor.Component model={$editor} isEditing={isEditing} />}
|
<div style={{ flexGrow: 1, display: 'flex', gap: '8px', overflow: 'auto' }}>
|
||||||
|
<layout.Component model={layout} isEditing={isEditing} />
|
||||||
|
{$editor && <$editor.Component model={$editor} isEditing={isEditing} />}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import { Field, Input } from '@grafana/ui';
|
|||||||
|
|
||||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||||
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
|
import { SceneComponentProps, SceneLayoutChildState } from '../core/types';
|
||||||
|
import { sceneTemplateInterpolator } from '../variables/sceneTemplateInterpolator';
|
||||||
|
|
||||||
export interface SceneCanvasTextState extends SceneLayoutChildState {
|
export interface SceneCanvasTextState extends SceneLayoutChildState {
|
||||||
text: string;
|
text: string;
|
||||||
@ -13,8 +14,10 @@ export interface SceneCanvasTextState extends SceneLayoutChildState {
|
|||||||
|
|
||||||
export class SceneCanvasText extends SceneObjectBase<SceneCanvasTextState> {
|
export class SceneCanvasText extends SceneObjectBase<SceneCanvasTextState> {
|
||||||
public static Editor = Editor;
|
public static Editor = Editor;
|
||||||
|
|
||||||
public static Component = ({ model }: SceneComponentProps<SceneCanvasText>) => {
|
public static Component = ({ model }: SceneComponentProps<SceneCanvasText>) => {
|
||||||
const { text, fontSize = 20, align = 'left' } = model.useState();
|
const { text, fontSize = 20, align = 'left' } = model.useState();
|
||||||
|
const textInterpolated = sceneTemplateInterpolator(text, model);
|
||||||
|
|
||||||
const style: CSSProperties = {
|
const style: CSSProperties = {
|
||||||
fontSize: fontSize,
|
fontSize: fontSize,
|
||||||
@ -25,7 +28,7 @@ export class SceneCanvasText extends SceneObjectBase<SceneCanvasTextState> {
|
|||||||
justifyContent: align,
|
justifyContent: align,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <div style={style}>{text}</div>;
|
return <div style={style}>{textInterpolated}</div>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
22
public/app/features/scenes/components/SceneSubMenu.tsx
Normal file
22
public/app/features/scenes/components/SceneSubMenu.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||||
|
import { SceneLayoutState, SceneComponentProps } from '../core/types';
|
||||||
|
|
||||||
|
interface SceneSubMenuState extends SceneLayoutState {}
|
||||||
|
|
||||||
|
export class SceneSubMenu extends SceneObjectBase<SceneSubMenuState> {
|
||||||
|
public static Component = SceneSubMenuRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SceneSubMenuRenderer({ model }: SceneComponentProps<SceneSubMenu>) {
|
||||||
|
const { children } = model.useState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', gap: '16px' }}>
|
||||||
|
{children.map((child) => (
|
||||||
|
<child.Component key={child.state.key} model={child} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { SceneVariableSet } from '../variables/SceneVariableSet';
|
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||||
|
|
||||||
import { SceneDataNode } from './SceneDataNode';
|
import { SceneDataNode } from './SceneDataNode';
|
||||||
import { SceneObjectBase } from './SceneObjectBase';
|
import { SceneObjectBase } from './SceneObjectBase';
|
||||||
|
@ -9,7 +9,9 @@ import { SceneComponentWrapper } from './SceneComponentWrapper';
|
|||||||
import { SceneObjectStateChangedEvent } from './events';
|
import { SceneObjectStateChangedEvent } from './events';
|
||||||
import { SceneDataState, SceneObject, SceneComponent, SceneEditor, SceneTimeRange, SceneObjectState } from './types';
|
import { SceneDataState, SceneObject, SceneComponent, SceneEditor, SceneTimeRange, SceneObjectState } from './types';
|
||||||
|
|
||||||
export abstract class SceneObjectBase<TState extends SceneObjectState = {}> implements SceneObject<TState> {
|
export abstract class SceneObjectBase<TState extends SceneObjectState = SceneObjectState>
|
||||||
|
implements SceneObject<TState>
|
||||||
|
{
|
||||||
private _isActive = false;
|
private _isActive = false;
|
||||||
private _subject = new Subject<TState>();
|
private _subject = new Subject<TState>();
|
||||||
private _state: TState;
|
private _state: TState;
|
||||||
|
@ -114,6 +114,7 @@ export interface SceneEditor extends SceneObject<SceneEditorState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneTimeRangeState extends SceneObjectStatePlain, TimeRange {}
|
export interface SceneTimeRangeState extends SceneObjectStatePlain, TimeRange {}
|
||||||
|
|
||||||
export interface SceneTimeRange extends SceneObject<SceneTimeRangeState> {
|
export interface SceneTimeRange extends SceneObject<SceneTimeRangeState> {
|
||||||
onTimeRangeChange(timeRange: TimeRange): void;
|
onTimeRangeChange(timeRange: TimeRange): void;
|
||||||
onIntervalChanged(interval: string): void;
|
onIntervalChanged(interval: string): void;
|
||||||
|
@ -3,9 +3,10 @@ import { Scene } from '../components/Scene';
|
|||||||
import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo';
|
import { getFlexLayoutTest, getScenePanelRepeaterTest } from './demo';
|
||||||
import { getNestedScene } from './nested';
|
import { getNestedScene } from './nested';
|
||||||
import { getSceneWithRows } from './sceneWithRows';
|
import { getSceneWithRows } from './sceneWithRows';
|
||||||
|
import { getVariablesDemo } from './variablesDemo';
|
||||||
|
|
||||||
export function getScenes(): Scene[] {
|
export function getScenes(): Scene[] {
|
||||||
return [getFlexLayoutTest(), getScenePanelRepeaterTest(), getNestedScene(), getSceneWithRows()];
|
return [getFlexLayoutTest(), getScenePanelRepeaterTest(), getNestedScene(), getSceneWithRows(), getVariablesDemo()];
|
||||||
}
|
}
|
||||||
|
|
||||||
const cache: Record<string, Scene> = {};
|
const cache: Record<string, Scene> = {};
|
||||||
|
63
public/app/features/scenes/scenes/variablesDemo.tsx
Normal file
63
public/app/features/scenes/scenes/variablesDemo.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import { getDefaultTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
|
import { Scene } from '../components/Scene';
|
||||||
|
import { SceneCanvasText } from '../components/SceneCanvasText';
|
||||||
|
import { SceneFlexLayout } from '../components/SceneFlexLayout';
|
||||||
|
import { SceneSubMenu } from '../components/SceneSubMenu';
|
||||||
|
import { SceneTimePicker } from '../components/SceneTimePicker';
|
||||||
|
import { SceneTimeRange } from '../core/SceneTimeRange';
|
||||||
|
import { VariableValueSelectors } from '../variables/components/VariableValueSelectors';
|
||||||
|
import { SceneVariableSet } from '../variables/sets/SceneVariableSet';
|
||||||
|
import { TestVariable } from '../variables/variants/TestVariable';
|
||||||
|
|
||||||
|
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({
|
||||||
|
name: 'server',
|
||||||
|
query: 'A.*',
|
||||||
|
value: 'server',
|
||||||
|
text: '',
|
||||||
|
delayMs: 1000,
|
||||||
|
options: [],
|
||||||
|
}),
|
||||||
|
new TestVariable({
|
||||||
|
name: 'pod',
|
||||||
|
query: 'A.$server.*',
|
||||||
|
value: 'pod',
|
||||||
|
delayMs: 1000,
|
||||||
|
text: '',
|
||||||
|
options: [],
|
||||||
|
}),
|
||||||
|
new TestVariable({
|
||||||
|
name: 'handler',
|
||||||
|
query: 'A.$server.$pod.*',
|
||||||
|
value: 'handler',
|
||||||
|
delayMs: 1000,
|
||||||
|
isMulti: true,
|
||||||
|
text: '',
|
||||||
|
options: [],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
$timeRange: new SceneTimeRange(getDefaultTimeRange()),
|
||||||
|
actions: [new SceneTimePicker({})],
|
||||||
|
subMenu: new SceneSubMenu({
|
||||||
|
children: [new VariableValueSelectors({})],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return scene;
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
import { isArray } from 'lodash';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Select, MultiSelect } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { SceneComponentProps } from '../../core/types';
|
||||||
|
import { MultiValueVariable } from '../variants/MultiValueVariable';
|
||||||
|
|
||||||
|
export function VariableValueSelect({ model }: SceneComponentProps<MultiValueVariable>) {
|
||||||
|
const { value, key, loading, isMulti, options } = model.useState();
|
||||||
|
|
||||||
|
if (isMulti) {
|
||||||
|
return (
|
||||||
|
<MultiSelect
|
||||||
|
id={key}
|
||||||
|
placeholder="Select value"
|
||||||
|
width="auto"
|
||||||
|
value={isArray(value) ? value : [value]}
|
||||||
|
allowCustomValue
|
||||||
|
isLoading={loading}
|
||||||
|
options={options}
|
||||||
|
onChange={model.onMultiValueChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
id={key}
|
||||||
|
placeholder="Select value"
|
||||||
|
width="auto"
|
||||||
|
value={value}
|
||||||
|
allowCustomValue
|
||||||
|
isLoading={loading}
|
||||||
|
options={options}
|
||||||
|
onChange={model.onSingleValueChange}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { VariableHide } from '@grafana/data';
|
||||||
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
|
import { Tooltip } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||||
|
import { SceneComponentProps, SceneObject, SceneObjectStatePlain } from '../../core/types';
|
||||||
|
import { SceneVariables, SceneVariableState } from '../types';
|
||||||
|
|
||||||
|
export class VariableValueSelectors extends SceneObjectBase<SceneObjectStatePlain> {
|
||||||
|
public static Component = VariableValueSelectorsRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariableValueSelectorsRenderer({ model }: SceneComponentProps<VariableValueSelectors>) {
|
||||||
|
const variables = getVariables(model).useState();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{variables.variables.map((variable) => (
|
||||||
|
<VariableValueSelectWrapper key={variable.state.key} variable={variable} />
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (state.hide === VariableHide.hideVariable) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="gf-form">
|
||||||
|
<VariableLabel state={state} />
|
||||||
|
<variable.Component model={variable} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function VariableLabel({ state }: { state: SceneVariableState }) {
|
||||||
|
if (state.hide === VariableHide.hideLabel) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const elementId = `var-${state.key}`;
|
||||||
|
const labelOrName = state.label ?? state.name;
|
||||||
|
|
||||||
|
if (state.description) {
|
||||||
|
return (
|
||||||
|
<Tooltip content={state.description} placement={'bottom'}>
|
||||||
|
<label
|
||||||
|
className="gf-form-label gf-form-label--variable"
|
||||||
|
data-testid={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
|
||||||
|
htmlFor={elementId}
|
||||||
|
>
|
||||||
|
{labelOrName}
|
||||||
|
</label>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
className="gf-form-label gf-form-label--variable"
|
||||||
|
data-testid={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
|
||||||
|
htmlFor={elementId}
|
||||||
|
>
|
||||||
|
{labelOrName}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
import { getVariableDependencies } from './getVariableDependencies';
|
||||||
|
|
||||||
|
describe('getVariableDependencies', () => {
|
||||||
|
it('Can get dependencies', () => {
|
||||||
|
expect(getVariableDependencies('test.$plain ${withcurly} ${withformat:csv} [[deprecated]]')).toEqual([
|
||||||
|
'plain',
|
||||||
|
'withcurly',
|
||||||
|
'withformat',
|
||||||
|
'deprecated',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,20 @@
|
|||||||
|
import { variableRegex } from 'app/features/variables/utils';
|
||||||
|
|
||||||
|
export function getVariableDependencies(stringToCheck: string): string[] {
|
||||||
|
variableRegex.lastIndex = 0;
|
||||||
|
|
||||||
|
const matches = stringToCheck.matchAll(variableRegex);
|
||||||
|
if (!matches) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const dependencies: string[] = [];
|
||||||
|
|
||||||
|
for (const match of matches) {
|
||||||
|
const [, var1, var2, , var3] = match;
|
||||||
|
const variableName = var1 || var2 || var3;
|
||||||
|
dependencies.push(variableName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return dependencies;
|
||||||
|
}
|
@ -1,7 +1,9 @@
|
|||||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
import { SceneObjectBase } from '../core/SceneObjectBase';
|
||||||
import { SceneObjectStatePlain } from '../core/types';
|
import { SceneObjectStatePlain } from '../core/types';
|
||||||
|
|
||||||
import { sceneTemplateInterpolator, SceneVariableSet, TextBoxSceneVariable } from './SceneVariableSet';
|
import { sceneTemplateInterpolator } from './sceneTemplateInterpolator';
|
||||||
|
import { SceneVariableSet } from './sets/SceneVariableSet';
|
||||||
|
import { ConstantVariable } from './variants/ConstantVariable';
|
||||||
|
|
||||||
interface TestSceneState extends SceneObjectStatePlain {
|
interface TestSceneState extends SceneObjectStatePlain {
|
||||||
nested?: TestScene;
|
nested?: TestScene;
|
||||||
@ -9,27 +11,27 @@ interface TestSceneState extends SceneObjectStatePlain {
|
|||||||
|
|
||||||
class TestScene extends SceneObjectBase<TestSceneState> {}
|
class TestScene extends SceneObjectBase<TestSceneState> {}
|
||||||
|
|
||||||
describe('SceneObject with variables', () => {
|
describe('sceneTemplateInterpolator', () => {
|
||||||
it('Should be interpolate and use closest variable', () => {
|
it('Should be interpolate and use closest variable', () => {
|
||||||
const scene = new TestScene({
|
const scene = new TestScene({
|
||||||
$variables: new SceneVariableSet({
|
$variables: new SceneVariableSet({
|
||||||
variables: [
|
variables: [
|
||||||
new TextBoxSceneVariable({
|
new ConstantVariable({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
current: { value: 'hello' },
|
value: 'hello',
|
||||||
}),
|
}),
|
||||||
new TextBoxSceneVariable({
|
new ConstantVariable({
|
||||||
name: 'atRootOnly',
|
name: 'atRootOnly',
|
||||||
current: { value: 'RootValue' },
|
value: 'RootValue',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
nested: new TestScene({
|
nested: new TestScene({
|
||||||
$variables: new SceneVariableSet({
|
$variables: new SceneVariableSet({
|
||||||
variables: [
|
variables: [
|
||||||
new TextBoxSceneVariable({
|
new ConstantVariable({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
current: { value: 'nestedValue' },
|
value: 'nestedValue',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
@ -1,18 +1,10 @@
|
|||||||
|
import { isArray } from 'lodash';
|
||||||
|
|
||||||
import { variableRegex } from 'app/features/variables/utils';
|
import { variableRegex } from 'app/features/variables/utils';
|
||||||
|
|
||||||
import { SceneObjectBase } from '../core/SceneObjectBase';
|
|
||||||
import { SceneObject } from '../core/types';
|
import { SceneObject } from '../core/types';
|
||||||
|
|
||||||
import { SceneVariable, SceneVariables, SceneVariableSetState, SceneVariableState } from './types';
|
import { SceneVariable } from './types';
|
||||||
|
|
||||||
export class TextBoxSceneVariable extends SceneObjectBase<SceneVariableState> implements SceneVariable {}
|
|
||||||
|
|
||||||
export class SceneVariableSet extends SceneObjectBase<SceneVariableSetState> implements SceneVariables {
|
|
||||||
public getVariableByName(name: string): SceneVariable | undefined {
|
|
||||||
// TODO: Replace with index
|
|
||||||
return this.state.variables.find((x) => x.state.name === name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sceneTemplateInterpolator(target: string, sceneObject: SceneObject) {
|
export function sceneTemplateInterpolator(target: string, sceneObject: SceneObject) {
|
||||||
variableRegex.lastIndex = 0;
|
variableRegex.lastIndex = 0;
|
||||||
@ -25,7 +17,13 @@ export function sceneTemplateInterpolator(target: string, sceneObject: SceneObje
|
|||||||
return match;
|
return match;
|
||||||
}
|
}
|
||||||
|
|
||||||
return variable.state.current.value;
|
const value = variable.getValue(fieldPath);
|
||||||
|
|
||||||
|
if (isArray(value)) {
|
||||||
|
return 'not supported yet';
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +37,7 @@ function lookupSceneVariable(name: string, sceneObject: SceneObject): SceneVaria
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = variables.getVariableByName(name);
|
const found = variables.getByName(name);
|
||||||
if (found) {
|
if (found) {
|
||||||
return found;
|
return found;
|
||||||
} else if (sceneObject.parent) {
|
} else if (sceneObject.parent) {
|
@ -0,0 +1,95 @@
|
|||||||
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||||
|
import { SceneObjectStatePlain } from '../../core/types';
|
||||||
|
import { TestVariable } from '../variants/TestVariable';
|
||||||
|
|
||||||
|
import { SceneVariableSet } from './SceneVariableSet';
|
||||||
|
|
||||||
|
interface TestSceneState extends SceneObjectStatePlain {
|
||||||
|
nested?: TestScene;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TestScene extends SceneObjectBase<TestSceneState> {}
|
||||||
|
|
||||||
|
describe('SceneVariableList', () => {
|
||||||
|
describe('When activated', () => {
|
||||||
|
it('Should update variables in dependency order', 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 C = new TestVariable({ name: 'C', query: 'A.$A.$B.*', value: '', text: '', options: [] });
|
||||||
|
|
||||||
|
const scene = new TestScene({
|
||||||
|
$variables: new SceneVariableSet({ variables: [C, B, A] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
|
||||||
|
// Should start variables with no dependencies
|
||||||
|
expect(A.state.loading).toBe(true);
|
||||||
|
expect(B.state.loading).toBe(undefined);
|
||||||
|
expect(C.state.loading).toBe(undefined);
|
||||||
|
|
||||||
|
// When A complete should start B
|
||||||
|
A.signalUpdateCompleted();
|
||||||
|
expect(A.state.value).toBe('AA');
|
||||||
|
expect(A.state.issuedQuery).toBe('A.*');
|
||||||
|
expect(A.state.loading).toBe(false);
|
||||||
|
expect(B.state.loading).toBe(true);
|
||||||
|
|
||||||
|
// Should wait with C as B is not completed yet
|
||||||
|
expect(C.state.loading).toBe(undefined);
|
||||||
|
|
||||||
|
// When B completes should now start C
|
||||||
|
B.signalUpdateCompleted();
|
||||||
|
expect(B.state.loading).toBe(false);
|
||||||
|
expect(C.state.loading).toBe(true);
|
||||||
|
|
||||||
|
// When C completes issue correct interpolated query containing the new values for A and B
|
||||||
|
C.signalUpdateCompleted();
|
||||||
|
expect(C.state.issuedQuery).toBe('A.AA.AAA.*');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When variable changes value', () => {
|
||||||
|
it('Should start update process', 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 C = new TestVariable({ name: 'C', query: 'A.$A.$B.*', value: '', text: '', options: [] });
|
||||||
|
|
||||||
|
const scene = new TestScene({
|
||||||
|
$variables: new SceneVariableSet({ variables: [C, B, A] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
|
||||||
|
A.signalUpdateCompleted();
|
||||||
|
B.signalUpdateCompleted();
|
||||||
|
C.signalUpdateCompleted();
|
||||||
|
|
||||||
|
// When changing A should start B but not C (yet)
|
||||||
|
A.onSingleValueChange({ value: 'AB', text: 'AB' });
|
||||||
|
|
||||||
|
expect(B.state.loading).toBe(true);
|
||||||
|
expect(C.state.loading).toBe(false);
|
||||||
|
|
||||||
|
B.signalUpdateCompleted();
|
||||||
|
expect(B.state.value).toBe('ABA');
|
||||||
|
expect(C.state.loading).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('When deactivated', () => {
|
||||||
|
it('Should cancel running variable queries', async () => {
|
||||||
|
const A = new TestVariable({ name: 'A', query: 'A.*', value: '', text: '', options: [] });
|
||||||
|
|
||||||
|
const scene = new TestScene({
|
||||||
|
$variables: new SceneVariableSet({ variables: [A] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
expect(A.isGettingValues).toBe(true);
|
||||||
|
|
||||||
|
scene.deactivate();
|
||||||
|
expect(A.isGettingValues).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
157
public/app/features/scenes/variables/sets/SceneVariableSet.ts
Normal file
157
public/app/features/scenes/variables/sets/SceneVariableSet.ts
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||||
|
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>();
|
||||||
|
|
||||||
|
/** Variables that are scheduled to be validated and updated */
|
||||||
|
private variablesToUpdate = new Map<string, SceneVariable>();
|
||||||
|
|
||||||
|
/** Cached variable dependencies */
|
||||||
|
private dependencies = new Map<string, string[]>();
|
||||||
|
|
||||||
|
/** Variables currently updating */
|
||||||
|
private updating = new Map<string, 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() {
|
||||||
|
for (const [name, variable] of this.variablesToUpdate) {
|
||||||
|
if (!variable.validateAndUpdate) {
|
||||||
|
throw new Error('Variable added to variablesToUpdate but does not have validateAndUpdate');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for variables that has dependencies that also needs updates
|
||||||
|
if (this.hasDependendencyInUpdateQueue(variable)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updating.set(name, {
|
||||||
|
variable,
|
||||||
|
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.state.name);
|
||||||
|
update?.subscription.unsubscribe();
|
||||||
|
|
||||||
|
this.updating.delete(variable.state.name);
|
||||||
|
this.variablesToUpdate.delete(variable.state.name);
|
||||||
|
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) {
|
||||||
|
variable.setState({ loading: false, error: err });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (dependencies) {
|
||||||
|
for (const dep of dependencies) {
|
||||||
|
for (const otherVariable of this.variablesToUpdate.values()) {
|
||||||
|
if (otherVariable.state.name === dep) {
|
||||||
|
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.set(variable.state.name, variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (variable.getDependencies) {
|
||||||
|
this.dependencies.set(variable.state.name, variable.getDependencies());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateNextBatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This will trigger an update of all variables that depend on it.
|
||||||
|
* */
|
||||||
|
private onVariableValueChanged = (event: SceneVariableValueChangedEvent) => {
|
||||||
|
const variable = event.payload;
|
||||||
|
|
||||||
|
// Ignore this change if it is currently updating
|
||||||
|
if (this.updating.has(variable.state.name)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateNextBatch();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VariableUpdateInProgress {
|
||||||
|
variable: SceneVariable;
|
||||||
|
subscription: Unsubscribable;
|
||||||
|
}
|
@ -1,24 +1,62 @@
|
|||||||
import { LoadingState } from '@grafana/data';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { BusEventWithPayload } from '@grafana/data';
|
||||||
import { VariableHide } from 'app/features/variables/types';
|
import { VariableHide } from 'app/features/variables/types';
|
||||||
|
|
||||||
import { SceneObject, SceneObjectStatePlain } from '../core/types';
|
import { SceneObject, SceneObjectStatePlain } from '../core/types';
|
||||||
|
|
||||||
export interface SceneVariableState extends SceneObjectStatePlain {
|
export interface SceneVariableState extends SceneObjectStatePlain {
|
||||||
name: string;
|
name: string;
|
||||||
|
label?: string;
|
||||||
hide?: VariableHide;
|
hide?: VariableHide;
|
||||||
skipUrlSync?: boolean;
|
skipUrlSync?: boolean;
|
||||||
state?: LoadingState;
|
loading?: boolean;
|
||||||
error?: any | null;
|
error?: any | null;
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
current: { value: string; text?: string };
|
//text: string | string[];
|
||||||
|
//value: string | string[]; // old current.value
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneVariable extends SceneObject<SceneVariableState> {}
|
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.
|
||||||
|
*/
|
||||||
|
validateAndUpdate?(): Observable<ValidateAndUpdateResult>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return the value for the given field path
|
||||||
|
*/
|
||||||
|
getValue(fieldPath?: string): VariableValue;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Should return the value display text, used by the "text" formatter
|
||||||
|
* Example: ${podId:text}
|
||||||
|
* Useful for variables that have non user friendly values but friendly display text names.
|
||||||
|
*/
|
||||||
|
getValueText?(): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VariableValue = string | string[] | number | number[] | boolean | boolean[] | null | undefined;
|
||||||
|
|
||||||
|
export interface ValidateAndUpdateResult {}
|
||||||
|
export interface VariableValueOption {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface SceneVariableSetState extends SceneObjectStatePlain {
|
export interface SceneVariableSetState extends SceneObjectStatePlain {
|
||||||
variables: SceneVariable[];
|
variables: SceneVariable[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneVariables extends SceneObject<SceneVariableSetState> {
|
export interface SceneVariables extends SceneObject<SceneVariableSetState> {
|
||||||
getVariableByName(name: string): SceneVariable | undefined;
|
getByName(name: string): SceneVariable | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SceneVariableValueChangedEvent extends BusEventWithPayload<SceneVariable> {
|
||||||
|
public static type = 'scene-variable-changed-value';
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,15 @@
|
|||||||
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||||
|
import { SceneVariable, SceneVariableState, VariableValue } from '../types';
|
||||||
|
|
||||||
|
export interface ConstantVariableState extends SceneVariableState {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ConstantVariable
|
||||||
|
extends SceneObjectBase<ConstantVariableState>
|
||||||
|
implements SceneVariable<ConstantVariableState>
|
||||||
|
{
|
||||||
|
public getValue(): VariableValue {
|
||||||
|
return this.state.value;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
import { lastValueFrom, Observable, of } from 'rxjs';
|
||||||
|
|
||||||
|
import { VariableValueOption } from '../types';
|
||||||
|
import { MultiValueVariable, MultiValueVariableState, VariableGetOptionsArgs } from '../variants/MultiValueVariable';
|
||||||
|
|
||||||
|
export interface ExampleVariableState extends MultiValueVariableState {
|
||||||
|
optionsToReturn: VariableValueOption[];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExampleVariable extends MultiValueVariable<ExampleVariableState> {
|
||||||
|
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
|
||||||
|
return of(this.state.optionsToReturn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('MultiValueVariable', () => {
|
||||||
|
describe('When validateAndUpdate is called', () => {
|
||||||
|
it('Should pick first value if current value is not valid', async () => {
|
||||||
|
const variable = new ExampleVariable({
|
||||||
|
name: 'test',
|
||||||
|
options: [],
|
||||||
|
optionsToReturn: [
|
||||||
|
{ label: 'B', value: 'B' },
|
||||||
|
{ label: 'C', value: 'C' },
|
||||||
|
],
|
||||||
|
value: 'A',
|
||||||
|
text: 'A',
|
||||||
|
});
|
||||||
|
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
|
||||||
|
expect(variable.state.value).toBe('B');
|
||||||
|
expect(variable.state.text).toBe('B');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should keep current value if current value is valid', async () => {
|
||||||
|
const variable = new ExampleVariable({
|
||||||
|
name: 'test',
|
||||||
|
options: [],
|
||||||
|
optionsToReturn: [{ label: 'A', value: 'A' }],
|
||||||
|
value: 'A',
|
||||||
|
text: 'A',
|
||||||
|
});
|
||||||
|
|
||||||
|
await lastValueFrom(variable.validateAndUpdate());
|
||||||
|
|
||||||
|
expect(variable.state.value).toBe('A');
|
||||||
|
expect(variable.state.text).toBe('A');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,106 @@
|
|||||||
|
import { map, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
||||||
|
import { SceneObject } from '../../core/types';
|
||||||
|
import {
|
||||||
|
SceneVariable,
|
||||||
|
SceneVariableValueChangedEvent,
|
||||||
|
SceneVariableState,
|
||||||
|
ValidateAndUpdateResult,
|
||||||
|
VariableValue,
|
||||||
|
VariableValueOption,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
|
export interface MultiValueVariableState extends SceneVariableState {
|
||||||
|
value: string | string[]; // old current.text
|
||||||
|
text: string | string[]; // old current.value
|
||||||
|
options: VariableValueOption[];
|
||||||
|
isMulti?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VariableGetOptionsArgs {
|
||||||
|
searchFilter?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class MultiValueVariable<TState extends MultiValueVariableState = MultiValueVariableState>
|
||||||
|
extends SceneObjectBase<TState>
|
||||||
|
implements SceneVariable<TState>
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The source of value options.
|
||||||
|
*/
|
||||||
|
public abstract getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function is called on when SceneVariableSet is activated or when a dependency changes.
|
||||||
|
*/
|
||||||
|
public validateAndUpdate(): Observable<ValidateAndUpdateResult> {
|
||||||
|
return this.getValueOptions({}).pipe(
|
||||||
|
map((options) => {
|
||||||
|
this.updateValueGivenNewOptions(options);
|
||||||
|
return {};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if current value is valid given new options. If not update the value.
|
||||||
|
* TODO: Handle multi valued variables
|
||||||
|
*/
|
||||||
|
private updateValueGivenNewOptions(options: VariableValueOption[]) {
|
||||||
|
if (options.length === 0) {
|
||||||
|
// TODO handle the no value state
|
||||||
|
this.setStateHelper({ value: '?', loading: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const foundCurrent = options.find((x) => x.value === this.state.value);
|
||||||
|
if (!foundCurrent) {
|
||||||
|
// Current value is not valid. Set to first of the available options
|
||||||
|
this.changeValueAndPublishChangeEvent(options[0].value, options[0].label);
|
||||||
|
} else {
|
||||||
|
// current value is still ok
|
||||||
|
this.setStateHelper({ loading: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValue(): VariableValue {
|
||||||
|
return this.state.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getValueText(): string {
|
||||||
|
if (Array.isArray(this.state.text)) {
|
||||||
|
return this.state.text.join(' + ');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.state.text;
|
||||||
|
}
|
||||||
|
|
||||||
|
private changeValueAndPublishChangeEvent(value: string | string[], text: string | string[]) {
|
||||||
|
if (value !== this.state.value || text !== this.state.text) {
|
||||||
|
this.setStateHelper({ value, text, loading: false });
|
||||||
|
this.publishEvent(new SceneVariableValueChangedEvent(this), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This helper function is to counter the contravariance of setState
|
||||||
|
*/
|
||||||
|
private setStateHelper(state: Partial<MultiValueVariableState>) {
|
||||||
|
const test: SceneObject<MultiValueVariableState> = this;
|
||||||
|
test.setState(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public onSingleValueChange = (value: SelectableValue<string>) => {
|
||||||
|
this.changeValueAndPublishChangeEvent(value.value!, value.label!);
|
||||||
|
};
|
||||||
|
|
||||||
|
public onMultiValueChange = (value: Array<SelectableValue<string>>) => {
|
||||||
|
this.changeValueAndPublishChangeEvent(
|
||||||
|
value.map((v) => v.value!),
|
||||||
|
value.map((v) => v.label!)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,79 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Observable, Subject } from 'rxjs';
|
||||||
|
|
||||||
|
import { queryMetricTree } from 'app/plugins/datasource/testdata/metricTree';
|
||||||
|
|
||||||
|
import { SceneComponentProps } from '../../core/types';
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This variable is only designed for unit tests and potentially e2e tests.
|
||||||
|
*/
|
||||||
|
export class TestVariable extends MultiValueVariable<TestVariableState> {
|
||||||
|
private completeUpdate = new Subject<number>();
|
||||||
|
public isGettingValues = true;
|
||||||
|
|
||||||
|
public getValueOptions(args: VariableGetOptionsArgs): Observable<VariableValueOption[]> {
|
||||||
|
const { delayMs } = this.state;
|
||||||
|
|
||||||
|
return new Observable<VariableValueOption[]>((observer) => {
|
||||||
|
this.setState({ loading: true });
|
||||||
|
|
||||||
|
this.completeUpdate.subscribe({
|
||||||
|
next: () => {
|
||||||
|
observer.next(this.issueQuery());
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let timeout: NodeJS.Timeout | undefined;
|
||||||
|
|
||||||
|
if (delayMs) {
|
||||||
|
timeout = setTimeout(() => this.signalUpdateCompleted(), delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isGettingValues = true;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
this.isGettingValues = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private issueQuery() {
|
||||||
|
const interpolatedQuery = sceneTemplateInterpolator(this.state.query, this);
|
||||||
|
const options = queryMetricTree(interpolatedQuery).map((x) => ({ label: x.name, value: x.name }));
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
issuedQuery: interpolatedQuery,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Useful from tests */
|
||||||
|
public signalUpdateCompleted() {
|
||||||
|
this.completeUpdate.next(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDependencies() {
|
||||||
|
return getVariableDependencies(this.state.query);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Component = ({ model }: SceneComponentProps<MultiValueVariable>) => {
|
||||||
|
return <VariableValueSelect model={model} />;
|
||||||
|
};
|
||||||
|
}
|
@ -10,7 +10,7 @@ interface Props {
|
|||||||
text: string;
|
text: string;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
disabled: boolean; // todo: optional?
|
disabled?: boolean;
|
||||||
/**
|
/**
|
||||||
* htmlFor, needed for the label
|
* htmlFor, needed for the label
|
||||||
*/
|
*/
|
||||||
|
@ -8,6 +8,7 @@ import { matchers } from './matchers';
|
|||||||
failOnConsole({
|
failOnConsole({
|
||||||
shouldFailOnLog: true,
|
shouldFailOnLog: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect.extend(matchers);
|
expect.extend(matchers);
|
||||||
|
|
||||||
i18next.use(initReactI18next).init({
|
i18next.use(initReactI18next).init({
|
||||||
|
Loading…
Reference in New Issue
Block a user