[NP] Timelion (#44039) (#46766)

* Move index.js to index.ts

* Migrate Vis, interpreter, home and 2 hacks to setup() and start()

* Move Timechart hack to start()

* Add featureCatalogueRegistryProvider to an interface

* Add types to a server param

* Move some .js to .ts

* Add ExpressionFunction<> to interpreter

* Remove Feature Provider

* Remove extra export

* Add an interface to a timelion response

* Add an inteface to the panel

* Add IPrivate interface

* Make nit notes

* Edit uiCapabilities() type

* Shim Timelion plugin

* Shim start() plugin method

* Change InternalCoreStart to LegacyCoreStart

* Move Angular dependencies to a separate module

* Change visualizations import path due to recent changes

* Rename directives

* Take a common property out

* Get rid of require in schema

* Use core.uiSettings

* Refactor timelion request handler

* Remove Private from tests

* Remove redundant dependencies from tests

* Update visualizations paths

* Change expressions paths due to expessions movement

* Refactoring according to reviews

* Add a comment over the uiCapabilities field

* Edit the comment

* Ignore uiCapabilities issue

* Take angular controller out

* Get rid of

* Get rid of config

* Get rid of config in start

* Unwrap handler from redundant promise

* Move npSetup npStart dependencies in a high level

* Rename some details

* Fix reviews

* fix CI

* Take visFactory out
This commit is contained in:
Artyom Gospodarsky 2019-09-27 15:22:12 +03:00 committed by GitHub
parent 48ebe3d8c3
commit e20fdcb74c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 689 additions and 393 deletions

View file

@ -19,28 +19,32 @@
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 { plugin } from './server';
import { CustomCoreSetup } from './server/plugin';
const experimentalLabel = i18n.translate('timelion.uiSettings.experimentalLabel', {
defaultMessage: 'experimental',
});
export default function (kibana) {
return new kibana.Plugin({
const timelionPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
new Plugin({
require: ['kibana', 'elasticsearch'],
config(Joi) {
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
ui: Joi.object({
enabled: Joi.boolean().default(false),
}).default(),
graphiteUrls: Joi.array().items(
Joi.string().uri({ scheme: ['http', 'https'] }),
).default([]),
graphiteUrls: Joi.array()
.items(Joi.string().uri({ scheme: ['http', 'https'] }))
.default([]),
}).default();
},
// @ts-ignore
// https://github.com/elastic/kibana/pull/44039#discussion_r326582255
uiCapabilities() {
return {
timelion: {
@ -48,7 +52,7 @@ export default function (kibana) {
},
};
},
publicDir: resolve(__dirname, 'public'),
uiExports: {
app: {
title: 'Timelion',
@ -58,11 +62,7 @@ export default function (kibana) {
main: 'plugins/timelion/app',
},
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [
'plugins/timelion/hacks/toggle_app_link_in_nav',
'plugins/timelion/lib/panel_registry',
'plugins/timelion/panels/timechart/timechart',
],
hacks: [resolve(__dirname, 'public/legacy')],
injectDefaultVars(server) {
const config = server.config();
@ -71,13 +71,6 @@ export default function (kibana) {
kbnIndex: config.get('kibana.index'),
};
},
visTypes: [
'plugins/timelion/vis',
],
interpreter: ['plugins/timelion/timelion_vis_fn'],
home: [
'plugins/timelion/register_feature',
],
mappings: require('./mappings.json'),
uiSettingDefaults: {
'timelion:showTutorial': {
@ -159,17 +152,19 @@ export default function (kibana) {
value: '1ms',
description: i18n.translate('timelion.uiSettings.minimumIntervalDescription', {
defaultMessage: 'The smallest interval that will be calculated when using "auto"',
description: '"auto" is a technical value in that context, that should not be translated.',
description:
'"auto" is a technical value in that context, that should not be translated.',
}),
category: ['timelion'],
},
'timelion:graphite.url': {
name: i18n.translate('timelion.uiSettings.graphiteURLLabel', {
defaultMessage: 'Graphite URL',
description: 'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
description:
'The URL should be in the form of https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite',
}),
value: (server) => {
const urls = server.config().get('timelion.graphiteUrls');
value: (server: Legacy.Server) => {
const urls = server.config().get('timelion.graphiteUrls') as string[];
if (urls.length === 0) {
return null;
} else {
@ -177,11 +172,12 @@ export default function (kibana) {
}
},
description: i18n.translate('timelion.uiSettings.graphiteURLDescription', {
defaultMessage: '{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
defaultMessage:
'{experimentalLabel} The <a href="https://www.hostedgraphite.com/UID/ACCESS_KEY/graphite" target="_blank" rel="noopener">URL</a> of your graphite host',
values: { experimentalLabel: `<em>[${experimentalLabel}]</em>` },
}),
type: 'select',
options: (server) => (server.config().get('timelion.graphiteUrls')),
options: (server: Legacy.Server) => server.config().get('timelion.graphiteUrls'),
category: ['timelion'],
},
'timelion:quandl.key': {
@ -197,11 +193,13 @@ export default function (kibana) {
},
},
},
init: (server) => {
const initializerContext = {};
const core = { http: { server } };
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
export default timelionPluginInitializer;

View file

@ -18,48 +18,53 @@
*/
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { generateTicksProvider } from '../panels/timechart/tick_generator';
describe('Tick Generator', function () {
let generateTicks;
const axes = [
{
min: 0,
max: 5000,
delta: 100
},
{
min: 0,
max: 50000,
delta: 2000
},
{
min: 4096,
max: 6000,
delta: 250
}
];
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
generateTicks = Private(require('plugins/timelion/panels/timechart/tick_generator'));
}));
it('returns a function', function () {
expect(generateTicks).to.be.a('function');
beforeEach(function () {
generateTicks = generateTicksProvider();
});
axes.forEach(axis => {
it(`generates ticks from ${axis.min} to ${axis.max}`, function () {
const ticks = generateTicks(axis);
let n = 1;
while (Math.pow(2, n) < axis.delta) n++;
const expectedDelta = Math.pow(2, n);
const expectedNr = parseInt((axis.max - axis.min) / expectedDelta) + 2;
expect(ticks instanceof Array).to.be(true);
expect(ticks.length).to.be(expectedNr);
expect(ticks[0]).to.equal(axis.min);
expect(ticks[parseInt(ticks.length / 2)]).to.equal(axis.min + expectedDelta * parseInt(ticks.length / 2));
expect(ticks[ticks.length - 1]).to.equal(axis.min + expectedDelta * (ticks.length - 1));
describe('generateTicksProvider()', function () {
it('should return a function', function () {
expect(generateTicks).to.be.a('function');
});
});
describe('generateTicks()', function () {
const axes = [
{
min: 0,
max: 5000,
delta: 100
},
{
min: 0,
max: 50000,
delta: 2000
},
{
min: 4096,
max: 6000,
delta: 250
}
];
axes.forEach(axis => {
it(`generates ticks from ${axis.min} to ${axis.max}`, function () {
const ticks = generateTicks(axis);
let n = 1;
while (Math.pow(2, n) < axis.delta) n++;
const expectedDelta = Math.pow(2, n);
const expectedNr = parseInt((axis.max - axis.min) / expectedDelta) + 2;
expect(ticks instanceof Array).to.be(true);
expect(ticks.length).to.be(expectedNr);
expect(ticks[0]).to.equal(axis.min);
expect(ticks[parseInt(ticks.length / 2)]).to.equal(axis.min + expectedDelta * parseInt(ticks.length / 2));
expect(ticks[ticks.length - 1]).to.equal(axis.min + expectedDelta * (ticks.length - 1));
});
});
});
});

View file

@ -18,20 +18,19 @@
*/
import expect from '@kbn/expect';
import ngMock from 'ng_mock';
import { tickFormatters } from '../../services/tick_formatters';
describe('Tick Formatters', function () {
let formatters;
let tickFormatters;
beforeEach(ngMock.module('kibana'));
beforeEach(ngMock.inject(function (Private) {
tickFormatters = Private(require('plugins/timelion/services/tick_formatters'));
}));
beforeEach(function () {
formatters = tickFormatters();
});
describe('Bits mode', function () {
let bitFormatter;
beforeEach(function () {
bitFormatter = tickFormatters.bits;
bitFormatter = formatters.bits;
});
it('is a function', function () {
@ -56,7 +55,7 @@ describe('Tick Formatters', function () {
describe('Bits/s mode', function () {
let bitsFormatter;
beforeEach(function () {
bitsFormatter = tickFormatters['bits/s'];
bitsFormatter = formatters['bits/s'];
});
it('is a function', function () {
@ -81,7 +80,7 @@ describe('Tick Formatters', function () {
describe('Bytes mode', function () {
let byteFormatter;
beforeEach(function () {
byteFormatter = tickFormatters.bytes;
byteFormatter = formatters.bytes;
});
it('is a function', function () {
@ -106,7 +105,7 @@ describe('Tick Formatters', function () {
describe('Bytes/s mode', function () {
let bytesFormatter;
beforeEach(function () {
bytesFormatter = tickFormatters['bytes/s'];
bytesFormatter = formatters['bytes/s'];
});
it('is a function', function () {
@ -131,7 +130,7 @@ describe('Tick Formatters', function () {
describe('Currency mode', function () {
let currencyFormatter;
beforeEach(function () {
currencyFormatter = tickFormatters.currency;
currencyFormatter = formatters.currency;
});
it('is a function', function () {
@ -163,7 +162,7 @@ describe('Tick Formatters', function () {
describe('Percent mode', function () {
let percentFormatter;
beforeEach(function () {
percentFormatter = tickFormatters.percent;
percentFormatter = formatters.percent;
});
it('is a function', function () {
@ -197,7 +196,7 @@ describe('Tick Formatters', function () {
describe('Custom mode', function () {
let customFormatter;
beforeEach(function () {
customFormatter = tickFormatters.custom;
customFormatter = formatters.custom;
});
it('is a function', function () {

View file

@ -17,55 +17,52 @@
* under the License.
*/
import panelRegistryProvider from '../../lib/panel_registry';
import { i18n } from '@kbn/i18n';
require('ui/modules')
.get('apps/timelion', [])
.directive('chart', function (Private) {
return {
restrict: 'A',
scope: {
seriesList: '=chart', // The flot object, data, config and all
search: '=', // The function to execute to kick off a search
interval: '=', // Required for formatting x-axis ticks
rerenderTrigger: '=',
},
link: function ($scope, $elem) {
export function Chart(Private) {
return {
restrict: 'A',
scope: {
seriesList: '=chart', // The flot object, data, config and all
search: '=', // The function to execute to kick off a search
interval: '=', // Required for formatting x-axis ticks
rerenderTrigger: '=',
},
link: function ($scope, $elem) {
const panelRegistry = Private(panelRegistryProvider);
let panelScope = $scope.$new(true);
const panelRegistry = Private(panelRegistryProvider);
let panelScope = $scope.$new(true);
function render() {
panelScope.$destroy();
function render() {
panelScope.$destroy();
if (!$scope.seriesList) return;
if (!$scope.seriesList) return;
$scope.seriesList.render = $scope.seriesList.render || {
type: 'timechart'
};
$scope.seriesList.render = $scope.seriesList.render || {
type: 'timechart'
};
const panelSchema = panelRegistry.byName[$scope.seriesList.render.type];
const panelSchema = panelRegistry.byName[$scope.seriesList.render.type];
if (!panelSchema) {
$elem.text(
i18n.translate('timelion.chart.seriesList.noSchemaWarning', {
defaultMessage: 'No such panel type: {renderType}',
values: { renderType: $scope.seriesList.render.type },
})
);
return;
}
panelScope = $scope.$new(true);
panelScope.seriesList = $scope.seriesList;
panelScope.interval = $scope.interval;
panelScope.search = $scope.search;
panelSchema.render(panelScope, $elem);
if (!panelSchema) {
$elem.text(
i18n.translate('timelion.chart.seriesList.noSchemaWarning', {
defaultMessage: 'No such panel type: {renderType}',
values: { renderType: $scope.seriesList.render.type },
})
);
return;
}
$scope.$watchGroup(['seriesList', 'rerenderTrigger'], render);
panelScope = $scope.$new(true);
panelScope.seriesList = $scope.seriesList;
panelScope.interval = $scope.interval;
panelScope.search = $scope.search;
panelSchema.render(panelScope, $elem);
}
};
});
$scope.$watchGroup(['seriesList', 'rerenderTrigger'], render);
}
};
}

View file

@ -43,9 +43,7 @@
import _ from 'lodash';
import $ from 'jquery';
import PEG from 'pegjs';
import grammar from 'raw-loader!../chain.peg';
import './timelion_expression_suggestions/timelion_expression_suggestions';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,
@ -57,9 +55,8 @@ import { comboBoxKeyCodes } from '@elastic/eui';
import { ArgValueSuggestionsProvider } from './timelion_expression_suggestions/arg_value_suggestions';
const Parser = PEG.generate(grammar);
const app = require('ui/modules').get('apps/timelion', []);
app.directive('timelionExpressionInput', function ($http, $timeout, Private) {
export function TimelionExpInput($http, $timeout, Private) {
return {
restrict: 'E',
scope: {
@ -276,4 +273,4 @@ app.directive('timelionExpressionInput', function ($http, $timeout, Private) {
init();
}
};
});
}

View file

@ -19,9 +19,7 @@
import template from './timelion_expression_suggestions.html';
const app = require('ui/modules').get('apps/timelion', []);
app.directive('timelionExpressionSuggestions', () => {
export function TimelionExpressionSuggestions() {
return {
restrict: 'E',
scope: {
@ -38,4 +36,4 @@ app.directive('timelionExpressionSuggestions', () => {
scope.onMouseDown = e => e.preventDefault();
}
};
});
}

View file

@ -19,11 +19,9 @@
import _ from 'lodash';
import $ from 'jquery';
const app = require('ui/modules').get('apps/timelion', []);
import template from './timelion_interval.html';
app.directive('timelionInterval', function ($timeout) {
export function TimelionInterval($timeout) {
return {
restrict: 'E',
scope: {
@ -81,4 +79,5 @@ app.directive('timelionInterval', function ($timeout) {
});
}
};
});
}

View file

@ -17,9 +17,9 @@
* under the License.
*/
import { npStart } from 'ui/new_platform';
import { PluginInitializerContext } from 'kibana/public';
import { TimelionPlugin as Plugin } from './plugin';
const timelionUiEnabled = npStart.core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
npStart.core.chrome.navLinks.update('timelion', { hidden: true });
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
}

View file

@ -0,0 +1,47 @@
/*
* 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 { PluginInitializerContext } from 'kibana/public';
import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from '.';
import { setup as visualizations } from '../../visualizations/public/legacy';
import { TimelionPluginSetupDependencies, TimelionPluginStartDependencies } from './plugin';
// @ts-ignore
import panelRegistry from './lib/panel_registry';
import { LegacyDependenciesPlugin } from './shim';
// Temporary solution
// It will be removed when all dependent services are migrated to the new platform.
const __LEGACY = new LegacyDependenciesPlugin();
const setupPlugins: Readonly<TimelionPluginSetupDependencies> = {
visualizations,
expressions: npSetup.plugins.expressions,
__LEGACY,
};
const startPlugins: Readonly<TimelionPluginStartDependencies> = {
panelRegistry,
__LEGACY,
};
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
export const start = pluginInstance.start(npStart.core, startPlugins);

View file

@ -0,0 +1,45 @@
/*
* 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 { i18n } from '@kbn/i18n';
interface PanelConfig {
help?: string;
render?: Function;
}
export class Panel {
name: string;
help: string;
render: Function | undefined;
constructor(name: string, config: PanelConfig) {
this.name = name;
this.help = config.help || '';
this.render = config.render;
if (!config.render) {
throw new Error(
i18n.translate('timelion.panels.noRenderFunctionErrorMessage', {
defaultMessage: 'Panel must have a rendering function',
})
);
}
}
}

View file

@ -17,29 +17,36 @@
* under the License.
*/
require('./flot');
import './flot';
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment-timezone';
import observeResize from '../../lib/observe_resize';
import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../common/lib';
import { timefilter } from 'ui/timefilter';
// @ts-ignore
import observeResize from '../../lib/observe_resize';
// @ts-ignore
import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../common/lib';
import { TimelionStartDependencies } from '../../plugin';
import { tickFormatters } from '../../services/tick_formatters';
import { xaxisFormatterProvider } from './xaxis_formatter';
import { generateTicksProvider } from './tick_generator';
const DEBOUNCE_DELAY = 50;
export default function timechartFn(Private, config, $rootScope, $compile) {
return function () {
export function timechartFn(dependencies: TimelionStartDependencies) {
const { $rootScope, $compile, uiSettings } = dependencies;
return function() {
return {
help: 'Draw a timeseries chart',
render: function ($scope, $elem) {
render($scope: any, $elem: any) {
const template = '<div class="chart-top-title"></div><div class="chart-canvas"></div>';
const tickFormatters = require('plugins/timelion/services/tick_formatters')();
const getxAxisFormatter = Private(require('plugins/timelion/panels/timechart/xaxis_formatter'));
const generateTicks = Private(require('plugins/timelion/panels/timechart/tick_generator'));
const formatters = tickFormatters() as any;
const getxAxisFormatter = xaxisFormatterProvider(uiSettings);
const generateTicks = generateTicksProvider();
// TODO: I wonder if we should supply our own moment that sets this every time?
// could just use angular's injection to provide a moment service?
moment.tz.setDefault(config.get('dateFormat:tz'));
moment.tz.setDefault(uiSettings.get('dateFormat:tz'));
const render = $scope.seriesList.render || {};
@ -47,12 +54,12 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
$scope.interval = $scope.interval;
$scope.search = $scope.search || _.noop;
let legendValueNumbers;
let legendCaption;
let legendValueNumbers: any;
let legendCaption: any;
const debouncedSetLegendNumbers = _.debounce(setLegendNumbers, DEBOUNCE_DELAY, {
maxWait: DEBOUNCE_DELAY,
leading: true,
trailing: false
trailing: false,
});
// ensure legend is the same height with or without a caption so legend items do not move around
const emptyCaption = '<br>';
@ -65,12 +72,12 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
},
selection: {
mode: 'x',
color: '#ccc'
color: '#ccc',
},
crosshair: {
mode: 'x',
color: '#C66',
lineWidth: 2
lineWidth: 2,
},
grid: {
show: render.grid,
@ -78,13 +85,13 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
borderColor: null,
margin: 10,
hoverable: true,
autoHighlight: false
autoHighlight: false,
},
legend: {
backgroundColor: 'rgb(255,255,255,0)',
position: 'nw',
labelBoxBorderColor: 'rgb(255,255,255,0)',
labelFormatter: function (label, series) {
labelFormatter(label: any, series: any) {
const wrapperSpan = document.createElement('span');
const labelSpan = document.createElement('span');
const numberSpan = document.createElement('span');
@ -103,13 +110,24 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
wrapperSpan.appendChild(numberSpan);
return wrapperSpan.outerHTML;
}
},
},
colors: ['#01A4A4', '#C66', '#D0D102', '#616161', '#00A1CB', '#32742C', '#F18D05', '#113F8C', '#61AE24', '#D70060']
colors: [
'#01A4A4',
'#C66',
'#D0D102',
'#616161',
'#00A1CB',
'#32742C',
'#F18D05',
'#113F8C',
'#61AE24',
'#D70060',
],
};
const originalColorMap = new Map();
$scope.chart.forEach((series, seriesIndex) => {
$scope.chart.forEach((series: any, seriesIndex: any) => {
if (!series.color) {
const colorIndex = seriesIndex % defaultOptions.colors.length;
series.color = defaultOptions.colors[colorIndex];
@ -117,8 +135,8 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
originalColorMap.set(series, series.color);
});
let highlightedSeries;
let focusedSeries;
let highlightedSeries: any;
let focusedSeries: any;
function unhighlightSeries() {
if (highlightedSeries === null) {
return;
@ -126,18 +144,18 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
highlightedSeries = null;
focusedSeries = null;
$scope.chart.forEach((series) => {
$scope.chart.forEach((series: any) => {
series.color = originalColorMap.get(series); // reset the colors
});
drawPlot($scope.chart);
}
$scope.highlightSeries = _.debounce(function (id) {
$scope.highlightSeries = _.debounce(function(id: any) {
if (highlightedSeries === id) {
return;
}
highlightedSeries = id;
$scope.chart.forEach((series, seriesIndex) => {
$scope.chart.forEach((series: any, seriesIndex: any) => {
if (seriesIndex !== id) {
series.color = 'rgba(128,128,128,0.1)'; // mark as grey
} else {
@ -146,58 +164,57 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
});
drawPlot($scope.chart);
}, DEBOUNCE_DELAY);
$scope.focusSeries = function (id) {
$scope.focusSeries = function(id: any) {
focusedSeries = id;
$scope.highlightSeries(id);
};
$scope.toggleSeries = function (id) {
$scope.toggleSeries = function(id: any) {
const series = $scope.chart[id];
series._hide = !series._hide;
drawPlot($scope.chart);
};
const cancelResize = observeResize($elem, function () {
const cancelResize = observeResize($elem, function() {
drawPlot($scope.chart);
});
$scope.$on('$destroy', function () {
$scope.$on('$destroy', function() {
cancelResize();
$elem.off('plothover');
$elem.off('plotselected');
$elem.off('mouseleave');
});
$elem.on('plothover', function (event, pos, item) {
$elem.on('plothover', function(event: any, pos: any, item: any) {
$rootScope.$broadcast('timelionPlotHover', event, pos, item);
});
$elem.on('plotselected', function (event, ranges) {
$elem.on('plotselected', function(event: any, ranges: any) {
timefilter.setTime({
from: moment(ranges.xaxis.from),
to: moment(ranges.xaxis.to),
mode: 'absolute',
});
});
$elem.on('mouseleave', function () {
$elem.on('mouseleave', function() {
$rootScope.$broadcast('timelionPlotLeave');
});
$scope.$on('timelionPlotHover', function (angularEvent, flotEvent, pos) {
$scope.$on('timelionPlotHover', function(angularEvent: any, flotEvent: any, pos: any) {
if (!$scope.plot) return;
$scope.plot.setCrosshair(pos);
debouncedSetLegendNumbers(pos);
});
$scope.$on('timelionPlotLeave', function () {
$scope.$on('timelionPlotLeave', function() {
if (!$scope.plot) return;
$scope.plot.clearCrosshair();
clearLegendNumbers();
});
// Shamelessly borrowed from the flotCrosshairs example
function setLegendNumbers(pos) {
function setLegendNumbers(pos: any) {
unhighlightSeries();
const plot = $scope.plot;
@ -211,10 +228,13 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
let j;
const dataset = plot.getData();
if (legendCaption) {
legendCaption.text(moment(pos.x).format(_.get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT)));
legendCaption.text(
moment(pos.x).format(
_.get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT)
)
);
}
for (i = 0; i < dataset.length; ++i) {
const series = dataset[i];
const precision = _.get(series, '_meta.precision', 2);
@ -248,13 +268,13 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
if (legendCaption) {
legendCaption.html(emptyCaption);
}
_.each(legendValueNumbers, function (num) {
_.each(legendValueNumbers, function(num) {
$(num).empty();
});
}
let legendScope = $scope.$new();
function drawPlot(plotConfig) {
function drawPlot(plotConfig: any) {
if (!$('.chart-canvas', $elem).length) $elem.html(template);
const canvasElem = $('.chart-canvas', $elem);
@ -264,56 +284,63 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
return;
}
const title = _(plotConfig).map('_title').compact().last();
const title = _(plotConfig)
.map('_title')
.compact()
.last() as any;
$('.chart-top-title', $elem).text(title == null ? '' : title);
const options = _.cloneDeep(defaultOptions);
const options = _.cloneDeep(defaultOptions) as any;
// Get the X-axis tick format
const time = timefilter.getBounds();
const time = timefilter.getBounds() as any;
const interval = calculateInterval(
time.min.valueOf(),
time.max.valueOf(),
config.get('timelion:target_buckets') || 200,
uiSettings.get('timelion:target_buckets') || 200,
$scope.interval,
config.get('timelion:min_interval') || '1ms',
uiSettings.get('timelion:min_interval') || '1ms'
);
const format = getxAxisFormatter(interval);
// Use moment to format ticks so we get timezone correction
options.xaxis.tickFormatter = function (val) {
options.xaxis.tickFormatter = function(val: any) {
return moment(val).format(format);
};
// Calculate how many ticks can fit on the axis
const tickLetterWidth = 7;
const tickPadding = 45;
options.xaxis.ticks = Math.floor($elem.width() / ((format.length * tickLetterWidth) + tickPadding));
options.xaxis.ticks = Math.floor(
$elem.width() / (format.length * tickLetterWidth + tickPadding)
);
const series = _.map(plotConfig, function (series, index) {
series = _.cloneDeep(_.defaults(series, {
shadowSize: 0,
lines: {
lineWidth: 3
}
}));
series._id = index;
const series = _.map(plotConfig, function(serie: any, index) {
serie = _.cloneDeep(
_.defaults(serie, {
shadowSize: 0,
lines: {
lineWidth: 3,
},
})
);
serie._id = index;
if (series.color) {
if (serie.color) {
const span = document.createElement('span');
span.style.color = series.color;
series.color = span.style.color;
span.style.color = serie.color;
serie.color = span.style.color;
}
if (series._hide) {
series.data = [];
series.stack = false;
//series.color = "#ddd";
series.label = '(hidden) ' + series.label;
if (serie._hide) {
serie.data = [];
serie.stack = false;
// serie.color = "#ddd";
serie.label = '(hidden) ' + serie.label;
}
if (series._global) {
_.merge(options, series._global, function (objVal, srcVal) {
if (serie._global) {
_.merge(options, serie._global, function(objVal, srcVal) {
// This is kind of gross, it means that you can't replace a global value with a null
// best you can do is an empty string. Deal with it.
if (objVal == null) return srcVal;
@ -321,13 +348,13 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
});
}
return series;
return serie;
});
if (options.yaxes) {
options.yaxes.forEach(yaxis => {
options.yaxes.forEach((yaxis: any) => {
if (yaxis && yaxis.units) {
yaxis.tickFormatter = tickFormatters[yaxis.units.type];
yaxis.tickFormatter = formatters[yaxis.units.type];
const byteModes = ['bytes', 'bytes/s'];
if (byteModes.includes(yaxis.units.type)) {
yaxis.tickGenerator = generateTicks;
@ -336,6 +363,7 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
});
}
// @ts-ignore
$scope.plot = $.plot(canvasElem, _.compact(series), options);
if ($scope.plot) {
@ -346,7 +374,7 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
legendScope = $scope.$new();
// Used to toggle the series, and for displaying values on hover
legendValueNumbers = canvasElem.find('.ngLegendValueNumber');
_.each(canvasElem.find('.ngLegendValue'), function (elem) {
_.each(canvasElem.find('.ngLegendValue'), function(elem) {
$compile(elem)(legendScope);
});
@ -363,7 +391,7 @@ export default function timechartFn(Private, config, $rootScope, $compile) {
}
}
$scope.$watch('chart', drawPlot);
}
},
};
};
}

View file

@ -17,13 +17,12 @@
* under the License.
*/
export default function generateTicksProvider() {
function floorInBase(n, base) {
export function generateTicksProvider() {
function floorInBase(n: any, base: any) {
return base * Math.floor(n / base);
}
function generateTicks(axis) {
function generateTicks(axis: any) {
const returnTicks = [];
let tickSize = 2;
let delta = axis.delta;
@ -31,13 +30,13 @@ export default function generateTicksProvider() {
let tickVal;
let tickCount = 0;
//Count the steps
// Count the steps
while (Math.abs(delta) >= 1024) {
steps++;
delta /= 1024;
}
//Set the tick size relative to the remaining delta
// Set the tick size relative to the remaining delta
while (tickSize <= 1024) {
if (delta <= tickSize) {
break;
@ -46,17 +45,17 @@ export default function generateTicksProvider() {
}
axis.tickSize = tickSize * Math.pow(1024, steps);
//Calculate the new ticks
// Calculate the new ticks
const tickMin = floorInBase(axis.min, axis.tickSize);
do {
tickVal = tickMin + (tickCount++) * axis.tickSize;
tickVal = tickMin + tickCount++ * axis.tickSize;
returnTicks.push(tickVal);
} while (tickVal < axis.max);
return returnTicks;
}
return function (axis) {
return function(axis: any) {
return generateTicks(axis);
};
}

View file

@ -17,12 +17,12 @@
* under the License.
*/
import Panel from '../panel';
import { i18n } from '@kbn/i18n';
import panelRegistry from '../../lib/panel_registry';
import { timechartFn } from './schema';
import { Panel } from '../panel';
import { TimelionStartDependencies } from '../../plugin';
panelRegistry.register(function timeChartProvider(Private) {
export function getTimeChart(dependencies: TimelionStartDependencies) {
// Schema is broken out so that it may be extended for use in other plugins
// Its also easier to test.
return new Panel('timechart', Private(require('./schema'))(), i18n);
});
return new Panel('timechart', timechartFn(dependencies)());
}

View file

@ -21,13 +21,12 @@ import moment from 'moment';
import { i18n } from '@kbn/i18n';
export default function xaxisFormatterProvider(config) {
function getFormat(esInterval) {
export function xaxisFormatterProvider(config: any) {
function getFormat(esInterval: any) {
const parts = esInterval.match(/(\d+)(ms|s|m|h|d|w|M|y|)/);
if (parts == null || parts[1] == null || parts[2] == null) {
throw new Error (
throw new Error(
i18n.translate('timelion.panels.timechart.unknownIntervalErrorMessage', {
defaultMessage: 'Unknown interval',
})
@ -49,7 +48,7 @@ export default function xaxisFormatterProvider(config) {
return config.get('dateFormat');
}
return function (esInterval) {
return function(esInterval: any) {
return getFormat(esInterval);
};
}

View file

@ -0,0 +1,100 @@
/*
* 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 {
CoreSetup,
CoreStart,
LegacyCoreStart,
Plugin,
PluginInitializerContext,
UiSettingsClientContract,
HttpSetup,
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { VisualizationsSetup } from '../../visualizations/public/np_ready';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisualization } from './vis';
import { getTimeChart } from './panels/timechart/timechart';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginStart } from './shim';
/** @internal */
export interface TimelionPluginSetupDependencies {
expressions: ReturnType<ExpressionsPlugin['setup']>;
visualizations: VisualizationsSetup;
// Temporary solution
__LEGACY: LegacyDependenciesPlugin;
}
/** @internal */
export interface TimelionPluginStartDependencies {
panelRegistry: any;
// Temporary solution
__LEGACY: LegacyDependenciesPlugin;
}
/** @private */
export interface TimelionVisualizationDependencies {
uiSettings: UiSettingsClientContract;
http: HttpSetup;
}
/** @internal */
export type TimelionStartDependencies = Pick<TimelionVisualizationDependencies, 'uiSettings'> &
LegacyDependenciesPluginStart;
/** @internal */
export class TimelionPlugin
implements
Plugin<Promise<void>, void, TimelionPluginSetupDependencies, TimelionPluginStartDependencies> {
initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
this.initializerContext = initializerContext;
}
public async setup(core: CoreSetup, plugins: TimelionPluginSetupDependencies) {
const dependencies: TimelionVisualizationDependencies = {
uiSettings: core.uiSettings,
http: core.http,
};
plugins.__LEGACY.setup();
plugins.expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies));
plugins.visualizations.types.registerVisualization(() =>
getTimelionVisualization(dependencies)
);
}
public async start(core: CoreStart & LegacyCoreStart, plugins: TimelionPluginStartDependencies) {
const dependencies: TimelionStartDependencies = {
uiSettings: core.uiSettings,
...(await plugins.__LEGACY.start()),
};
const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
core.chrome.navLinks.update('timelion', { hidden: true });
}
plugins.panelRegistry.register(() => getTimeChart(dependencies));
}
public stop(): void {}
}

View file

@ -17,14 +17,10 @@
* under the License.
*/
import {
FeatureCatalogueRegistryProvider,
FeatureCatalogueCategory,
} from 'ui/registry/feature_catalogue';
import { FeatureCatalogueCategory } from 'ui/registry/feature_catalogue';
import { i18n } from '@kbn/i18n';
FeatureCatalogueRegistryProvider.register(() => {
export const registerFeature = () => {
return {
id: 'timelion',
title: 'Timelion',
@ -37,4 +33,4 @@ FeatureCatalogueRegistryProvider.register(() => {
showOnHomePage: false,
category: FeatureCatalogueCategory.DATA,
};
});
};

View file

@ -19,7 +19,7 @@
import _ from 'lodash';
function baseTickFormatter(value, axis) {
function baseTickFormatter(value: any, axis: any) {
const factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
const formatted = '' + Math.round(value * factor) / factor;
@ -30,15 +30,18 @@ function baseTickFormatter(value, axis) {
const decimal = formatted.indexOf('.');
const precision = decimal === -1 ? 0 : formatted.length - decimal - 1;
if (precision < axis.tickDecimals) {
return (precision ? formatted : formatted + '.') + ('' + factor).substr(1, axis.tickDecimals - precision);
return (
(precision ? formatted : formatted + '.') +
('' + factor).substr(1, axis.tickDecimals - precision)
);
}
}
return formatted;
}
function unitFormatter(divisor, units) {
return (val) => {
function unitFormatter(divisor: any, units: any) {
return (val: any) => {
let index = 0;
const isNegative = val < 0;
val = Math.abs(val);
@ -46,22 +49,26 @@ function unitFormatter(divisor, units) {
val /= divisor;
index++;
}
const value = Math.round(val * 100) / 100 * (isNegative ? -1 : 1);
const value = (Math.round(val * 100) / 100) * (isNegative ? -1 : 1);
return `${value}${units[index]}`;
};
}
export default function tickFormatters() {
const formatters = {
'bits': unitFormatter(1000, ['b', 'kb', 'mb', 'gb', 'tb', 'pb']),
export function tickFormatters() {
const formatters = {
bits: unitFormatter(1000, ['b', 'kb', 'mb', 'gb', 'tb', 'pb']),
'bits/s': unitFormatter(1000, ['b/s', 'kb/s', 'mb/s', 'gb/s', 'tb/s', 'pb/s']),
'bytes': unitFormatter(1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB']),
bytes: unitFormatter(1024, ['B', 'KB', 'MB', 'GB', 'TB', 'PB']),
'bytes/s': unitFormatter(1024, ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']),
'currency': function (val, axis) {
return val.toLocaleString('en', { style: 'currency', currency: axis.options.units.prefix || 'USD' });
currency(val: any, axis: any) {
return val.toLocaleString('en', {
style: 'currency',
currency: axis.options.units.prefix || 'USD',
});
},
'percent': function (val, axis) {
let precision = _.get(axis, 'tickDecimals', 0) - _.get(axis, 'options.units.tickDecimalsShift', 0);
percent(val: any, axis: any) {
let precision =
_.get(axis, 'tickDecimals', 0) - _.get(axis, 'options.units.tickDecimalsShift', 0);
// toFixed only accepts values between 0 and 20
if (precision < 0) {
precision = 0;
@ -71,12 +78,12 @@ export default function tickFormatters() {
return (val * 100).toFixed(precision) + '%';
},
'custom': function (val, axis) {
custom(val: any, axis: any) {
const formattedVal = baseTickFormatter(val, axis);
const prefix = axis.options.units.prefix;
const suffix = axis.options.units.suffix;
return prefix + formattedVal + suffix;
}
},
};
return formatters;

View file

@ -17,22 +17,4 @@
* under the License.
*/
import { i18n } from '@kbn/i18n';
export default function Panel(name, config) {
this.name = name;
this.help = config.help || '';
this.render = config.render;
if (!config.render) {
throw new Error (
i18n.translate('timelion.panels.noRenderFunctionErrorMessage', {
defaultMessage: 'Panel must have a rendering function'
})
);
}
}
export * from './legacy_dependencies_plugin';

View file

@ -17,18 +17,28 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { Plugin } from 'kibana/public';
import { initTimelionLegacyModule } from './timelion_legacy_module';
import '../directives/chart/chart';
import '../directives/timelion_interval/timelion_interval';
import 'ui/state_management/app_state';
/** @internal */
export interface LegacyDependenciesPluginStart {
$rootScope: any;
$compile: any;
}
import { uiModules } from 'ui/modules';
export class LegacyDependenciesPlugin
implements Plugin<void, Promise<LegacyDependenciesPluginStart>> {
public setup() {
initTimelionLegacyModule();
}
uiModules
.get('kibana/timelion_vis', ['kibana'])
.controller('TimelionVisController', function ($scope) {
$scope.$on('timelionChartRendered', event => {
event.stopPropagation();
$scope.renderComplete();
});
});
public async start() {
const $injector = await chrome.dangerouslyGetActiveInjector();
return {
$rootScope: $injector.get('$rootScope'),
$compile: $injector.get('$compile'),
} as LegacyDependenciesPluginStart;
}
}

View file

@ -0,0 +1,54 @@
/*
* 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 'ngreact';
import 'brace/mode/hjson';
import 'brace/ext/searchbox';
import 'ui/accessibility/kbn_ui_ace_keyboard_mode';
import 'ui/vis/map/service_settings';
import { once } from 'lodash';
// @ts-ignore
import { uiModules } from 'ui/modules';
// @ts-ignore
import { Chart } from '../directives/chart/chart';
// @ts-ignore
import { TimelionInterval } from '../directives/timelion_interval/timelion_interval';
// @ts-ignore
import { TimelionExpInput } from '../directives/timelion_expression_input';
// @ts-ignore
import { TimelionExpressionSuggestions } from '../directives/timelion_expression_suggestions/timelion_expression_suggestions';
/** @internal */
export const initTimelionLegacyModule = once((): void => {
require('ui/state_management/app_state');
uiModules
.get('apps/timelion', [])
.controller('TimelionVisController', function($scope: any) {
$scope.$on('timelionChartRendered', (event: any) => {
event.stopPropagation();
$scope.renderComplete();
});
})
.directive('chart', Chart)
.directive('timelionInterval', TimelionInterval)
.directive('timelionExpressionSuggestions', TimelionExpressionSuggestions)
.directive('timelionExpressionInput', TimelionExpInput);
});

View file

@ -17,51 +17,65 @@
* under the License.
*/
import { functionsRegistry } from 'plugins/interpreter/registries';
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { TimelionRequestHandlerProvider } from './vis/timelion_request_handler';
import { ExpressionFunction, KibanaContext, Render } from 'src/plugins/expressions/public';
import { getTimelionRequestHandler, TimelionSuccessResponse } from './vis/timelion_request_handler';
import { TimelionVisualizationDependencies } from './plugin';
const name = 'timelion_vis';
import chrome from 'ui/chrome';
interface Arguments {
expression: string;
interval: any;
}
export const timelionVis = () => ({
name: 'timelion_vis',
interface RenderValue {
visData: Context;
visType: typeof name;
visParams: VisParams;
}
type Context = KibanaContext | null;
type VisParams = Arguments;
type Return = Promise<Render<RenderValue>>;
export const getTimelionVisualizationConfig = (
dependencies: TimelionVisualizationDependencies
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name,
type: 'render',
context: {
types: [
'kibana_context',
'null',
],
types: ['kibana_context', 'null'],
},
help: i18n.translate('timelion.function.help', {
defaultMessage: 'Timelion visualization'
defaultMessage: 'Timelion visualization',
}),
args: {
expression: {
types: ['string'],
aliases: ['_'],
default: '".es(*)"',
help: '',
},
interval: {
types: ['string', 'null'],
default: 'auto',
}
help: '',
},
},
async fn(context, args) {
const $injector = await chrome.dangerouslyGetActiveInjector();
const Private = $injector.get('Private');
const timelionRequestHandler = Private(TimelionRequestHandlerProvider).handler;
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
const visParams = { expression: args.expression, interval: args.interval };
const response = await timelionRequestHandler({
timeRange: get(context, 'timeRange', null),
query: get(context, 'query', null),
filters: get(context, 'filters', null),
const response = (await timelionRequestHandler({
timeRange: get(context, 'timeRange'),
query: get(context, 'query'),
filters: get(context, 'filters'),
visParams,
forceFetch: true,
visParams: visParams,
});
})) as TimelionSuccessResponse;
response.visType = 'timelion';
@ -70,11 +84,9 @@ export const timelionVis = () => ({
as: 'visualization',
value: {
visParams,
visType: 'timelion',
visType: name,
visData: response,
},
};
},
});
functionsRegistry.register(timelionVis);

View file

@ -17,25 +17,19 @@
* under the License.
*/
import { visFactory } from 'ui/vis/vis_factory';
import { i18n } from '@kbn/i18n';
import { VisTypesRegistryProvider } from 'ui/registry/vis_types';
import { TimelionRequestHandlerProvider } from './timelion_request_handler';
// @ts-ignore
import { DefaultEditorSize } from 'ui/vis/editor_size';
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
// we also need to load the controller and directive used by the template
import './timelion_vis_controller';
import '../directives/timelion_expression_input';
import { visFactory } from '../../../visualizations/public';
import { getTimelionRequestHandler } from './timelion_request_handler';
import visConfigTemplate from './timelion_vis.html';
import editorConfigTemplate from './timelion_vis_params.html';
import { TimelionVisualizationDependencies } from '../plugin';
// @ts-ignore
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
// register the provider with the visTypes registry so that other know it exists
VisTypesRegistryProvider.register(TimelionVisProvider);
export default function TimelionVisProvider(Private) {
const timelionRequestHandler = Private(TimelionRequestHandlerProvider);
export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) {
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
// return the visType object, which kibana will use to display and configure new
// Vis object of this type.
@ -50,7 +44,7 @@ export default function TimelionVisProvider(Private) {
visConfig: {
defaults: {
expression: '.es(*)',
interval: 'auto'
interval: 'auto',
},
template: visConfigTemplate,
},
@ -58,7 +52,7 @@ export default function TimelionVisProvider(Private) {
optionsTemplate: editorConfigTemplate,
defaultSize: DefaultEditorSize.MEDIUM,
},
requestHandler: timelionRequestHandler.handler,
requestHandler: timelionRequestHandler,
responseHandler: 'none',
options: {
showIndexSelection: false,

View file

@ -1,71 +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';
import { buildEsQuery, getEsQueryConfig } from '@kbn/es-query';
import { timezoneProvider } from 'ui/vis/lib/timezone';
import { toastNotifications } from 'ui/notify';
import { i18n } from '@kbn/i18n';
const TimelionRequestHandlerProvider = function (Private, $http, config) {
const timezone = Private(timezoneProvider)();
return {
name: 'timelion',
handler: function ({ timeRange, filters, query, visParams }) {
return new Promise((resolve, reject) => {
const expression = visParams.expression;
if (!expression) return;
const esQueryConfigs = getEsQueryConfig(config);
const httpResult = $http.post('../api/timelion/run', {
sheet: [expression],
extended: {
es: {
filter: buildEsQuery(undefined, query, filters, esQueryConfigs)
}
},
time: _.extend(timeRange, {
interval: visParams.interval,
timezone: timezone
}),
})
.then(resp => resp.data)
.catch(resp => { throw resp.data; });
httpResult
.then(function (resp) {
resolve(resp);
})
.catch(function (resp) {
const err = new Error(resp.message);
err.stack = resp.stack;
toastNotifications.addError(err, {
title: i18n.translate('timelion.requestHandlerErrorTitle', {
defaultMessage: 'Timelion request error',
}),
});
reject(err);
});
});
}
};
};
export { TimelionRequestHandlerProvider };

View file

@ -0,0 +1,99 @@
/*
* 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.
*/
// @ts-ignore
import { buildEsQuery, getEsQueryConfig, Filter } from '@kbn/es-query';
// @ts-ignore
import { timezoneProvider } from 'ui/vis/lib/timezone';
import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public';
import { Query } from 'src/legacy/core_plugins/data/public';
import { TimeRange } from 'src/plugins/data/public';
import { VisParams } from 'ui/vis';
import { toastNotifications } from 'ui/notify';
import { i18n } from '@kbn/i18n';
import { TimelionVisualizationDependencies } from '../plugin';
interface Stats {
cacheCount: number;
invokeTime: number;
queryCount: number;
queryTime: number;
sheetTime: number;
}
interface Sheet {
list: Array<Record<string, unknown>>;
render: Record<string, unknown>;
type: string;
}
export interface TimelionSuccessResponse {
sheet: Sheet[];
stats: Stats;
visType: string;
type: KIBANA_CONTEXT_NAME;
}
export function getTimelionRequestHandler(dependencies: TimelionVisualizationDependencies) {
const { uiSettings, http } = dependencies;
const timezone = timezoneProvider(uiSettings)();
return async function({
timeRange,
filters,
query,
visParams,
}: {
timeRange: TimeRange;
filters: Filter[];
query: Query;
visParams: VisParams;
forceFetch?: boolean;
}): Promise<TimelionSuccessResponse | void> {
const expression = visParams.expression;
if (!expression) return;
const esQueryConfigs = getEsQueryConfig(uiSettings);
try {
return await http.post('../api/timelion/run', {
body: JSON.stringify({
sheet: [expression],
extended: {
es: {
filter: buildEsQuery(undefined, query, filters, esQueryConfigs),
},
},
time: { ...timeRange, interval: visParams.interval, timezone },
}),
});
} catch (e) {
const err = new Error(e.data.message);
err.stack = e.data.stack;
toastNotifications.addError(err, {
title: i18n.translate('timelion.requestHandlerErrorTitle', {
defaultMessage: 'Timelion request error',
}),
});
}
};
}

View file

@ -73,6 +73,7 @@ export interface LegacyPluginOptions {
visTypes: string[];
embeddableActions?: string[];
embeddableFactories?: string[];
uiSettingDefaults?: Record<string, any>;
}>;
uiCapabilities?: Capabilities;
publicDir: any;

View file

@ -21,6 +21,7 @@ import { Filter } from '@kbn/es-query';
import { TimeRange, Query } from 'src/plugins/data/public';
const name = 'kibana_context';
export type KIBANA_CONTEXT_NAME = 'kibana_context';
export interface KibanaContext {
type: typeof name;