|
| 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 | +} |
0 commit comments