Skip to content

Commit 22b8bdf

Browse files
committed
Refactor language provider
1 parent 922bb90 commit 22b8bdf

File tree

5 files changed

+64
-47
lines changed

5 files changed

+64
-47
lines changed

src/components/ConfigEditor.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { JsonApiDataSourceOptions } from '../types';
66

77
import {} from '@emotion/core';
88

9-
interface Props extends DataSourcePluginOptionsEditorProps<JsonApiDataSourceOptions> {}
9+
type Props = DataSourcePluginOptionsEditorProps<JsonApiDataSourceOptions>;
1010

1111
/**
1212
* ConfigEditor lets the user configure connection details like the URL or

src/components/JsonPathQueryField.tsx

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

33
import { QueryField, SlatePrism, BracesPlugin, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
4-
import { JsonPathLanguageProvider } from 'languageProvider';
54
import { JsonDataSource } from 'datasource';
65
import { JsonApiQuery } from 'types';
76
import { TimeRange } from '@grafana/data';
@@ -39,25 +38,15 @@ export const JsonPathQueryField: React.FC<Props> = ({
3938
}),
4039
];
4140

42-
const jsonPathLanguageProvider = datasource.languageProvider as JsonPathLanguageProvider;
43-
44-
const cleanText = datasource.languageProvider ? jsonPathLanguageProvider.cleanText : undefined;
45-
4641
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);
42+
return datasource.languageProvider.getSuggestions(input, context, timeRange);
5443
};
5544

5645
return (
5746
<QueryField
5847
additionalPlugins={plugins}
5948
query={query}
60-
cleanText={cleanText}
49+
cleanText={datasource.languageProvider.cleanText}
6150
onTypeahead={suggestions ? onTypeahead : undefined}
6251
onRunQuery={onBlur}
6352
onChange={onChange}

src/components/QueryEditor.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,6 @@ import { css } from 'emotion';
2121
import { Pair } from '../types';
2222
import { JsonDataSource } from 'datasource';
2323

24-
// type Props = QueryEditorProps<DataSource, JsonApiQuery, JsonApiDataSourceOptions>;
25-
2624
interface Props {
2725
onRunQuery: () => void;
2826
onChange: (query: JsonApiQuery) => void;

src/datasource.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
FieldType,
1313
ScopedVars,
1414
TimeRange,
15-
LanguageProvider,
1615
} from '@grafana/data';
1716
import { getTemplateSrv } from '@grafana/runtime';
1817

@@ -21,7 +20,7 @@ import { JsonApiQuery, JsonApiDataSourceOptions, Pair } from './types';
2120
import { JsonPathLanguageProvider } from './languageProvider';
2221

2322
export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
24-
languageProvider: LanguageProvider;
23+
languageProvider: JsonPathLanguageProvider;
2524
api: API;
2625

2726
constructor(instanceSettings: DataSourceInstanceSettings<JsonApiDataSourceOptions>) {

src/languageProvider.ts

Lines changed: 60 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,39 @@
1-
import { LanguageProvider, TimeRange } from '@grafana/data';
2-
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
1+
import { TimeRange } from '@grafana/data';
2+
import { TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
33
import { JSONPath } from 'jsonpath-plus';
44

55
import { JsonApiQuery } from 'types';
66
import { JsonDataSource } from 'datasource';
77

8-
export class JsonPathLanguageProvider extends LanguageProvider {
8+
/**
9+
* JsonPathLanguageProvider provides suggestions for the QueryField onTypeahead
10+
* callback.
11+
*/
12+
export class JsonPathLanguageProvider {
913
datasource: JsonDataSource;
1014

1115
constructor(datasource: JsonDataSource) {
12-
super();
1316
this.datasource = datasource;
1417
}
1518

19+
// This is important if you don't want punctuation to interfere with your suggestions.
1620
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%\|\$@\.]/g, '').trim();
1721

18-
async provideCompletionItems(
19-
input: TypeaheadInput,
20-
context: JsonApiQuery,
21-
timeRange?: TimeRange
22-
): Promise<TypeaheadOutput> {
22+
/**
23+
* getSuggestions returns completion items for the current JSON Path and
24+
* cursor position.
25+
*
26+
* In addition to the typeahead input, this method accepts the current query
27+
* and time range as a context for the actual data source request.
28+
*
29+
* Since the language provider has a reference to the data source instance,
30+
* we can invoke methods that aren't part of the DataSourceApi interface. In this case, we call a `metadataRequest`
31+
* method to query the data source on-demand.
32+
*
33+
* Normally you'd have to parse the query language here. This function
34+
* simplifies this by instead using JSON Path for getting the actual values.
35+
*/
36+
async getSuggestions(input: TypeaheadInput, context: JsonApiQuery, timeRange?: TimeRange): Promise<TypeaheadOutput> {
2337
const { value } = input;
2438

2539
const emptyResult: TypeaheadOutput = { suggestions: [] };
@@ -37,22 +51,32 @@ export class JsonPathLanguageProvider extends LanguageProvider {
3751

3852
const toCursor = currentLine.slice(0, value.selection.anchor.offset);
3953

54+
// $.dat|
4055
const currIdentifier = /[a-zA-Z0-9]$/;
56+
57+
// $.data.|
4158
const nextIdentifier = /[\$\]a-zA-Z0-9]\.$/;
42-
const currentNodeIdentifier = /@\.$/;
59+
60+
// $.data[|
4361
const enterBrackets = /\[$/;
4462

45-
const isValid =
63+
// $.data[?(@.|
64+
const currentNodeIdentifier = /@\.$/;
65+
66+
// Here we check whether the cursor is in a position where it should
67+
// suggest.
68+
const shouldSuggest =
4669
currIdentifier.test(toCursor) ||
4770
nextIdentifier.test(toCursor) ||
4871
currentNodeIdentifier.test(toCursor) ||
4972
enterBrackets.test(toCursor);
5073

51-
if (!isValid) {
74+
if (!shouldSuggest) {
5275
return emptyResult;
5376
}
5477

55-
const response: any = await this.datasource.metadataRequest(context, timeRange);
78+
// Get the actual JSON for parsing.
79+
const response = await this.datasource.metadataRequest(context, timeRange);
5680

5781
if (enterBrackets.test(toCursor)) {
5882
return {
@@ -64,9 +88,9 @@ export class JsonPathLanguageProvider extends LanguageProvider {
6488
{ label: ':', documentation: 'Returns a slice of the array.' },
6589
{
6690
label: '?',
67-
documentation: 'Returns elements based on a filter expression.',
6891
insertText: '?()',
6992
move: -1,
93+
documentation: 'Returns elements based on a filter expression.',
7094
},
7195
],
7296
},
@@ -76,30 +100,37 @@ export class JsonPathLanguageProvider extends LanguageProvider {
76100

77101
const insideBrackets = toCursor.lastIndexOf('[') > toCursor.lastIndexOf(']');
78102

103+
// Construct a JSON Path that returns the items in the current context.
79104
const path = insideBrackets
80105
? toCursor.slice(0, toCursor.lastIndexOf('[') + 1) + ':]'
81106
: currentLine.slice(0, currentLine.lastIndexOf('.'));
82107

83108
const values = JSONPath({ path, json: response });
84109

110+
// Don't attempt to suggest if this is a leaf node, e.g. strings, numbers, and booleans.
85111
if (typeof values[0] !== 'object') {
86112
return emptyResult;
87113
}
88114

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 }] };
115+
return {
116+
suggestions: [
117+
{
118+
label: 'Elements', // Name of the suggestion group
119+
items: Object.entries(values[0]).map(([key, value]) => {
120+
return Array.isArray(value)
121+
? {
122+
label: key, // Text to display in the suggestion list
123+
insertText: key + '[]', // When selecting an array, we automatically insert the brackets ...
124+
move: -1, // ... and put the cursor between them
125+
documentation: `_array (${value.length})_`, // Markdown documentation for the suggestion
126+
}
127+
: {
128+
label: key,
129+
documentation: `_${typeof value}_\n\n**Preview:**\n\n\`${value}\``,
130+
};
131+
}),
132+
},
133+
],
134+
};
96135
}
97-
98-
request = async (url: string, params?: any): Promise<any> => {
99-
return undefined;
100-
};
101-
102-
start = async (): Promise<any[]> => {
103-
return [];
104-
};
105136
}

0 commit comments

Comments
 (0)