[Console] Refactor and cleanup of public and server (#60513)

* Clean up use of ace in autocomplete in public

Remove ace from lib/autocomplete.ts and set up hooking up of ace
in legacy_core_editor. Also remove use of ace mocks in tests.

* Added TODO in lib/kb (console public)

* Server-side cleanup

Refactored the loading of spec into a new SpecDefinitionsService.
In this way, state can be contained inside of the service as much
as possible. Also converted all JS spec to TS and updated the
Console plugin contract so that processors (which alter loaded
spec) happen at plugin "start" phase.

* Fix types

* Small refactor

- Updated naming of argument variable in registerAutocompleter
- Refactored the SpecDefinitionsService to handle binding of
it's own functions
This commit is contained in:
Jean-Louis Leysens 2020-03-19 17:32:39 +01:00 committed by GitHub
parent d5ed93ee63
commit 304b322a47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
37 changed files with 509 additions and 437 deletions

View file

@ -18,9 +18,17 @@
*/
import ace from 'brace';
import { Editor as IAceEditor } from 'brace';
import { Editor as IAceEditor, IEditSession as IAceEditSession } from 'brace';
import $ from 'jquery';
import { CoreEditor, Position, Range, Token, TokensProvider, EditorEvent } from '../../../types';
import {
CoreEditor,
Position,
Range,
Token,
TokensProvider,
EditorEvent,
AutoCompleterFunction,
} from '../../../types';
import { AceTokensProvider } from '../../../lib/ace_token_provider';
import * as curl from '../sense_editor/curl';
import smartResize from './smart_resize';
@ -354,4 +362,48 @@ export class LegacyCoreEditor implements CoreEditor {
}
}
}
registerAutocompleter(autocompleter: AutoCompleterFunction): void {
// Hook into Ace
// disable standard context based autocompletion.
// @ts-ignore
ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function(
require: any,
exports: any
) {
exports.getCompletions = function(
innerEditor: any,
session: any,
pos: any,
prefix: any,
callback: any
) {
callback(null, []);
};
});
const langTools = ace.acequire('ace/ext/language_tools');
langTools.setCompleters([
{
identifierRegexps: [
/[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character
],
getCompletions: (
DO_NOT_USE_1: IAceEditor,
DO_NOT_USE_2: IAceEditSession,
pos: { row: number; column: number },
prefix: string,
callback: (...args: any[]) => void
) => {
const position: Position = {
lineNumber: pos.row + 1,
column: pos.column + 1,
};
autocompleter(position, prefix, callback);
},
},
]);
}
}

View file

@ -84,93 +84,90 @@ describe('Integration', () => {
changeListener: function() {},
}; // mimic auto complete
senseEditor.autocomplete._test.getCompletions(
senseEditor,
null,
{ row: cursor.lineNumber - 1, column: cursor.column - 1 },
'',
function(err, terms) {
if (testToRun.assertThrows) {
done();
return;
}
if (err) {
throw err;
}
if (testToRun.no_context) {
expect(!terms || terms.length === 0).toBeTruthy();
} else {
expect(terms).not.toBeNull();
expect(terms.length).toBeGreaterThan(0);
}
if (!terms || terms.length === 0) {
done();
return;
}
if (testToRun.autoCompleteSet) {
const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) {
if (typeof t !== 'object') {
t = { name: t };
}
return t;
});
if (terms.length !== expectedTerms.length) {
expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name'));
} else {
const filteredActualTerms = _.map(terms, function(actualTerm, i) {
const expectedTerm = expectedTerms[i];
const filteredTerm = {};
_.each(expectedTerm, function(v, p) {
filteredTerm[p] = actualTerm[p];
});
return filteredTerm;
});
expect(filteredActualTerms).toEqual(expectedTerms);
}
}
const context = terms[0].context;
const {
cursor: { lineNumber, column },
} = testToRun;
senseEditor.autocomplete._test.addReplacementInfoToContext(
context,
{ lineNumber, column },
terms[0].value
);
function ac(prop, propTest) {
if (typeof testToRun[prop] !== 'undefined') {
if (propTest) {
propTest(context[prop], testToRun[prop], prop);
} else {
expect(context[prop]).toEqual(testToRun[prop]);
}
}
}
function posCompare(actual, expected) {
expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset);
expect(actual.column).toEqual(expected.column);
}
function rangeCompare(actual, expected, name) {
posCompare(actual.start, expected.start, name + '.start');
posCompare(actual.end, expected.end, name + '.end');
}
ac('prefixToAdd');
ac('suffixToAdd');
ac('addTemplate');
ac('textBoxPosition', posCompare);
ac('rangeToReplace', rangeCompare);
senseEditor.autocomplete._test.getCompletions(senseEditor, null, cursor, '', function(
err,
terms
) {
if (testToRun.assertThrows) {
done();
return;
}
);
if (err) {
throw err;
}
if (testToRun.no_context) {
expect(!terms || terms.length === 0).toBeTruthy();
} else {
expect(terms).not.toBeNull();
expect(terms.length).toBeGreaterThan(0);
}
if (!terms || terms.length === 0) {
done();
return;
}
if (testToRun.autoCompleteSet) {
const expectedTerms = _.map(testToRun.autoCompleteSet, function(t) {
if (typeof t !== 'object') {
t = { name: t };
}
return t;
});
if (terms.length !== expectedTerms.length) {
expect(_.pluck(terms, 'name')).toEqual(_.pluck(expectedTerms, 'name'));
} else {
const filteredActualTerms = _.map(terms, function(actualTerm, i) {
const expectedTerm = expectedTerms[i];
const filteredTerm = {};
_.each(expectedTerm, function(v, p) {
filteredTerm[p] = actualTerm[p];
});
return filteredTerm;
});
expect(filteredActualTerms).toEqual(expectedTerms);
}
}
const context = terms[0].context;
const {
cursor: { lineNumber, column },
} = testToRun;
senseEditor.autocomplete._test.addReplacementInfoToContext(
context,
{ lineNumber, column },
terms[0].value
);
function ac(prop, propTest) {
if (typeof testToRun[prop] !== 'undefined') {
if (propTest) {
propTest(context[prop], testToRun[prop], prop);
} else {
expect(context[prop]).toEqual(testToRun[prop]);
}
}
}
function posCompare(actual, expected) {
expect(actual.lineNumber).toEqual(expected.lineNumber + lineOffset);
expect(actual.column).toEqual(expected.column);
}
function rangeCompare(actual, expected, name) {
posCompare(actual.start, expected.start, name + '.start');
posCompare(actual.end, expected.end, name + '.end');
}
ac('prefixToAdd');
ac('suffixToAdd');
ac('addTemplate');
ac('textBoxPosition', posCompare);
ac('rangeToReplace', rangeCompare);
done();
});
});
}

View file

@ -44,6 +44,7 @@ export class SenseEditor {
coreEditor,
parser: this.parser,
});
this.coreEditor.registerAutocompleter(this.autocomplete.getCompletions);
this.coreEditor.on(
'tokenizerUpdate',
this.highlightCurrentRequestsAndUpdateActionBar.bind(this)

View file

@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import '../../../application/models/sense_editor/sense_editor.test.mocks';
const _ = require('lodash');
import {

View file

@ -16,10 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import '../../../application/models/sense_editor/sense_editor.test.mocks';
import 'brace';
import 'brace/mode/javascript';
import 'brace/mode/json';
const _ = require('lodash');
import { UrlParams } from '../../autocomplete/url_params';
import { populateContext } from '../../autocomplete/engine';

View file

@ -18,9 +18,9 @@
*/
import _ from 'lodash';
import ace, { Editor as AceEditor, IEditSession } from 'brace';
import { i18n } from '@kbn/i18n';
// TODO: All of these imports need to be moved to the core editor so that it can inject components from there.
import {
getTopLevelUrlCompleteComponents,
getEndpointBodyCompleteComponents,
@ -39,7 +39,7 @@ import { createTokenIterator } from '../../application/factories';
import { Position, Token, Range, CoreEditor } from '../../types';
let LAST_EVALUATED_TOKEN: any = null;
let lastEvaluatedToken: any = null;
function isUrlParamsToken(token: any) {
switch ((token || {}).type) {
@ -889,7 +889,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor
if (!currentToken) {
if (pos.lineNumber === 1) {
LAST_EVALUATED_TOKEN = null;
lastEvaluatedToken = null;
return;
}
currentToken = { position: { column: 0, lineNumber: 0 }, value: '', type: '' }; // empty row
@ -902,26 +902,26 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor
if (parser.isEmptyToken(nextToken)) {
// Empty line, or we're not on the edge of current token. Save the current position as base
currentToken.position.column = pos.column;
LAST_EVALUATED_TOKEN = currentToken;
lastEvaluatedToken = currentToken;
} else {
nextToken.position.lineNumber = pos.lineNumber;
LAST_EVALUATED_TOKEN = nextToken;
lastEvaluatedToken = nextToken;
}
return;
}
if (!LAST_EVALUATED_TOKEN) {
LAST_EVALUATED_TOKEN = currentToken;
if (!lastEvaluatedToken) {
lastEvaluatedToken = currentToken;
return; // wait for the next typing.
}
if (
LAST_EVALUATED_TOKEN.position.column !== currentToken.position.column ||
LAST_EVALUATED_TOKEN.position.lineNumber !== currentToken.position.lineNumber ||
LAST_EVALUATED_TOKEN.value === currentToken.value
lastEvaluatedToken.position.column !== currentToken.position.column ||
lastEvaluatedToken.position.lineNumber !== currentToken.position.lineNumber ||
lastEvaluatedToken.value === currentToken.value
) {
// not on the same place or nothing changed, cache and wait for the next time
LAST_EVALUATED_TOKEN = currentToken;
lastEvaluatedToken = currentToken;
return;
}
@ -935,7 +935,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor
return;
}
LAST_EVALUATED_TOKEN = currentToken;
lastEvaluatedToken = currentToken;
editor.execCommand('startAutocomplete');
},
100);
@ -947,17 +947,7 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor
}
}
function getCompletions(
DO_NOT_USE: AceEditor,
DO_NOT_USE_SESSION: IEditSession,
pos: { row: number; column: number },
prefix: string,
callback: (...args: any[]) => void
) {
const position: Position = {
lineNumber: pos.row + 1,
column: pos.column + 1,
};
function getCompletions(position: Position, prefix: string, callback: (...args: any[]) => void) {
try {
const context = getAutoCompleteContext(editor, position);
if (!context) {
@ -1028,39 +1018,12 @@ export default function({ coreEditor: editor, parser }: { coreEditor: CoreEditor
editor.on('changeSelection', editorChangeListener);
// Hook into Ace
// disable standard context based autocompletion.
// @ts-ignore
ace.define('ace/autocomplete/text_completer', ['require', 'exports', 'module'], function(
require: any,
exports: any
) {
exports.getCompletions = function(
innerEditor: any,
session: any,
pos: any,
prefix: any,
callback: any
) {
callback(null, []);
};
});
const langTools = ace.acequire('ace/ext/language_tools');
langTools.setCompleters([
{
identifierRegexps: [
/[a-zA-Z_0-9\.\$\-\u00A2-\uFFFF]/, // adds support for dot character
],
getCompletions,
},
]);
return {
getCompletions,
// TODO: This needs to be cleaned up
_test: {
getCompletions,
getCompletions: (_editor: any, _editSession: any, pos: any, prefix: any, callback: any) =>
getCompletions(pos, prefix, callback),
addReplacementInfoToContext,
addChangeListener: () => editor.on('changeSelection', editorChangeListener),
removeChangeListener: () => editor.off('changeSelection', editorChangeListener),

View file

@ -115,7 +115,6 @@ class ScopeResolver extends SharedComponent {
next: [],
};
const components = this.resolveLinkToComponents(context, editor);
_.each(components, function(component) {
const componentResult = component.match(token, context, editor);
if (componentResult && componentResult.next) {

View file

@ -43,7 +43,7 @@ export function wrapComponentWithDefaults(component, defaults) {
const tracer = function() {
if (window.engine_trace) {
console.log.call(console, arguments);
console.log.call(console, ...arguments);
}
};

View file

@ -146,6 +146,10 @@ function loadApisFromJson(
return api;
}
// TODO: clean up setting up of active API and use of jQuery.
// This function should be attached to a class that holds the current state, not setup
// when the file is required. Also, jQuery should not be used to make network requests
// like this, it looks like a minor security issue.
export function setActiveApi(api) {
if (!api) {
$.ajax({

View file

@ -29,6 +29,12 @@ export type EditorEvent =
| 'change'
| 'changeSelection';
export type AutoCompleterFunction = (
pos: Position,
prefix: string,
callback: (...args: any[]) => void
) => void;
export interface Position {
/**
* The line number, not zero-indexed.
@ -256,4 +262,10 @@ export interface CoreEditor {
* Register a keyboard shortcut and provide a function to be called.
*/
registerKeyboardShortcut(opts: { keys: any; fn: () => void; name: string }): void;
/**
* Register a completions function that will be called when the editor
* detects a change
*/
registerAutocompleter(autocompleter: AutoCompleterFunction): void;
}

View file

@ -21,7 +21,7 @@ import { PluginConfigDescriptor, PluginInitializerContext } from 'kibana/server'
import { ConfigType, config as configSchema } from './config';
import { ConsoleServerPlugin } from './plugin';
export { ConsoleSetup } from './types';
export { ConsoleSetup, ConsoleStart } from './types';
export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx);

View file

@ -22,4 +22,4 @@ export { ProxyConfigCollection } from './proxy_config_collection';
export { proxyRequest } from './proxy_request';
export { getElasticsearchProxyConfig } from './elasticsearch_proxy_config';
export { setHeaders } from './set_headers';
export { addProcessorDefinition, addExtensionSpecFilePath, loadSpec } from './spec_definitions';
export { jsSpecLoaders } from './spec_definitions';

View file

@ -1,72 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _ from 'lodash';
class Api {
constructor(name) {
this.globalRules = {};
this.endpoints = {};
this.name = name;
}
addGlobalAutocompleteRules = (parentNode, rules) => {
this.globalRules[parentNode] = rules;
};
addEndpointDescription = (endpoint, description = {}) => {
let copiedDescription = {};
if (this.endpoints[endpoint]) {
copiedDescription = { ...this.endpoints[endpoint] };
}
let urlParamsDef;
_.each(description.patterns || [], function(p) {
if (p.indexOf('{indices}') >= 0) {
urlParamsDef = urlParamsDef || {};
urlParamsDef.ignore_unavailable = '__flag__';
urlParamsDef.allow_no_indices = '__flag__';
urlParamsDef.expand_wildcards = ['open', 'closed'];
}
});
if (urlParamsDef) {
description.url_params = _.extend(description.url_params || {}, copiedDescription.url_params);
_.defaults(description.url_params, urlParamsDef);
}
_.extend(copiedDescription, description);
_.defaults(copiedDescription, {
id: endpoint,
patterns: [endpoint],
methods: ['GET'],
});
this.endpoints[endpoint] = copiedDescription;
};
asJson() {
return {
name: this.name,
globals: this.globalRules,
endpoints: this.endpoints,
};
}
}
export default Api;

View file

@ -1,47 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import Api from './api';
import { getSpec } from './json';
import { register } from './js/ingest';
const ES = new Api('es');
export const loadSpec = () => {
const spec = getSpec();
// adding generated specs
Object.keys(spec).forEach(endpoint => {
ES.addEndpointDescription(endpoint, spec[endpoint]);
});
// adding globals and custom API definitions
require('./js/aliases')(ES);
require('./js/aggregations')(ES);
require('./js/document')(ES);
require('./js/filter')(ES);
require('./js/globals')(ES);
register(ES);
require('./js/mappings')(ES);
require('./js/settings')(ES);
require('./js/query')(ES);
require('./js/reindex')(ES);
require('./js/search')(ES);
};
export default ES;

View file

@ -17,4 +17,4 @@
* under the License.
*/
export { queryDsl as default } from './dsl';
export { jsSpecLoaders } from './js';

View file

@ -16,8 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
/*eslint camelcase: 0*/
/* eslint-disable @typescript-eslint/camelcase */
const significantTermsArgs = {
__template: {
field: '',
@ -77,7 +78,7 @@ const simple_pipeline = {
},
buckets_path: '',
format: '',
gap_policy: gap_policy,
gap_policy,
};
const rules = {
'*': {
@ -461,7 +462,7 @@ const rules = {
},
buckets_path: '',
format: '',
gap_policy: gap_policy,
gap_policy,
window: 5,
model: { __one_of: ['simple', 'linear', 'ewma', 'holt', 'holt_winters'] },
settings: {
@ -485,7 +486,7 @@ const rules = {
lag: 7,
},
lag: 7,
gap_policy: gap_policy,
gap_policy,
buckets_path: '',
format: '',
},
@ -496,7 +497,7 @@ const rules = {
},
buckets_path: {},
format: '',
gap_policy: gap_policy,
gap_policy,
script: '',
},
bucket_selector: {
@ -505,7 +506,7 @@ const rules = {
script: '',
},
buckets_path: {},
gap_policy: gap_policy,
gap_policy,
script: '',
},
bucket_sort: {
@ -515,7 +516,7 @@ const rules = {
sort: ['{field}'],
from: 0,
size: 0,
gap_policy: gap_policy,
gap_policy,
},
matrix_stats: {
__template: {
@ -526,8 +527,11 @@ const rules = {
},
};
const { terms, histogram, date_histogram } = rules['*'];
export default function(api) {
api.addGlobalAutocompleteRules('aggregations', rules);
api.addGlobalAutocompleteRules('aggs', rules);
api.addGlobalAutocompleteRules('groupByAggs', { '*': { terms, histogram, date_histogram } });
}
export const aggs = (specService: SpecDefinitionsService) => {
specService.addGlobalAutocompleteRules('aggregations', rules);
specService.addGlobalAutocompleteRules('aggs', rules);
specService.addGlobalAutocompleteRules('groupByAggs', {
'*': { terms, histogram, date_histogram },
});
};

View file

@ -16,15 +16,17 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
export default function(api) {
/* eslint-disable @typescript-eslint/camelcase */
export const aliases = (specService: SpecDefinitionsService) => {
const aliasRules = {
filter: {},
routing: '1',
search_routing: '1,2',
index_routing: '1',
};
api.addGlobalAutocompleteRules('aliases', {
specService.addGlobalAutocompleteRules('aliases', {
'*': aliasRules,
});
}
};

View file

@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
export default function(api) {
api.addEndpointDescription('update', {
/* eslint-disable @typescript-eslint/camelcase */
export const document = (specService: SpecDefinitionsService) => {
specService.addEndpointDescription('update', {
data_autocomplete_rules: {
script: {
// populated by a global rule
@ -29,7 +31,7 @@ export default function(api) {
},
});
api.addEndpointDescription('put_script', {
specService.addEndpointDescription('put_script', {
methods: ['POST', 'PUT'],
patterns: ['_scripts/{lang}/{id}', '_scripts/{lang}/{id}/_create'],
url_components: {
@ -40,7 +42,7 @@ export default function(api) {
},
});
api.addEndpointDescription('termvectors', {
specService.addEndpointDescription('termvectors', {
data_autocomplete_rules: {
fields: ['{field}'],
offsets: { __one_of: [false, true] },
@ -68,4 +70,4 @@ export default function(api) {
},
},
});
}
};

View file

@ -16,8 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
const filters = {};
/* eslint-disable @typescript-eslint/camelcase */
const filters: Record<string, any> = {};
filters.and = {
__template: {
@ -324,6 +326,6 @@ filters.nested = {
_name: '',
};
export default function(api) {
api.addGlobalAutocompleteRules('filter', filters);
}
export const filter = (specService: SpecDefinitionsService) => {
specService.addGlobalAutocompleteRules('filter', filters);
};

View file

@ -16,7 +16,9 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
/* eslint-disable @typescript-eslint/camelcase */
const highlightOptions = {
boundary_chars: {},
boundary_max_scan: 20,
@ -48,8 +50,9 @@ const highlightOptions = {
},
tags_schema: {},
};
export default function(api) {
api.addGlobalAutocompleteRules('highlight', {
export const globals = (specService: SpecDefinitionsService) => {
specService.addGlobalAutocompleteRules('highlight', {
...highlightOptions,
fields: {
'{field}': {
@ -60,7 +63,7 @@ export default function(api) {
},
});
api.addGlobalAutocompleteRules('script', {
specService.addGlobalAutocompleteRules('script', {
__template: {
source: 'SCRIPT',
},
@ -70,4 +73,4 @@ export default function(api) {
lang: '',
params: {},
});
}
};

View file

@ -17,15 +17,30 @@
* under the License.
*/
export declare function addProcessorDefinition(...args: any[]): any;
import { SpecDefinitionsService } from '../../../services';
export declare function resolveApi(): object;
import { aggs } from './aggregations';
import { aliases } from './aliases';
import { document } from './document';
import { filter } from './filter';
import { globals } from './globals';
import { ingest } from './ingest';
import { mappings } from './mappings';
import { settings } from './settings';
import { query } from './query';
import { reindex } from './reindex';
import { search } from './search';
export declare function addExtensionSpecFilePath(...args: any[]): any;
/**
* A function that synchronously reads files JSON from disk and builds
* the autocomplete structures served to the client. This must be called
* after any extensions have been loaded.
*/
export declare function loadSpec(): any;
export const jsSpecLoaders: Array<(registry: SpecDefinitionsService) => void> = [
aggs,
aliases,
document,
filter,
globals,
ingest,
mappings,
settings,
query,
reindex,
search,
];

View file

@ -17,6 +17,9 @@
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
/* eslint-disable @typescript-eslint/camelcase */
const commonPipelineParams = {
on_failure: [],
ignore_failure: {
@ -427,27 +430,23 @@ const pipelineDefinition = {
version: 123,
};
export const register = api => {
export const ingest = (specService: SpecDefinitionsService) => {
// Note: this isn't an actual API endpoint. It exists so the forEach processor's "processor" field
// may recursively use the autocomplete rules for any processor.
api.addEndpointDescription('_processor', {
specService.addEndpointDescription('_processor', {
data_autocomplete_rules: processorDefinition,
});
api.addEndpointDescription('ingest.put_pipeline', {
specService.addEndpointDescription('ingest.put_pipeline', {
methods: ['PUT'],
patterns: ['_ingest/pipeline/{id}'],
data_autocomplete_rules: pipelineDefinition,
});
api.addEndpointDescription('ingest.simulate', {
specService.addEndpointDescription('ingest.simulate', {
data_autocomplete_rules: {
pipeline: pipelineDefinition,
docs: [],
},
});
};
export const addProcessorDefinition = processor => {
processorDefinition.__one_of.push(processor);
};

View file

@ -17,12 +17,15 @@
* under the License.
*/
const _ = require('lodash');
import _ from 'lodash';
import { SpecDefinitionsService } from '../../../services';
import { BOOLEAN } from './shared';
export default function(api) {
api.addEndpointDescription('put_mapping', {
/* eslint-disable @typescript-eslint/camelcase */
export const mappings = (specService: SpecDefinitionsService) => {
specService.addEndpointDescription('put_mapping', {
priority: 10, // collides with put doc by id
data_autocomplete_rules: {
__template: {
@ -249,4 +252,4 @@ export default function(api) {
},
},
});
}
};

View file

@ -18,6 +18,9 @@
*/
import _ from 'lodash';
import { SpecDefinitionsService } from '../../../../services';
import {
spanFirstTemplate,
spanNearTemplate,
@ -32,6 +35,8 @@ import {
rangeTemplate,
regexpTemplate,
} from './templates';
/* eslint-disable @typescript-eslint/camelcase */
const matchOptions = {
cutoff_frequency: 0.001,
query: '',
@ -57,6 +62,7 @@ const matchOptions = {
prefix_length: 1,
minimum_should_match: 1,
};
const innerHits = {
docvalue_fields: ['FIELD'],
from: {},
@ -84,6 +90,7 @@ const innerHits = {
__one_of: ['true', 'false'],
},
};
const SPAN_QUERIES_NO_FIELD_MASK = {
// TODO add one_of for objects
span_first: {
@ -115,6 +122,7 @@ const SPAN_QUERIES_NO_FIELD_MASK = {
__scope_link: '.span_within',
},
};
const SPAN_QUERIES = {
...SPAN_QUERIES_NO_FIELD_MASK,
field_masking_span: {
@ -165,13 +173,14 @@ const DECAY_FUNC_DESC = {
decay: 0.5,
},
};
const SCORING_FUNCS = {
script_score: {
__template: {
script: "_score * doc['f'].value",
},
script: {
//populated by a global rule
// populated by a global rule
},
},
boost_factor: 2.0,
@ -204,8 +213,8 @@ const SCORING_FUNCS = {
},
};
export function queryDsl(api) {
api.addGlobalAutocompleteRules('query', {
export const query = (specService: SpecDefinitionsService) => {
specService.addGlobalAutocompleteRules('query', {
match: {
__template: {
FIELD: 'TEXT',
@ -631,7 +640,7 @@ export function queryDsl(api) {
filter: {},
boost: 2.0,
script: {
//populated by a global rule
// populated by a global rule
},
},
],
@ -695,7 +704,7 @@ export function queryDsl(api) {
script: "_score * doc['f'].value",
},
script: {
//populated by a global rule
// populated by a global rule
},
},
wrapper: {
@ -705,4 +714,4 @@ export function queryDsl(api) {
query: '',
},
});
}
};

View file

@ -17,10 +17,4 @@
* under the License.
*/
import es from './es';
export function resolveApi() {
return {
es: es.asJson(),
};
}
export { query } from './dsl';

View file

@ -17,23 +17,28 @@
* under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
export const regexpTemplate = {
FIELD: 'REGEXP',
};
export const fuzzyTemplate = {
FIELD: {},
};
export const prefixTemplate = {
FIELD: {
value: '',
},
};
export const rangeTemplate = {
FIELD: {
gte: 10,
lte: 20,
},
};
export const spanFirstTemplate = {
match: {
span_term: {
@ -42,6 +47,7 @@ export const spanFirstTemplate = {
},
end: 3,
};
export const spanNearTemplate = {
clauses: [
{
@ -55,11 +61,13 @@ export const spanNearTemplate = {
slop: 12,
in_order: false,
};
export const spanTermTemplate = {
FIELD: {
value: 'VALUE',
},
};
export const spanNotTemplate = {
include: {
span_term: {
@ -76,6 +84,7 @@ export const spanNotTemplate = {
},
},
};
export const spanOrTemplate = {
clauses: [
{
@ -87,6 +96,7 @@ export const spanOrTemplate = {
},
],
};
export const spanContainingTemplate = {
little: {
span_term: {
@ -118,6 +128,7 @@ export const spanContainingTemplate = {
},
},
};
export const spanWithinTemplate = {
little: {
span_term: {
@ -149,6 +160,7 @@ export const spanWithinTemplate = {
},
},
};
export const wildcardTemplate = {
FIELD: {
value: 'VALUE',

View file

@ -17,8 +17,11 @@
* under the License.
*/
export default function(api) {
api.addEndpointDescription('reindex', {
import { SpecDefinitionsService } from '../../../services';
/* eslint-disable @typescript-eslint/camelcase */
export const reindex = (specService: SpecDefinitionsService) => {
specService.addEndpointDescription('reindex', {
methods: ['POST'],
patterns: ['_reindex'],
data_autocomplete_rules: {
@ -62,4 +65,4 @@ export default function(api) {
script: { __scope_link: 'GLOBAL.script' },
},
});
}
};

View file

@ -16,9 +16,11 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
export default function(api) {
api.addEndpointDescription('search', {
/* eslint-disable @typescript-eslint/camelcase */
export const search = (specService: SpecDefinitionsService) => {
specService.addEndpointDescription('search', {
priority: 10, // collides with get doc by id
data_autocomplete_rules: {
query: {
@ -191,7 +193,7 @@ export default function(api) {
},
});
api.addEndpointDescription('search_template', {
specService.addEndpointDescription('search_template', {
data_autocomplete_rules: {
template: {
__one_of: [{ __scope_link: 'search' }, { __scope_link: 'GLOBAL.script' }],
@ -200,18 +202,18 @@ export default function(api) {
},
});
api.addEndpointDescription('render_search_template', {
specService.addEndpointDescription('render_search_template', {
data_autocomplete_rules: {
__one_of: [{ source: { __scope_link: 'search' } }, { __scope_link: 'GLOBAL.script' }],
params: {},
},
});
api.addEndpointDescription('_search/template/{id}', {
specService.addEndpointDescription('_search/template/{id}', {
data_autocomplete_rules: {
template: {
__scope_link: 'search',
},
},
});
}
};

View file

@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import { SpecDefinitionsService } from '../../../services';
import { BOOLEAN } from './shared';
export default function(api) {
api.addEndpointDescription('put_settings', {
/* eslint-disable @typescript-eslint/camelcase */
export const settings = (specService: SpecDefinitionsService) => {
specService.addEndpointDescription('put_settings', {
data_autocomplete_rules: {
refresh_interval: '1s',
number_of_shards: 1,
@ -71,4 +72,4 @@ export default function(api) {
},
},
});
}
};

View file

@ -17,6 +17,7 @@
* under the License.
*/
/* eslint-disable @typescript-eslint/camelcase */
export const BOOLEAN = Object.freeze({
__one_of: [true, false],
});

View file

@ -1,59 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import glob from 'glob';
import { join, basename } from 'path';
import { readFileSync } from 'fs';
import { merge } from 'lodash';
const extensionSpecFilePaths = [];
function _getSpec(dirname = __dirname) {
const generatedFiles = glob.sync(join(dirname, 'generated', '*.json'));
const overrideFiles = glob.sync(join(dirname, 'overrides', '*.json'));
return generatedFiles.reduce((acc, file) => {
const overrideFile = overrideFiles.find(f => basename(f) === basename(file));
const loadedSpec = JSON.parse(readFileSync(file, 'utf8'));
if (overrideFile) {
merge(loadedSpec, JSON.parse(readFileSync(overrideFile, 'utf8')));
}
const spec = {};
Object.entries(loadedSpec).forEach(([key, value]) => {
if (acc[key]) {
// add time to remove key collision
spec[`${key}${Date.now()}`] = value;
} else {
spec[key] = value;
}
});
return { ...acc, ...spec };
}, {});
}
export function getSpec() {
const result = _getSpec();
extensionSpecFilePaths.forEach(extensionSpecFilePath => {
merge(result, _getSpec(extensionSpecFilePath));
});
return result;
}
export function addExtensionSpecFilePath(extensionSpecFilePath) {
extensionSpecFilePaths.push(extensionSpecFilePath);
}

View file

@ -21,20 +21,18 @@ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/serv
import { readLegacyEsConfig } from '../../../legacy/core_plugins/console_legacy';
import {
ProxyConfigCollection,
addExtensionSpecFilePath,
addProcessorDefinition,
loadSpec,
} from './lib';
import { ProxyConfigCollection } from './lib';
import { SpecDefinitionsService } from './services';
import { ConfigType } from './config';
import { registerProxyRoute } from './routes/api/console/proxy';
import { registerSpecDefinitionsRoute } from './routes/api/console/spec_definitions';
import { ESConfigForProxy, ConsoleSetup } from './types';
import { ESConfigForProxy, ConsoleSetup, ConsoleStart } from './types';
export class ConsoleServerPlugin implements Plugin<ConsoleSetup> {
export class ConsoleServerPlugin implements Plugin<ConsoleSetup, ConsoleStart> {
log: Logger;
specDefinitionsService = new SpecDefinitionsService();
constructor(private readonly ctx: PluginInitializerContext<ConfigType>) {
this.log = this.ctx.logger.get();
}
@ -72,15 +70,19 @@ export class ConsoleServerPlugin implements Plugin<ConsoleSetup> {
router,
});
registerSpecDefinitionsRoute({ router });
registerSpecDefinitionsRoute({
router,
services: { specDefinitions: this.specDefinitionsService },
});
return {
addExtensionSpecFilePath,
addProcessorDefinition,
...this.specDefinitionsService.setup(),
};
}
start() {
loadSpec();
return {
...this.specDefinitionsService.start(),
};
}
}

View file

@ -17,12 +17,30 @@
* under the License.
*/
import { IRouter, RequestHandler } from 'kibana/server';
import { resolveApi } from '../../../../lib/spec_definitions';
import { SpecDefinitionsService } from '../../../../services';
export const registerSpecDefinitionsRoute = ({ router }: { router: IRouter }) => {
interface SpecDefinitionsRouteResponse {
es: {
name: string;
globals: Record<string, any>;
endpoints: Record<string, any>;
};
}
export const registerSpecDefinitionsRoute = ({
router,
services,
}: {
router: IRouter;
services: { specDefinitions: SpecDefinitionsService };
}) => {
const handler: RequestHandler = async (ctx, request, response) => {
const specResponse: SpecDefinitionsRouteResponse = {
es: services.specDefinitions.asJson(),
};
return response.ok({
body: resolveApi(),
body: specResponse,
headers: {
'Content-Type': 'application/json',
},

View file

@ -17,10 +17,4 @@
* under the License.
*/
export { addProcessorDefinition } from './js/ingest';
export { addExtensionSpecFilePath } from './json';
export { loadSpec } from './es';
export { resolveApi } from './server';
export { SpecDefinitionsService } from './spec_definitions_service';

View file

@ -0,0 +1,150 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import _, { merge } from 'lodash';
import glob from 'glob';
import { basename, join, resolve } from 'path';
import { readFileSync } from 'fs';
import { jsSpecLoaders } from '../lib';
const PATH_TO_OSS_JSON_SPEC = resolve(__dirname, '../lib/spec_definitions/json');
export class SpecDefinitionsService {
private readonly name = 'es';
private readonly globalRules: Record<string, any> = {};
private readonly endpoints: Record<string, any> = {};
private readonly extensionSpecFilePaths: string[] = [];
private hasLoadedSpec = false;
public addGlobalAutocompleteRules(parentNode: string, rules: any) {
this.globalRules[parentNode] = rules;
}
public addEndpointDescription(endpoint: string, description: any = {}) {
let copiedDescription: any = {};
if (this.endpoints[endpoint]) {
copiedDescription = { ...this.endpoints[endpoint] };
}
let urlParamsDef: any;
_.each(description.patterns || [], function(p) {
if (p.indexOf('{indices}') >= 0) {
urlParamsDef = urlParamsDef || {};
urlParamsDef.ignore_unavailable = '__flag__';
urlParamsDef.allow_no_indices = '__flag__';
urlParamsDef.expand_wildcards = ['open', 'closed'];
}
});
if (urlParamsDef) {
description.url_params = _.extend(description.url_params || {}, copiedDescription.url_params);
_.defaults(description.url_params, urlParamsDef);
}
_.extend(copiedDescription, description);
_.defaults(copiedDescription, {
id: endpoint,
patterns: [endpoint],
methods: ['GET'],
});
this.endpoints[endpoint] = copiedDescription;
}
public asJson() {
return {
name: this.name,
globals: this.globalRules,
endpoints: this.endpoints,
};
}
public addExtensionSpecFilePath(path: string) {
this.extensionSpecFilePaths.push(path);
}
public addProcessorDefinition(processor: any) {
if (!this.hasLoadedSpec) {
throw new Error(
'Cannot add a processor definition because spec definitions have not loaded!'
);
}
this.endpoints._processor!.data_autocomplete_rules.__one_of.push(processor);
}
public setup() {
return {
addExtensionSpecFilePath: this.addExtensionSpecFilePath.bind(this),
};
}
public start() {
if (!this.hasLoadedSpec) {
this.loadJsonSpec();
this.loadJSSpec();
this.hasLoadedSpec = true;
return {
addProcessorDefinition: this.addProcessorDefinition.bind(this),
};
} else {
throw new Error('Service has already started!');
}
}
private loadJSONSpecInDir(dirname: string) {
const generatedFiles = glob.sync(join(dirname, 'generated', '*.json'));
const overrideFiles = glob.sync(join(dirname, 'overrides', '*.json'));
return generatedFiles.reduce((acc, file) => {
const overrideFile = overrideFiles.find(f => basename(f) === basename(file));
const loadedSpec = JSON.parse(readFileSync(file, 'utf8'));
if (overrideFile) {
merge(loadedSpec, JSON.parse(readFileSync(overrideFile, 'utf8')));
}
const spec: any = {};
Object.entries(loadedSpec).forEach(([key, value]) => {
if (acc[key]) {
// add time to remove key collision
spec[`${key}${Date.now()}`] = value;
} else {
spec[key] = value;
}
});
return { ...acc, ...spec };
}, {} as any);
}
private loadJsonSpec() {
const result = this.loadJSONSpecInDir(PATH_TO_OSS_JSON_SPEC);
this.extensionSpecFilePaths.forEach(extensionSpecFilePath => {
merge(result, this.loadJSONSpecInDir(extensionSpecFilePath));
});
Object.keys(result).forEach(endpoint => {
this.addEndpointDescription(endpoint, result[endpoint]);
});
}
private loadJSSpec() {
jsSpecLoaders.forEach(addJsSpec => addJsSpec(this));
}
}

View file

@ -25,6 +25,11 @@ export type ConsoleSetup = ReturnType<ConsoleServerPlugin['setup']> extends Prom
? U
: ReturnType<ConsoleServerPlugin['setup']>;
/** @public */
export type ConsoleStart = ReturnType<ConsoleServerPlugin['start']> extends Promise<infer U>
? U
: ReturnType<ConsoleServerPlugin['start']>;
/** @internal */
export interface ESConfigForProxy {
hosts: string[];

View file

@ -4,9 +4,9 @@
* you may not use this file except in compliance with the Elastic License.
*/
import { join } from 'path';
import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server';
import { ConsoleSetup } from '../../../../src/plugins/console/server';
import { ConsoleSetup, ConsoleStart } from '../../../../src/plugins/console/server';
import { processors } from './spec/ingest/index';
@ -14,19 +14,25 @@ interface SetupDependencies {
console: ConsoleSetup;
}
interface StartDependencies {
console: ConsoleStart;
}
const CONSOLE_XPACK_JSON_SPEC_PATH = join(__dirname, 'spec/');
export class ConsoleExtensionsServerPlugin implements Plugin<void, void, SetupDependencies> {
log: Logger;
constructor(private readonly ctx: PluginInitializerContext) {
this.log = this.ctx.logger.get();
}
setup(
core: CoreSetup,
{ console: { addProcessorDefinition, addExtensionSpecFilePath } }: SetupDependencies
) {
addExtensionSpecFilePath(join(__dirname, 'spec/'));
processors.forEach(processor => addProcessorDefinition(processor));
this.log.debug('Installed console autocomplete extensions.');
setup(core: CoreSetup, { console: { addExtensionSpecFilePath } }: SetupDependencies) {
addExtensionSpecFilePath(CONSOLE_XPACK_JSON_SPEC_PATH);
this.log.debug(`Added extension path to ${CONSOLE_XPACK_JSON_SPEC_PATH}...`);
}
start(core: CoreStart, { console: { addProcessorDefinition } }: StartDependencies) {
processors.forEach(processor => addProcessorDefinition(processor));
this.log.debug('Added processor definition extensions.');
}
start() {}
}