Templating: Introduce macros to simplify and optimize some scopedVars (#65317)

* Templating: Introduce macros to simplify and optimize some scopedVars

* Fixing tests

* fix test

* minor fix

* refactoring so macros work with formatting

* remove breaking change and keep current inconsistency

* Rename valueIndex to rowIndex

* Minor fixes

* Added test dashboard

* Added tags to dashboard

* Update

* Added test to check it returns match

* Update

* Fixed dashboard

* fix
This commit is contained in:
Torkel Ödegaard 2023-03-28 19:22:34 +02:00 committed by GitHub
parent 2b73f8cfd5
commit b7b608418d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1390 additions and 327 deletions

View File

@ -3089,8 +3089,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/features/dashboard/state/PanelModel.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
@ -3873,6 +3872,14 @@ exports[`better eslint`] = {
"public/app/features/teams/state/selectors.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/templating/formatVariableValue.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, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/templating/macroRegistry.test.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/templating/template_srv.mock.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
@ -3891,12 +3898,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "11"],
[0, 0, 0, "Unexpected any. Specify a different type.", "12"],
[0, 0, 0, "Unexpected any. Specify a different type.", "13"],
[0, 0, 0, "Unexpected any. Specify a different type.", "14"],
[0, 0, 0, "Do not use any type assertions.", "14"],
[0, 0, 0, "Unexpected any. Specify a different type.", "15"],
[0, 0, 0, "Unexpected any. Specify a different type.", "16"],
[0, 0, 0, "Do not use any type assertions.", "17"],
[0, 0, 0, "Unexpected any. Specify a different type.", "18"],
[0, 0, 0, "Do not use any type assertions.", "19"]
[0, 0, 0, "Do not use any type assertions.", "16"]
],
"public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -0,0 +1,871 @@
{
"annotations": {
"list": [
{
"builtIn": 1,
"datasource": {
"type": "grafana",
"uid": "-- Grafana --"
},
"enable": true,
"hide": true,
"iconColor": "rgba(0, 211, 255, 1)",
"name": "Annotations & Alerts",
"type": "dashboard"
}
]
},
"editable": true,
"fiscalYearStartMonth": 0,
"graphTooltip": 0,
"id": 1267,
"links": [],
"liveNow": false,
"panels": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"gridPos": {
"h": 3,
"w": 24,
"x": 0,
"y": 0
},
"id": 8,
"options": {
"code": {
"language": "plaintext",
"showLineNumbers": false,
"showMiniMap": false
},
"content": "* `__all_variables`=${__all_variables}\n* `__url_time_range`=${__url_time_range}",
"mode": "markdown"
},
"pluginVersion": "9.5.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"targetBlank": true,
"title": "value=${__value.raw}&time=${__value.time}&__value:percentencode=${__value:percentencode}&text=${__value.text}",
"url": "value=${__value.raw}&time=${__value.time}justvalue=${__value:percentencode}&text=${__value.text}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 3
},
"id": 2,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"showRowNums": false
},
"pluginVersion": "9.5.0-pre",
"title": "DataLink: with __value.raw=&__value.time=&__value:percentencode=",
"type": "table"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"links": [
{
"targetBlank": true,
"title": "Value link",
"url": "value=${__value.raw}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 3
},
"id": 3,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "random_walk",
"seriesCount": 5
}
],
"title": "Stat panel with __value.raw ",
"type": "stat"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "continuous-GrYlRd"
},
"links": [
{
"title": "${__value.raw}",
"url": "${__value.raw}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 10
},
"id": 6,
"options": {
"displayMode": "basic",
"minVizHeight": 10,
"minVizWidth": 0,
"orientation": "horizontal",
"reduceOptions": {
"calcs": [],
"fields": "",
"values": true
},
"showUnfilled": true,
"valueMode": "color"
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"csvFileName": "browser_marketshare.csv",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_file"
}
],
"title": "data link __value.raw",
"transformations": [
{
"id": "limit",
"options": {
"limitField": 5
}
}
],
"type": "bargauge"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"description": "Since this is using getFrameDisplayName it works kind badly (especially with testdata) and only returns the `Series (refId)`. \n\nSo this should show:\n* Series (Query1)\n* Series (Query2)",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"displayName": "${__series.name}",
"links": [
{
"targetBlank": true,
"title": "Value link",
"url": "value=${__calc}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 10
},
"id": 12,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": false
},
"textMode": "auto"
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"alias": "",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "Query1",
"scenarioId": "random_walk",
"seriesCount": 1
},
{
"alias": "",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"hide": false,
"refId": "Query2",
"scenarioId": "random_walk",
"seriesCount": 1
}
],
"title": "${series.name} in display name",
"type": "stat"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"title": "__data.refId=${__data.refId}&__data.fields[0]=${__data.fields[0]}&cluster=${__field.labels.cluster}",
"url": "refId=${__data.refId}&__data.fields[0]=${__data.fields[0]}&cluster=${__field.labels.cluster}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 17
},
"id": 11,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"showRowNums": false
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"labels": "cluster=US",
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "DataLink: refId=${__data.refId}&__data.fields[0]=${__data.fields[0]}&cluster=${__field.labels.cluster}",
"type": "table"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "palette-classic"
},
"custom": {
"axisCenteredZero": false,
"axisColorMode": "text",
"axisLabel": "",
"axisPlacement": "auto",
"barAlignment": 0,
"drawStyle": "line",
"fillOpacity": 0,
"gradientMode": "none",
"hideFrom": {
"legend": false,
"tooltip": false,
"viz": false
},
"lineInterpolation": "linear",
"lineWidth": 1,
"pointSize": 5,
"scaleDistribution": {
"type": "linear"
},
"showPoints": "auto",
"spanNulls": false,
"stacking": {
"group": "A",
"mode": "none"
},
"thresholdsStyle": {
"mode": "off"
}
},
"links": [
{
"title": "${__value.raw}",
"url": "${__value.raw}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 17
},
"id": 10,
"options": {
"legend": {
"calcs": [],
"displayMode": "list",
"placement": "bottom",
"showLegend": true
},
"tooltip": {
"mode": "single",
"sort": "none"
}
},
"targets": [
{
"alias": "10,20,30,40",
"csvContent": "Time, value, test\n\"2023-03-24T17:12:12.347Z\", 10,hello\n\"2023-03-24T17:22:12.347Z\", 20,asd\n\"2023-03-24T17:32:12.347Z\", 30,asd2\n\"2023-03-24T17:42:12.347Z\", 40,as34\n",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
},
{
"alias": "5,6,7",
"csvContent": "Time, value, test\n\"2023-03-24T17:12:12.347Z\", 5,hello\n\"2023-03-24T17:22:12.347Z\", 6,asd\n\"2023-03-24T17:42:12.347Z\", 7,as34\n",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "B",
"scenarioId": "csv_content"
}
],
"title": "Data links with ${__value.raw}",
"transformations": [
{
"id": "joinByField",
"options": {}
}
],
"type": "timeseries"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"links": [
{
"title": "__field.name=${__field.name}&__field.labels.cluster=${__field.labels.cluster}",
"url": "__field.name=${__field.name}&__field.labels.cluster=${__field.labels.cluster}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
},
"unit": "percent"
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 24
},
"id": 13,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"showRowNums": false
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"labels": "cluster=US",
"refId": "A",
"scenarioId": "random_walk"
}
],
"title": "DataLink: __field.name=&__field.labels.cluster",
"type": "table"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"description": "The stat display names should be \n* Stockholm = Bad\n* New York = Good \n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"displayName": "$__cell_0 = $__cell_2",
"links": [],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 24
},
"id": 14,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": true
},
"textMode": "auto"
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"alias": "",
"csvContent": "name, value, name2\nStockholm, 10, Good\nNew York, 15, Bad",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "DisplayName with __cell_0 = __cell_2",
"type": "stat"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"description": "The stat display names should be \n* Stockholm = Bad\n* New York = Good \n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"custom": {
"align": "auto",
"cellOptions": {
"type": "auto"
},
"inspect": false
},
"displayName": "${__field.name}",
"links": [
{
"targetBlank": true,
"title": "Value link",
"url": "value=${__calc}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 0,
"y": 31
},
"id": 15,
"options": {
"cellHeight": "sm",
"footer": {
"countRows": false,
"fields": "",
"reducer": [
"sum"
],
"show": false
},
"showHeader": true,
"showRowNums": false
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"alias": "",
"csvContent": "name, value, name2\nStockholm, 10, Good\nNew York, 15, Bad",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "DisplayName: __field.name",
"type": "table"
},
{
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"description": "The stat display names should be \n* Stockholm = Bad\n* New York = Good \n",
"fieldConfig": {
"defaults": {
"color": {
"mode": "thresholds"
},
"displayName": "${__data.fields[0]} = ${__data.fields[2]}",
"links": [
{
"targetBlank": true,
"title": "__data.fields[0] = ${__data.fields[0]} = __value.raw = ${__value.raw}",
"url": "__data.fields[0] = ${__data.fields[0]} = __value.raw = ${__value.raw}"
}
],
"mappings": [],
"thresholds": {
"mode": "absolute",
"steps": [
{
"color": "green"
},
{
"color": "red",
"value": 80
}
]
}
},
"overrides": []
},
"gridPos": {
"h": 7,
"w": 12,
"x": 12,
"y": 31
},
"id": 16,
"options": {
"colorMode": "value",
"graphMode": "area",
"justifyMode": "auto",
"orientation": "auto",
"reduceOptions": {
"calcs": [
"lastNotNull"
],
"fields": "",
"values": true
},
"textMode": "auto"
},
"pluginVersion": "9.5.0-pre",
"targets": [
{
"alias": "",
"csvContent": "name, value, name2\nStockholm, 10, Good\nNew York, 15, Bad",
"datasource": {
"type": "testdata",
"uid": "PD8C576611E62080A"
},
"refId": "A",
"scenarioId": "csv_content"
}
],
"title": "$__data.fields[0] = $__data.fields[2] with datalinks",
"type": "stat"
}
],
"refresh": "",
"schemaVersion": 38,
"style": "dark",
"tags": ["gdev", "templating"],
"templating": {
"list": [
{
"current": {
"selected": false,
"text": "A",
"value": "A"
},
"hide": 0,
"includeAll": false,
"multi": false,
"name": "customVar",
"options": [
{
"selected": true,
"text": "A",
"value": "A"
},
{
"selected": false,
"text": "B",
"value": "B"
},
{
"selected": false,
"text": "C",
"value": "C"
}
],
"query": "A,B,C",
"queryValue": "",
"skipUrlSync": false,
"type": "custom"
}
]
},
"time": {
"from": "2023-03-24T17:12:12.347Z",
"to": "2023-03-24T17:42:12.347Z"
},
"timepicker": {},
"timezone": "",
"title": "Templating - Macros",
"uid": "e7c29343-6d1e-4167-9c13-803fe5be8c46",
"version": 48,
"weekStart": ""
}

View File

@ -149,6 +149,13 @@ local dashboard = grafana.dashboard;
id: 0,
}
},
dashboard.new('datadata-macros', import '../dev-dashboards/feature-templating/datadata-macros.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{
spec+: {
id: 0,
}
},
dashboard.new('demo1', import '../dev-dashboards/datasource-testdata/demo1.json') +
resource.addMetadata('folder', 'dev-dashboards') +
{

View File

@ -329,7 +329,7 @@ describe('applyFieldOverrides', () => {
data.fields[1].getLinks!({ valueRowIndex: 0 });
expect(data.fields[1].config.decimals).toEqual(1);
expect(replaceVariablesCalls[3].__value.value.text).toEqual('100.0');
expect(replaceVariablesCalls[3].__dataContext?.value.rowIndex).toEqual(0);
});
it('creates a deep clone of field config', () => {

View File

@ -5,13 +5,13 @@ import usePrevious from 'react-use/lib/usePrevious';
import { VariableFormatID } from '@grafana/schema';
import { compareArrayValues, compareDataFrameStructures, guessFieldTypeForField } from '../dataframe';
import { getTimeField } from '../dataframe/processDataFrame';
import { PanelPlugin } from '../panel/PanelPlugin';
import { GrafanaTheme2 } from '../themes';
import { asHexString } from '../themes/colorManipulator';
import { fieldMatchers, reduceField, ReducerID } from '../transformations';
import {
ApplyFieldOverrideOptions,
DataContextScopedVar,
DataFrame,
DataLink,
DecimalCount,
@ -34,9 +34,8 @@ import {
ValueLinkConfig,
} from '../types';
import { FieldMatcher } from '../types/transformations';
import { DataLinkBuiltInVars, locationUtil } from '../utils';
import { locationUtil } from '../utils';
import { mapInternalLinkToExplore } from '../utils/dataLinks';
import { formattedValueToString } from '../valueFormats';
import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
import { getDisplayProcessor, getRawDisplayProcessor } from './displayProcessor';
@ -370,29 +369,21 @@ export const getLinksSupplier =
if (!field.config.links || field.config.links.length === 0) {
return [];
}
const timeRangeUrl = locationUtil.getTimeRangeUrlParams();
const { timeField } = getTimeField(frame);
return field.config.links.map((link: DataLink) => {
const variablesQuery = locationUtil.getVariablesUrlParams();
let dataFrameVars = {};
let valueVars = {};
let dataContext: DataContextScopedVar = { value: { frame, field } };
// We are not displaying reduction result
if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) {
dataContext.value.rowIndex = config.valueRowIndex;
const fieldsProxy = getFieldDisplayValuesProxy({
frame,
rowIndex: config.valueRowIndex,
timeZone: timeZone,
});
valueVars = {
raw: field.values.get(config.valueRowIndex),
numeric: fieldsProxy[field.name].numeric,
text: fieldsProxy[field.name].text,
time: timeField ? timeField.values.get(config.valueRowIndex) : undefined,
};
dataFrameVars = {
__data: {
value: {
@ -404,32 +395,13 @@ export const getLinksSupplier =
},
};
} else {
if (config.calculatedValue) {
valueVars = {
raw: config.calculatedValue.numeric,
numeric: config.calculatedValue.numeric,
text: formattedValueToString(config.calculatedValue),
};
}
dataContext.value.calculatedValue = config.calculatedValue;
}
const variables: ScopedVars = {
...fieldScopedVars,
__value: {
text: 'Value',
value: valueVars,
},
...dataFrameVars,
[DataLinkBuiltInVars.keepTime]: {
text: timeRangeUrl,
value: timeRangeUrl,
skipFormat: true,
},
[DataLinkBuiltInVars.includeVars]: {
text: variablesQuery,
value: variablesQuery,
skipFormat: true,
},
__dataContext: dataContext,
};
if (link.onClick) {

View File

@ -273,7 +273,7 @@ describe('calculateField transformer w/ timeseries', () => {
};
for (const key of Object.keys(variables)) {
if (target === `$${key}`) {
return variables[key].value + '';
return variables[key]!.value + '';
}
}
return target;

View File

@ -219,7 +219,7 @@ describe('filterByName transformer', () => {
},
};
for (const key of Object.keys(variables)) {
return target.replace(`$${key}`, variables[key].value);
return target.replace(`$${key}`, variables[key]!.value);
}
return target;
},

View File

@ -1,8 +1,24 @@
import { DataFrame, Field } from './dataFrame';
import { DisplayValue } from './displayValue';
export interface ScopedVar<T = any> {
text?: any;
value: T;
skipUrlSync?: boolean;
skipFormat?: boolean;
}
export interface ScopedVars extends Record<string, ScopedVar> {}
export interface ScopedVars {
__dataContext?: DataContextScopedVar;
[key: string]: ScopedVar | undefined;
}
/**
* Used by data link macros
*/
export interface DataContextScopedVar {
value: {
frame: DataFrame;
field: Field;
rowIndex?: number;
calculatedValue?: DisplayValue;
};
}

View File

@ -114,7 +114,7 @@ describe('mapInternalLinkToExplore', () => {
config: {},
values: new ArrayVector([2]),
},
replaceVariables: (val, scopedVars) => val.replace(/\$var/g, scopedVars!['var1'].value),
replaceVariables: (val, scopedVars) => val.replace(/\$var/g, scopedVars!['var1']!.value),
});
expect(decodeURIComponent(link.href)).toEqual(

View File

@ -65,7 +65,7 @@ function getDefaultDataFrame(): DataFrame {
overrides: [],
},
replaceVariables: (value, vars, format) => {
return vars && value === '${__value.text}' ? vars['__value'].value.text : value;
return vars && value === '${__value.text}' ? '${__value.text} interpolation' : value;
},
timeZone: 'utc',
theme: createTheme(),
@ -144,10 +144,10 @@ describe('Table', () => {
const rows = within(getTable()).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 00:00:00', temperature: '10', link: '10' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: 'NaN' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '11' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
]);
});
});
@ -203,10 +203,10 @@ describe('Table', () => {
const rows = within(getTable()).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '11' },
{ time: '2021-01-01 00:00:00', temperature: '10', link: '10' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: 'NaN' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
]);
});
});
@ -582,10 +582,10 @@ describe('Table', () => {
const rows = within(getTable()).getAllByRole('row');
expect(rows).toHaveLength(5);
expect(getRowsData(rows)).toEqual([
{ time: '2021-01-01 00:00:00', temperature: '10', link: '10' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: 'NaN' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '11' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '12' },
{ time: '2021-01-01 00:00:00', temperature: '10', link: '${__value.text} interpolation' },
{ time: '2021-01-01 03:00:00', temperature: 'NaN', link: '${__value.text} interpolation' },
{ time: '2021-01-01 01:00:00', temperature: '11', link: '${__value.text} interpolation' },
{ time: '2021-01-01 02:00:00', temperature: '12', link: '${__value.text} interpolation' },
]);
await userEvent.click(within(rows[1]).getByLabelText('Expand row'));

View File

@ -784,7 +784,7 @@ export function queryLogsSample<TQuery extends DataQuery, TOptions extends DataS
}
function getIntervalInfo(scopedVars: ScopedVars, timespanMs: number): { interval: string; intervalMs?: number } {
if (scopedVars.__interval) {
if (scopedVars.__interval_ms) {
let intervalMs: number = scopedVars.__interval_ms.value;
let interval = '';
// below 5 seconds we force the resolution to be per 1ms as interval in scopedVars is not less than 10ms

View File

@ -644,8 +644,8 @@ describe('DashboardModel', () => {
model.processRepeats();
expect(model.panels.filter((x) => x.type === 'row')).toHaveLength(2);
expect(model.panels.filter((x) => x.type !== 'row')).toHaveLength(4);
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.dc.value).toBe('dc1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.app.value).toBe('se1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.dc?.value).toBe('dc1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.app?.value).toBe('se1');
const saveModel = model.getSaveModelClone();
expect(saveModel.panels.length).toBe(2);
@ -697,15 +697,15 @@ describe('DashboardModel', () => {
model.processRepeats();
expect(model.panels.filter((x) => x.type === 'row')).toHaveLength(2);
expect(model.panels.filter((x) => x.type !== 'row')).toHaveLength(4);
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.dc.value).toBe('dc1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.app.value).toBe('se1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.dc?.value).toBe('dc1');
expect(model.panels.find((x) => x.type !== 'row')?.scopedVars?.app?.value).toBe('se1');
model.snapshot = { timestamp: new Date() };
const saveModel = model.getSaveModelClone();
expect(saveModel.panels.filter((x) => x.type === 'row')).toHaveLength(2);
expect(saveModel.panels.filter((x) => x.type !== 'row')).toHaveLength(4);
expect(saveModel.panels.find((x) => x.type !== 'row')?.scopedVars?.dc.value).toBe('dc1');
expect(saveModel.panels.find((x) => x.type !== 'row')?.scopedVars?.app.value).toBe('se1');
expect(saveModel.panels.find((x) => x.type !== 'row')?.scopedVars?.dc?.value).toBe('dc1');
expect(saveModel.panels.find((x) => x.type !== 'row')?.scopedVars?.app?.value).toBe('se1');
model.collapseRows();
const savedModelWithCollapsedRows = model.getSaveModelClone();

View File

@ -1,7 +1,6 @@
import { ComponentClass } from 'react';
import {
DataLinkBuiltInVars,
FieldConfigProperty,
PanelData,
PanelProps,
@ -19,7 +18,6 @@ import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { TemplateSrv } from '../../templating/template_srv';
import { variableAdapters } from '../../variables/adapters';
import { createQueryVariableAdapter } from '../../variables/query/adapter';
import { setTimeSrv } from '../services/TimeSrv';
import { TimeOverrideResult } from '../utils/panel';
import { PanelModel } from './PanelModel';
@ -27,13 +25,6 @@ import { PanelModel } from './PanelModel';
standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions());
standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions());
setTimeSrv({
timeRangeForUrl: () => ({
from: 1607687293000,
to: 1607687293100,
}),
} as any);
const getVariables = () => variablesMock;
const getVariableWithName = (name: string) => variablesMock.filter((v) => v.name === name)[0];
const getFilteredVariables = jest.fn();
@ -211,21 +202,12 @@ describe('PanelModel', () => {
bbb: { value: 'BBB', text: 'upperB' },
};
});
it('should interpolate variables', () => {
const out = model.replaceVariables('hello $aaa');
expect(out).toBe('hello AAA');
});
it('should interpolate $__url_time_range variable', () => {
const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.keepTime}`);
expect(out).toBe('/d/1?from=1607687293000&to=1607687293100');
});
it('should interpolate $__all_variables variable', () => {
const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.includeVars}`);
expect(out).toBe('/d/1?var-test1=val1&var-test2=val2&var-test3=Value%203&var-test4=A&var-test4=B');
});
it('should prefer the local variable value', () => {
const extra = { aaa: { text: '???', value: 'XXX' } };
const out = model.replaceVariables('hello $aaa and $bbb', extra);

View File

@ -5,7 +5,6 @@ import {
DataConfigSource,
DataFrameDTO,
DataLink,
DataLinkBuiltInVars,
DataQuery,
DataTransformerConfig,
EventBusSrv,
@ -13,7 +12,6 @@ import {
PanelPlugin,
PanelPluginDataSupport,
ScopedVars,
urlUtil,
PanelModel as IPanelModel,
DataSourceRef,
CoreApp,
@ -36,8 +34,6 @@ import {
} from 'app/types/events';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { getVariablesUrlParams } from '../../variables/getAllVariableValuesForUrl';
import { getTimeSrv } from '../services/TimeSrv';
import { TimeOverrideResult } from '../utils/panel';
export interface GridPos {
@ -639,23 +635,6 @@ export class PanelModel implements DataConfigSource, IPanelModel {
replaceVariables(value: string, extraVars: ScopedVars | undefined, format?: string | Function) {
const lastRequest = this.getQueryRunner().getLastRequest();
const vars: ScopedVars = Object.assign({}, this.scopedVars, lastRequest?.scopedVars, extraVars);
const allVariablesParams = getVariablesUrlParams(vars);
const variablesQuery = urlUtil.toUrlParams(allVariablesParams);
const timeRangeUrl = urlUtil.toUrlParams(getTimeSrv().timeRangeForUrl());
vars[DataLinkBuiltInVars.keepTime] = {
text: timeRangeUrl,
value: timeRangeUrl,
skipFormat: true,
};
vars[DataLinkBuiltInVars.includeVars] = {
text: variablesQuery,
value: variablesQuery,
skipFormat: true,
};
return getTemplateSrv().replace(value, vars, format);
}

View File

@ -168,9 +168,9 @@ describe('PanelQueryRunner', () => {
});
it('should pass scopedVars to datasource with interval props', async () => {
expect(ctx.queryCalledWith?.scopedVars.server.text).toBe('Server1');
expect(ctx.queryCalledWith?.scopedVars.__interval.text).toBe('5m');
expect(ctx.queryCalledWith?.scopedVars.__interval_ms.text).toBe('300000');
expect(ctx.queryCalledWith?.scopedVars.server!.text).toBe('Server1');
expect(ctx.queryCalledWith?.scopedVars.__interval!.text).toBe('5m');
expect(ctx.queryCalledWith?.scopedVars.__interval_ms!.text).toBe('300000');
});
});

View File

@ -26,24 +26,15 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
public static Component = DashboardSceneRenderer;
private urlSyncManager?: UrlSyncManager;
public activate() {
super.activate();
}
/**
* It's better to do this before activate / mount to not trigger unnessary re-renders
*/
public initUrlSync() {
this.urlSyncManager = new UrlSyncManager(this);
this.urlSyncManager.initSync();
}
public deactivate() {
super.deactivate();
if (this.urlSyncManager) {
this.urlSyncManager!.cleanUp();
if (!this.urlSyncManager) {
this.urlSyncManager = new UrlSyncManager(this);
}
this.urlSyncManager.initSync();
}
}

View File

@ -45,7 +45,7 @@ describe('SearchResultsTable', () => {
overrides: [],
},
replaceVariables: (value, vars, format) => {
return vars && value === '${__value.text}' ? vars['__value'].value.text : value;
return vars && value === '${__value.text}' ? vars['__value']!.value.text : value;
},
theme: createTheme(),
});

View File

@ -0,0 +1,86 @@
import { initTemplateSrv } from 'test/helpers/initTemplateSrv';
import { DataContextScopedVar, FieldType, toDataFrame } from '@grafana/data';
import { TemplateSrv } from '@grafana/runtime';
describe('templateSrv', () => {
let _templateSrv: TemplateSrv;
beforeEach(() => {
_templateSrv = initTemplateSrv('hello', []);
});
const data = toDataFrame({
name: 'A',
fields: [
{
name: 'number',
type: FieldType.number,
values: [5, 10],
display: (value: number) => {
return { text: value.toString(), numeric: value, suffix: '%' };
},
},
{
name: 'time',
type: FieldType.time,
values: [5000, 10000],
},
],
});
it('Should interpolate __value.* expressions with dataContext in scopedVars', () => {
const dataContext: DataContextScopedVar = {
value: {
frame: data,
field: data.fields[0],
rowIndex: 1,
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__value.raw}', scopedVars)).toBe('10');
expect(_templateSrv.replace('${__value.numeric}', scopedVars)).toBe('10');
expect(_templateSrv.replace('${__value}', scopedVars)).toBe('10%');
expect(_templateSrv.replace('${__value.text}', scopedVars)).toBe('10');
expect(_templateSrv.replace('${__value.time}', scopedVars)).toBe('10000');
// can apply format as well
expect(_templateSrv.replace('${__value:percentencode}', scopedVars)).toBe('10%25');
});
it('Should interpolate __value.* with calculatedValue', () => {
const dataContext: DataContextScopedVar = {
value: {
frame: data,
field: data.fields[0],
calculatedValue: {
text: '15',
numeric: 15,
suffix: '%',
},
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__value.raw}', scopedVars)).toBe('15');
expect(_templateSrv.replace('${__value.numeric}', scopedVars)).toBe('15');
expect(_templateSrv.replace('${__value}', scopedVars)).toBe('15%');
expect(_templateSrv.replace('${__value.text}', scopedVars)).toBe('15%');
expect(_templateSrv.replace('${__value.time}', scopedVars)).toBe('');
});
it('Should return match when ${__value.*} is used and no dataContext or rowIndex is found', () => {
const dataContext: DataContextScopedVar = {
value: {
frame: data,
field: data.fields[0],
},
};
const scopedVars = { __dataContext: dataContext };
expect(_templateSrv.replace('${__value.raw}', scopedVars)).toBe('${__value.raw}');
});
});

View File

@ -0,0 +1,76 @@
import { DisplayProcessor, FieldType, formattedValueToString, getDisplayProcessor, ScopedVars } from '@grafana/data';
import { VariableCustomFormatterFn } from '@grafana/scenes';
import { formatVariableValue } from './formatVariableValue';
/**
* ${__value.raw/nummeric/text/time} macro
*/
export function valueMacro(
match: string,
fieldPath?: string,
scopedVars?: ScopedVars,
format?: string | VariableCustomFormatterFn
) {
const value = getValueForValueMacro(match, fieldPath, scopedVars);
return formatVariableValue(value, format);
}
function getValueForValueMacro(match: string, fieldPath?: string, scopedVars?: ScopedVars) {
const dataContext = scopedVars?.__dataContext;
if (!dataContext) {
return match;
}
const { frame, rowIndex, field, calculatedValue } = dataContext.value;
if (calculatedValue) {
switch (fieldPath) {
case 'numeric':
return calculatedValue.numeric.toString();
case 'raw':
return calculatedValue.numeric;
case 'time':
return '';
case 'text':
default:
return formattedValueToString(calculatedValue);
}
}
if (rowIndex === undefined) {
return match;
}
if (fieldPath === 'time') {
const timeField = frame.fields.find((f) => f.type === FieldType.time);
return timeField ? timeField.values.get(rowIndex) : undefined;
}
const value = field.values.get(rowIndex);
if (fieldPath === 'raw') {
return value;
}
const displayProcessor = field.display ?? getFallbackDisplayProcessor();
const result = displayProcessor(value);
switch (fieldPath) {
case 'numeric':
return result.numeric;
case 'text':
return result.text;
default:
return formattedValueToString(result);
}
}
let fallbackDisplayProcessor: DisplayProcessor | undefined;
function getFallbackDisplayProcessor() {
if (!fallbackDisplayProcessor) {
fallbackDisplayProcessor = getDisplayProcessor();
}
return fallbackDisplayProcessor;
}

View File

@ -0,0 +1,105 @@
import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput';
import { VariableFormatID } from '@grafana/schema';
import { formatVariableValue } from './formatVariableValue';
describe('format variable to string values', () => {
silenceConsoleOutput();
it('single value should return value', () => {
const result = formatVariableValue('test');
expect(result).toBe('test');
});
it('should use glob format when unknown format provided', () => {
let result = formatVariableValue('test', 'nonexistentformat');
expect(result).toBe('test');
result = formatVariableValue(['test', 'test1'], 'nonexistentformat');
expect(result).toBe('{test,test1}');
});
it('multi value and glob format should render glob string', () => {
const result = formatVariableValue(['test', 'test2'], 'glob');
expect(result).toBe('{test,test2}');
});
it('multi value and lucene should render as lucene expr', () => {
const result = formatVariableValue(['test', 'test2'], 'lucene');
expect(result).toBe('("test" OR "test2")');
});
it('multi value and regex format should render regex string', () => {
const result = formatVariableValue(['test.', 'test2'], 'regex');
expect(result).toBe('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', () => {
const result = formatVariableValue(['test', 'test2'], 'pipe');
expect(result).toBe('test|test2');
});
it('multi value and distributed should render distributed string', () => {
const result = formatVariableValue(['test', 'test2'], 'distributed', {
name: 'build',
});
expect(result).toBe('test,build=test2');
});
it('multi value and distributed should render when not string', () => {
const result = formatVariableValue(['test'], 'distributed', {
name: 'build',
});
expect(result).toBe('test');
});
it('multi value and csv format should render csv string', () => {
const result = formatVariableValue(['test', 'test2'], VariableFormatID.CSV);
expect(result).toBe('test,test2');
});
it('multi value and percentencode format should render percent-encoded string', () => {
const result = formatVariableValue(['foo()bar BAZ', 'test2'], VariableFormatID.PercentEncode);
expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
});
it('slash should be properly escaped in regex format', () => {
const result = formatVariableValue('Gi3/14', 'regex');
expect(result).toBe('Gi3\\/14');
});
it('single value and singlequote format should render string with value enclosed in single quotes', () => {
const result = formatVariableValue('test', 'singlequote');
expect(result).toBe("'test'");
});
it('multi value and singlequote format should render string with values enclosed in single quotes', () => {
const result = formatVariableValue(['test', "test'2"], 'singlequote');
expect(result).toBe("'test','test\\'2'");
});
it('single value and doublequote format should render string with value enclosed in double quotes', () => {
const result = formatVariableValue('test', 'doublequote');
expect(result).toBe('"test"');
});
it('multi value and doublequote format should render string with values enclosed in double quotes', () => {
const result = formatVariableValue(['test', 'test"2'], 'doublequote');
expect(result).toBe('"test","test\\"2"');
});
it('single value and sqlstring format should render string with value enclosed in single quotes', () => {
const result = formatVariableValue("test'value", 'sqlstring');
expect(result).toBe(`'test''value'`);
});
it('multi value and sqlstring format should render string with values enclosed in single quotes', () => {
const result = formatVariableValue(['test', "test'value2"], 'sqlstring');
expect(result).toBe(`'test','test''value2'`);
});
it('raw format should leave value intact and do no escaping', () => {
const result = formatVariableValue("'test\n", 'raw');
expect(result).toBe("'test\n");
});
});

View File

@ -0,0 +1,50 @@
import { formatRegistry, FormatRegistryID } from '@grafana/scenes';
import { isAdHoc } from '../variables/guard';
import { getVariableWrapper } from './LegacyVariableWrapper';
export function formatVariableValue(value: any, format?: any, variable?: any, text?: string): string {
// for some scopedVars there is no variable
variable = variable || {};
if (value === null || value === undefined) {
return '';
}
if (isAdHoc(variable) && format !== FormatRegistryID.queryParam) {
return '';
}
// if it's an object transform value to string
if (!Array.isArray(value) && typeof value === 'object') {
value = `${value}`;
}
if (typeof format === 'function') {
return format(value, variable, formatVariableValue);
}
if (!format) {
format = FormatRegistryID.glob;
}
// some formats have arguments that come after ':' character
let args = format.split(':');
if (args.length > 1) {
format = args[0];
args = args.slice(1);
} else {
args = [];
}
let formatItem = formatRegistry.getIfExists(format);
if (!formatItem) {
console.error(`Variable format ${format} not found. Using glob format as fallback.`);
formatItem = formatRegistry.get(FormatRegistryID.glob);
}
const formatVariable = getVariableWrapper(variable, value, text ?? value);
return formatItem.formatter(value, args, formatVariable);
}

View File

@ -0,0 +1,54 @@
import { initTemplateSrv } from 'test/helpers/initTemplateSrv';
import { DataLinkBuiltInVars } from '@grafana/data';
import { getTemplateSrv, setTemplateSrv } from '@grafana/runtime';
import { setTimeSrv } from '../dashboard/services/TimeSrv';
import { variableAdapters } from '../variables/adapters';
import { createQueryVariableAdapter } from '../variables/query/adapter';
describe('__all_variables', () => {
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
setTemplateSrv(
initTemplateSrv('hello', [
{
type: 'query',
name: 'test',
rootStateKey: 'hello',
current: { value: ['val1', 'val2'] },
getValueForUrl: function () {
return this.current.value;
},
},
])
);
});
it('should interpolate correctly', () => {
const out = getTemplateSrv().replace(`/d/1?$${DataLinkBuiltInVars.includeVars}`);
expect(out).toBe('/d/1?var-test=val1&var-test=val2');
});
it('should interpolate and take scopedVars into account', () => {
const out = getTemplateSrv().replace(`/d/1?$${DataLinkBuiltInVars.includeVars}`, { test: { value: 'val3' } });
expect(out).toBe('/d/1?var-test=val3');
});
});
describe('__url_time_range', () => {
beforeAll(() => {
setTimeSrv({
timeRangeForUrl: () => ({
from: 1607687293000,
to: 1607687293100,
}),
} as any);
});
it('should interpolate to url params', () => {
const out = getTemplateSrv().replace(`/d/1?$${DataLinkBuiltInVars.keepTime}`);
expect(out).toBe('/d/1?from=1607687293000&to=1607687293100');
});
});

View File

@ -0,0 +1,22 @@
import { DataLinkBuiltInVars, ScopedVars, urlUtil } from '@grafana/data';
import { getTimeSrv } from '../dashboard/services/TimeSrv';
import { getVariablesUrlParams } from '../variables/getAllVariableValuesForUrl';
import { valueMacro } from './dataMacros';
import { MacroHandler } from './types';
export const macroRegistry: Record<string, MacroHandler> = {
['__value']: valueMacro,
[DataLinkBuiltInVars.includeVars]: includeVarsMacro,
[DataLinkBuiltInVars.keepTime]: urlTimeRangeMacro,
};
function includeVarsMacro(match: string, fieldPath?: string, scopedVars?: ScopedVars) {
const allVariablesParams = getVariablesUrlParams(scopedVars);
return urlUtil.toUrlParams(allVariablesParams);
}
function urlTimeRangeMacro() {
return urlUtil.toUrlParams(getTimeSrv().timeRangeForUrl());
}

View File

@ -1,7 +1,6 @@
import { dateTime, TimeRange } from '@grafana/data';
import { setDataSourceSrv, VariableInterpolation } from '@grafana/runtime';
import { FormatRegistryID, TestVariable } from '@grafana/scenes';
import { VariableFormatID } from '@grafana/schema';
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput';
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
@ -424,104 +423,6 @@ describe('templateSrv', () => {
});
});
describe('format variable to string values', () => {
it('single value should return value', () => {
const result = _templateSrv.formatValue('test');
expect(result).toBe('test');
});
it('should use glob format when unknown format provided', () => {
let result = _templateSrv.formatValue('test', 'nonexistentformat');
expect(result).toBe('test');
result = _templateSrv.formatValue(['test', 'test1'], 'nonexistentformat');
expect(result).toBe('{test,test1}');
});
it('multi value and glob format should render glob string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'glob');
expect(result).toBe('{test,test2}');
});
it('multi value and lucene should render as lucene expr', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'lucene');
expect(result).toBe('("test" OR "test2")');
});
it('multi value and regex format should render regex string', () => {
const result = _templateSrv.formatValue(['test.', 'test2'], 'regex');
expect(result).toBe('(test\\.|test2)');
});
it('multi value and pipe should render pipe string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'pipe');
expect(result).toBe('test|test2');
});
it('multi value and distributed should render distributed string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], 'distributed', {
name: 'build',
});
expect(result).toBe('test,build=test2');
});
it('multi value and distributed should render when not string', () => {
const result = _templateSrv.formatValue(['test'], 'distributed', {
name: 'build',
});
expect(result).toBe('test');
});
it('multi value and csv format should render csv string', () => {
const result = _templateSrv.formatValue(['test', 'test2'], VariableFormatID.CSV);
expect(result).toBe('test,test2');
});
it('multi value and percentencode format should render percent-encoded string', () => {
const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], VariableFormatID.PercentEncode);
expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D');
});
it('slash should be properly escaped in regex format', () => {
const result = _templateSrv.formatValue('Gi3/14', 'regex');
expect(result).toBe('Gi3\\/14');
});
it('single value and singlequote format should render string with value enclosed in single quotes', () => {
const result = _templateSrv.formatValue('test', 'singlequote');
expect(result).toBe("'test'");
});
it('multi value and singlequote format should render string with values enclosed in single quotes', () => {
const result = _templateSrv.formatValue(['test', "test'2"], 'singlequote');
expect(result).toBe("'test','test\\'2'");
});
it('single value and doublequote format should render string with value enclosed in double quotes', () => {
const result = _templateSrv.formatValue('test', 'doublequote');
expect(result).toBe('"test"');
});
it('multi value and doublequote format should render string with values enclosed in double quotes', () => {
const result = _templateSrv.formatValue(['test', 'test"2'], 'doublequote');
expect(result).toBe('"test","test\\"2"');
});
it('single value and sqlstring format should render string with value enclosed in single quotes', () => {
const result = _templateSrv.formatValue("test'value", 'sqlstring');
expect(result).toBe(`'test''value'`);
});
it('multi value and sqlstring format should render string with values enclosed in single quotes', () => {
const result = _templateSrv.formatValue(['test', "test'value2"], 'sqlstring');
expect(result).toBe(`'test','test''value2'`);
});
it('raw format should leave value intact and do no escaping', () => {
const result = _templateSrv.formatValue("'test\n", 'raw');
expect(result).toBe("'test\n");
});
});
describe('can check if variable exists', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv(key, [{ type: 'query', name: 'test', current: { value: 'oogle' } }]);
@ -747,6 +648,7 @@ describe('templateSrv', () => {
let passedValue: string | null = null;
_templateSrv.replace('this.${test}.filters', {}, (value: string) => {
passedValue = value;
return '';
});
expect(passedValue).toBe('[object Object]');
@ -763,6 +665,7 @@ describe('templateSrv', () => {
let passedValue: string | null = null;
_templateSrv.replace('this.${test}.filters', {}, (value: string) => {
passedValue = value;
return '';
});
expect(passedValue).toBe('hello');
@ -904,15 +807,6 @@ describe('templateSrv', () => {
const target = _templateSrv.replace('${adhoc}', { adhoc: { value: 'value2', text: 'value2' } }, 'queryparam');
expect(target).toBe('var-adhoc=value2');
});
it('Variable named ${__all_variables} is already formatted so skip any formatting', () => {
const target = _templateSrv.replace(
'${__all_variables}',
{ __all_variables: { value: 'var-server=server+name+with+plus%2B', skipFormat: true } },
'percentencode'
);
expect(target).toBe('var-server=server+name+with+plus%2B');
});
});
describe('scenes compatibility', () => {

View File

@ -7,6 +7,7 @@ import {
AdHocVariableFilter,
AdHocVariableModel,
TypedVariableModel,
ScopedVar,
} from '@grafana/data';
import {
getDataSourceSrv,
@ -14,7 +15,7 @@ import {
TemplateSrv as BaseTemplateSrv,
VariableInterpolation,
} from '@grafana/runtime';
import { sceneGraph, FormatRegistryID, formatRegistry, VariableCustomFormatterFn } from '@grafana/scenes';
import { sceneGraph, FormatRegistryID, VariableCustomFormatterFn } from '@grafana/scenes';
import { variableAdapters } from '../variables/adapters';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/constants';
@ -22,7 +23,8 @@ import { isAdHoc } from '../variables/guard';
import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors';
import { variableRegex } from '../variables/utils';
import { getVariableWrapper } from './LegacyVariableWrapper';
import { formatVariableValue } from './formatVariableValue';
import { macroRegistry } from './macroRegistry';
interface FieldAccessorCache {
[key: string]: (obj: any) => any;
@ -135,51 +137,6 @@ export class TemplateSrv implements BaseTemplateSrv {
return filters;
}
formatValue(value: any, format?: any, variable?: any, text?: string): string {
// for some scopedVars there is no variable
variable = variable || {};
if (value === null || value === undefined) {
return '';
}
if (isAdHoc(variable) && format !== FormatRegistryID.queryParam) {
return '';
}
// if it's an object transform value to string
if (!Array.isArray(value) && typeof value === 'object') {
value = `${value}`;
}
if (typeof format === 'function') {
return format(value, variable, this.formatValue);
}
if (!format) {
format = FormatRegistryID.glob;
}
// some formats have arguments that come after ':' character
let args = format.split(':');
if (args.length > 1) {
format = args[0];
args = args.slice(1);
} else {
args = [];
}
let formatItem = formatRegistry.getIfExists(format);
if (!formatItem) {
console.error(`Variable format ${format} not found. Using glob format as fallback.`);
formatItem = formatRegistry.get(FormatRegistryID.glob);
}
const formatVariable = getVariableWrapper(variable, value, text ?? value);
return formatItem.formatter(value, args, formatVariable);
}
setGrafanaVariable(name: string, value: any) {
this.grafanaVariables.set(name, value);
}
@ -257,12 +214,7 @@ export class TemplateSrv implements BaseTemplateSrv {
return (this.fieldAccessorCache[fieldPath] = property(fieldPath));
}
private getVariableValue(variableName: string, fieldPath: string | undefined, scopedVars: ScopedVars) {
const scopedVar = scopedVars[variableName];
if (!scopedVar) {
return null;
}
private getVariableValue(scopedVar: ScopedVar, fieldPath: string | undefined) {
if (fieldPath) {
return this.getFieldAccessor(fieldPath)(scopedVar.value);
}
@ -270,13 +222,7 @@ export class TemplateSrv implements BaseTemplateSrv {
return scopedVar.value;
}
private getVariableText(variableName: string, value: any, scopedVars: ScopedVars) {
const scopedVar = scopedVars[variableName];
if (!scopedVar) {
return null;
}
private getVariableText(scopedVar: ScopedVar, value: any) {
if (scopedVar.value === value || typeof value !== 'string') {
return scopedVar.text;
}
@ -287,7 +233,7 @@ export class TemplateSrv implements BaseTemplateSrv {
replace(
target?: string,
scopedVars?: ScopedVars,
format?: string | Function,
format?: string | Function | undefined,
interpolations?: VariableInterpolation[]
): string {
if (scopedVars && scopedVars.__sceneObject) {
@ -321,25 +267,26 @@ export class TemplateSrv implements BaseTemplateSrv {
match: string,
variableName: string,
fieldPath: string,
format: string | Function | undefined,
format: string | VariableCustomFormatterFn | undefined,
scopedVars: ScopedVars | undefined
) {
const variable = this.getVariableAtIndex(variableName);
const scopedVar = scopedVars?.[variableName];
if (scopedVars) {
const value = this.getVariableValue(variableName, fieldPath, scopedVars);
const text = this.getVariableText(variableName, value, scopedVars);
if (scopedVar) {
const value = this.getVariableValue(scopedVar, fieldPath);
const text = this.getVariableText(scopedVar, value);
if (value !== null && value !== undefined) {
if (scopedVars[variableName]?.skipFormat) {
format = undefined;
}
return this.formatValue(value, format, variable, text);
return formatVariableValue(value, format, variable, text);
}
}
if (!variable) {
if (macroRegistry[variableName]) {
return macroRegistry[variableName](match, fieldPath, scopedVars, format);
}
return match;
}
@ -347,12 +294,12 @@ export class TemplateSrv implements BaseTemplateSrv {
const value = variableAdapters.get(variable.type).getValueForUrl(variable);
const text = isAdHoc(variable) ? variable.id : variable.current.text;
return this.formatValue(value, format, variable, text);
return formatVariableValue(value, format, variable, text);
}
const systemValue = this.grafanaVariables.get(variable.current.value);
if (systemValue) {
return this.formatValue(systemValue, format, variable);
return formatVariableValue(systemValue, format, variable);
}
let value = variable.current.value;
@ -368,15 +315,13 @@ export class TemplateSrv implements BaseTemplateSrv {
}
if (fieldPath) {
const fieldValue = this.getVariableValue(variableName, fieldPath, {
[variableName]: { value, text },
});
const fieldValue = this.getVariableValue({ value, text }, fieldPath);
if (fieldValue !== null && fieldValue !== undefined) {
return this.formatValue(fieldValue, format, variable, text);
return formatVariableValue(fieldValue, format, variable, text);
}
}
return this.formatValue(value, format, variable, text);
return formatVariableValue(value, format, variable, text);
}
/**

View File

@ -0,0 +1,11 @@
import { ScopedVars } from '@grafana/data';
import { VariableCustomFormatterFn } from '@grafana/scenes';
export interface MacroHandler {
(
match: string,
fieldPath: string | undefined,
scopedVars: ScopedVars | undefined,
format: string | VariableCustomFormatterFn | undefined
): string;
}

View File

@ -101,13 +101,15 @@ describe('getAllVariableValuesForUrl', () => {
describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => {
beforeEach(() => {
setTemplateSrv(
initTemplateSrv(key, [{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] } }])
initTemplateSrv(key, [
{ type: 'query', name: 'test', rootStateKey: key, current: { value: ['val1', 'val2'] }, skipUrlSync: true },
])
);
});
it('should not set scoped value as url params', () => {
const params = getVariablesUrlParams({
test: { value: 'val1', text: 'val1text', skipUrlSync: true },
test: { value: 'val1', text: 'val1text' },
});
expect(params['var-test']).toBe(undefined);
});

View File

@ -10,16 +10,15 @@ export function getVariablesUrlParams(scopedVars?: ScopedVars): UrlQueryMap {
for (let i = 0; i < variables.length; i++) {
const variable = variables[i];
if (scopedVars && scopedVars[variable.name] !== void 0) {
if (scopedVars[variable.name].skipUrlSync) {
continue;
}
params[VARIABLE_PREFIX + variable.name] = scopedVars[variable.name].value;
const scopedVar = scopedVars && scopedVars[variable.name];
if (variable.skipUrlSync) {
continue;
}
if (scopedVar) {
params[VARIABLE_PREFIX + variable.name] = scopedVar.value;
} else {
// @ts-ignore
if (variable.skipUrlSync) {
continue;
}
params[VARIABLE_PREFIX + variable.name] = variableAdapters.get(variable.type).getValueForUrl(variable as any);
}
}

View File

@ -193,10 +193,7 @@ export class TestDataDataSource extends DataSourceWithBackend<TestData> {
variablesQuery(target: TestData, options: DataQueryRequest<TestData>): Observable<DataQueryResponse> {
const query = target.stringInput ?? '';
const interpolatedQuery = this.templateSrv.replace(
query,
getSearchFilterScopedVar({ query, wildcardChar: '*', options: options.scopedVars })
);
const interpolatedQuery = this.templateSrv.replace(query, getSearchFilterScopedVar({ query, wildcardChar: '*' }));
const children = queryMetricTree(interpolatedQuery);
const items = children.map((item) => ({ value: item.name, text: item.name }));
const dataFrame = new ArrayDataFrame(items);

View File

@ -18,7 +18,7 @@ const templateSrv = {
if (scopedVars) {
// For testing variables replacement in link
each(scopedVars, (val, key) => {
value = value.replace('$' + key, val.value);
value = value.replace('$' + key, val?.value);
});
}
return value;