Timelion api migration (#53005) (#55730)

This commit is contained in:
Joe Reuter 2020-01-24 09:01:59 +01:00 committed by GitHub
parent e7d3e95c7a
commit 4473f1e547
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
157 changed files with 518 additions and 574 deletions

View file

@ -32,7 +32,7 @@
"statusPage": "src/legacy/core_plugins/status_page",
"telemetry": "src/legacy/core_plugins/telemetry",
"tileMap": "src/legacy/core_plugins/tile_map",
"timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion"],
"timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion", "src/plugins/timelion"],
"uiActions": "src/plugins/ui_actions",
"visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown",
"visTypeMetric": "src/legacy/core_plugins/vis_type_metric",

View file

@ -21,10 +21,7 @@ import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { Legacy } from 'kibana';
import { LegacyPluginApi, LegacyPluginInitializer } from 'src/legacy/plugin_discovery/types';
import { CoreSetup, PluginInitializerContext } from 'src/core/server';
import { DEFAULT_APP_CATEGORIES } from '../../../core/utils';
import { plugin } from './server';
import { CustomCoreSetup } from './server/plugin';
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
defaultMessage: 'experimental',
@ -195,12 +192,6 @@ const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPl
},
},
},
init: (server: Legacy.Server) => {
const initializerContext = {} as PluginInitializerContext;
const core = { http: { server } } as CoreSetup & CustomCoreSetup;
plugin(initializerContext).setup(core);
},
});
// eslint-disable-next-line import/no-default-export

View file

@ -43,7 +43,7 @@
import _ from 'lodash';
import $ from 'jquery';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../../vis_type_timelion/public/chain.peg';
import grammar from 'raw-loader!../../../../../plugins/timelion/common/chain.peg';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,

View file

@ -1,65 +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.
*/
const filename = require('path').basename(__filename);
const fn = require(`../calculate_interval`);
const moment = require('moment');
const expect = require('chai').expect;
const from = (count, unit) =>
moment()
.subtract(count, unit)
.valueOf();
const to = moment().valueOf();
const size = 200;
const min = '1ms';
describe(filename, () => {
it('Exports a function', () => {
expect(fn).to.be.a('function');
});
it('Only calculates when interval = auto', () => {
const partialFn = interval => fn(from(1, 'y'), to, size, interval, min);
expect(partialFn('1ms')).to.equal('1ms');
expect(partialFn('bag_of_beans')).to.equal('bag_of_beans');
expect(partialFn('auto')).to.not.equal('auto');
});
it('Calculates nice round intervals', () => {
const partialFn = (count, unit) => fn(from(count, unit), to, size, 'auto', min);
expect(partialFn(15, 'm')).to.equal('1s');
expect(partialFn(1, 'h')).to.equal('30s');
expect(partialFn(3, 'd')).to.equal('30m');
expect(partialFn(1, 'w')).to.equal('1h');
expect(partialFn(1, 'y')).to.equal('24h');
expect(partialFn(100, 'y')).to.equal('1y');
});
it('Does not calculate an interval lower than the minimum', () => {
const partialFn = (count, unit) => fn(from(count, unit), to, size, 'auto', '1m');
expect(partialFn(5, 's')).to.equal('1m');
expect(partialFn(15, 'm')).to.equal('1m');
expect(partialFn(1, 'h')).to.equal('1m');
expect(partialFn(3, 'd')).to.equal('30m');
expect(partialFn(1, 'w')).to.equal('1h');
expect(partialFn(1, 'y')).to.equal('24h');
expect(partialFn(100, 'y')).to.equal('1y');
});
});

View file

@ -24,7 +24,11 @@ import moment from 'moment-timezone';
import { timefilter } from 'ui/timefilter';
// @ts-ignore
import observeResize from '../../lib/observe_resize';
import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../../vis_type_timelion/common/lib';
import {
calculateInterval,
DEFAULT_TIME_FORMAT,
// @ts-ignore
} from '../../../../../../plugins/timelion/common/lib';
import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters';
import { TimelionVisualizationDependencies } from '../../plugin';
import { xaxisFormatterProvider } from '../../../../vis_type_timelion/public/helpers/xaxis_formatter';

View file

@ -1,62 +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 { Legacy } from 'kibana';
import { i18n } from '@kbn/i18n';
import { PluginInitializerContext, CoreSetup } from 'kibana/server';
import loadFunctions, { LoadFunctions } from './lib/load_functions';
import { initRoutes } from './routes';
function getFunction(functions: LoadFunctions, name: string) {
if (functions[name]) {
return functions[name];
}
throw new Error(
i18n.translate('timelion.noFunctionErrorMessage', {
defaultMessage: 'No such function: {name}',
values: { name },
})
);
}
// TODO: Remove as CoreSetup is completed.
export interface CustomCoreSetup {
http: {
server: Legacy.Server;
};
}
export class TimelionServerPlugin {
public initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public setup(core: CoreSetup & CustomCoreSetup) {
const { server } = core.http;
const functions = loadFunctions('series_functions');
server.expose('functions', functions);
server.expose('getFunction', (name: string) => getFunction(functions, name));
initRoutes(server);
}
}

View file

@ -1,32 +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 { Legacy } from 'kibana';
// @ts-ignore
import { runRoute } from './run';
// @ts-ignore
import { functionsRoute } from './functions';
// @ts-ignore
import { validateEsRoute } from './validate_es';
export function initRoutes(server: Legacy.Server) {
runRoute(server);
functionsRoute(server);
validateEsRoute(server);
}

View file

@ -1,125 +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 Joi from 'joi';
import Bluebird from 'bluebird';
import _ from 'lodash';
import { Legacy } from 'kibana';
// @ts-ignore
import chainRunnerFn from '../handlers/chain_runner.js';
// @ts-ignore
import getNamespacesSettings from '../lib/get_namespaced_settings';
// @ts-ignore
import getTlConfig from '../handlers/lib/tl_config';
const timelionDefaults = getNamespacesSettings();
export interface TimelionRequestQuery {
payload: {
sheet: string[];
extended?: {
es: {
filter: {
bool: {
filter: string[] | object;
must: string[];
should: string[];
must_not: string[];
};
};
};
};
};
time?: {
from?: string;
interval: string;
timezone: string;
to?: string;
};
}
function formatErrorResponse(e: Error, h: Legacy.ResponseToolkit) {
return h
.response({
title: e.toString(),
message: e.toString(),
})
.code(500);
}
const requestPayload = {
payload: Joi.object({
sheet: Joi.array()
.items(Joi.string())
.required(),
extended: Joi.object({
es: Joi.object({
filter: Joi.object({
bool: Joi.object({
filter: Joi.array().allow(null),
must: Joi.array().allow(null),
should: Joi.array().allow(null),
must_not: Joi.array().allow(null),
}),
}),
}),
}).optional(),
time: Joi.object({
from: Joi.string(),
interval: Joi.string().required(),
timezone: Joi.string().required(),
to: Joi.string(),
}).required(),
}),
};
export function runRoute(server: Legacy.Server) {
server.route({
method: 'POST',
path: '/api/timelion/run',
options: {
validate: requestPayload,
},
handler: async (request: Legacy.Request & TimelionRequestQuery, h: Legacy.ResponseToolkit) => {
try {
const uiSettings = await request.getUiSettingsService().getAll();
const tlConfig = getTlConfig({
server,
request,
settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting.
});
const chainRunner = chainRunnerFn(tlConfig);
const sheet = await Bluebird.all(chainRunner.processRequest(request.payload));
return {
sheet,
stats: chainRunner.getStats(),
};
} catch (err) {
server.log(['timelion', 'error'], `${err.toString()}: ${err.stack}`);
// TODO Maybe we should just replace everywhere we throw with Boom? Probably.
if (err.isBoom) {
return err;
} else {
return formatErrorResponse(err, h);
}
}
},
});
}

View file

@ -24,7 +24,7 @@ import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash';
import { useKibana } from '../../../../../plugins/kibana_react/public';
import '../flot';
import { DEFAULT_TIME_FORMAT } from '../../common/lib';
import { DEFAULT_TIME_FORMAT } from '../../../../../plugins/timelion/common/lib';
import {
buildSeriesData,

View file

@ -24,8 +24,11 @@ import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import { CodeEditor, useKibana } from '../../../../../plugins/kibana_react/public';
import { suggest, getSuggestion } from './timelion_expression_input_helpers';
import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
import { getArgValueSuggestions } from '../helpers/arg_value_suggestions';
import {
ITimelionFunction,
TimelionFunctionArgs,
} from '../../../../../plugins/timelion/common/types';
const LANGUAGE_ID = 'timelion_expression';
monacoEditor.languages.register({ id: LANGUAGE_ID });

View file

@ -22,7 +22,7 @@ import { getArgValueSuggestions } from '../helpers/arg_value_suggestions';
import { setIndexPatterns, setSavedObjectsClient } from '../helpers/plugin_services';
import { IndexPatterns } from 'src/plugins/data/public';
import { SavedObjectsClient } from 'kibana/public';
import { ITimelionFunction } from '../../common/types';
import { ITimelionFunction } from '../../../../../plugins/timelion/common/types';
describe('Timelion expression suggestions', () => {
setIndexPatterns({} as IndexPatterns);

View file

@ -18,16 +18,19 @@
*/
import { get, startsWith } from 'lodash';
import { i18n } from '@kbn/i18n';
import * as monacoEditor from 'monaco-editor/esm/vs/editor/editor.api';
import { i18n } from '@kbn/i18n';
import { Parser } from 'pegjs';
// @ts-ignore
import { parse } from '../_generated_/chain';
import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions';
import {
ITimelionFunction,
TimelionFunctionArgs,
} from '../../../../../plugins/timelion/common/types';
export enum SUGGESTION_TYPE {
ARGUMENTS = 'arguments',

View file

@ -18,8 +18,8 @@
*/
import { get } from 'lodash';
import { TimelionFunctionArgs } from '../../common/types';
import { getIndexPatterns, getSavedObjectsClient } from './plugin_services';
import { TimelionFunctionArgs } from '../../../../../plugins/timelion/common/types';
export interface Location {
min: number;

View file

@ -23,7 +23,7 @@ import moment, { Moment } from 'moment-timezone';
import { TimefilterContract } from 'src/plugins/data/public';
import { IUiSettingsClient } from 'kibana/public';
import { calculateInterval } from '../../common/lib';
import { calculateInterval } from '../../../../../plugins/timelion/common/lib';
import { xaxisFormatterProvider } from './xaxis_formatter';
import { Series } from './timelion_request_handler';

View file

@ -0,0 +1,66 @@
/*
* 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 { calculateInterval as fn } from './calculate_interval';
import moment, { unitOfTime } from 'moment';
const from = (count: number, unit: unitOfTime.DurationConstructor) =>
moment()
.subtract(count, unit)
.valueOf();
const to = moment().valueOf();
const size = 200;
const min = '1ms';
describe('calculate_interval', () => {
it('Exports a function', () => {
expect(typeof fn).toBe('function');
});
it('Only calculates when interval = auto', () => {
const partialFn = (interval: string) => fn(from(1, 'y'), to, size, interval, min);
expect(partialFn('1ms')).toEqual('1ms');
expect(partialFn('bag_of_beans')).toEqual('bag_of_beans');
expect(partialFn('auto')).not.toEqual('auto');
});
it('Calculates nice round intervals', () => {
const partialFn = (count: number, unit: unitOfTime.DurationConstructor) =>
fn(from(count, unit), to, size, 'auto', min);
expect(partialFn(15, 'm')).toEqual('1s');
expect(partialFn(1, 'h')).toEqual('30s');
expect(partialFn(3, 'd')).toEqual('30m');
expect(partialFn(1, 'w')).toEqual('1h');
expect(partialFn(1, 'y')).toEqual('24h');
expect(partialFn(100, 'y')).toEqual('1y');
});
it('Does not calculate an interval lower than the minimum', () => {
const partialFn = (count: number, unit: unitOfTime.DurationConstructor) =>
fn(from(count, unit), to, size, 'auto', '1m');
expect(partialFn(5, 's')).toEqual('1m');
expect(partialFn(15, 'm')).toEqual('1m');
expect(partialFn(1, 'h')).toEqual('1m');
expect(partialFn(3, 'd')).toEqual('30m');
expect(partialFn(1, 'w')).toEqual('1h');
expect(partialFn(1, 'y')).toEqual('24h');
expect(partialFn(100, 'y')).toEqual('1y');
});
});

View file

@ -17,10 +17,10 @@
* under the License.
*/
import { toMS } from './to_milliseconds';
// Totally cribbed this from Kibana 3.
// I bet there's something similar in the Kibana 4 code. Somewhere. Somehow.
import { toMS } from './to_milliseconds';
function roundInterval(interval: number) {
switch (true) {
case interval <= 500: // <= 0.5s

View file

@ -22,6 +22,7 @@ import { schema } from '@kbn/config-schema';
export const ConfigSchema = schema.object(
{
ui: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
graphiteUrls: schema.maybe(schema.arrayOf(schema.string())),
},
// This option should be removed as soon as we entirely migrate config from legacy Timelion plugin.
{ allowUnknowns: true }

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../average`);
import fn from './average';
import moment from 'moment';
const expect = require('chai').expect;
import _ from 'lodash';

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../carry`);
import fn from './carry';
import moment from 'moment';
const expect = require('chai').expect;
import _ from 'lodash';

View file

@ -26,7 +26,7 @@ import parseSheet from './lib/parse_sheet.js';
import repositionArguments from './lib/reposition_arguments.js';
import indexArguments from './lib/index_arguments.js';
import validateTime from './lib/validate_time.js';
import { calculateInterval } from '../../../vis_type_timelion/common/lib';
import { calculateInterval } from '../../common/lib';
export default function chainRunner(tlConfig) {
const preprocessChain = require('./lib/preprocess_chain')(tlConfig);
@ -41,7 +41,7 @@ export default function chainRunner(tlConfig) {
// Invokes a modifier function, resolving arguments into series as needed
function invoke(fnName, args) {
const functionDef = tlConfig.server.plugins.timelion.getFunction(fnName);
const functionDef = tlConfig.getFunction(fnName);
function resolveArgument(item) {
if (Array.isArray(item)) {
@ -51,7 +51,7 @@ export default function chainRunner(tlConfig) {
if (_.isObject(item)) {
switch (item.type) {
case 'function': {
const itemFunctionDef = tlConfig.server.plugins.timelion.getFunction(item.function);
const itemFunctionDef = tlConfig.getFunction(item.function);
if (itemFunctionDef.cacheKey && queryCache[itemFunctionDef.cacheKey(item)]) {
stats.queryCount++;
return Bluebird.resolve(_.cloneDeep(queryCache[itemFunctionDef.cacheKey(item)]));
@ -168,7 +168,7 @@ export default function chainRunner(tlConfig) {
stats.queryTime = new Date().getTime();
_.each(queries, function(query, i) {
const functionDef = tlConfig.server.plugins.timelion.getFunction(query.function);
const functionDef = tlConfig.getFunction(query.function);
const resolvedDatasource = resolvedDatasources[i];
if (resolvedDatasource.isRejected()) {

View file

@ -21,10 +21,7 @@ import { i18n } from '@kbn/i18n';
import fs from 'fs';
import path from 'path';
import _ from 'lodash';
const grammar = fs.readFileSync(
path.resolve(__dirname, '../../../../vis_type_timelion/public/chain.peg'),
'utf8'
);
const grammar = fs.readFileSync(path.resolve(__dirname, '../../../common/chain.peg'), 'utf8');
import PEG from 'pegjs';
const Parser = PEG.generate(grammar);

View file

@ -17,7 +17,7 @@
* under the License.
*/
const parseSheet = require('../lib/parse_sheet');
const parseSheet = require('./parse_sheet');
const expect = require('chai').expect;

View file

@ -24,7 +24,7 @@ export default function preProcessChainFn(tlConfig) {
queries = queries || {};
function validateAndStore(item) {
if (_.isObject(item) && item.type === 'function') {
const functionDef = tlConfig.server.plugins.timelion.getFunction(item.function);
const functionDef = tlConfig.getFunction(item.function);
if (functionDef.datasource) {
queries[functionDef.cacheKey(item)] = item;

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import moment from 'moment';
import { toMS } from '../../../../vis_type_timelion/common/lib';
import { toMS } from '../../../common/lib/to_milliseconds';
export default function validateTime(time, tlConfig) {
const span = moment.duration(moment(time.to).diff(moment(time.from))).asMilliseconds();

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import unzipPairs from './unzipPairs.js';
import unzipPairs from './unzip_pairs.js';
export default function asSorted(timeValObject, fn) {
const data = unzipPairs(timeValObject);

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { TimelionFunctionArgs } from '../../../../vis_type_timelion/common/types';
import { TimelionFunctionArgs } from '../../../common/types';
export interface TimelionFunctionInterface extends TimelionFunctionConfig {
chainable: boolean;

View file

@ -18,8 +18,28 @@
*/
import { PluginInitializerContext } from 'kibana/server';
import { TimelionServerPlugin as Plugin } from './plugin';
import { TypeOf } from '@kbn/config-schema';
import { ConfigSchema } from '../config';
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
export class ConfigManager {
private esShardTimeout: number = 0;
private graphiteUrls: string[] = [];
constructor(config: PluginInitializerContext['config']) {
config.create<TypeOf<typeof ConfigSchema>>().subscribe(configUpdate => {
this.graphiteUrls = configUpdate.graphiteUrls || [];
});
config.legacy.globalConfig$.subscribe(configUpdate => {
this.esShardTimeout = configUpdate.elasticsearch.shardTimeout.asMilliseconds();
});
}
getEsShardTimeout() {
return this.esShardTimeout;
}
getGraphiteUrls() {
return this.graphiteUrls;
}
}

View file

@ -18,7 +18,7 @@
*/
import _ from 'lodash';
import configFile from '../../timelion.json';
import configFile from '../timelion.json';
export default function() {
function flattenWith(dot, nestedObj, flattenArrays) {

View file

@ -28,18 +28,18 @@ export default function(directory) {
}
// Get a list of all files and use the filename as the object key
const files = _.map(glob.sync(path.resolve(__dirname, '../' + directory + '/*.js')), function(
file
) {
const name = file.substring(file.lastIndexOf('/') + 1, file.lastIndexOf('.'));
return getTuple(directory, name);
});
const files = _.map(
glob
.sync(path.resolve(__dirname, '../' + directory + '/*.js'))
.filter(filename => !filename.includes('.test')),
function(file) {
const name = file.substring(file.lastIndexOf('/') + 1, file.lastIndexOf('.'));
return getTuple(directory, name);
}
);
// Get a list of all directories with an index.js, use the directory name as the key in the object
const directories = _.chain(glob.sync(path.resolve(__dirname, '../' + directory + '/*/index.js')))
.filter(function(file) {
return file.match(/__test__/) == null;
})
.map(function(file) {
const parts = file.split('/');
const name = parts[parts.length - 2];

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../load_functions`);
const fn = require(`src/plugins/timelion/server/lib/load_functions`);
const expect = require('chai').expect;

View file

@ -17,11 +17,21 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
import { first } from 'rxjs/operators';
import { TypeOf } from '@kbn/config-schema';
import { PluginInitializerContext, RecursiveReadonly } from '../../../../src/core/server';
import {
CoreSetup,
PluginInitializerContext,
RecursiveReadonly,
} from '../../../../src/core/server';
import { deepFreeze } from '../../../../src/core/utils';
import { ConfigSchema } from './config';
import loadFunctions from './lib/load_functions';
import { functionsRoute } from './routes/functions';
import { validateEsRoute } from './routes/validate_es';
import { runRoute } from './routes/run';
import { ConfigManager } from './lib/config_manager';
/**
* Describes public Timelion plugin contract returned at the `setup` stage.
@ -36,12 +46,44 @@ export interface PluginSetupContract {
export class Plugin {
constructor(private readonly initializerContext: PluginInitializerContext) {}
public async setup(): Promise<RecursiveReadonly<PluginSetupContract>> {
public async setup(core: CoreSetup): Promise<RecursiveReadonly<PluginSetupContract>> {
const config = await this.initializerContext.config
.create<TypeOf<typeof ConfigSchema>>()
.pipe(first())
.toPromise();
const configManager = new ConfigManager(this.initializerContext.config);
const functions = loadFunctions('series_functions');
const getFunction = (name: string) => {
if (functions[name]) {
return functions[name];
}
throw new Error(
i18n.translate('timelion.noFunctionErrorMessage', {
defaultMessage: 'No such function: {name}',
values: { name },
})
);
};
const logger = this.initializerContext.logger.get('timelion');
const router = core.http.createRouter();
const deps = {
configManager,
functions,
getFunction,
logger,
};
functionsRoute(router, deps);
runRoute(router, deps);
validateEsRoute(router);
return deepFreeze({ uiEnabled: config.ui.enabled });
}

View file

@ -18,18 +18,22 @@
*/
import _ from 'lodash';
import { IRouter } from 'kibana/server';
import { LoadFunctions } from '../lib/load_functions';
export function functionsRoute(server) {
server.route({
method: 'GET',
path: '/api/timelion/functions',
handler: () => {
const functionArray = _.map(server.plugins.timelion.functions, function(val, key) {
export function functionsRoute(router: IRouter, { functions }: { functions: LoadFunctions }) {
router.get(
{
path: '/api/timelion/functions',
validate: false,
},
async (context, request, response) => {
const functionArray = _.map(functions, function(val, key) {
// TODO: This won't work on frozen objects, it should be removed when everything is converted to datasources and chainables
return _.extend({}, val, { name: key });
});
return _.sortBy(functionArray, 'name');
},
});
return response.ok({ body: _.sortBy(functionArray, 'name') });
}
);
}

View file

@ -0,0 +1,144 @@
/*
* 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 { IRouter, Logger } from 'kibana/server';
import { schema } from '@kbn/config-schema';
import Bluebird from 'bluebird';
import _ from 'lodash';
// @ts-ignore
import chainRunnerFn from '../handlers/chain_runner.js';
// @ts-ignore
import getNamespacesSettings from '../lib/get_namespaced_settings';
// @ts-ignore
import getTlConfig from '../handlers/lib/tl_config';
import { TimelionFunctionInterface } from '../types';
import { ConfigManager } from '../lib/config_manager';
const timelionDefaults = getNamespacesSettings();
export interface TimelionRequestQuery {
payload: {
sheet: string[];
extended?: {
es: {
filter: {
bool: {
filter: string[] | object;
must: string[];
should: string[];
must_not: string[];
};
};
};
};
};
time?: {
from?: string;
interval: string;
timezone: string;
to?: string;
};
}
export function runRoute(
router: IRouter,
{
logger,
getFunction,
configManager,
}: {
logger: Logger;
getFunction: (name: string) => TimelionFunctionInterface;
configManager: ConfigManager;
}
) {
router.post(
{
path: '/api/timelion/run',
validate: {
body: schema.object({
sheet: schema.arrayOf(schema.string()),
extended: schema.maybe(
schema.object({
es: schema.object({
filter: schema.object({
bool: schema.object({
filter: schema.maybe(
schema.arrayOf(schema.object({}, { allowUnknowns: true }))
),
must: schema.maybe(schema.arrayOf(schema.object({}, { allowUnknowns: true }))),
should: schema.maybe(
schema.arrayOf(schema.object({}, { allowUnknowns: true }))
),
must_not: schema.maybe(
schema.arrayOf(schema.object({}, { allowUnknowns: true }))
),
}),
}),
}),
})
),
time: schema.maybe(
schema.object({
from: schema.maybe(schema.string()),
interval: schema.string(),
timezone: schema.string(),
to: schema.maybe(schema.string()),
})
),
}),
},
},
router.handleLegacyErrors(async (context, request, response) => {
try {
const uiSettings = await context.core.uiSettings.client.getAll();
const tlConfig = getTlConfig({
request,
settings: _.defaults(uiSettings, timelionDefaults), // Just in case they delete some setting.
getFunction,
allowedGraphiteUrls: configManager.getGraphiteUrls(),
esShardTimeout: configManager.getEsShardTimeout(),
savedObjectsClient: context.core.savedObjects.client,
esDataClient: () => context.core.elasticsearch.dataClient,
});
const chainRunner = chainRunnerFn(tlConfig);
const sheet = await Bluebird.all(chainRunner.processRequest(request.body));
return response.ok({
body: {
sheet,
stats: chainRunner.getStats(),
},
});
} catch (err) {
logger.error(`${err.toString()}: ${err.stack}`);
// TODO Maybe we should just replace everywhere we throw with Boom? Probably.
if (err.isBoom) {
throw err;
} else {
return response.internalError({
body: {
message: err.toString(),
},
});
}
}
})
);
}

View file

@ -18,15 +18,18 @@
*/
import _ from 'lodash';
import { IRouter } from 'kibana/server';
export function validateEsRoute(server) {
server.route({
method: 'GET',
path: '/api/timelion/validate/es',
handler: async function(request) {
const uiSettings = await request.getUiSettingsService().getAll();
export function validateEsRoute(router: IRouter) {
router.get(
{
path: '/api/timelion/validate/es',
validate: false,
},
async function(context, request, response) {
const uiSettings = await context.core.uiSettings.client.getAll();
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const { callAsCurrentUser } = context.core.elasticsearch.dataClient;
const timefield = uiSettings['timelion:es.timefield'];
@ -51,24 +54,28 @@ export function validateEsRoute(server) {
let resp = {};
try {
resp = await callWithRequest(request, 'search', body);
resp = await callAsCurrentUser('search', body);
} catch (errResp) {
resp = errResp;
}
if (_.has(resp, 'aggregations.maxAgg.value') && _.has(resp, 'aggregations.minAgg.value')) {
return {
ok: true,
field: timefield,
min: _.get(resp, 'aggregations.minAgg.value'),
max: _.get(resp, 'aggregations.maxAgg.value'),
};
return response.ok({
body: {
ok: true,
field: timefield,
min: _.get(resp, 'aggregations.minAgg.value'),
max: _.get(resp, 'aggregations.maxAgg.value'),
},
});
}
return {
ok: false,
resp: resp,
};
},
});
return response.ok({
body: {
ok: false,
resp,
},
});
}
);
}

View file

@ -17,11 +17,11 @@
* under the License.
*/
const fn = require(`../abs`);
import fn from './abs';
import _ from 'lodash';
const expect = require('chai').expect;
const seriesList = require('./fixtures/seriesList.js')();
const seriesList = require('./fixtures/series_list.js')();
import invoke from './helpers/invoke_series_fn.js';
describe('abs.js', function() {

View file

@ -17,17 +17,16 @@
* under the License.
*/
const filename = require('path').basename(__filename);
const fn = require(`../aggregate/index.js`);
import fn from './index';
import _ from 'lodash';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
import invoke from '../helpers/invoke_series_fn.js';
describe(filename, () => {
describe('aggregate', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('../fixtures/series_list.js')();
});
it('first', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../bars`);
import fn from './bars';
import _ from 'lodash';
const expect = require('chai').expect;
@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js';
describe('bars.js', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
});
it('creates the bars property, with defaults, on all series', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../color`);
import fn from './color';
import _ from 'lodash';
const expect = require('chai').expect;
@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js';
describe('color.js', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
});
it('sets the color, on all series', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../condition`);
import fn from './condition';
import moment from 'moment';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';
@ -28,7 +28,7 @@ describe('condition.js', function() {
let comparable;
let seriesList;
beforeEach(function() {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
comparable = getSeriesList('', [
[moment.utc('1980-01-01T00:00:00.000Z'), 12],
[moment.utc('1981-01-01T00:00:00.000Z'), 33],

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../cusum`);
import fn from './cusum';
import _ from 'lodash';
const expect = require('chai').expect;
@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js';
describe('cusum.js', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
});
it('progressively adds the numbers in the list', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../derivative`);
import fn from './derivative';
import _ from 'lodash';
const expect = require('chai').expect;
@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js';
describe('derivative.js', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
});
it('gets the change in the set', () => {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../divide`);
import fn from './divide';
import _ from 'lodash';
const expect = require('chai').expect;
@ -26,7 +26,7 @@ import invoke from './helpers/invoke_series_fn.js';
describe('divide.js', () => {
let seriesList;
beforeEach(() => {
seriesList = require('./fixtures/seriesList.js')();
seriesList = require('./fixtures/series_list.js')();
});
it('divides by a single number', () => {

View file

@ -17,52 +17,38 @@
* under the License.
*/
const filename = require('path').basename(__filename);
import es from '../es';
import es from './index';
import tlConfigFn from './fixtures/tlConfig';
import * as aggResponse from '../es/lib/agg_response_to_series_list';
import buildRequest from '../es/lib/build_request';
import createDateAgg from '../es/lib/create_date_agg';
import esResponse from './fixtures/es_response';
import tlConfigFn from '../fixtures/tl_config';
import * as aggResponse from './lib/agg_response_to_series_list';
import buildRequest from './lib/build_request';
import createDateAgg from './lib/create_date_agg';
import esResponse from '../fixtures/es_response';
import Bluebird from 'bluebird';
import _ from 'lodash';
import { expect } from 'chai';
import sinon from 'sinon';
import invoke from './helpers/invoke_series_fn.js';
import invoke from '../helpers/invoke_series_fn.js';
function stubRequestAndServer(response, indexPatternSavedObjects = []) {
return {
server: {
plugins: {
elasticsearch: {
getCluster: sinon
.stub()
.withArgs('data')
.returns({
callWithRequest: function() {
return Bluebird.resolve(response);
},
}),
},
esDataClient: sinon.stub().returns({
callAsCurrentUser: function() {
return Bluebird.resolve(response);
},
},
request: {
getSavedObjectsClient: function() {
return {
find: function() {
return Bluebird.resolve({
saved_objects: indexPatternSavedObjects,
});
},
};
}),
savedObjectsClient: {
find: function() {
return Bluebird.resolve({
saved_objects: indexPatternSavedObjects,
});
},
},
};
}
describe(filename, () => {
describe('es', () => {
let tlConfig;
describe('seriesList processor', () => {

View file

@ -17,7 +17,6 @@
* under the License.
*/
import { first, map } from 'rxjs/operators';
import { i18n } from '@kbn/i18n';
import _ from 'lodash';
import Datasource from '../../lib/classes/datasource';
@ -109,7 +108,7 @@ export default new Datasource('es', {
fit: 'nearest',
});
const findResp = await tlConfig.request.getSavedObjectsClient().find({
const findResp = await tlConfig.savedObjectsClient.find({
type: 'index-pattern',
fields: ['title', 'fields'],
search: `"${config.index}"`,
@ -126,17 +125,12 @@ export default new Datasource('es', {
});
}
const esShardTimeout = await tlConfig.server.newPlatform.__internals.elasticsearch.legacy.config$
.pipe(
first(),
map(config => config.shardTimeout.asMilliseconds())
)
.toPromise();
const esShardTimeout = tlConfig.esShardTimeout;
const body = buildRequest(config, tlConfig, scriptedFields, esShardTimeout);
const { callWithRequest } = tlConfig.server.plugins.elasticsearch.getCluster('data');
const resp = await callWithRequest(tlConfig.request, 'search', body);
const { callAsCurrentUser: callWithRequest } = tlConfig.esDataClient();
const resp = await callWithRequest('search', body);
if (!resp._shards.total) {
throw new Error(
i18n.translate('timelion.serverSideErrors.esFunction.indexNotFoundErrorMessage', {

View file

@ -17,10 +17,10 @@
* under the License.
*/
const fn = require(`../first`);
import fn from './first';
const expect = require('chai').expect;
const seriesList = require('./fixtures/seriesList.js')();
const seriesList = require('./fixtures/series_list.js')();
import invoke from './helpers/invoke_series_fn.js';
describe('first.js', function() {

View file

@ -17,7 +17,7 @@
* under the License.
*/
const fn = require(`../fit`);
const fn = require(`src/plugins/timelion/server/series_functions/fit`);
import moment from 'moment';
const expect = require('chai').expect;
import invoke from './helpers/invoke_series_fn.js';

View file

@ -17,6 +17,8 @@
* under the License.
*/
/* eslint-disable quotes */
/*
Really didn't want to do this, but testing the agg flatten logic
in units isn't really possible since the functions depend on each other

View file

@ -17,7 +17,7 @@
* under the License.
*/
import buckets from './bucketList';
import buckets from './bucket_list';
import getSeries from '../helpers/get_series';
import getSeriesList from '../helpers/get_series_list';

View file

@ -18,48 +18,25 @@
*/
import moment from 'moment';
import { of } from 'rxjs';
import sinon from 'sinon';
import timelionDefaults from '../../../lib/get_namespaced_settings';
import timelionDefaults from '../../lib/get_namespaced_settings';
import esResponse from './es_response';
export default function() {
const functions = require('../../../lib/load_functions')('series_functions');
const kibanaServerConfigs = {
'timelion.graphiteUrls': ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'],
};
const server = {
plugins: {
timelion: {
getFunction: name => {
if (!functions[name]) throw new Error('No such function: ' + name);
return functions[name];
},
},
elasticsearch: {
getCluster: sinon
.stub()
.withArgs('data')
.returns({
callWithRequest: function() {
return Promise.resolve(esResponse);
},
}),
},
},
newPlatform: {
__internals: {
elasticsearch: {
legacy: { config$: of({ shardTimeout: moment.duration(30000) }) },
},
},
},
config: () => ({ get: key => kibanaServerConfigs[key] }),
};
const functions = require('../../lib/load_functions')('series_functions');
const tlConfig = require('../../../handlers/lib/tl_config.js')({
server,
request: {},
const tlConfig = require('../../handlers/lib/tl_config.js')({
getFunction: name => {
if (!functions[name]) throw new Error('No such function: ' + name);
return functions[name];
},
esDataClient: sinon.stub().returns({
callAsCurrentUser: function() {
return Promise.resolve(esResponse);
},
}),
esShardTimeout: moment.duration(30000),
allowedGraphiteUrls: ['https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite'],
});
tlConfig.time = {

View file

@ -46,7 +46,7 @@ export default new Datasource('graphite', {
min: moment(tlConfig.time.from).format('HH:mm[_]YYYYMMDD'),
max: moment(tlConfig.time.to).format('HH:mm[_]YYYYMMDD'),
};
const allowedUrls = tlConfig.server.config().get('timelion.graphiteUrls');
const allowedUrls = tlConfig.allowedGraphiteUrls;
const configuredUrl = tlConfig.settings['timelion:graphite.url'];
if (!allowedUrls.includes(configuredUrl)) {
throw new Error(

View file

@ -17,12 +17,12 @@
* under the License.
*/
import proxyquire from 'proxyquire';
import Bluebird from 'bluebird';
const expect = require('chai').expect;
const graphiteResponse = function() {
return Bluebird.resolve({
import fn from './graphite';
jest.mock('node-fetch', () => () => {
return Promise.resolve({
json: function() {
return [
{
@ -37,14 +37,11 @@ const graphiteResponse = function() {
];
},
});
};
const filename = require('path').basename(__filename);
const fn = proxyquire(`../${filename}`, { 'node-fetch': graphiteResponse });
});
import invoke from './helpers/invoke_series_fn.js';
describe(filename, function() {
describe('graphite', function() {
it('should wrap the graphite response up in a seriesList', function() {
return invoke(fn, []).then(function(result) {
expect(result.output.list[0].data[0][1]).to.eql(3);

Some files were not shown because too many files have changed in this diff Show more