mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 04:00:55 +08:00
Variables: Use new format registry from templateSrv (#58813)
* Variables: Use new format registry from templateSrv * Updated comment * Fixed e2e
This commit is contained in:
@ -4708,13 +4708,6 @@ exports[`better eslint`] = {
|
|||||||
"public/app/features/teams/state/selectors.ts:5381": [
|
"public/app/features/teams/state/selectors.ts:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
"public/app/features/templating/formatRegistry.ts:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
|
||||||
[0, 0, 0, "Do not use any type assertions.", "2"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
|
||||||
],
|
|
||||||
"public/app/features/templating/template_srv.mock.ts:5381": [
|
"public/app/features/templating/template_srv.mock.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -33,7 +33,7 @@ e2e.scenario({
|
|||||||
`Server:singlequote = 'A\\'A"A','BB\\B','CCC'`,
|
`Server:singlequote = 'A\\'A"A','BB\\B','CCC'`,
|
||||||
`Server:doublequote = "A'A\\"A","BB\\B","CCC"`,
|
`Server:doublequote = "A'A\\"A","BB\\B","CCC"`,
|
||||||
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
|
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
|
||||||
`Server:date = null`,
|
`Server:date = NaN`,
|
||||||
`Server:text = All`,
|
`Server:text = All`,
|
||||||
`Server:queryparam = var-Server=All`,
|
`Server:queryparam = var-Server=All`,
|
||||||
`1 < 2`,
|
`1 < 2`,
|
||||||
|
@ -2,19 +2,19 @@ import { property } from 'lodash';
|
|||||||
|
|
||||||
import { ScopedVar } from '@grafana/data';
|
import { ScopedVar } from '@grafana/data';
|
||||||
|
|
||||||
import { SceneObjectBase } from '../../core/SceneObjectBase';
|
import { VariableValue } from '../types';
|
||||||
import { SceneVariable, SceneVariableState, VariableValue } from '../types';
|
|
||||||
|
|
||||||
export interface ScopedVarsProxyVariableState extends SceneVariableState {
|
import { FormatVariable } from './formatRegistry';
|
||||||
value: ScopedVar;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ScopedVarsVariable
|
export class ScopedVarsVariable implements FormatVariable {
|
||||||
extends SceneObjectBase<ScopedVarsProxyVariableState>
|
|
||||||
implements SceneVariable<ScopedVarsProxyVariableState>
|
|
||||||
{
|
|
||||||
private static fieldAccessorCache: FieldAccessorCache = {};
|
private static fieldAccessorCache: FieldAccessorCache = {};
|
||||||
|
|
||||||
|
public state: { name: string; value: ScopedVar };
|
||||||
|
|
||||||
|
public constructor(name: string, value: ScopedVar) {
|
||||||
|
this.state = { name, value };
|
||||||
|
}
|
||||||
|
|
||||||
public getValue(fieldPath: string): VariableValue {
|
public getValue(fieldPath: string): VariableValue {
|
||||||
let { value } = this.state;
|
let { value } = this.state;
|
||||||
let realValue = value.value;
|
let realValue = value.value;
|
||||||
@ -63,9 +63,10 @@ let scopedVarsVariable: ScopedVarsVariable | undefined;
|
|||||||
*/
|
*/
|
||||||
export function getSceneVariableForScopedVar(name: string, value: ScopedVar) {
|
export function getSceneVariableForScopedVar(name: string, value: ScopedVar) {
|
||||||
if (!scopedVarsVariable) {
|
if (!scopedVarsVariable) {
|
||||||
scopedVarsVariable = new ScopedVarsVariable({ name, value });
|
scopedVarsVariable = new ScopedVarsVariable(name, value);
|
||||||
} else {
|
} else {
|
||||||
scopedVarsVariable.setState({ name, value });
|
scopedVarsVariable.state.name = name;
|
||||||
|
scopedVarsVariable.state.value = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
return scopedVarsVariable;
|
return scopedVarsVariable;
|
||||||
|
@ -4,10 +4,24 @@ import { dateTime, Registry, RegistryItem, textUtil } from '@grafana/data';
|
|||||||
import kbn from 'app/core/utils/kbn';
|
import kbn from 'app/core/utils/kbn';
|
||||||
import { ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
import { ALL_VARIABLE_VALUE } from 'app/features/variables/constants';
|
||||||
|
|
||||||
import { SceneVariable, VariableValue, VariableValueSingle } from '../types';
|
import { VariableValue, VariableValueSingle } from '../types';
|
||||||
|
|
||||||
export interface FormatRegistryItem extends RegistryItem {
|
export interface FormatRegistryItem extends RegistryItem {
|
||||||
formatter(value: VariableValue, args: string[], variable: SceneVariable): string;
|
formatter(value: VariableValue, args: string[], variable: FormatVariable): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slimmed down version of the SceneVariable interface so that it only contains what the formatters actually use.
|
||||||
|
* This is useful as we have some implementations of this interface that does not need to be full scene objects.
|
||||||
|
* For example ScopedVarsVariable and LegacyVariableWrapper.
|
||||||
|
*/
|
||||||
|
export interface FormatVariable {
|
||||||
|
state: {
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
getValue(fieldPath?: string): VariableValue | undefined | null;
|
||||||
|
getValueText?(fieldPath?: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum FormatRegistryID {
|
export enum FormatRegistryID {
|
||||||
@ -231,14 +245,16 @@ export const formatRegistry = new Registry<FormatRegistryItem>(() => {
|
|||||||
name: 'Date',
|
name: 'Date',
|
||||||
description: 'Format date in different ways',
|
description: 'Format date in different ways',
|
||||||
formatter: (value, args) => {
|
formatter: (value, args) => {
|
||||||
let nrValue = 0;
|
let nrValue = NaN;
|
||||||
|
|
||||||
if (typeof value === 'number') {
|
if (typeof value === 'number') {
|
||||||
nrValue = value;
|
nrValue = value;
|
||||||
} else if (typeof value === 'string') {
|
} else if (typeof value === 'string') {
|
||||||
nrValue = parseInt(value, 10);
|
nrValue = parseInt(value, 10);
|
||||||
} else {
|
}
|
||||||
return '';
|
|
||||||
|
if (isNaN(nrValue)) {
|
||||||
|
return 'NaN';
|
||||||
}
|
}
|
||||||
|
|
||||||
const arg = args[0] ?? 'iso';
|
const arg = args[0] ?? 'iso';
|
||||||
@ -270,10 +286,6 @@ export const formatRegistry = new Registry<FormatRegistryItem>(() => {
|
|||||||
name: 'Text',
|
name: 'Text',
|
||||||
description: 'Format variables in their text representation. Example in multi-variable scenario A + B + C.',
|
description: 'Format variables in their text representation. Example in multi-variable scenario A + B + C.',
|
||||||
formatter: (value, _args, variable) => {
|
formatter: (value, _args, variable) => {
|
||||||
// if (typeof options.text === 'string') {
|
|
||||||
// return options.value === ALL_VARIABLE_VALUE ? ALL_VARIABLE_TEXT : options.text;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (variable.getValueText) {
|
if (variable.getValueText) {
|
||||||
return variable.getValueText();
|
return variable.getValueText();
|
||||||
}
|
}
|
||||||
|
@ -4,10 +4,10 @@ import { variableRegex } from 'app/features/variables/utils';
|
|||||||
|
|
||||||
import { EmptyVariableSet, sceneGraph } from '../../core/sceneGraph';
|
import { EmptyVariableSet, sceneGraph } from '../../core/sceneGraph';
|
||||||
import { SceneObject } from '../../core/types';
|
import { SceneObject } from '../../core/types';
|
||||||
import { SceneVariable, VariableValue } from '../types';
|
import { VariableValue } from '../types';
|
||||||
|
|
||||||
import { getSceneVariableForScopedVar } from './ScopedVarsVariable';
|
import { getSceneVariableForScopedVar } from './ScopedVarsVariable';
|
||||||
import { formatRegistry, FormatRegistryID } from './formatRegistry';
|
import { formatRegistry, FormatRegistryID, FormatVariable } from './formatRegistry';
|
||||||
|
|
||||||
type CustomFormatterFn = (
|
type CustomFormatterFn = (
|
||||||
value: unknown,
|
value: unknown,
|
||||||
@ -42,7 +42,7 @@ export function sceneInterpolator(
|
|||||||
return target.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
return target.replace(variableRegex, (match, var1, var2, fmt2, var3, fieldPath, fmt3) => {
|
||||||
const variableName = var1 || var2 || var3;
|
const variableName = var1 || var2 || var3;
|
||||||
const fmt = fmt2 || fmt3 || format;
|
const fmt = fmt2 || fmt3 || format;
|
||||||
let variable: SceneVariable | undefined | null;
|
let variable: FormatVariable | undefined | null;
|
||||||
|
|
||||||
if (scopedVars && scopedVars[variableName]) {
|
if (scopedVars && scopedVars[variableName]) {
|
||||||
variable = getSceneVariableForScopedVar(variableName, scopedVars[variableName]);
|
variable = getSceneVariableForScopedVar(variableName, scopedVars[variableName]);
|
||||||
@ -58,7 +58,7 @@ export function sceneInterpolator(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function lookupSceneVariable(name: string, sceneObject: SceneObject): SceneVariable | null | undefined {
|
function lookupSceneVariable(name: string, sceneObject: SceneObject): FormatVariable | null | undefined {
|
||||||
const variables = sceneObject.state.$variables;
|
const variables = sceneObject.state.$variables;
|
||||||
if (!variables) {
|
if (!variables) {
|
||||||
if (sceneObject.parent) {
|
if (sceneObject.parent) {
|
||||||
@ -79,7 +79,7 @@ function lookupSceneVariable(name: string, sceneObject: SceneObject): SceneVaria
|
|||||||
}
|
}
|
||||||
|
|
||||||
function formatValue(
|
function formatValue(
|
||||||
variable: SceneVariable,
|
variable: FormatVariable,
|
||||||
value: VariableValue | undefined | null,
|
value: VariableValue | undefined | null,
|
||||||
formatNameOrFn: string | CustomFormatterFn
|
formatNameOrFn: string | CustomFormatterFn
|
||||||
): string {
|
): string {
|
||||||
|
@ -31,7 +31,7 @@ export interface SceneVariable<TState extends SceneVariableState = SceneVariable
|
|||||||
* Example: ${podId:text}
|
* Example: ${podId:text}
|
||||||
* Useful for variables that have non user friendly values but friendly display text names.
|
* Useful for variables that have non user friendly values but friendly display text names.
|
||||||
*/
|
*/
|
||||||
getValueText?(): string;
|
getValueText?(fieldPath?: string): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type VariableValue = VariableValueSingle | VariableValueSingle[];
|
export type VariableValue = VariableValueSingle | VariableValueSingle[];
|
||||||
|
53
public/app/features/templating/LegacyVariableWrapper.ts
Normal file
53
public/app/features/templating/LegacyVariableWrapper.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { FormatVariable } from '../scenes/variables/interpolation/formatRegistry';
|
||||||
|
import { VariableValue } from '../scenes/variables/types';
|
||||||
|
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
|
||||||
|
|
||||||
|
export class LegacyVariableWrapper implements FormatVariable {
|
||||||
|
state: { name: string; value: VariableValue; text: VariableValue };
|
||||||
|
|
||||||
|
constructor(name: string, value: VariableValue, text: VariableValue) {
|
||||||
|
this.state = { name, value, text };
|
||||||
|
}
|
||||||
|
|
||||||
|
getValue(_fieldPath: string): VariableValue {
|
||||||
|
let { value } = this.state;
|
||||||
|
|
||||||
|
if (value === 'string' || value === 'number' || value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
getValueText(): string {
|
||||||
|
const { value, text } = this.state;
|
||||||
|
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
return value === ALL_VARIABLE_VALUE ? ALL_VARIABLE_TEXT : text;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(text)) {
|
||||||
|
return text.join(' + ');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('value', text);
|
||||||
|
return String(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let legacyVariableWrapper: LegacyVariableWrapper | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reuses a single instance to avoid unnecessary memory allocations
|
||||||
|
*/
|
||||||
|
export function getVariableWrapper(name: string, value: VariableValue, text: VariableValue) {
|
||||||
|
if (!legacyVariableWrapper) {
|
||||||
|
legacyVariableWrapper = new LegacyVariableWrapper(name, value, text);
|
||||||
|
} else {
|
||||||
|
legacyVariableWrapper.state.name = name;
|
||||||
|
legacyVariableWrapper.state.value = value;
|
||||||
|
legacyVariableWrapper.state.text = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
return legacyVariableWrapper;
|
||||||
|
}
|
@ -1,75 +0,0 @@
|
|||||||
import { customBuilder } from '../variables/shared/testing/builders';
|
|
||||||
|
|
||||||
import { FormatRegistryID, formatRegistry } from './formatRegistry';
|
|
||||||
|
|
||||||
const dummyVar = customBuilder().withId('variable').build();
|
|
||||||
describe('formatRegistry', () => {
|
|
||||||
describe('with lucene formatter', () => {
|
|
||||||
const { formatter } = formatRegistry.get(FormatRegistryID.lucene);
|
|
||||||
|
|
||||||
it('should escape single value', () => {
|
|
||||||
expect(
|
|
||||||
formatter(
|
|
||||||
{
|
|
||||||
value: 'foo bar',
|
|
||||||
text: '',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
dummyVar
|
|
||||||
)
|
|
||||||
).toBe('foo\\ bar');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not escape negative number', () => {
|
|
||||||
expect(
|
|
||||||
formatter(
|
|
||||||
{
|
|
||||||
value: '-1',
|
|
||||||
text: '',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
dummyVar
|
|
||||||
)
|
|
||||||
).toBe('-1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should escape string prepended with dash', () => {
|
|
||||||
expect(
|
|
||||||
formatter(
|
|
||||||
{
|
|
||||||
value: '-test',
|
|
||||||
text: '',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
dummyVar
|
|
||||||
)
|
|
||||||
).toBe('\\-test');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should escape multi value', () => {
|
|
||||||
expect(
|
|
||||||
formatter(
|
|
||||||
{
|
|
||||||
value: ['foo bar', 'baz'],
|
|
||||||
text: '',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
dummyVar
|
|
||||||
)
|
|
||||||
).toBe('("foo\\ bar" OR "baz")');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should escape empty value', () => {
|
|
||||||
expect(
|
|
||||||
formatter(
|
|
||||||
{
|
|
||||||
value: [],
|
|
||||||
text: '',
|
|
||||||
args: [],
|
|
||||||
},
|
|
||||||
dummyVar
|
|
||||||
)
|
|
||||||
).toBe('__empty__');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,287 +0,0 @@
|
|||||||
import { isArray, map, replace } from 'lodash';
|
|
||||||
|
|
||||||
import { dateTime, Registry, RegistryItem, textUtil, TypedVariableModel } from '@grafana/data';
|
|
||||||
import kbn from 'app/core/utils/kbn';
|
|
||||||
|
|
||||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
|
|
||||||
import { formatVariableLabel } from '../variables/shared/formatVariable';
|
|
||||||
|
|
||||||
export interface FormatOptions {
|
|
||||||
value: any;
|
|
||||||
text: string;
|
|
||||||
args: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FormatRegistryItem extends RegistryItem {
|
|
||||||
formatter(options: FormatOptions, variable: TypedVariableModel): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum FormatRegistryID {
|
|
||||||
lucene = 'lucene',
|
|
||||||
raw = 'raw',
|
|
||||||
regex = 'regex',
|
|
||||||
pipe = 'pipe',
|
|
||||||
distributed = 'distributed',
|
|
||||||
csv = 'csv',
|
|
||||||
html = 'html',
|
|
||||||
json = 'json',
|
|
||||||
percentEncode = 'percentencode',
|
|
||||||
singleQuote = 'singlequote',
|
|
||||||
doubleQuote = 'doublequote',
|
|
||||||
sqlString = 'sqlstring',
|
|
||||||
date = 'date',
|
|
||||||
glob = 'glob',
|
|
||||||
text = 'text',
|
|
||||||
queryParam = 'queryparam',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const formatRegistry = new Registry<FormatRegistryItem>(() => {
|
|
||||||
const formats: FormatRegistryItem[] = [
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.lucene,
|
|
||||||
name: 'Lucene',
|
|
||||||
description: 'Values are lucene escaped and multi-valued variables generate an OR expression',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return luceneEscape(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (value instanceof Array && value.length === 0) {
|
|
||||||
return '__empty__';
|
|
||||||
}
|
|
||||||
|
|
||||||
const quotedValues = map(value, (val: string) => {
|
|
||||||
return '"' + luceneEscape(val) + '"';
|
|
||||||
});
|
|
||||||
|
|
||||||
return '(' + quotedValues.join(' OR ') + ')';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.raw,
|
|
||||||
name: 'raw',
|
|
||||||
description: 'Keep value as is',
|
|
||||||
formatter: ({ value }) => value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.regex,
|
|
||||||
name: 'Regex',
|
|
||||||
description: 'Values are regex escaped and multi-valued variables generate a (<value>|<value>) expression',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return kbn.regexEscape(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const escapedValues = map(value, kbn.regexEscape);
|
|
||||||
if (escapedValues.length === 1) {
|
|
||||||
return escapedValues[0];
|
|
||||||
}
|
|
||||||
return '(' + escapedValues.join('|') + ')';
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.pipe,
|
|
||||||
name: 'Pipe',
|
|
||||||
description: 'Values are separated by | character',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
return value.join('|');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.distributed,
|
|
||||||
name: 'Distributed',
|
|
||||||
description: 'Multiple values are formatted like variable=value',
|
|
||||||
formatter: ({ value }, variable) => {
|
|
||||||
if (typeof value === 'string') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = map(value, (val: any, index: number) => {
|
|
||||||
if (index !== 0) {
|
|
||||||
return variable.name + '=' + val;
|
|
||||||
} else {
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return value.join(',');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.csv,
|
|
||||||
name: 'Csv',
|
|
||||||
description: 'Comma-separated values',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (isArray(value)) {
|
|
||||||
return value.join(',');
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.html,
|
|
||||||
name: 'HTML',
|
|
||||||
description: 'HTML escaping of values',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (isArray(value)) {
|
|
||||||
return textUtil.escapeHtml(value.join(', '));
|
|
||||||
}
|
|
||||||
return textUtil.escapeHtml(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.json,
|
|
||||||
name: 'JSON',
|
|
||||||
description: 'JSON stringify valu',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
return JSON.stringify(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.percentEncode,
|
|
||||||
name: 'Percent encode',
|
|
||||||
description: 'Useful for URL escaping values',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
// like glob, but url escaped
|
|
||||||
if (isArray(value)) {
|
|
||||||
return encodeURIComponentStrict('{' + value.join(',') + '}');
|
|
||||||
}
|
|
||||||
return encodeURIComponentStrict(value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.singleQuote,
|
|
||||||
name: 'Single quote',
|
|
||||||
description: 'Single quoted values',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
// escape single quotes with backslash
|
|
||||||
const regExp = new RegExp(`'`, 'g');
|
|
||||||
if (isArray(value)) {
|
|
||||||
return map(value, (v: string) => `'${replace(v, regExp, `\\'`)}'`).join(',');
|
|
||||||
}
|
|
||||||
return `'${replace(value, regExp, `\\'`)}'`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.doubleQuote,
|
|
||||||
name: 'Double quote',
|
|
||||||
description: 'Double quoted values',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
// escape double quotes with backslash
|
|
||||||
const regExp = new RegExp('"', 'g');
|
|
||||||
if (isArray(value)) {
|
|
||||||
return map(value, (v: string) => `"${replace(v, regExp, '\\"')}"`).join(',');
|
|
||||||
}
|
|
||||||
return `"${replace(value, regExp, '\\"')}"`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.sqlString,
|
|
||||||
name: 'SQL string',
|
|
||||||
description: 'SQL string quoting and commas for use in IN statements and other scenarios',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
// escape single quotes by pairing them
|
|
||||||
const regExp = new RegExp(`'`, 'g');
|
|
||||||
if (isArray(value)) {
|
|
||||||
return map(value, (v) => `'${replace(v, regExp, "''")}'`).join(',');
|
|
||||||
}
|
|
||||||
return `'${replace(value, regExp, "''")}'`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.date,
|
|
||||||
name: 'Date',
|
|
||||||
description: 'Format date in different ways',
|
|
||||||
formatter: ({ value, args }) => {
|
|
||||||
const arg = args[0] ?? 'iso';
|
|
||||||
|
|
||||||
switch (arg) {
|
|
||||||
case 'ms':
|
|
||||||
return value;
|
|
||||||
case 'seconds':
|
|
||||||
return `${Math.round(parseInt(value, 10)! / 1000)}`;
|
|
||||||
case 'iso':
|
|
||||||
return dateTime(parseInt(value, 10)).toISOString();
|
|
||||||
default:
|
|
||||||
return dateTime(parseInt(value, 10)).format(arg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.glob,
|
|
||||||
name: 'Glob',
|
|
||||||
description: 'Format multi-valued variables using glob syntax, example {value1,value2}',
|
|
||||||
formatter: ({ value }) => {
|
|
||||||
if (isArray(value) && value.length > 1) {
|
|
||||||
return '{' + value.join(',') + '}';
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.text,
|
|
||||||
name: 'Text',
|
|
||||||
description: 'Format variables in their text representation. Example in multi-variable scenario A + B + C.',
|
|
||||||
formatter: (options, variable) => {
|
|
||||||
if (typeof options.text === 'string') {
|
|
||||||
return options.value === ALL_VARIABLE_VALUE ? ALL_VARIABLE_TEXT : options.text;
|
|
||||||
}
|
|
||||||
|
|
||||||
const current = (variable as any)?.current;
|
|
||||||
|
|
||||||
if (!current) {
|
|
||||||
return options.value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatVariableLabel(variable);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: FormatRegistryID.queryParam,
|
|
||||||
name: 'Query parameter',
|
|
||||||
description:
|
|
||||||
'Format variables as URL parameters. Example in multi-variable scenario A + B + C => var-foo=A&var-foo=B&var-foo=C.',
|
|
||||||
formatter: (options, variable) => {
|
|
||||||
const { value } = options;
|
|
||||||
const { name } = variable;
|
|
||||||
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
return value.map((v) => formatQueryParameter(name, v)).join('&');
|
|
||||||
}
|
|
||||||
|
|
||||||
return formatQueryParameter(name, value);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return formats;
|
|
||||||
});
|
|
||||||
|
|
||||||
function luceneEscape(value: string) {
|
|
||||||
if (isNaN(+value) === false) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* encode string according to RFC 3986; in contrast to encodeURIComponent()
|
|
||||||
* also the sub-delims "!", "'", "(", ")" and "*" are encoded;
|
|
||||||
* unicode handling uses UTF-8 as in ECMA-262.
|
|
||||||
*/
|
|
||||||
function encodeURIComponentStrict(str: string) {
|
|
||||||
return encodeURIComponent(str).replace(/[!'()*]/g, (c) => {
|
|
||||||
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatQueryParameter(name: string, value: string): string {
|
|
||||||
return `var-${name}=${encodeURIComponentStrict(value)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAllValue(value: any) {
|
|
||||||
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
|
|
||||||
}
|
|
@ -4,13 +4,12 @@ import { setDataSourceSrv } from '@grafana/runtime';
|
|||||||
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput';
|
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput';
|
||||||
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
|
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
|
||||||
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
|
import { mockDataSource, MockDataSourceSrv } from '../alerting/unified/mocks';
|
||||||
|
import { FormatRegistryID } from '../scenes/variables/interpolation/formatRegistry';
|
||||||
import { VariableAdapter, variableAdapters } from '../variables/adapters';
|
import { VariableAdapter, variableAdapters } from '../variables/adapters';
|
||||||
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
|
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
|
||||||
import { createQueryVariableAdapter } from '../variables/query/adapter';
|
import { createQueryVariableAdapter } from '../variables/query/adapter';
|
||||||
import { VariableModel } from '../variables/types';
|
import { VariableModel } from '../variables/types';
|
||||||
|
|
||||||
import { FormatRegistryID } from './formatRegistry';
|
|
||||||
|
|
||||||
const key = 'key';
|
const key = 'key';
|
||||||
|
|
||||||
variableAdapters.setInit(() => [
|
variableAdapters.setInit(() => [
|
||||||
|
@ -10,13 +10,14 @@ import {
|
|||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
import { getDataSourceSrv, setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
|
||||||
|
|
||||||
|
import { formatRegistry, FormatRegistryID } from '../scenes/variables/interpolation/formatRegistry';
|
||||||
import { variableAdapters } from '../variables/adapters';
|
import { variableAdapters } from '../variables/adapters';
|
||||||
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
|
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
|
||||||
import { isAdHoc } from '../variables/guard';
|
import { isAdHoc } from '../variables/guard';
|
||||||
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
|
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
|
||||||
import { variableRegex } from '../variables/utils';
|
import { variableRegex } from '../variables/utils';
|
||||||
|
|
||||||
import { FormatOptions, formatRegistry, FormatRegistryID } from './formatRegistry';
|
import { getVariableWrapper } from './LegacyVariableWrapper';
|
||||||
|
|
||||||
interface FieldAccessorCache {
|
interface FieldAccessorCache {
|
||||||
[key: string]: (obj: any) => any;
|
[key: string]: (obj: any) => any;
|
||||||
@ -38,7 +39,7 @@ export class TemplateSrv implements BaseTemplateSrv {
|
|||||||
private _variables: any[];
|
private _variables: any[];
|
||||||
private regex = variableRegex;
|
private regex = variableRegex;
|
||||||
private index: any = {};
|
private index: any = {};
|
||||||
private grafanaVariables: any = {};
|
private grafanaVariables = new Map<string, any>();
|
||||||
private timeRange?: TimeRange | null = null;
|
private timeRange?: TimeRange | null = null;
|
||||||
private fieldAccessorCache: FieldAccessorCache = {};
|
private fieldAccessorCache: FieldAccessorCache = {};
|
||||||
|
|
||||||
@ -165,12 +166,12 @@ export class TemplateSrv implements BaseTemplateSrv {
|
|||||||
formatItem = formatRegistry.get(FormatRegistryID.glob);
|
formatItem = formatRegistry.get(FormatRegistryID.glob);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: FormatOptions = { value, args, text: text ?? value };
|
const formatVariable = getVariableWrapper(variable.name, value, text ?? value);
|
||||||
return formatItem.formatter(options, variable);
|
return formatItem.formatter(value, args, formatVariable);
|
||||||
}
|
}
|
||||||
|
|
||||||
setGrafanaVariable(name: string, value: any) {
|
setGrafanaVariable(name: string, value: any) {
|
||||||
this.grafanaVariables[name] = value;
|
this.grafanaVariables.set(name, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,7 +307,7 @@ export class TemplateSrv implements BaseTemplateSrv {
|
|||||||
return this.formatValue(value, fmt, variable, text);
|
return this.formatValue(value, fmt, variable, text);
|
||||||
}
|
}
|
||||||
|
|
||||||
const systemValue = this.grafanaVariables[variable.current.value];
|
const systemValue = this.grafanaVariables.get(variable.current.value);
|
||||||
if (systemValue) {
|
if (systemValue) {
|
||||||
return this.formatValue(systemValue, fmt, variable);
|
return this.formatValue(systemValue, fmt, variable);
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { ScopedVars } from '@grafana/data';
|
|||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { applyQueryDefaults } from 'app/features/plugins/sql/defaults';
|
import { applyQueryDefaults } from 'app/features/plugins/sql/defaults';
|
||||||
import { SQLQuery, SqlQueryModel } from 'app/features/plugins/sql/types';
|
import { SQLQuery, SqlQueryModel } from 'app/features/plugins/sql/types';
|
||||||
import { FormatRegistryID } from 'app/features/templating/formatRegistry';
|
import { FormatRegistryID } from 'app/features/scenes/variables/interpolation/formatRegistry';
|
||||||
|
|
||||||
export class PostgresQueryModel implements SqlQueryModel {
|
export class PostgresQueryModel implements SqlQueryModel {
|
||||||
target: SQLQuery;
|
target: SQLQuery;
|
||||||
|
Reference in New Issue
Block a user