Skip to content

Commit 4c58fa0

Browse files
committed
Add auto-completion
1 parent 0d459fb commit 4c58fa0

File tree

7 files changed

+220
-14
lines changed

7 files changed

+220
-14
lines changed
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import React from 'react';
22
import { QueryEditorProps } from '@grafana/data';
3-
import { DataSource } from '../datasource';
3+
import { JsonDataSource } from '../datasource';
44
import { JsonApiDataSourceOptions, JsonApiQuery } from '../types';
55
import { QueryEditor } from './QueryEditor';
66

7-
type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
7+
type Props = QueryEditorProps<JsonDataSource, JsonApiQuery, JsonApiDataSourceOptions>;
88

9-
export const DashboardQueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query }) => {
10-
return <QueryEditor onRunQuery={onRunQuery} onChange={onChange} query={query} />;
9+
export const DashboardQueryEditor: React.FC<Props> = (props) => {
10+
return <QueryEditor {...props} disableSuggestions={false} />;
1111
};

src/components/JsonPathQueryField.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
import React from 'react';
22

3-
import { QueryField, SlatePrism, BracesPlugin } from '@grafana/ui';
3+
import { QueryField, SlatePrism, BracesPlugin, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
4+
import { JsonPathLanguageProvider } from 'languageProvider';
5+
import { JsonDataSource } from 'datasource';
6+
import { JsonApiQuery } from 'types';
7+
import { TimeRange } from '@grafana/data';
48

59
interface Props {
610
query: string;
711
onBlur: () => void;
812
onChange: (v: string) => void;
13+
datasource: JsonDataSource;
14+
context: JsonApiQuery;
15+
timeRange?: TimeRange;
16+
suggestions: boolean;
917
}
1018

1119
/**
1220
* JsonPathQueryField is an editor for JSON Path.
1321
*/
14-
export const JsonPathQueryField: React.FC<Props> = ({ query, onBlur, onChange }) => {
22+
export const JsonPathQueryField: React.FC<Props> = ({
23+
query,
24+
onBlur,
25+
onChange,
26+
datasource,
27+
context,
28+
timeRange,
29+
suggestions,
30+
}) => {
1531
/**
1632
* The QueryField supports Slate plugins, so let's add a few useful ones.
1733
*/
@@ -23,10 +39,26 @@ export const JsonPathQueryField: React.FC<Props> = ({ query, onBlur, onChange })
2339
}),
2440
];
2541

42+
const jsonPathLanguageProvider = datasource.languageProvider as JsonPathLanguageProvider;
43+
44+
const cleanText = datasource.languageProvider ? jsonPathLanguageProvider.cleanText : undefined;
45+
46+
const onTypeahead = async (input: TypeaheadInput): Promise<TypeaheadOutput> => {
47+
if (!datasource.languageProvider) {
48+
return { suggestions: [] };
49+
}
50+
51+
const languageProvider = datasource.languageProvider as JsonPathLanguageProvider;
52+
53+
return languageProvider.provideCompletionItems(input, context, timeRange);
54+
};
55+
2656
return (
2757
<QueryField
2858
additionalPlugins={plugins}
2959
query={query}
60+
cleanText={cleanText}
61+
onTypeahead={suggestions ? onTypeahead : undefined}
3062
onRunQuery={onBlur}
3163
onChange={onChange}
3264
portalOrigin="jsonapi"

src/components/QueryEditor.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
useTheme,
1313
InfoBox,
1414
} from '@grafana/ui';
15-
import { SelectableValue, FieldType } from '@grafana/data';
15+
import { SelectableValue, FieldType, TimeRange } from '@grafana/data';
1616
import { JsonApiQuery, defaultQuery } from '../types';
1717
import { JsonPathQueryField } from './JsonPathQueryField';
1818
import { KeyValueEditor } from './KeyValueEditor';
1919
import AutoSizer from 'react-virtualized-auto-sizer';
2020
import { css } from 'emotion';
2121
import { Pair } from '../types';
22+
import { JsonDataSource } from 'datasource';
2223

2324
// type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
2425

@@ -27,9 +28,20 @@ interface Props {
2728
onChange: (query: JsonApiQuery) => void;
2829
query: JsonApiQuery;
2930
limitFields?: number;
31+
datasource: JsonDataSource;
32+
range?: TimeRange;
33+
disableSuggestions: boolean;
3034
}
3135

32-
export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query, limitFields }) => {
36+
export const QueryEditor: React.FC<Props> = ({
37+
onRunQuery,
38+
onChange,
39+
query,
40+
limitFields,
41+
datasource,
42+
range,
43+
disableSuggestions,
44+
}) => {
3345
const [bodyType, setBodyType] = useState('plaintext');
3446
const [tabIndex, setTabIndex] = useState(0);
3547
const theme = useTheme();
@@ -111,7 +123,15 @@ export const QueryEditor: React.FC<Props> = ({ onRunQuery, onChange, query, limi
111123
}
112124
grow
113125
>
114-
<JsonPathQueryField onBlur={onRunQuery} onChange={onChangePath(index)} query={field.jsonPath} />
126+
<JsonPathQueryField
127+
datasource={datasource}
128+
onBlur={onRunQuery}
129+
onChange={onChangePath(index)}
130+
query={field.jsonPath}
131+
context={query}
132+
timeRange={range}
133+
suggestions={!disableSuggestions}
134+
/>
115135
</InlineField>
116136
<InlineField
117137
label="Type"

src/components/VariableQueryEditor.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
1+
import { TimeRange } from '@grafana/data';
2+
import { JsonDataSource } from 'datasource';
13
import React, { useState } from 'react';
24
import { JsonApiQuery } from '../types';
35
import { QueryEditor } from './QueryEditor';
46

57
interface VariableQueryProps {
68
query: JsonApiQuery;
79
onChange: (query: JsonApiQuery, definition: string) => void;
10+
datasource: JsonDataSource;
11+
range: TimeRange;
12+
disableSuggestions: boolean;
813
}
914

1015
// VariableQueryEditor is used to query values for a dashboard variable.
11-
export const VariableQueryEditor: React.FC<VariableQueryProps> = ({ onChange, query }) => {
16+
export const VariableQueryEditor: React.FC<VariableQueryProps> = (props) => {
17+
const { query, onChange } = props;
18+
1219
// Backwards compatibility with previous query editor for variables.
1320
const compatQuery = query.jsonPath ? { ...query, fields: [{ jsonPath: query.jsonPath }] } : query;
1421

@@ -20,5 +27,5 @@ export const VariableQueryEditor: React.FC<VariableQueryProps> = ({ onChange, qu
2027
}
2128
};
2229

23-
return <QueryEditor onRunQuery={saveQuery} onChange={setState} query={state} limitFields={1} />;
30+
return <QueryEditor {...props} onRunQuery={saveQuery} onChange={setState} query={state} limitFields={1} />;
2431
};

src/datasource.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,60 @@ import {
1212
FieldType,
1313
ScopedVars,
1414
TimeRange,
15+
LanguageProvider,
1516
} from '@grafana/data';
1617
import { getTemplateSrv } from '@grafana/runtime';
1718

1819
import API from './api';
1920
import { JsonApiQuery, JsonApiDataSourceOptions, Pair } from './types';
21+
import { JsonPathLanguageProvider } from './languageProvider';
2022

21-
export class DataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
23+
export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
24+
languageProvider: LanguageProvider;
2225
api: API;
2326

2427
constructor(instanceSettings: DataSourceInstanceSettings<JsonApiDataSourceOptions>) {
2528
super(instanceSettings);
29+
2630
this.api = new API(instanceSettings.url!, instanceSettings.jsonData.queryParams || '');
31+
this.languageProvider = new JsonPathLanguageProvider(this);
32+
}
33+
34+
async metadataRequest(query: JsonApiQuery, range?: TimeRange) {
35+
const scopedVars = {};
36+
const templateSrv = getTemplateSrv();
37+
38+
const replaceMacros = (str: string) => {
39+
return range
40+
? str
41+
.replace(/\$__unixEpochFrom\(\)/g, range.from.unix().toString())
42+
.replace(/\$__unixEpochTo\(\)/g, range.to.unix().toString())
43+
: str;
44+
};
45+
46+
const urlPathTreated = templateSrv.replace(query.urlPath, scopedVars);
47+
const bodyTreated = templateSrv.replace(query.body, scopedVars);
48+
49+
const paramsTreated: Array<Pair<string, string>> = (query.params ?? []).map(([key, value]) => {
50+
const keyTreated = replaceMacros(templateSrv.replace(key, scopedVars));
51+
const valueTreated = replaceMacros(templateSrv.replace(value, scopedVars));
52+
return [keyTreated, valueTreated];
53+
});
54+
55+
const headersTreated: Array<Pair<string, string>> = (query.headers ?? []).map(([key, value]) => {
56+
const keyTreated = templateSrv.replace(key, scopedVars);
57+
const valueTreated = templateSrv.replace(value, scopedVars);
58+
return [keyTreated, valueTreated];
59+
});
60+
61+
return await this.api.cachedGet(
62+
query.cacheDurationSeconds,
63+
query.method,
64+
urlPathTreated,
65+
paramsTreated,
66+
headersTreated,
67+
bodyTreated
68+
);
2769
}
2870

2971
async query(request: DataQueryRequest<JsonApiQuery>): Promise<DataQueryResponse> {

src/languageProvider.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { LanguageProvider, TimeRange } from '@grafana/data';
2+
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
3+
import { JSONPath } from 'jsonpath-plus';
4+
5+
import { JsonApiQuery } from 'types';
6+
import { JsonDataSource } from 'datasource';
7+
8+
export class JsonPathLanguageProvider extends LanguageProvider {
9+
datasource: JsonDataSource;
10+
11+
constructor(datasource: JsonDataSource) {
12+
super();
13+
this.datasource = datasource;
14+
}
15+
16+
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%\|\$@\.]/g, '').trim();
17+
18+
async provideCompletionItems(
19+
input: TypeaheadInput,
20+
context: JsonApiQuery,
21+
timeRange?: TimeRange
22+
): Promise<TypeaheadOutput> {
23+
const { value } = input;
24+
25+
const emptyResult: TypeaheadOutput = { suggestions: [] };
26+
27+
if (!value) {
28+
return emptyResult;
29+
}
30+
31+
const selectedLines = value.document.getTextsAtRange(value.selection);
32+
const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
33+
34+
if (!currentLine) {
35+
return emptyResult;
36+
}
37+
38+
const toCursor = currentLine.slice(0, value.selection.anchor.offset);
39+
40+
const currIdentifier = /[a-zA-Z0-9]$/;
41+
const nextIdentifier = /[\$\]a-zA-Z0-9]\.$/;
42+
const currentNodeIdentifier = /@\.$/;
43+
const enterBrackets = /\[$/;
44+
45+
const isValid =
46+
currIdentifier.test(toCursor) ||
47+
nextIdentifier.test(toCursor) ||
48+
currentNodeIdentifier.test(toCursor) ||
49+
enterBrackets.test(toCursor);
50+
51+
if (!isValid) {
52+
return emptyResult;
53+
}
54+
55+
const response: any = await this.datasource.metadataRequest(context, timeRange);
56+
57+
if (enterBrackets.test(toCursor)) {
58+
return {
59+
suggestions: [
60+
{
61+
label: 'Operators',
62+
items: [
63+
{ label: '*', documentation: 'Returns all elements.' },
64+
{ label: ':', documentation: 'Returns a slice of the array.' },
65+
{
66+
label: '?',
67+
documentation: 'Returns elements based on a filter expression.',
68+
insertText: '?()',
69+
move: -1,
70+
},
71+
],
72+
},
73+
],
74+
};
75+
}
76+
77+
const insideBrackets = toCursor.lastIndexOf('[') > toCursor.lastIndexOf(']');
78+
79+
const path = insideBrackets
80+
? toCursor.slice(0, toCursor.lastIndexOf('[') + 1) + ':]'
81+
: currentLine.slice(0, currentLine.lastIndexOf('.'));
82+
83+
const values = JSONPath({ path, json: response });
84+
85+
if (typeof values[0] !== 'object') {
86+
return emptyResult;
87+
}
88+
89+
const items: CompletionItem[] = Object.entries(values[0]).map(([key, value]) => {
90+
return Array.isArray(value)
91+
? { label: key, insertText: key + '[]', move: -1, documentation: `_array (${value.length})_` }
92+
: { label: key, documentation: `_${typeof value}_\n\n**Preview:**\n\n\`${value}\`` };
93+
});
94+
95+
return { suggestions: [{ label: 'Elements', items }] };
96+
}
97+
98+
request = async (url: string, params?: any): Promise<any> => {
99+
return undefined;
100+
};
101+
102+
start = async (): Promise<any[]> => {
103+
return [];
104+
};
105+
}

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';
2-
import { DataSource } from './datasource';
2+
import { JsonDataSource } from './datasource';
33
import { ConfigEditor } from './components/ConfigEditor';
44
import { DashboardQueryEditor } from './components/DashboardQueryEditor';
55
import { VariableQueryEditor } from './components/VariableQueryEditor';
66
import { JsonApiQuery, JsonApiDataSourceOptions } from './types';
77

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

0 commit comments

Comments
 (0)