Dashboards: Data source template variable options now specify a current value using uid. (#69259)

* feat: template data source option current by uid

* fix: e2e with explicit uid for "slow-prometheus"

* revert: unrelated change

* revert: unreverted the actually related change

* chore: e2e support for testing ${variable:text}

* fix: use `:raw` instead of `:value` in e2e dashboard
This commit is contained in:
Darren Janeczek 2023-06-12 04:05:50 -04:00 committed by GitHub
parent 3f6d55b041
commit d61af3adde
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 164 additions and 17 deletions

View File

@ -63,6 +63,7 @@ datasources:
prometheusVersion: 2.40.0
- name: gdev-slow-prometheus
uid: gdev-slow-prometheus-uid
type: prometheus
access: proxy
url: http://localhost:3011

View File

@ -55,6 +55,38 @@
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 9
},
"id": 2,
"options": {
"mode": "markdown",
"content": "VariableUnderTestText: ${VariableUnderTest:text}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
},
{
"gridPos": {
"h": 9,
"w": 12,
"x": 0,
"y": 18
},
"id": 2,
"options": {
"mode": "markdown",
"content": "VariableUnderTestRaw: ${VariableUnderTest:raw}"
},
"pluginVersion": "8.4.0-pre",
"title": "Panel Title",
"type": "text"
}
],
"schemaVersion": 35,

View File

@ -38,6 +38,7 @@ describe('Variables - Datasource', () => {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('gdev-slow-prometheus').click();
// Assert it was rendered
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus');
e2e().get('.markdown-html').should('include.text', 'VariableUnderTest: gdev-slow-prometheus-uid');
e2e().get('.markdown-html').should('include.text', 'VariableUnderTestText: gdev-slow-prometheus');
});
});

View File

@ -254,7 +254,8 @@ export class DatasourceSrv implements DataSourceService {
// Support for multi-value variables with only one selected datasource
dsValue = dsValue[0];
}
const dsSettings = !Array.isArray(dsValue) && this.settingsMapByName[dsValue];
const dsSettings =
!Array.isArray(dsValue) && (this.settingsMapByName[dsValue] || this.settingsMapByUid[dsValue]);
if (dsSettings) {
const key = `$\{${variable.name}\}`;

View File

@ -18,6 +18,13 @@ const templateSrv: any = {
value: 'BBB',
},
},
{
type: 'datasource',
name: 'datasourceByUid',
current: {
value: 'uid-code-DDDD',
},
},
{
type: 'datasource',
name: 'datasourceDefault',
@ -32,6 +39,7 @@ const templateSrv: any = {
}
let result = v.replace('${datasource}', 'BBB');
result = result.replace('${datasourceByUid}', 'DDDD');
result = result.replace('${datasourceDefault}', 'default');
return result;
},
@ -103,6 +111,12 @@ describe('datasource_srv', () => {
meta: { metrics: true },
isDefault: true,
},
DDDD: {
type: 'test-db',
name: 'DDDD',
uid: 'uid-code-DDDD',
meta: { metrics: true },
},
Jaeger: {
type: 'jaeger-db',
name: 'Jaeger',
@ -165,7 +179,7 @@ describe('datasource_srv', () => {
expect(dataSourceSrv.getInstanceSettings({ uid: 'uid-code-mmm' })).toBe(ds);
});
it('should work with variable', () => {
it('should work with variable by ds name', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasource}');
expect(ds?.name).toBe('${datasource}');
expect(ds?.uid).toBe('${datasource}');
@ -177,6 +191,18 @@ describe('datasource_srv', () => {
`);
});
it('should work with variable by ds value (uid)', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasourceByUid}');
expect(ds?.name).toBe('${datasourceByUid}');
expect(ds?.uid).toBe('${datasourceByUid}');
expect(ds?.rawRef).toMatchInlineSnapshot(`
{
"type": "test-db",
"uid": "uid-code-DDDD",
}
`);
});
it('should work with variable via scopedVars', () => {
const ds = dataSourceSrv.getInstanceSettings('${datasource}', {
datasource: { text: 'Prom', value: 'uid-code-aaa' },
@ -247,7 +273,7 @@ describe('datasource_srv', () => {
describe('when getting external metric sources', () => {
it('should return list of explore sources', () => {
const externalSources = dataSourceSrv.getExternal();
expect(externalSources.length).toBe(6);
expect(externalSources.length).toBe(7);
});
});
@ -260,8 +286,9 @@ describe('datasource_srv', () => {
it('Can get list of data sources with variables: true', () => {
const list = dataSourceSrv.getList({ metrics: true, variables: true });
expect(list[0].name).toBe('${datasourceDefault}');
expect(list[1].name).toBe('${datasource}');
expect(list[0].name).toBe('${datasourceByUid}');
expect(list[1].name).toBe('${datasourceDefault}');
expect(list[2].name).toBe('${datasource}');
});
it('Can get list of data sources with tracing: true', () => {
@ -300,6 +327,14 @@ describe('datasource_srv', () => {
"type": "test-db",
"uid": "uid-code-BBB",
},
{
"meta": {
"metrics": true,
},
"name": "DDDD",
"type": "test-db",
"uid": "uid-code-DDDD",
},
{
"meta": {
"annotations": true,

View File

@ -42,7 +42,7 @@ export const dataSourceVariableSlice = createSlice({
}
if (isValid(source, regex)) {
options.push({ text: source.name, value: source.name, selected: false });
options.push({ text: source.name, value: source.uid, selected: false });
}
if (isDefault(source, regex)) {

View File

@ -3,7 +3,7 @@ import { DataSourceInstanceSettings, DataSourceJsonData, DataSourcePluginMeta }
export function getDataSourceInstanceSetting(name: string, meta: DataSourcePluginMeta): DataSourceInstanceSettings {
return {
id: 1,
uid: '',
uid: name,
type: '',
name,
meta,

View File

@ -3,14 +3,20 @@ import { VariableOption, VariableWithOptions } from 'app/features/variables/type
import { VariableBuilder } from './variableBuilder';
export class OptionsVariableBuilder<T extends VariableWithOptions> extends VariableBuilder<T> {
withOptions(...texts: string[]) {
withOptions(...options: Array<string | { text: string; value: string }>) {
this.variable.options = [];
for (let index = 0; index < texts.length; index++) {
this.variable.options.push({
text: texts[index],
value: texts[index],
selected: false,
});
for (let index = 0; index < options.length; index++) {
const option = options[index];
if (typeof option === 'string') {
this.variable.options.push({
text: option,
value: option,
selected: false,
});
} else {
this.variable.options.push({ ...option, selected: false });
}
}
return this;
}

View File

@ -381,6 +381,74 @@ describe('shared actions', () => {
});
});
describe('and not multivalue, but with currentValue specified', () => {
const A = { text: 'A', value: 'a-uid' };
const B = { text: 'B', value: 'b-uid' };
const C = { text: 'C', value: 'c-uid' };
it.each`
withOptions | currentText | currentValue | defaultValue | expected
${[A, B, C]} | ${undefined} | ${undefined} | ${undefined} | ${A}
${[A, B, C]} | ${'B'} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${'B'} | ${undefined} | ${undefined} | ${B}
${[A, B, C]} | ${undefined} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${'Old B'} | ${'b-uid'} | ${undefined} | ${B}
${[A, B, C]} | ${undefined} | ${'x-uid'} | ${'b-uid'} | ${B}
${[A, B, C]} | ${undefined} | ${'b-uid'} | ${'c-uid'} | ${B}
${[A, B, C]} | ${undefined} | ${'x-uid'} | ${undefined} | ${A}
${undefined} | ${undefined} | ${'b-uid'} | ${undefined} | ${'should not dispatch setCurrentVariableValue'}
`(
'then correct actions are dispatched',
async ({ withOptions, currentText, currentValue, defaultValue, expected }) => {
let custom;
const key = 'key';
if (!withOptions) {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withCurrent(currentText, currentValue)
.withoutOptions()
.build();
} else {
custom = customBuilder()
.withId('0')
.withRootStateKey(key)
.withOptions(...withOptions)
.withCurrent(currentText, currentValue)
.build();
}
const tester = await reduxTester<TemplatingReducerType>()
.givenRootReducer(getTemplatingRootReducer())
.whenActionIsDispatched(
toKeyedAction(key, addVariable(toVariablePayload(custom, { global: false, index: 0, model: custom })))
)
.whenAsyncActionIsDispatched(
validateVariableSelectionState(toKeyedVariableIdentifier(custom), defaultValue),
true
);
await tester.thenDispatchedActionsPredicateShouldEqual((dispatchedActions) => {
const expectedActions: AnyAction[] = withOptions
? [
toKeyedAction(
key,
setCurrentVariableValue(
toVariablePayload(
{ type: 'custom', id: '0' },
{ option: { text: expected.text, value: expected.value, selected: false } }
)
)
),
]
: [];
expect(dispatchedActions).toEqual(expectedActions);
return true;
});
}
);
});
describe('and multivalue', () => {
it.each`
withOptions | withCurrent | defaultValue | expectedText | expectedSelected

View File

@ -60,6 +60,7 @@ import {
ensureStringValues,
ExtendedUrlQueryMap,
getCurrentText,
getCurrentValue,
getVariableRefresh,
hasOngoingTransaction,
toKeyedVariableIdentifier,
@ -522,14 +523,16 @@ export const validateVariableSelectionState = (
// 1. find the current value
const text = getCurrentText(variableInState);
option = variableInState.options?.find((v) => v.text === text);
const value = getCurrentValue(variableInState);
option = variableInState.options?.find((v: VariableOption) => v.text === text || v.value === value);
if (option) {
return setValue(variableInState, option);
}
// 2. find the default value
if (defaultValue) {
option = variableInState.options?.find((v) => v.text === defaultValue);
option = variableInState.options?.find((v) => v.text === defaultValue || v.value === defaultValue);
if (option) {
return setValue(variableInState, option);
}