Skip to content

Commit 66d31c4

Browse files
committed
added task for adding template variable support.
1 parent 1dd4fcb commit 66d31c4

File tree

1 file changed

+271
-0
lines changed

1 file changed

+271
-0
lines changed
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
# Add Support for Template & Query Variables
2+
3+
Variables are placeholders you can use to create templated queries and dynamic dashboards. In a data source, this means:
4+
5+
- Using template variables inside query fields
6+
- Implementing `metricFindQuery` and a `VariableQueryEditor` so the data source can be used as a **Query** type variable.
7+
8+
Always complete **all three sections**:
9+
10+
1. Interpolate template variables in queries
11+
2. Support query variables via `metricFindQuery`
12+
3. Add a `VariableQueryEditor`
13+
14+
## 1. Interpolate Template Variables in Queries
15+
16+
File: `src/datasource.ts` (or your data source class file)
17+
18+
### 1.1 Import `getTemplateSrv`
19+
20+
```ts
21+
import { getTemplateSrv } from '@grafana/runtime';
22+
```
23+
24+
- `getTemplateSrv()` returns `TemplateSrv`, which exposes `replace()` for variable interpolation.
25+
26+
### 1.2 Decide which fields support variables
27+
28+
In your query model (e.g. `MyQuery`), identify all string fields that should accept `$var`:
29+
30+
```ts
31+
export interface MyQuery {
32+
// existing…
33+
rawQuery?: string; // e.g. SQL / text query
34+
namespace?: string; // optional selector
35+
// other fields…
36+
}
37+
```
38+
39+
Rules:
40+
41+
- Only enable variables where they actually make sense (query strings, selectors, filters).
42+
- Document which fields support variables if you have query editor help.
43+
44+
### 1.3 Apply `replace()` in `query()`
45+
46+
Inside your `DataSource` implementation:
47+
48+
```ts
49+
import { DataQueryRequest, DataQueryResponse } from '@grafana/schema';
50+
51+
export class DataSource extends DataSourceApi<MyQuery> {
52+
async query(options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
53+
const targets = options.targets.filter((t) => !t.hide);
54+
55+
const interpolatedTargets = targets.map((target) => {
56+
const rawQuery = getTemplateSrv().replace(
57+
target.rawQuery ?? '',
58+
options.scopedVars // include scoped vars for panel/time range
59+
);
60+
61+
const namespace = getTemplateSrv().replace(target.namespace ?? '', options.scopedVars);
62+
63+
return {
64+
...target,
65+
rawQuery,
66+
namespace,
67+
};
68+
});
69+
70+
// Use interpolatedTargets when building your backend request.
71+
return this.doRequest(interpolatedTargets, options);
72+
}
73+
74+
private async doRequest(targets: MyQuery[], options: DataQueryRequest<MyQuery>): Promise<DataQueryResponse> {
75+
// existing implementation…
76+
}
77+
}
78+
```
79+
80+
- `replace(template, scopedVars?)` replaces `$var` with current values and supports built-ins like `$__from` / `$__to`.
81+
- You can keep interpolation localized to **only** the fields that support variables (e.g. `rawQuery`, `namespace`).
82+
83+
### 1.4 Handle multi-value variables (optional)
84+
85+
For multi-value variables, choose a format that matches your backend (CSV, JSON array, etc.):
86+
87+
```ts
88+
const csvQuery = getTemplateSrv().replace(
89+
target.rawQuery ?? '',
90+
options.scopedVars,
91+
'csv' // built-in format option
92+
);
93+
```
94+
95+
Or provide a custom formatter:
96+
97+
```ts
98+
const formatter = (value: string | string[]): string => {
99+
if (typeof value === 'string') {
100+
return value;
101+
}
102+
103+
// Example: join with OR
104+
if (value.length > 1) {
105+
return '(' + value.map((v) => `"${v}"`).join(' OR ') + ')';
106+
}
107+
108+
return value[0];
109+
};
110+
111+
const formattedQuery = getTemplateSrv().replace(target.rawQuery ?? '', options.scopedVars, formatter);
112+
```
113+
114+
Rules:
115+
116+
- Use a built-in format (`csv`, etc.) where possible.
117+
- Use a custom interpolation function only if built-ins don’t match your protocol.
118+
119+
## 2. Implement `metricFindQuery` for Query Variables
120+
121+
File: `src/datasource.ts`
122+
123+
A **query variable** lets Grafana call your data source to get variable values. To support this, you override `metricFindQuery` in your `DataSourceApi` implementation.
124+
125+
### 2.1 Define a variable query model
126+
127+
File: `src/types.ts`
128+
129+
```ts
130+
export interface MyVariableQuery {
131+
namespace: string;
132+
rawQuery: string;
133+
}
134+
```
135+
136+
- Keep this model minimal – only the fields needed to fetch variable values.
137+
- This is separate from your “regular” `MyQuery` if that simplifies things.
138+
139+
> If a plain text query is enough, you can leave `query` as `string` and skip the model entirely:
140+
> `async metricFindQuery(query: string, options?: any)`
141+
142+
### 2.2 Implement `metricFindQuery`
143+
144+
File: `src/datasource.ts`
145+
146+
```ts
147+
import { MetricFindValue } from '@grafana/data';
148+
import { getTemplateSrv } from '@grafana/runtime';
149+
import { MyVariableQuery } from './types';
150+
151+
export class DataSource extends DataSourceApi<MyQuery> {
152+
// existing query()…
153+
154+
async metricFindQuery(variableQuery: MyVariableQuery | string, options?: any): Promise<MetricFindValue[]> {
155+
if (typeof variableQuery === 'string') {
156+
const interpolated = getTemplateSrv().replace(variableQuery);
157+
const response = await this.fetchVariableValues({ rawQuery: interpolated });
158+
return response.map((name) => ({ text: name }));
159+
}
160+
161+
// If using MyVariableQuery model:
162+
const namespace = getTemplateSrv().replace(variableQuery.namespace);
163+
const rawQuery = getTemplateSrv().replace(variableQuery.rawQuery);
164+
165+
const response = await this.fetchMetricNames(namespace, rawQuery);
166+
167+
// Adapt this to match your backend response
168+
return response.data.map((item: any) => ({
169+
text: item.name,
170+
// optional: value: item.id,
171+
}));
172+
}
173+
174+
private async fetchMetricNames(namespace: string, rawQuery: string) {
175+
// call backend/API and return data in a consistent shape
176+
}
177+
178+
private async fetchVariableValues(args: { rawQuery: string }) {
179+
// simplified variant if using a simple string-based query
180+
}
181+
}
182+
```
183+
184+
Rules:
185+
186+
- Return an array of `{ text: string }` (`MetricFindValue[]`).
187+
- Use `getTemplateSrv().replace` inside `metricFindQuery` so variable queries can themselves use other variables (e.g. cascading variables).
188+
- Keep queries lightweight – `metricFindQuery` can be called often by Grafana.
189+
190+
## 3. Add a `VariableQueryEditor`
191+
192+
If you use a structured `MyVariableQuery` model, add a small React editor so users can configure it from the variable UI.
193+
194+
### 3.1 Create `VariableQueryEditor`
195+
196+
File: `src/VariableQueryEditor.tsx`
197+
198+
```tsx
199+
import React, { useState } from 'react';
200+
import { MyVariableQuery } from './types';
201+
202+
interface VariableQueryProps {
203+
query: MyVariableQuery;
204+
onChange: (query: MyVariableQuery, definition: string) => void;
205+
}
206+
207+
export const VariableQueryEditor = ({ query, onChange }: VariableQueryProps) => {
208+
const [state, setState] = useState<MyVariableQuery>(query);
209+
210+
const saveQuery = () => {
211+
// Second argument is the human-readable label shown in the variable list
212+
const definition = `${state.rawQuery} (${state.namespace})`;
213+
onChange(state, definition);
214+
};
215+
216+
const handleChange = (event: React.FormEvent<HTMLInputElement>) => {
217+
const { name, value } = event.currentTarget;
218+
219+
const next = {
220+
...state,
221+
[name]: value,
222+
};
223+
224+
setState(next);
225+
};
226+
227+
return (
228+
<>
229+
<div className="gf-form">
230+
<span className="gf-form-label width-10">Namespace</span>
231+
<input
232+
name="namespace"
233+
className="gf-form-input"
234+
value={state.namespace}
235+
onChange={handleChange}
236+
onBlur={saveQuery}
237+
/>
238+
</div>
239+
240+
<div className="gf-form">
241+
<span className="gf-form-label width-10">Query</span>
242+
<input
243+
name="rawQuery"
244+
className="gf-form-input"
245+
value={state.rawQuery}
246+
onChange={handleChange}
247+
onBlur={saveQuery}
248+
/>
249+
</div>
250+
</>
251+
);
252+
};
253+
```
254+
255+
### 3.2 Register `VariableQueryEditor` in the plugin
256+
257+
File: `src/module.ts`
258+
259+
```ts
260+
import { DataSourcePlugin } from '@grafana/data';
261+
import { DataSource } from './datasource';
262+
import { QueryEditor } from './QueryEditor';
263+
import { VariableQueryEditor } from './VariableQueryEditor';
264+
import { MyQuery, MyDataSourceOptions, MyVariableQuery } from './types';
265+
266+
export const plugin = new DataSourcePlugin<DataSource, MyQuery, MyDataSourceOptions>(DataSource)
267+
.setQueryEditor(QueryEditor)
268+
.setVariableQueryEditor(VariableQueryEditor);
269+
```
270+
271+
- `setVariableQueryEditor` wires your editor into Grafana’s variable UI.

0 commit comments

Comments
 (0)