Skip to content

Commit ad7d5e0

Browse files
committed
Extract query editor into a reusable component
Fixes #53
1 parent 8d9eb3b commit ad7d5e0

File tree

6 files changed

+135
-176
lines changed

6 files changed

+135
-176
lines changed
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import React from 'react';
2+
import { QueryEditorProps } from '@grafana/data';
3+
import { DataSource } from '../datasource';
4+
import { JsonApiDataSourceOptions, JsonApiQuery } from '../types';
5+
import { QueryEditor } from './QueryEditor';
6+
7+
type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
8+
9+
export const DashboardQueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) => {
10+
return <QueryEditor onRunQuery={onRunQuery} onChange={onChange} query={query} />;
11+
};

src/components/QueryEditor.tsx

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,24 @@ import {
1212
useTheme,
1313
InfoBox,
1414
} from '@grafana/ui';
15-
import { QueryEditorProps, SelectableValue, FieldType } from '@grafana/data';
16-
import { DataSource } from '../datasource';
17-
import { JsonApiDataSourceOptions, JsonApiQuery, defaultQuery } from '../types';
15+
import { SelectableValue, FieldType } from '@grafana/data';
16+
import { JsonApiQuery, defaultQuery } from '../types';
1817
import { JsonPathQueryField } from './JsonPathQueryField';
1918
import { KeyValueEditor } from './KeyValueEditor';
2019
import AutoSizer from 'react-virtualized-auto-sizer';
2120
import { css } from 'emotion';
2221
import { Pair } from '../types';
2322

24-
type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
23+
// type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
2524

26-
export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) => {
25+
interface Props {
26+
onRunQuery: () => void;
27+
onChange: (query: JsonApiQuery) => void;
28+
query: JsonApiQuery;
29+
limitFields?: number;
30+
}
31+
32+
export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query, limitFields }) => {
2733
const [bodyType, setBodyType] = useState('plaintext');
2834
const [tabIndex, setTabIndex] = useState(0);
2935
const theme = useTheme();
@@ -72,10 +78,13 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) =>
7278
};
7379

7480
const addField = (i: number) => () => {
75-
if (fields) {
76-
fields.splice(i + 1, 0, { name: '', jsonPath: '' });
81+
console.log(limitFields, fields.length);
82+
if (!limitFields || fields.length < limitFields) {
83+
if (fields) {
84+
fields.splice(i + 1, 0, { name: '', jsonPath: '' });
85+
}
86+
onChange({ ...query, fields });
7787
}
78-
onChange({ ...query, fields });
7988
};
8089

8190
const removeField = (i: number) => () => {
@@ -121,9 +130,13 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) =>
121130
]}
122131
/>
123132
</InlineField>
124-
<a className="gf-form-label" onClick={addField(index)}>
125-
<Icon name="plus" />
126-
</a>
133+
134+
{(!limitFields || fields.length < limitFields) && (
135+
<a className="gf-form-label" onClick={addField(index)}>
136+
<Icon name="plus" />
137+
</a>
138+
)}
139+
127140
{fields.length > 1 ? (
128141
<a className="gf-form-label" onClick={removeField(index)}>
129142
<Icon name="minus" />
Lines changed: 11 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,24 @@
1-
import defaults from 'lodash/defaults';
2-
31
import React, { useState } from 'react';
4-
import { JsonApiVariableQuery, defaultVariableQuery } from '../types';
5-
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
6-
import { JsonPathQueryField } from './JsonPathQueryField';
2+
import { JsonApiQuery } from '../types';
3+
import { QueryEditor } from './QueryEditor';
74

85
interface VariableQueryProps {
9-
query: JsonApiVariableQuery;
10-
onChange: (query: JsonApiVariableQuery, definition: string) => void;
6+
query: JsonApiQuery;
7+
onChange: (query: JsonApiQuery, definition: string) => void;
118
}
129

1310
// VariableQueryEditor is used to query values for a dashboard variable.
1411
export const VariableQueryEditor: React.FC<VariableQueryProps> = ({ onChange, query }) => {
15-
const init = defaults(query, defaultVariableQuery);
12+
// Backwards compatibility with previous query editor for variables.
13+
const compatQuery = query.jsonPath ? { ...query, fields: [{ jsonPath: query.jsonPath }] } : query;
1614

17-
const [state, setState] = useState<JsonApiVariableQuery>(init);
15+
const [state, setState] = useState<JsonApiQuery>(compatQuery);
1816

1917
const saveQuery = () => {
20-
onChange(state, state.jsonPath);
18+
if (state && state.fields[0].jsonPath) {
19+
onChange(state, state.fields[0].jsonPath);
20+
}
2121
};
2222

23-
const onChangePath = (jsonPath: string) => setState({ ...state, jsonPath });
24-
const onChangeUrlPath = (urlPath: string) => setState({ ...state, urlPath });
25-
const onQueryParams = (queryParams: string) => setState({ ...state, queryParams });
26-
27-
return (
28-
<>
29-
<InlineFieldRow>
30-
<InlineField
31-
label="Path"
32-
tooltip="Append a custom path to the data source URL. Should start with a forward slash (/)."
33-
grow
34-
>
35-
<Input
36-
placeholder="/orders/${orderId}"
37-
value={query.urlPath}
38-
onChange={e => onChangeUrlPath(e.currentTarget.value)}
39-
onBlur={saveQuery}
40-
/>
41-
</InlineField>
42-
<InlineField
43-
label="Query string"
44-
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."
45-
grow
46-
>
47-
<Input
48-
placeholder="page=1&limit=100"
49-
value={state.queryParams}
50-
onChange={e => onQueryParams(e.currentTarget.value)}
51-
onBlur={saveQuery}
52-
/>
53-
</InlineField>
54-
</InlineFieldRow>
55-
<InlineFieldRow>
56-
<InlineField
57-
label="Query"
58-
tooltip={
59-
<div>
60-
A <a href="https://goessner.net/articles/JsonPath/">JSON Path</a> query that selects one or more values
61-
from a JSON object.
62-
</div>
63-
}
64-
grow
65-
>
66-
<JsonPathQueryField onBlur={saveQuery} onChange={onChangePath} query={state.jsonPath} />
67-
</InlineField>
68-
</InlineFieldRow>
69-
</>
70-
);
23+
return <QueryEditor onRunQuery={saveQuery} onChange={setState} query={state} limitFields={1} />;
7124
};

src/datasource.ts

Lines changed: 84 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@ import {
1010
toDataFrame,
1111
MetricFindValue,
1212
FieldType,
13+
ScopedVars,
14+
TimeRange,
1315
} from '@grafana/data';
1416
import { getTemplateSrv } from '@grafana/runtime';
1517

1618
import API from './api';
17-
import { JsonApiQuery, JsonApiVariableQuery, JsonApiDataSourceOptions, Pair } from './types';
19+
import { JsonApiQuery, JsonApiDataSourceOptions, Pair } from './types';
1820

1921
export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
2022
api: API;
@@ -25,76 +27,7 @@ export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOpt
2527
}
2628

2729
async query(request: DataQueryRequest<JsonApiQuery>): Promise<DataQueryResponse> {
28-
const templateSrv = getTemplateSrv();
29-
30-
const replaceMacros = (str: string) => {
31-
return str
32-
.replace(/\$__unixEpochFrom\(\)/g, request.range.from.unix().toString())
33-
.replace(/\$__unixEpochTo\(\)/g, request.range.to.unix().toString());
34-
};
35-
36-
const promises = request.targets.map(async query => {
37-
const urlPathTreated = templateSrv.replace(query.urlPath, request.scopedVars);
38-
const bodyTreated = templateSrv.replace(query.body, request.scopedVars);
39-
40-
const paramsTreated: Array<Pair<string, string>> = (query.params ?? []).map(([key, value]) => {
41-
const keyTreated = replaceMacros(templateSrv.replace(key, request.scopedVars));
42-
const valueTreated = replaceMacros(templateSrv.replace(value, request.scopedVars));
43-
return [keyTreated, valueTreated];
44-
});
45-
46-
const headersTreated: Array<Pair<string, string>> = (query.headers ?? []).map(([key, value]) => {
47-
const keyTreated = templateSrv.replace(key, request.scopedVars);
48-
const valueTreated = templateSrv.replace(value, request.scopedVars);
49-
return [keyTreated, valueTreated];
50-
});
51-
52-
const response = await this.api.cachedGet(
53-
query.cacheDurationSeconds,
54-
query.method,
55-
urlPathTreated,
56-
paramsTreated,
57-
headersTreated,
58-
bodyTreated
59-
);
60-
61-
const fields = query.fields
62-
.filter(field => field.jsonPath)
63-
.map(field => {
64-
const jsonPathTreated = replaceMacros(templateSrv.replace(field.jsonPath, request.scopedVars));
65-
const nameTreated = templateSrv.replace(field.name, request.scopedVars);
66-
67-
const values = JSONPath({ path: jsonPathTreated, json: response });
68-
69-
// Get the path for automatic setting of the field name.
70-
//
71-
// Casted to any due to typing issues with JSONPath-Plus
72-
const paths = (JSONPath as any).toPathArray(jsonPathTreated);
73-
74-
const propertyType = field.type ? field.type : detectFieldType(values);
75-
const typedValues = parseValues(values, propertyType);
76-
77-
return {
78-
name: nameTreated || paths[paths.length - 1],
79-
type: propertyType,
80-
values: typedValues,
81-
};
82-
});
83-
84-
const fieldLengths = fields.map(field => field.values.length);
85-
const uniqueFieldLengths = Array.from(new Set(fieldLengths)).length;
86-
87-
// All fields need to have the same length for the data frame to be valid.
88-
if (uniqueFieldLengths > 1) {
89-
throw new Error('Fields have different lengths');
90-
}
91-
92-
return toDataFrame({
93-
name: query.refId,
94-
refId: query.refId,
95-
fields: fields,
96-
});
97-
});
30+
const promises = request.targets.map(query => this.doRequest(query, request.range, request.scopedVars));
9831

9932
// Wait for all queries to finish before returning the result.
10033
return Promise.all(promises).then(data => ({ data }));
@@ -105,28 +38,9 @@ export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOpt
10538
*
10639
* @param query
10740
*/
108-
async metricFindQuery?(query: JsonApiVariableQuery): Promise<MetricFindValue[]> {
109-
if (!query.jsonPath) {
110-
return [];
111-
}
112-
113-
const templateSrv = getTemplateSrv();
114-
115-
const queryParamsTreated = templateSrv.replace(query.queryParams);
116-
const urlPathTreated = templateSrv.replace(query.urlPath);
117-
const jsonPathTreated = templateSrv.replace(query.jsonPath);
118-
119-
const params: Array<Pair<string, string>> = [];
120-
new URLSearchParams('?' + queryParamsTreated).forEach((value: string, key: string) => {
121-
params.push([key, value]);
122-
});
123-
124-
const response = await this.api.get('GET', urlPathTreated, params);
125-
126-
return JSONPath({
127-
path: jsonPathTreated,
128-
json: response,
129-
}).map((_: any) => ({ text: _ }));
41+
async metricFindQuery?(query: JsonApiQuery): Promise<MetricFindValue[]> {
42+
const frame = await this.doRequest(query);
43+
return frame.fields[0].values.toArray().map(_ => ({ text: _ }));
13044
}
13145

13246
/**
@@ -169,6 +83,83 @@ export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOpt
16983
}
17084
}
17185
}
86+
87+
async doRequest(query: JsonApiQuery, range?: TimeRange, scopedVars?: ScopedVars) {
88+
const templateSrv = getTemplateSrv();
89+
90+
const replaceMacros = (str: string) => {
91+
return range
92+
? str
93+
.replace(/\$__unixEpochFrom\(\)/g, range.from.unix().toString())
94+
.replace(/\$__unixEpochTo\(\)/g, range.to.unix().toString())
95+
: str;
96+
};
97+
98+
const urlPathTreated = templateSrv.replace(query.urlPath, scopedVars);
99+
const bodyTreated = templateSrv.replace(query.body, scopedVars);
100+
101+
const paramsTreated: Array<Pair<string, string>> = (query.params ?? []).map(([key, value]) => {
102+
const keyTreated = replaceMacros(templateSrv.replace(key, scopedVars));
103+
const valueTreated = replaceMacros(templateSrv.replace(value, scopedVars));
104+
return [keyTreated, valueTreated];
105+
});
106+
107+
const headersTreated: Array<Pair<string, string>> = (query.headers ?? []).map(([key, value]) => {
108+
const keyTreated = templateSrv.replace(key, scopedVars);
109+
const valueTreated = templateSrv.replace(value, scopedVars);
110+
return [keyTreated, valueTreated];
111+
});
112+
113+
const response = await this.api.cachedGet(
114+
query.cacheDurationSeconds,
115+
query.method,
116+
urlPathTreated,
117+
paramsTreated,
118+
headersTreated,
119+
bodyTreated
120+
);
121+
122+
if (!response) {
123+
throw new Error('Query returned empty data');
124+
}
125+
126+
const fields = query.fields
127+
.filter(field => field.jsonPath)
128+
.map(field => {
129+
const jsonPathTreated = replaceMacros(templateSrv.replace(field.jsonPath, scopedVars));
130+
const nameTreated = templateSrv.replace(field.name, scopedVars);
131+
132+
const values = JSONPath({ path: jsonPathTreated, json: response });
133+
134+
// Get the path for automatic setting of the field name.
135+
//
136+
// Casted to any due to typing issues with JSONPath-Plus
137+
const paths = (JSONPath as any).toPathArray(jsonPathTreated);
138+
139+
const propertyType = field.type ? field.type : detectFieldType(values);
140+
const typedValues = parseValues(values, propertyType);
141+
142+
return {
143+
name: nameTreated || paths[paths.length - 1],
144+
type: propertyType,
145+
values: typedValues,
146+
};
147+
});
148+
149+
const fieldLengths = fields.map(field => field.values.length);
150+
const uniqueFieldLengths = Array.from(new Set(fieldLengths)).length;
151+
152+
// All fields need to have the same length for the data frame to be valid.
153+
if (uniqueFieldLengths > 1) {
154+
throw new Error('Fields have different lengths');
155+
}
156+
157+
return toDataFrame({
158+
name: query.refId,
159+
refId: query.refId,
160+
fields: fields,
161+
});
162+
}
172163
}
173164

174165
/**

src/module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import { DataSourcePlugin } from '@grafana/data';
22
import { DataSource } from './datasource';
33
import { ConfigEditor } from './components/ConfigEditor';
4-
import { QueryEditor } from './components/QueryEditor';
4+
import { DashboardQueryEditor } from './components/DashboardQueryEditor';
55
import { VariableQueryEditor } from './components/VariableQueryEditor';
66
import { JsonApiQuery, JsonApiDataSourceOptions } from './types';
77

88
export const plugin = new DataSourcePlugin<DataSource, JsonApiQuery, JsonApiDataSourceOptions>(DataSource)
99
.setConfigEditor(ConfigEditor)
10-
.setQueryEditor(QueryEditor)
10+
.setQueryEditor(DashboardQueryEditor)
1111
.setVariableQueryEditor(VariableQueryEditor);

0 commit comments

Comments
 (0)