Skip to content

Commit 8ad4389

Browse files
authored
Add support for custom paths (#24)
* Add support for custom paths * Fix variable query editor * Update docs
1 parent a1c644a commit 8ad4389

File tree

10 files changed

+109
-38
lines changed

10 files changed

+109
-38
lines changed

README.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,40 @@
11
# JSON API data source for Grafana
22

3-
[![License](https://img.shields.io/github/license/marcusolsson/grafana-jsonapi-datasource)](LICENSE)
3+
[![License](https://img.shields.io/github/license/marcusolsson/grafana-json-datasource)](LICENSE)
44
[![PRs welcome!](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](#contribute)
5-
[![Build](https://github.com/marcusolsson/grafana-jsonapi-datasource/workflows/CI/badge.svg)](https://github.com/marcusolsson/grafana-jsonapi-datasource/actions?query=workflow%3A%22CI%22)
6-
[![Release](https://github.com/marcusolsson/grafana-jsonapi-datasource/workflows/Release/badge.svg)](https://github.com/marcusolsson/grafana-jsonapi-datasource/actions?query=workflow%3ARelease)
5+
[![Build](https://github.com/marcusolsson/grafana-json-datasource/workflows/CI/badge.svg)](https://github.com/marcusolsson/grafana-json-datasource/actions?query=workflow%3A%22CI%22)
6+
[![Release](https://github.com/marcusolsson/grafana-json-datasource/workflows/Release/badge.svg)](https://github.com/marcusolsson/grafana-json-datasource/actions?query=workflow%3ARelease)
77

88
A data source plugin for loading JSON APIs into [Grafana](https://grafana.com).
99

10-
![Screenshot](https://github.com/marcusolsson/grafana-jsonapi-datasource/raw/master/src/img/screenshot.png)
10+
![Screenshot](https://github.com/marcusolsson/grafana-json-datasource/raw/master/src/img/screenshot.png)
1111

1212
Extract one or more values from a JSON API using [JSON Path](https://goessner.net/articles/JsonPath/). Each path results in a field in the query result. All fields need to be of the same length.
1313

14-
The field name defaults to the name of the property referenced by the JSON Path but can be set to an **Alias**. **This is going away in a future release, since Grafana now lets you rename any field.**
14+
## Configuration
1515

16-
The **Cache Time** determines the time in seconds to save the API response.
16+
This section lists the available configuration options for the JSON API data source.
1717

18-
**Custom query parameters** lets you override the query parameters configured by the data source.
18+
### Query editor
19+
20+
| Configuration | Description |
21+
|---------------|-------------|
22+
| **Path** | Appends a URL path to the URL configured by the data source. |
23+
| **Query string** | Overrides the custom query parameters configured by the data source. |
24+
| **Cache Time** | Determines the time in seconds to save the API response. |
25+
| **Query** | Defines the [JSON Path](https://goessner.net/articles/JsonPath/) used to extract the field. |
26+
27+
### Variables
28+
29+
[Variables](https://grafana.com/docs/grafana/latest/variables) are supported for all text configuration options.
30+
31+
### Macros
32+
33+
Use macros in your query string and JSON Path queries to add dashboard context to your queries. Macros are available for the **Query string** and (JSON Path) **Query** options.
34+
| Macro | Description |
35+
|-------|-------------|
36+
| `$__unixEpochFrom()` | Start of the dashboard time interval as a Unix timestamp, i.e. 1494410783 |
37+
| `$__unixEpochTo()` | End of the dashboard time interval as a Unix timestamp, i.e. 1494410783 |
1938

2039
## Public data sets
2140

@@ -42,7 +61,7 @@ List episodes from your favorite TV series.
4261
#### Configuration
4362

4463
- **URL:** `http://api.tvmaze.com/singlesearch/shows`
45-
- **Custom query parameters:** `q=rick-&-morty&embed=episodes`
64+
- **Query string:** `q=archer&embed=episodes`
4665

4766
#### Sample queries
4867

src/api.ts

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export default class Api {
1616
/**
1717
* Queries the API and returns the response data.
1818
*/
19-
async get(params?: string) {
19+
async get(path: string, params?: string) {
2020
const data: Record<string, string> = {};
2121

2222
this.params.forEach((value, key) => {
@@ -27,7 +27,7 @@ export default class Api {
2727
data[key] = value;
2828
});
2929

30-
const response = await this._request(data);
30+
const response = await this._request(path, data);
3131

3232
return response.data;
3333
}
@@ -42,42 +42,58 @@ export default class Api {
4242
data[key] = value;
4343
});
4444

45-
return this._request(data);
45+
return this._request('', data);
4646
}
4747

4848
/**
4949
* Returns a cached API response if it exists, otherwise queries the API.
5050
*/
51-
async cachedGet(cacheDurationSeconds: number, params: string) {
51+
async cachedGet(cacheDurationSeconds: number, path: string, params: string) {
5252
if (cacheDurationSeconds === 0) {
53-
return await this.get(params);
53+
return await this.get(path, params);
5454
}
5555

56+
const rawUrl = this.baseUrl + path;
57+
5658
const force = this.lastCacheDuration !== cacheDurationSeconds;
5759

5860
if (force) {
59-
this.cache.del(this.baseUrl);
61+
this.cache.del(rawUrl);
6062
}
6163

62-
const cachedItem = this.cache.get(this.baseUrl);
64+
const cachedItem = this.cache.get(rawUrl);
6365
if (cachedItem && !force) {
6466
return Promise.resolve(cachedItem);
6567
}
6668
this.lastCacheDuration = cacheDurationSeconds;
6769

68-
const result = await this.get(params);
70+
const result = await this.get(path, params);
6971

70-
this.cache.put(this.baseUrl, result, Math.max(cacheDurationSeconds * 1000, 1));
72+
this.cache.put(rawUrl, result, Math.max(cacheDurationSeconds * 1000, 1));
7173

7274
return result;
7375
}
7476

75-
async _request(data?: Record<string, string>) {
77+
/**
78+
* Make an API request using the data source configuration as defaults.
79+
* Allows the user to append a path, or override query parameters.
80+
*
81+
* @param path to append to URL
82+
* @param data to set as query parameters
83+
*/
84+
async _request(path: string, data?: Record<string, string>) {
7685
const req = {
77-
url: `${this.baseUrl}`,
86+
url: `${this.baseUrl}${path}`,
7887
method: 'GET',
7988
};
8089

90+
// Deduplicate forward slashes, i.e. /// becomes /. This enables sensible
91+
// defaults for empty variables.
92+
//
93+
// For example, `/orgs/${orgId}/list` becomes `/orgs/list` instead of
94+
// `/orgs//list`.
95+
req.url = req.url.replace(/[\/]+/g, '/');
96+
8197
if (data && Object.keys(data).length > 0) {
8298
req.url =
8399
req.url +

src/components/ConfigEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
3838
to set them explicitly. */}
3939
<h3 className="page-heading">Misc</h3>
4040
<InlineFieldRow>
41-
<InlineField label="Custom query parameters" tooltip="Add custom parameters to your queries.">
41+
<InlineField label="Query string" tooltip="Add a custom query string to your queries.">
4242
<Input
4343
width={50}
4444
value={options.jsonData.queryParams}

src/components/JsonPathQueryField.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ interface Props {
1212
* JsonPathQueryField is an editor for JSON Path.
1313
*/
1414
export const JsonPathQueryField: React.FC<Props> = ({ query, onBlur, onChange }) => {
15+
/**
16+
* The QueryField supports Slate plugins, so let's add a few useful ones.
17+
*/
1518
const plugins = [
1619
BracesPlugin(),
1720
SlatePrism({

src/components/QueryEditor.tsx

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,28 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) =>
3434
return (
3535
<>
3636
<InlineFieldRow>
37+
<InlineField
38+
label="Path"
39+
tooltip="Append a custom path to the data source URL. Should start with a forward slash (/)."
40+
grow
41+
>
42+
<Input
43+
placeholder="/orders/${orderId}"
44+
value={query.urlPath}
45+
onChange={e => onChange({ ...query, urlPath: e.currentTarget.value })}
46+
/>
47+
</InlineField>
48+
<InlineField
49+
label="Query string"
50+
tooltip="Add custom query parameters to your URL. Any parameters you add here overrides the custom parameters that have been configured by the data source."
51+
grow
52+
>
53+
<Input
54+
placeholder="page=1&limit=100"
55+
value={query.queryParams}
56+
onChange={e => onChange({ ...query, queryParams: e.currentTarget.value })}
57+
/>
58+
</InlineField>
3759
<InlineField
3860
label="Cache Time"
3961
tooltip="Time in seconds that the response will be cached in Grafana after receiving it."
@@ -49,19 +71,6 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) =>
4971
/>
5072
</InlineField>
5173
</InlineFieldRow>
52-
<InlineFieldRow>
53-
<InlineField
54-
label="Custom query parameters"
55-
tooltip="Add custom parameters to your queries. Any parameters you add here overrides the custom parameters that have been configured by the data source."
56-
grow
57-
>
58-
<Input
59-
placeholder="page=1&limit=100"
60-
value={query.queryParams}
61-
onChange={e => onChange({ ...query, queryParams: e.currentTarget.value })}
62-
/>
63-
</InlineField>
64-
</InlineFieldRow>
6574
{fields
6675
? fields.map((field, index) => (
6776
<InlineFieldRow key={index}>

src/components/VariableQueryEditor.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,27 @@ export const VariableQueryEditor: React.FC<VariableQueryProps> = ({ onChange, qu
2020
};
2121

2222
const onChangePath = (jsonPath: string) => setState({ ...state, jsonPath });
23+
const onChangeUrlPath = (urlPath: string) => setState({ ...state, urlPath });
2324
const onQueryParams = (queryParams: string) => setState({ ...state, queryParams });
2425

2526
return (
2627
<>
2728
<InlineFieldRow>
2829
<InlineField
29-
label="Custom query parameters"
30-
tooltip="Add custom parameters to your queries. Any parameters you add here overrides the custom parameters that have been configured by the data source."
30+
label="Path"
31+
tooltip="Append a custom path to the data source URL. Should start with a forward slash (/)."
32+
grow
33+
>
34+
<Input
35+
placeholder="/orders/${orderId}"
36+
value={query.urlPath}
37+
onChange={e => onChangeUrlPath(e.currentTarget.value)}
38+
onBlur={saveQuery}
39+
/>
40+
</InlineField>
41+
<InlineField
42+
label="Query string"
43+
tooltip="Add custom query parameters to your URL. Any parameters you add here overrides the custom parameters that have been configured by the data source."
3144
grow
3245
>
3346
<Input

src/datasource.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOpt
3535

3636
const promises = request.targets.map(async query => {
3737
const queryParamsTreated = replaceMacros(templateSrv.replace(query.queryParams, request.scopedVars));
38+
const urlPathTreated = templateSrv.replace(query.urlPath, request.scopedVars);
3839

39-
const response = await this.api.cachedGet(query.cacheDurationSeconds, queryParamsTreated);
40+
const response = await this.api.cachedGet(query.cacheDurationSeconds, urlPathTreated, queryParamsTreated);
4041

4142
const fields = query.fields
4243
.filter(field => field.jsonPath)
@@ -87,10 +88,16 @@ export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOpt
8788
return [];
8889
}
8990

90-
const response = await this.api.get(query.queryParams);
91+
const templateSrv = getTemplateSrv();
92+
93+
const queryParamsTreated = templateSrv.replace(query.queryParams);
94+
const urlPathTreated = templateSrv.replace(query.urlPath);
95+
const jsonPathTreated = templateSrv.replace(query.jsonPath);
96+
97+
const response = await this.api.get(urlPathTreated, queryParamsTreated);
9198

9299
return JSONPath({
93-
path: query.jsonPath,
100+
path: jsonPathTreated,
94101
json: response,
95102
}).map((_: any) => ({ text: _ }));
96103
}

src/img/screenshot.png

45 KB
Loading

src/img/variable.png

179 KB
Loading

src/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,22 +9,26 @@ export interface JsonApiQuery extends DataQuery {
99
fields: JsonField[];
1010
cacheDurationSeconds: number;
1111
queryParams: string;
12+
urlPath: string;
1213
}
1314

1415
export interface JsonApiVariableQuery extends DataQuery {
1516
jsonPath: string;
1617
queryParams: string;
18+
urlPath: string;
1719
}
1820

1921
export const defaultQuery: Partial<JsonApiQuery> = {
2022
fields: [{ name: '', jsonPath: '' }],
2123
cacheDurationSeconds: 300,
2224
queryParams: '',
25+
urlPath: '',
2326
};
2427

2528
export const defaultVariableQuery: Partial<JsonApiVariableQuery> = {
2629
jsonPath: '',
2730
queryParams: '',
31+
urlPath: '',
2832
};
2933

3034
export interface JsonApiDataSourceOptions extends DataSourceJsonData {

0 commit comments

Comments
 (0)