Skip to content

Commit c66af71

Browse files
committed
Add support for JSONata
1 parent e289cc8 commit c66af71

File tree

6 files changed

+151
-38
lines changed

6 files changed

+151
-38
lines changed

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,22 +24,24 @@
2424
},
2525
"dependencies": {
2626
"dayjs": "^1.10.4",
27+
"jsonata": "^1.8.5",
2728
"jsonpath-plus": "^4.0.0",
2829
"memory-cache": "^0.2.0",
2930
"tslib": "^2.3.0"
3031
},
3132
"devDependencies": {
32-
"@types/jsonpath": "^0.2.0",
33-
"@types/lodash": "^4.14.168",
34-
"@types/memory-cache": "^0.2.1",
35-
"@types/react-virtualized-auto-sizer": "^1.0.0",
33+
"@emotion/core": "10.0.27",
3634
"@grafana/data": "8.0.1",
3735
"@grafana/runtime": "8.0.1",
3836
"@grafana/toolkit": "8.0.1",
3937
"@grafana/ui": "8.0.1",
4038
"@testing-library/jest-dom": "5.4.0",
4139
"@testing-library/react": "^10.0.2",
42-
"@emotion/core": "10.0.27",
40+
"@types/jsonata": "^1.5.1",
41+
"@types/jsonpath": "^0.2.0",
42+
"@types/lodash": "^4.14.168",
43+
"@types/memory-cache": "^0.2.1",
44+
"@types/react-virtualized-auto-sizer": "^1.0.0",
4345
"emotion": "10.0.27",
4446
"react-virtualized-auto-sizer": "^1.0.4"
4547
},

src/components/FieldEditor.tsx

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { FieldType, SelectableValue } from '@grafana/data';
22
import { Icon, InlineField, InlineFieldRow, Input, Select } from '@grafana/ui';
33
import React from 'react';
4-
import { JsonField } from 'types';
4+
import { JsonField, QueryLanguage } from 'types';
5+
import { JsonataQueryField } from './JsonataQueryField';
56
import { JsonPathQueryField } from './JsonPathQueryField';
67

78
interface Props {
@@ -15,25 +16,25 @@ export const FieldEditor = ({ value = [], onChange, limit, onComplete }: Props)
1516
const onChangePath = (i: number) => (e: string) => {
1617
onChange(value.map((field, n) => (i === n ? { ...value[i], jsonPath: e } : field)));
1718
};
18-
19+
const onLanguageChange = (i: number) => (e: SelectableValue<QueryLanguage>) => {
20+
onChange(value.map((field, n) => (i === n ? { ...value[i], language: e.value } : field)));
21+
};
1922
const onChangeType = (i: number) => (e: SelectableValue<string>) => {
2023
onChange(
2124
value.map((field, n) =>
2225
i === n ? { ...value[i], type: (e.value === 'auto' ? undefined : e.value) as FieldType } : field
2326
)
2427
);
2528
};
26-
2729
const onAliasChange = (i: number) => (e: any) => {
2830
onChange(value.map((field, n) => (i === n ? { ...value[i], name: e.currentTarget.value } : field)));
2931
};
3032

31-
const addField = (i: number) => () => {
33+
const addField = (i: number, defaults?: { language: QueryLanguage }) => () => {
3234
if (!limit || value.length < limit) {
33-
onChange([...value.slice(0, i + 1), { name: '', jsonPath: '' }, ...value.slice(i + 1)]);
35+
onChange([...value.slice(0, i + 1), { name: '', jsonPath: '', ...defaults }, ...value.slice(i + 1)]);
3436
}
3537
};
36-
3738
const removeField = (i: number) => () => {
3839
onChange([...value.slice(0, i), ...value.slice(i + 1)]);
3940
};
@@ -52,13 +53,26 @@ export const FieldEditor = ({ value = [], onChange, limit, onComplete }: Props)
5253
}
5354
grow
5455
>
55-
<JsonPathQueryField
56-
onBlur={() => {
57-
onChange(value);
58-
}}
59-
onChange={onChangePath(index)}
60-
query={field.jsonPath}
61-
onData={onComplete}
56+
{field.language === 'jsonata' ? (
57+
<JsonataQueryField onBlur={() => onChange(value)} onChange={onChangePath(index)} query={field.jsonPath} />
58+
) : (
59+
<JsonPathQueryField
60+
onBlur={() => onChange(value)}
61+
onChange={onChangePath(index)}
62+
query={field.jsonPath}
63+
onData={onComplete}
64+
/>
65+
)}
66+
</InlineField>
67+
<InlineField label="Query language">
68+
<Select
69+
value={field.language ?? 'jsonpath'}
70+
width={14}
71+
onChange={onLanguageChange(index)}
72+
options={[
73+
{ label: 'JSONPath', value: 'jsonpath' },
74+
{ label: 'JSONata', value: 'jsonata' },
75+
]}
6276
/>
6377
</InlineField>
6478
<InlineField label="Type" tooltip="If Auto is set, the JSON property type is used to detect the field type.">
@@ -80,7 +94,7 @@ export const FieldEditor = ({ value = [], onChange, limit, onComplete }: Props)
8094
</InlineField>
8195

8296
{(!limit || value.length < limit) && (
83-
<a className="gf-form-label" onClick={addField(index)}>
97+
<a className="gf-form-label" onClick={addField(index, { language: field.language ?? 'jsonpath' })}>
8498
<Icon name="plus" />
8599
</a>
86100
)}

src/components/JsonataQueryField.tsx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { BracesPlugin, QueryField, SlatePrism } from '@grafana/ui';
2+
import React from 'react';
3+
4+
interface Props {
5+
query: string;
6+
onBlur: () => void;
7+
onChange: (v: string) => void;
8+
}
9+
10+
/**
11+
* JsonataQueryField is an editor for JSONata expressions.
12+
*/
13+
export const JsonataQueryField: React.FC<Props> = ({ query, onBlur, onChange }) => {
14+
/**
15+
* The QueryField supports Slate plugins, so let's add a few useful ones.
16+
*/
17+
const plugins = [
18+
BracesPlugin(),
19+
SlatePrism({
20+
onlyIn: (node: any) => node.type === 'code_block',
21+
getSyntax: () => 'js',
22+
}),
23+
];
24+
25+
return (
26+
<QueryField
27+
additionalPlugins={plugins}
28+
query={query}
29+
onRunQuery={onBlur}
30+
onChange={onChange}
31+
portalOrigin="jsonapi"
32+
placeholder="$sum(orders.(price*quantity))"
33+
/>
34+
);
35+
};

src/datasource.ts

Lines changed: 66 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
toDataFrame,
1313
} from '@grafana/data';
1414
import { getTemplateSrv } from '@grafana/runtime';
15+
import jsonata from 'jsonata';
1516
import { JSONPath } from 'jsonpath-plus';
1617
import _ from 'lodash';
1718
import API from './api';
@@ -98,7 +99,7 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
9899
message: response.statusText ? response.statusText : defaultErrorMessage,
99100
};
100101
}
101-
} catch (err) {
102+
} catch (err: any) {
102103
if (_.isString(err)) {
103104
return {
104105
status: 'error',
@@ -130,24 +131,51 @@ export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourc
130131

131132
const fields: Field[] = query.fields
132133
.filter((field) => field.jsonPath)
133-
.map((field) => {
134-
const path = replaceWithVars(field.jsonPath);
135-
const values = JSONPath({ path, json });
136-
137-
// Get the path for automatic setting of the field name.
138-
//
139-
// Casted to any due to typing issues with JSONPath-Plus
140-
const paths = (JSONPath as any).toPathArray(path);
141-
142-
const propertyType = field.type ? field.type : detectFieldType(values);
143-
const typedValues = parseValues(values, propertyType);
144-
145-
return {
146-
name: replaceWithVars(field.name ?? '') || paths[paths.length - 1],
147-
type: propertyType,
148-
values: new ArrayVector(typedValues),
149-
config: {},
150-
};
134+
.map((field, index) => {
135+
switch (field.language) {
136+
case 'jsonata':
137+
const expression = jsonata(field.jsonPath);
138+
139+
const bindings: Record<string, any> = {};
140+
141+
// Bind dashboard variables to JSONata variables.
142+
getTemplateSrv()
143+
.getVariables()
144+
.map((v) => ({ name: v.name, value: getVariable(v.name) }))
145+
.forEach((v) => {
146+
bindings[v.name] = v.value;
147+
});
148+
149+
const result = expression.evaluate(json, bindings);
150+
151+
// Ensure that we always return an array.
152+
const arrayResult = Array.isArray(result) ? result : [result];
153+
154+
return {
155+
name: replaceWithVars(field.name ?? '') || (query.fields.length > 1 ? `result${index}` : 'result'),
156+
type: field.type ? field.type : detectFieldType(arrayResult),
157+
values: new ArrayVector(arrayResult),
158+
config: {},
159+
};
160+
default:
161+
const path = replaceWithVars(field.jsonPath);
162+
const values = JSONPath({ path, json });
163+
164+
// Get the path for automatic setting of the field name.
165+
//
166+
// Casted to any due to typing issues with JSONPath-Plus
167+
const paths = (JSONPath as any).toPathArray(path);
168+
169+
const propertyType = field.type ? field.type : detectFieldType(values);
170+
const typedValues = parseValues(values, propertyType);
171+
172+
return {
173+
name: replaceWithVars(field.name ?? '') || paths[paths.length - 1],
174+
type: propertyType,
175+
values: new ArrayVector(typedValues),
176+
config: {},
177+
};
178+
}
151179
});
152180

153181
const fieldLengths = fields.map((field) => field.values.length);
@@ -247,3 +275,22 @@ export const groupBy = (frame: DataFrame, fieldName: string): DataFrame[] => {
247275

248276
return frames;
249277
};
278+
279+
// Helper function to extract the values of a variable instead of interpolating it.
280+
const getVariable = (name: any): string[] => {
281+
const values: string[] = [];
282+
283+
// Instead of interpolating the string, we collect the values in an array.
284+
getTemplateSrv().replace(`$${name}`, {}, (value: string | string[]) => {
285+
if (Array.isArray(value)) {
286+
values.push(...value);
287+
} else {
288+
values.push(value);
289+
}
290+
291+
// We don't really care about the string here.
292+
return '';
293+
});
294+
295+
return values;
296+
};

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { DataQuery, DataSourceJsonData, FieldType } from '@grafana/data';
22

3+
export type QueryLanguage = 'jsonpath' | 'jsonata';
4+
35
export interface JsonField {
46
name?: string;
57
jsonPath: string;
68
type?: FieldType;
9+
language?: QueryLanguage;
710
}
811

912
export type Pair<T, K> = [T, K];

yarn.lock

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,13 @@
22262226
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad"
22272227
integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA==
22282228

2229+
"@types/jsonata@^1.5.1":
2230+
version "1.5.1"
2231+
resolved "https://registry.yarnpkg.com/@types/jsonata/-/jsonata-1.5.1.tgz#403176c3f265523c36e799f27752188b2fc08981"
2232+
integrity sha512-zBVjNYS7FUUIZ4Ckq3p7xOkJKndT6h+kHUeKiud1E6h/OLPkQOagzq8UJYN07QvtkA03XFadQER1zSIWSJj2EA==
2233+
dependencies:
2234+
jsonata "*"
2235+
22292236
"@types/jsonpath@^0.2.0":
22302237
version "0.2.0"
22312238
resolved "https://registry.yarnpkg.com/@types/jsonpath/-/jsonpath-0.2.0.tgz#13c62db22a34d9c411364fac79fd374d63445aa1"
@@ -7793,6 +7800,11 @@ json5@^1.0.1:
77937800
dependencies:
77947801
minimist "^1.2.0"
77957802

7803+
jsonata@*, jsonata@^1.8.5:
7804+
version "1.8.5"
7805+
resolved "https://registry.yarnpkg.com/jsonata/-/jsonata-1.8.5.tgz#c656c929c92b3fb097792cef661c27c52dfa5148"
7806+
integrity sha512-ilDyTBkg6qhNoNVr8PUPzz5GYvRK+REKOM5MdOGzH2y6V4yvPRMegSvbZLpbTtI0QAgz09QM7drDhSHUlwp9pA==
7807+
77967808
jsonfile@^4.0.0:
77977809
version "4.0.0"
77987810
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"

0 commit comments

Comments
 (0)