2022-11-16 04:36:30 -06:00
|
|
|
import { isEqual } from 'lodash';
|
2022-11-09 01:02:24 -06:00
|
|
|
import { map, Observable } from 'rxjs';
|
|
|
|
|
2022-11-16 04:36:30 -06:00
|
|
|
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
2022-11-09 01:02:24 -06:00
|
|
|
|
|
|
|
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
2022-12-01 02:48:26 -06:00
|
|
|
import { SceneObject, SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '../../core/types';
|
2022-11-09 01:02:24 -06:00
|
|
|
import {
|
|
|
|
SceneVariable,
|
|
|
|
SceneVariableValueChangedEvent,
|
|
|
|
SceneVariableState,
|
|
|
|
ValidateAndUpdateResult,
|
|
|
|
VariableValue,
|
|
|
|
VariableValueOption,
|
2022-12-13 01:20:09 -06:00
|
|
|
VariableValueCustom,
|
2022-12-01 02:48:26 -06:00
|
|
|
VariableValueSingle,
|
2022-11-09 01:02:24 -06:00
|
|
|
} from '../types';
|
|
|
|
|
|
|
|
export interface MultiValueVariableState extends SceneVariableState {
|
2022-11-16 04:36:30 -06:00
|
|
|
value: VariableValue; // old current.text
|
|
|
|
text: VariableValue; // old current.value
|
2022-11-09 01:02:24 -06:00
|
|
|
options: VariableValueOption[];
|
|
|
|
isMulti?: boolean;
|
2022-12-13 01:20:09 -06:00
|
|
|
includeAll?: boolean;
|
|
|
|
defaultToAll?: boolean;
|
|
|
|
allValue?: string;
|
|
|
|
placeholder?: string;
|
2022-11-09 01:02:24 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface VariableGetOptionsArgs {
|
|
|
|
searchFilter?: string;
|
|
|
|
}
|
|
|
|
|
|
|
|
export abstract class MultiValueVariable<TState extends MultiValueVariableState = MultiValueVariableState>
|
|
|
|
extends SceneObjectBase<TState>
|
|
|
|
implements SceneVariable<TState>
|
|
|
|
{
|
2022-12-01 02:48:26 -06:00
|
|
|
protected _urlSync: SceneObjectUrlSyncHandler<TState> = new MultiValueUrlSyncHandler(this);
|
|
|
|
|
2022-11-09 01:02:24 -06:00
|
|
|
/**
|
|
|
|
* 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.
|
|
|
|
*/
|
|
|
|
private updateValueGivenNewOptions(options: VariableValueOption[]) {
|
2022-11-16 04:36:30 -06:00
|
|
|
const stateUpdate: Partial<MultiValueVariableState> = {
|
|
|
|
options,
|
|
|
|
loading: false,
|
|
|
|
value: this.state.value,
|
|
|
|
text: this.state.text,
|
|
|
|
};
|
|
|
|
|
2022-11-09 01:02:24 -06:00
|
|
|
if (options.length === 0) {
|
|
|
|
// TODO handle the no value state
|
2022-11-16 04:36:30 -06:00
|
|
|
} else if (this.hasAllValue()) {
|
|
|
|
// If value is set to All then we keep it set to All but just store the options
|
|
|
|
} else if (this.state.isMulti) {
|
|
|
|
// If we are a multi valued variable validate the current values are among the options
|
|
|
|
const currentValues = Array.isArray(this.state.value) ? this.state.value : [this.state.value];
|
|
|
|
const validValues = currentValues.filter((v) => options.find((o) => o.value === v));
|
2022-11-09 01:02:24 -06:00
|
|
|
|
2022-11-16 04:36:30 -06:00
|
|
|
// If no valid values pick the first option
|
|
|
|
if (validValues.length === 0) {
|
2022-12-13 01:20:09 -06:00
|
|
|
const defaultState = this.getDefaultMultiState(options);
|
|
|
|
stateUpdate.value = defaultState.value;
|
|
|
|
stateUpdate.text = defaultState.text;
|
2022-11-16 04:36:30 -06:00
|
|
|
}
|
|
|
|
// We have valid values, if it's different from current valid values update current values
|
|
|
|
else if (!isEqual(validValues, this.state.value)) {
|
|
|
|
const validTexts = validValues.map((v) => options.find((o) => o.value === v)!.label);
|
|
|
|
stateUpdate.value = validValues;
|
|
|
|
stateUpdate.text = validTexts;
|
|
|
|
}
|
2022-11-09 01:02:24 -06:00
|
|
|
} else {
|
2022-11-16 04:36:30 -06:00
|
|
|
// Single valued variable
|
|
|
|
const foundCurrent = options.find((x) => x.value === this.state.value);
|
|
|
|
if (!foundCurrent) {
|
2022-12-13 01:20:09 -06:00
|
|
|
if (this.state.defaultToAll) {
|
|
|
|
stateUpdate.value = ALL_VARIABLE_VALUE;
|
|
|
|
stateUpdate.text = ALL_VARIABLE_TEXT;
|
|
|
|
} else {
|
|
|
|
// Current value is not valid. Set to first of the available options
|
|
|
|
stateUpdate.value = options[0].value;
|
|
|
|
stateUpdate.text = options[0].label;
|
|
|
|
}
|
2022-11-16 04:36:30 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remember current value and text
|
|
|
|
const { value: prevValue, text: prevText } = this.state;
|
|
|
|
|
|
|
|
// Perform state change
|
|
|
|
this.setStateHelper(stateUpdate);
|
|
|
|
|
|
|
|
// Publish value changed event only if value changed
|
|
|
|
if (stateUpdate.value !== prevValue || stateUpdate.text !== prevText || this.hasAllValue()) {
|
|
|
|
this.publishEvent(new SceneVariableValueChangedEvent(this), true);
|
2022-11-09 01:02:24 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public getValue(): VariableValue {
|
2022-11-16 04:36:30 -06:00
|
|
|
if (this.hasAllValue()) {
|
2022-12-13 01:20:09 -06:00
|
|
|
if (this.state.allValue) {
|
|
|
|
return new CustomAllValue(this.state.allValue);
|
|
|
|
}
|
|
|
|
|
2022-11-16 04:36:30 -06:00
|
|
|
return this.state.options.map((x) => x.value);
|
|
|
|
}
|
|
|
|
|
2022-11-09 01:02:24 -06:00
|
|
|
return this.state.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getValueText(): string {
|
2022-11-16 04:36:30 -06:00
|
|
|
if (this.hasAllValue()) {
|
|
|
|
return ALL_VARIABLE_TEXT;
|
|
|
|
}
|
|
|
|
|
2022-11-09 01:02:24 -06:00
|
|
|
if (Array.isArray(this.state.text)) {
|
|
|
|
return this.state.text.join(' + ');
|
|
|
|
}
|
|
|
|
|
2022-11-16 04:36:30 -06:00
|
|
|
return String(this.state.text);
|
|
|
|
}
|
|
|
|
|
|
|
|
private hasAllValue() {
|
|
|
|
const value = this.state.value;
|
|
|
|
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
|
|
|
|
}
|
|
|
|
|
2022-12-13 01:20:09 -06:00
|
|
|
private getDefaultMultiState(options: VariableValueOption[]) {
|
|
|
|
if (this.state.defaultToAll) {
|
|
|
|
return { value: [ALL_VARIABLE_VALUE], text: [ALL_VARIABLE_TEXT] };
|
|
|
|
} else {
|
|
|
|
return { value: [options[0].value], text: [options[0].label] };
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-16 04:36:30 -06:00
|
|
|
/**
|
|
|
|
* Change the value and publish SceneVariableValueChangedEvent event
|
|
|
|
*/
|
|
|
|
public changeValueTo(value: VariableValue, text?: VariableValue) {
|
2022-12-13 01:20:09 -06:00
|
|
|
// Igore if there is no change
|
|
|
|
if (value === this.state.value && text === this.state.text) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!text) {
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
text = value.map((v) => this.findLabelTextForValue(v));
|
|
|
|
} else {
|
|
|
|
text = this.findLabelTextForValue(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
// If we are a multi valued variable is cleared (empty array) we need to set the default empty state
|
|
|
|
if (value.length === 0) {
|
|
|
|
const state = this.getDefaultMultiState(this.state.options);
|
|
|
|
value = state.value;
|
|
|
|
text = state.text;
|
2022-12-01 02:48:26 -06:00
|
|
|
}
|
|
|
|
|
2022-12-13 01:20:09 -06:00
|
|
|
// If last value is the All value then replace all with it
|
|
|
|
if (value[value.length - 1] === ALL_VARIABLE_VALUE) {
|
|
|
|
value = [ALL_VARIABLE_VALUE];
|
|
|
|
text = [ALL_VARIABLE_TEXT];
|
|
|
|
}
|
|
|
|
// If the first value is the ALL value and we have other values, then remove the All value
|
|
|
|
else if (value[0] === ALL_VARIABLE_VALUE && value.length > 1) {
|
|
|
|
value.shift();
|
|
|
|
if (Array.isArray(text)) {
|
|
|
|
text.shift();
|
|
|
|
}
|
|
|
|
}
|
2022-11-09 01:02:24 -06:00
|
|
|
}
|
2022-12-13 01:20:09 -06:00
|
|
|
|
|
|
|
this.setStateHelper({ value, text, loading: false });
|
|
|
|
this.publishEvent(new SceneVariableValueChangedEvent(this), true);
|
2022-11-09 01:02:24 -06:00
|
|
|
}
|
|
|
|
|
2022-12-01 02:48:26 -06:00
|
|
|
private findLabelTextForValue(value: VariableValueSingle): VariableValueSingle {
|
|
|
|
const option = this.state.options.find((x) => x.value === value);
|
|
|
|
if (option) {
|
|
|
|
return option.label;
|
|
|
|
}
|
|
|
|
|
|
|
|
const optionByLabel = this.state.options.find((x) => x.label === value);
|
|
|
|
if (optionByLabel) {
|
|
|
|
return optionByLabel.label;
|
|
|
|
}
|
|
|
|
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
2022-11-09 01:02:24 -06:00
|
|
|
/**
|
|
|
|
* This helper function is to counter the contravariance of setState
|
|
|
|
*/
|
|
|
|
private setStateHelper(state: Partial<MultiValueVariableState>) {
|
|
|
|
const test: SceneObject<MultiValueVariableState> = this;
|
|
|
|
test.setState(state);
|
|
|
|
}
|
2022-12-13 01:20:09 -06:00
|
|
|
|
|
|
|
public getOptionsForSelect(): VariableValueOption[] {
|
|
|
|
let options = this.state.options;
|
|
|
|
|
|
|
|
if (this.state.includeAll) {
|
|
|
|
options = [{ value: ALL_VARIABLE_VALUE, label: ALL_VARIABLE_TEXT }, ...options];
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!Array.isArray(this.state.value)) {
|
|
|
|
const current = options.find((x) => x.value === this.state.value);
|
|
|
|
if (!current) {
|
|
|
|
options = [{ value: this.state.value, label: String(this.state.text) }, ...options];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return options;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The custom allValue needs a special wrapping / handling to make it not be formatted / escaped like normal values
|
|
|
|
*/
|
|
|
|
class CustomAllValue implements VariableValueCustom {
|
|
|
|
public isCustomValue: true = true;
|
|
|
|
|
|
|
|
public constructor(private _value: string) {}
|
|
|
|
|
|
|
|
public toString() {
|
|
|
|
return this._value;
|
|
|
|
}
|
2022-11-09 01:02:24 -06:00
|
|
|
}
|
2022-12-01 02:48:26 -06:00
|
|
|
|
|
|
|
export class MultiValueUrlSyncHandler<TState extends MultiValueVariableState = MultiValueVariableState>
|
|
|
|
implements SceneObjectUrlSyncHandler<TState>
|
|
|
|
{
|
|
|
|
public constructor(private _sceneObject: MultiValueVariable<TState>) {}
|
|
|
|
|
|
|
|
private getKey(): string {
|
|
|
|
return `var-${this._sceneObject.state.name}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
public getKeys(): string[] {
|
|
|
|
return [this.getKey()];
|
|
|
|
}
|
|
|
|
|
|
|
|
public getUrlState(state: TState): SceneObjectUrlValues {
|
|
|
|
let urlValue: string | string[] | null = null;
|
|
|
|
let value = state.value;
|
|
|
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
urlValue = value.map(String);
|
|
|
|
} else {
|
|
|
|
urlValue = String(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
return { [this.getKey()]: urlValue };
|
|
|
|
}
|
|
|
|
|
|
|
|
public updateFromUrl(values: SceneObjectUrlValues): void {
|
|
|
|
const urlValue = values[this.getKey()];
|
|
|
|
|
|
|
|
if (urlValue != null) {
|
|
|
|
this._sceneObject.changeValueTo(urlValue);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|