Skip to content

Commit f6992c5

Browse files
committed
Refactor suggestions
1 parent d3ca125 commit f6992c5

File tree

5 files changed

+124
-164
lines changed

5 files changed

+124
-164
lines changed

src/components/JsonPathQueryField.tsx

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,41 @@
11
import React from 'react';
22

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

86
interface Props {
97
query: string;
108
onBlur: () => void;
119
onChange: (v: string) => void;
12-
datasource: JsonDataSource;
13-
context: JsonApiQuery;
14-
timeRange?: TimeRange;
1510
suggestions: boolean;
11+
onData: () => Promise<any>;
1612
}
1713

1814
/**
1915
* JsonPathQueryField is an editor for JSON Path.
2016
*/
21-
export const JsonPathQueryField: React.FC<Props> = ({
22-
query,
23-
onBlur,
24-
onChange,
25-
datasource,
26-
context,
27-
timeRange,
28-
suggestions,
29-
}) => {
17+
export const JsonPathQueryField: React.FC<Props> = ({ query, onBlur, onChange, suggestions, onData }) => {
3018
/**
3119
* The QueryField supports Slate plugins, so let's add a few useful ones.
3220
*/
3321
const plugins = [
3422
BracesPlugin(),
3523
SlatePrism({
3624
onlyIn: (node: any) => node.type === 'code_block',
37-
getSyntax: (node: any) => 'js',
25+
getSyntax: () => 'js',
3826
}),
3927
];
4028

41-
const onTypeahead = async (input: TypeaheadInput): Promise<TypeaheadOutput> => {
42-
return datasource.languageProvider.getSuggestions(input, context, timeRange);
43-
};
29+
// This is important if you don't want punctuation to interfere with your suggestions.
30+
const cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%\|\$@\.]/g, '').trim();
31+
32+
const onTypeahead = (input: TypeaheadInput) => onSuggest(input, onData);
4433

4534
return (
4635
<QueryField
4736
additionalPlugins={plugins}
4837
query={query}
49-
cleanText={datasource.languageProvider.cleanText}
38+
cleanText={cleanText}
5039
onTypeahead={suggestions ? onTypeahead : undefined}
5140
onRunQuery={onBlur}
5241
onChange={onChange}

src/components/QueryEditor.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,13 +122,11 @@ export const QueryEditor: React.FC<Props> = ({
122122
grow
123123
>
124124
<JsonPathQueryField
125-
datasource={datasource}
126125
onBlur={onRunQuery}
127126
onChange={onChangePath(index)}
128127
query={field.jsonPath}
129-
context={query}
130-
timeRange={range}
131128
suggestions={!disableSuggestions}
129+
onData={() => datasource.metadataRequest(query, range)}
132130
/>
133131
</InlineField>
134132
<InlineField

src/datasource.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,14 @@ import { getTemplateSrv } from '@grafana/runtime';
1717

1818
import API from './api';
1919
import { JsonApiQuery, JsonApiDataSourceOptions, Pair } from './types';
20-
import { JsonPathLanguageProvider } from './languageProvider';
2120

2221
export class JsonDataSource extends DataSourceApi<JsonApiQuery, JsonApiDataSourceOptions> {
23-
languageProvider: JsonPathLanguageProvider;
2422
api: API;
2523

2624
constructor(instanceSettings: DataSourceInstanceSettings<JsonApiDataSourceOptions>) {
2725
super(instanceSettings);
2826

2927
this.api = new API(instanceSettings.url!, instanceSettings.jsonData.queryParams || '');
30-
this.languageProvider = new JsonPathLanguageProvider(this);
3128
}
3229

3330
/**

src/languageProvider.ts

Lines changed: 0 additions & 137 deletions
This file was deleted.

src/suggestions.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
2+
import { JSONPath } from 'jsonpath-plus';
3+
4+
/**
5+
* onSuggest returns suggestions for the current JSON Path and cursor position.
6+
*
7+
* Normally you'd have to parse the query language here. This function
8+
* simplifies this by instead using JSON Path for getting the actual values.
9+
*
10+
* The onData provides the source data used when generating the suggestions.
11+
* This makes it easier to test the suggestions logic.
12+
*/
13+
export const onSuggest = async (input: TypeaheadInput, onData: () => Promise<any>): Promise<TypeaheadOutput> => {
14+
const { value } = input;
15+
16+
const emptyResult: TypeaheadOutput = { suggestions: [] };
17+
18+
if (!value) {
19+
return emptyResult;
20+
}
21+
22+
const selectedLines = value.document.getTextsAtRange(value.selection);
23+
const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
24+
25+
if (!currentLine) {
26+
return emptyResult;
27+
}
28+
29+
const toCursor = currentLine.slice(0, value.selection.anchor.offset);
30+
31+
// $.dat|
32+
const currIdentifier = /[a-zA-Z0-9]$/;
33+
34+
// $.data.|
35+
const nextIdentifier = /[\$\]a-zA-Z0-9]\.$/;
36+
37+
// $.data[|
38+
const enterBrackets = /\[$/;
39+
40+
// $.data[?(@.|
41+
const currentNodeIdentifier = /@\.$/;
42+
43+
// Here we check whether the cursor is in a position where it should
44+
// suggest.
45+
const shouldSuggest =
46+
currIdentifier.test(toCursor) ||
47+
nextIdentifier.test(toCursor) ||
48+
currentNodeIdentifier.test(toCursor) ||
49+
enterBrackets.test(toCursor);
50+
51+
if (!shouldSuggest) {
52+
return emptyResult;
53+
}
54+
55+
// Suggest operators inside brackets.
56+
if (enterBrackets.test(toCursor)) {
57+
return {
58+
suggestions: [
59+
{
60+
label: 'Operators',
61+
items: [
62+
{ label: '*', documentation: 'Returns all elements.' },
63+
{ label: ':', documentation: 'Returns a slice of the array.' },
64+
{
65+
label: '?',
66+
insertText: '?()',
67+
move: -1,
68+
documentation: 'Returns elements based on a filter expression.',
69+
},
70+
],
71+
},
72+
],
73+
};
74+
}
75+
76+
const insideBrackets = toCursor.lastIndexOf('[') > toCursor.lastIndexOf(']');
77+
78+
// Construct a JSON Path that returns the items in the current context.
79+
const path = insideBrackets
80+
? toCursor.slice(0, toCursor.lastIndexOf('[') + 1) + ':]'
81+
: currentLine.slice(0, currentLine.lastIndexOf('.'));
82+
83+
// Get the actual JSON for parsing.
84+
const response = await onData();
85+
86+
const values = JSONPath({ path, json: response });
87+
88+
// Don't attempt to suggest if this is a leaf node, e.g. strings, numbers, and booleans.
89+
if (typeof values[0] !== 'object') {
90+
return emptyResult;
91+
}
92+
93+
return {
94+
suggestions: [
95+
{
96+
label: 'Elements', // Name of the suggestion group
97+
items: Object.entries(values[0]).map(([key, value]) => {
98+
return Array.isArray(value)
99+
? {
100+
label: key, // Text to display in the suggestion list
101+
insertText: key + '[]', // When selecting an array, we automatically insert the brackets ...
102+
move: -1, // ... and put the cursor between them
103+
documentation: `_array (${value.length})_`, // Markdown documentation for the suggestion
104+
}
105+
: {
106+
label: key,
107+
documentation: `_${typeof value}_\n\n**Preview:**\n\n\`${value}\``,
108+
};
109+
}),
110+
},
111+
],
112+
};
113+
};

0 commit comments

Comments
 (0)