[Vis] Move Timelion Vis to vis_type_timelion (#52069)

* Deangularize timelion vis

* Refactoring

* Fix path

* Update timelion_controller.ts

* Remove unused deps

* Create vis_type_timelion

* Create ChartComponent

* Render chart in react

* Reactify timelion editor

* Change translation ids

* Use hooks

* Add @types/pegjs into renovate.json5

* Add validation, add hover suggestions

* Style fixes

* Change plugin setup, use kibana context

* Update

* Fix ticks

* Fix plotselected listener

* Fix plothover handler

* Add TS for options

* Update TS

* Restructuring

* Change plugin start

* Remove vis from timelion plugin

* Rename class

* Mock services

* Fix other comments

* Remove duplicate files

* Convert test to jest

* Remove kibana_services from timelion

* Delete visualize_app.ts.~LOCAL

* Refactoring

* Fix TS

* Refactoring, TS

* Import eui variables

* Import styling constants

* Move react components to vis_type_timelion

* Fix TS

* Move ui imports to legacy_imports.ts

* Move chain.peg to vis_type_timelion

* Fix path

* Use KibanaContext instead kibana_services.ts

* Refactoring

* Refactoring

* Add @types/flot

* Fix issue with hovered series color

* Update renovate.json5

* Pass timelionPanels as dependencies

* Move common folder to vis_type_timelion

* Move back tick_formatters.ts

* Rename styles file

* Refactoring

* Update _index.scss

* Move to_milliseconds to common

* Revert yaxes formatting

* Refactoring

* Refactoring

* Use Panel directly

* Refactoring of to_milliseconds.ts

Co-authored-by: Daniil Suleiman <31325372+sulemanof@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Maryia Lapata 2020-01-17 15:30:26 +03:00 committed by GitHub
parent 856c85b400
commit 9e07a427c7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1105 additions and 229 deletions

View file

@ -16,7 +16,7 @@ src/legacy/core_plugins/vis_type_vislib/public/vislib/__tests__/lib/fixtures/moc
/src/legacy/core_plugins/console/public/webpackShims
/src/legacy/core_plugins/console/public/tests/webpackShims
/src/legacy/ui/public/utils/decode_geo_hash.js
/src/legacy/core_plugins/timelion/public/webpackShims/jquery.flot.*
/src/legacy/core_plugins/vis_type_timelion/public/webpackShims/jquery.flot.*
/src/core/lib/kbn_internal_native_observable
/packages/*/target
/packages/eslint-config-kibana

View file

@ -33,7 +33,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",
"timelion": ["src/legacy/core_plugins/timelion", "src/legacy/core_plugins/vis_type_timelion"],
"uiActions": "src/plugins/ui_actions",
"visTypeMarkdown": "src/legacy/core_plugins/vis_type_markdown",
"visTypeMetric": "src/legacy/core_plugins/vis_type_metric",

View file

@ -136,6 +136,7 @@
"@kbn/test-subj-selector": "0.2.1",
"@kbn/ui-framework": "1.0.0",
"@kbn/ui-shared-deps": "1.0.0",
"@types/flot": "^0.0.31",
"@types/json-stable-stringify": "^1.0.32",
"@types/lodash.clonedeep": "^4.5.4",
"@types/node-forge": "^0.9.0",

View file

@ -281,6 +281,14 @@
'@types/file-saver',
],
},
{
groupSlug: 'flot',
groupName: 'flot related packages',
packageNames: [
'flot',
'@types/flot',
],
},
{
groupSlug: 'getopts',
groupName: 'getopts related packages',

View file

@ -59,8 +59,6 @@ document.title = 'Timelion - Kibana';
const app = require('ui/modules').get('apps/timelion', []);
require('./vis');
require('ui/routes').enable();
require('ui/routes').when('/:id?', {

View file

@ -19,10 +19,13 @@
import expect from '@kbn/expect';
import PEG from 'pegjs';
import grammar from 'raw-loader!../../chain.peg';
import grammar from 'raw-loader!../../../../vis_type_timelion/public/chain.peg';
import { SUGGESTION_TYPE, suggest } from '../timelion_expression_input_helpers';
import { getArgValueSuggestions } from '../../services/arg_value_suggestions';
import { setIndexPatterns, setSavedObjectsClient } from '../../services/plugin_services';
import { getArgValueSuggestions } from '../../../../vis_type_timelion/public/helpers/arg_value_suggestions';
import {
setIndexPatterns,
setSavedObjectsClient,
} from '../../../../vis_type_timelion/public/helpers/plugin_services';
describe('Timelion expression suggestions', () => {
setIndexPatterns({});

View file

@ -1,6 +1,5 @@
@import './timelion_expression_input';
@import './cells/index';
@import './chart/index';
@import './timelion_expression_suggestions/index';
@import './timelion_help/index';
@import './timelion_interval/index';

View file

@ -1 +0,0 @@
@import './chart';

View file

@ -43,7 +43,7 @@
import _ from 'lodash';
import $ from 'jquery';
import PEG from 'pegjs';
import grammar from 'raw-loader!../chain.peg';
import grammar from 'raw-loader!../../../vis_type_timelion/public/chain.peg';
import timelionExpressionInputTemplate from './timelion_expression_input.html';
import {
SUGGESTION_TYPE,
@ -52,7 +52,7 @@ import {
insertAtLocation,
} from './timelion_expression_input_helpers';
import { comboBoxKeyCodes } from '@elastic/eui';
import { getArgValueSuggestions } from '../services/arg_value_suggestions';
import { getArgValueSuggestions } from '../../../vis_type_timelion/public/helpers/arg_value_suggestions';
const Parser = PEG.generate(grammar);

View file

@ -11,6 +11,4 @@
// timChart__legend-isLoading
@import './app';
@import './components/index';
@import './directives/index';
@import './vis/index';

View file

@ -20,15 +20,10 @@
import { PluginInitializerContext } from 'kibana/public';
import { npSetup, npStart } from 'ui/new_platform';
import { plugin } from '.';
import { setup as visualizations } from '../../visualizations/public/np_ready/public/legacy';
import { TimelionPluginSetupDependencies } from './plugin';
import { LegacyDependenciesPlugin } from './shim';
const setupPlugins: Readonly<TimelionPluginSetupDependencies> = {
visualizations,
data: npSetup.plugins.data,
expressions: npSetup.plugins.expressions,
// Temporary solution
// It will be removed when all dependent services are migrated to the new platform.
__LEGACY: new LegacyDependenciesPlugin(),
@ -37,4 +32,4 @@ const setupPlugins: Readonly<TimelionPluginSetupDependencies> = {
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
export const start = pluginInstance.start(npStart.core, npStart.plugins);
export const start = pluginInstance.start(npStart.core);

View file

@ -17,19 +17,18 @@
* under the License.
*/
import './flot';
import '../../../../vis_type_timelion/public/flot';
import _ from 'lodash';
import $ from 'jquery';
import moment from 'moment-timezone';
import { timefilter } from 'ui/timefilter';
// @ts-ignore
import observeResize from '../../lib/observe_resize';
// @ts-ignore
import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../common/lib';
import { calculateInterval, DEFAULT_TIME_FORMAT } from '../../../../vis_type_timelion/common/lib';
import { tickFormatters } from '../../../../vis_type_timelion/public/helpers/tick_formatters';
import { TimelionVisualizationDependencies } from '../../plugin';
import { tickFormatters } from '../../services/tick_formatters';
import { xaxisFormatterProvider } from './xaxis_formatter';
import { generateTicksProvider } from './tick_generator';
import { xaxisFormatterProvider } from '../../../../vis_type_timelion/public/helpers/xaxis_formatter';
import { generateTicksProvider } from '../../../../vis_type_timelion/public/helpers/tick_generator';
const DEBOUNCE_DELAY = 50;

View file

@ -22,33 +22,19 @@ import {
Plugin,
PluginInitializerContext,
IUiSettingsClient,
HttpSetup,
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public';
import { PluginsStart } from 'ui/new_platform/new_platform';
import { VisualizationsSetup } from '../../visualizations/public/np_ready/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisualization } from './vis';
import { getTimeChart } from './panels/timechart/timechart';
import { Panel } from './panels/panel';
import { LegacyDependenciesPlugin, LegacyDependenciesPluginSetup } from './shim';
import { setIndexPatterns, setSavedObjectsClient } from './services/plugin_services';
/** @internal */
export interface TimelionVisualizationDependencies extends LegacyDependenciesPluginSetup {
uiSettings: IUiSettingsClient;
http: HttpSetup;
timelionPanels: Map<string, Panel>;
timefilter: TimefilterContract;
}
/** @internal */
export interface TimelionPluginSetupDependencies {
expressions: ReturnType<ExpressionsPlugin['setup']>;
visualizations: VisualizationsSetup;
data: DataPublicPluginSetup;
// Temporary solution
__LEGACY: LegacyDependenciesPlugin;
}
@ -61,24 +47,16 @@ export class TimelionPlugin implements Plugin<Promise<void>, void> {
this.initializerContext = initializerContext;
}
public async setup(
core: CoreSetup,
{ __LEGACY, expressions, visualizations, data }: TimelionPluginSetupDependencies
) {
public async setup(core: CoreSetup, { __LEGACY }: TimelionPluginSetupDependencies) {
const timelionPanels: Map<string, Panel> = new Map();
const dependencies: TimelionVisualizationDependencies = {
uiSettings: core.uiSettings,
http: core.http,
timelionPanels,
timefilter: data.query.timefilter.timefilter,
...(await __LEGACY.setup(core, timelionPanels)),
};
this.registerPanels(dependencies);
expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies));
visualizations.types.createBaseVisualization(getTimelionVisualization(dependencies));
}
private registerPanels(dependencies: TimelionVisualizationDependencies) {
@ -87,15 +65,12 @@ export class TimelionPlugin implements Plugin<Promise<void>, void> {
dependencies.timelionPanels.set(timeChartPanel.name, timeChartPanel);
}
public start(core: CoreStart, plugins: PluginsStart) {
public start(core: CoreStart) {
const timelionUiEnabled = core.injectedMetadata.getInjectedVar('timelionUiEnabled');
if (timelionUiEnabled === false) {
core.chrome.navLinks.update('timelion', { hidden: true });
}
setIndexPatterns(plugins.data.indexPatterns);
setSavedObjectsClient(core.savedObjects.client);
}
public stop(): void {}

View file

@ -1,2 +0,0 @@
@import './timelion_vis';
@import './timelion_editor';

View file

@ -1,3 +0,0 @@
<div ng-controller="TimelionVisController" class="timVis">
<div chart="esResponse.sheet[0]" rerender-trigger="renderComplete" class="timChart" interval="visState.params.interval"></div>
</div>

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 '../../common/lib';
import { calculateInterval } from '../../../vis_type_timelion/common/lib';
export default function chainRunner(tlConfig) {
const preprocessChain = require('./lib/preprocess_chain')(tlConfig);

View file

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

View file

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

View file

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

View file

@ -23,7 +23,7 @@ import Chainable from '../../lib/classes/chainable';
import ses from './lib/ses';
import des from './lib/des';
import tes from './lib/tes';
import toMilliseconds from '../../lib/to_milliseconds';
import { toMS } from '../../../../vis_type_timelion/common/lib';
export default new Chainable('holt', {
args: [
@ -125,9 +125,7 @@ export default new Chainable('holt', {
})
);
}
const season = Math.round(
toMilliseconds(args.byName.season) / toMilliseconds(tlConfig.time.interval)
);
const season = Math.round(toMS(args.byName.season) / toMS(tlConfig.time.interval));
points = tes(points, alpha, beta, gamma, season, sample);
}

View file

@ -20,7 +20,7 @@
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import Chainable from '../lib/classes/chainable';
import { DEFAULT_TIME_FORMAT } from '../../common/lib';
import { DEFAULT_TIME_FORMAT } from '../../../vis_type_timelion/common/lib';
export default new Chainable('legend', {
args: [

View file

@ -21,7 +21,7 @@ import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';
import toMS from '../lib/to_milliseconds.js';
import { toMS } from '../../../vis_type_timelion/common/lib';
const validPositions = ['left', 'right', 'center'];
const defaultPosition = 'center';

View file

@ -19,7 +19,7 @@
import { i18n } from '@kbn/i18n';
import alter from '../lib/alter.js';
import toMS from '../lib/to_milliseconds.js';
import { toMS } from '../../../vis_type_timelion/common/lib';
import _ from 'lodash';
import Chainable from '../lib/classes/chainable';

View file

@ -17,11 +17,11 @@
* under the License.
*/
import toMS from '../../server/lib/to_milliseconds.js';
import { toMS } from './to_milliseconds';
// Totally cribbed this from Kibana 3.
// I bet there's something similar in the Kibana 4 code. Somewhere. Somehow.
function roundInterval(interval) {
function roundInterval(interval: number) {
switch (true) {
case interval <= 500: // <= 0.5s
return '100ms';
@ -58,9 +58,24 @@ function roundInterval(interval) {
}
}
export function calculateInterval(from, to, size, interval, min) {
if (interval !== 'auto') return interval;
const dateMathInterval = roundInterval((to - from) / size);
if (toMS(dateMathInterval) < toMS(min)) return min;
export function calculateInterval(
from: number,
to: number,
size: number,
interval: string,
min: string
) {
if (interval !== 'auto') {
return interval;
}
const dateMathInterval: string = roundInterval((to - from) / size);
const dateMathIntervalMs = toMS(dateMathInterval);
const minMs = toMS(min);
if (dateMathIntervalMs !== undefined && minMs !== undefined && dateMathIntervalMs < minMs) {
return min;
}
return dateMathInterval;
}

View file

@ -18,4 +18,6 @@
*/
export { calculateInterval } from './calculate_interval';
export { toMS } from './to_milliseconds';
export const DEFAULT_TIME_FORMAT = 'MMMM Do YYYY, HH:mm:ss.SSS';

View file

@ -17,42 +17,45 @@
* under the License.
*/
import _ from 'lodash';
import moment from 'moment';
import { keys } from 'lodash';
import moment, { unitOfTime } from 'moment';
type Units = unitOfTime.Base | unitOfTime._quarter;
type Values = { [key in Units]: number };
// map of moment's short/long unit ids and elasticsearch's long unit ids
// to their value in milliseconds
const vals = _.transform(
[
['ms', 'milliseconds', 'millisecond'],
['s', 'seconds', 'second', 'sec'],
['m', 'minutes', 'minute', 'min'],
['h', 'hours', 'hour'],
['d', 'days', 'day'],
['w', 'weeks', 'week'],
['M', 'months', 'month'],
['quarter'],
['y', 'years', 'year'],
],
function(vals, units) {
const normal = moment.normalizeUnits(units[0]);
const val = moment.duration(1, normal).asMilliseconds();
[].concat(normal, units).forEach(function(unit) {
vals[unit] = val;
});
},
{}
);
// match any key from the vals object preceded by an optional number
const parseRE = new RegExp('^(\\d+(?:\\.\\d*)?)?\\s*(' + _.keys(vals).join('|') + ')$');
const unitMappings = [
['ms', 'milliseconds', 'millisecond'],
['s', 'seconds', 'second', 'sec'],
['m', 'minutes', 'minute', 'min'],
['h', 'hours', 'hour'],
['d', 'days', 'day'],
['w', 'weeks', 'week'],
['M', 'months', 'month'],
['quarter'],
['y', 'years', 'year'],
] as Units[][];
export default function(expr) {
const vals = {} as Values;
unitMappings.forEach(units => {
const normal = moment.normalizeUnits(units[0]) as Units;
const val = moment.duration(1, normal).asMilliseconds();
([] as Units[]).concat(normal, units).forEach((unit: Units) => {
vals[unit] = val;
});
});
// match any key from the vals object preceded by an optional number
const parseRE = new RegExp('^(\\d+(?:\\.\\d*)?)?\\s*(' + keys(vals).join('|') + ')$');
export function toMS(expr: string) {
const match = expr.match(parseRE);
if (match) {
if (match[2] === 'M' && match[1] !== '1') {
throw new Error('Invalid interval. 1M is only valid monthly interval.');
}
return parseFloat(match[1] || 1) * vals[match[2]];
return parseFloat(match[1] || '1') * vals[match[2] as Units];
}
}

View file

@ -0,0 +1,44 @@
/*
* 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 { resolve } from 'path';
import { Legacy } from 'kibana';
import { LegacyPluginApi, LegacyPluginInitializer } from '../../../../src/legacy/types';
const timelionVisPluginInitializer: LegacyPluginInitializer = ({ Plugin }: LegacyPluginApi) =>
new Plugin({
id: 'timelion_vis',
require: ['kibana', 'elasticsearch', 'visualizations', 'data'],
publicDir: resolve(__dirname, 'public'),
uiExports: {
styleSheetPaths: resolve(__dirname, 'public/index.scss'),
hacks: [resolve(__dirname, 'public/legacy')],
injectDefaultVars: server => ({}),
},
init: (server: Legacy.Server) => ({}),
config(Joi: any) {
return Joi.object({
enabled: Joi.boolean().default(true),
}).default();
},
});
// eslint-disable-next-line import/no-default-export
export default timelionVisPluginInitializer;

View file

@ -0,0 +1,4 @@
{
"name": "timelion_vis",
"version": "kibana"
}

View file

@ -33,6 +33,7 @@
.ngLegendValue {
color: $euiTextColor;
cursor: pointer;
&:focus,
&:hover {

View file

@ -0,0 +1,39 @@
/*
* 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 React from 'react';
import { Sheet } from '../helpers/timelion_request_handler';
import { Panel } from './panel';
interface ChartComponentProp {
interval: string;
renderComplete(): void;
seriesList: Sheet;
}
function ChartComponent(props: ChartComponentProp) {
if (!props.seriesList) {
return null;
}
return <Panel {...props} />;
}
export { ChartComponent };

View file

@ -19,3 +19,4 @@
export * from './timelion_expression_input';
export * from './timelion_interval';
export * from './timelion_vis';

View file

@ -0,0 +1,386 @@
/*
* 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 React, { useState, useEffect, useMemo, useCallback } from 'react';
import $ from 'jquery';
import moment from 'moment-timezone';
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 {
buildSeriesData,
buildOptions,
SERIES_ID_ATTR,
colors,
Axis,
} from '../helpers/panel_utils';
import { Series, Sheet } from '../helpers/timelion_request_handler';
import { tickFormatters } from '../helpers/tick_formatters';
import { generateTicksProvider } from '../helpers/tick_generator';
import { TimelionVisDependencies } from '../plugin';
interface PanelProps {
interval: string;
seriesList: Sheet;
renderComplete(): void;
}
interface Position {
x: number;
x1: number;
y: number;
y1: number;
pageX: number;
pageY: number;
}
interface Range {
to: number;
from: number;
}
interface Ranges {
xaxis: Range;
yaxis: Range;
}
const DEBOUNCE_DELAY = 50;
// ensure legend is the same height with or without a caption so legend items do not move around
const emptyCaption = '<br>';
function Panel({ interval, seriesList, renderComplete }: PanelProps) {
const kibana = useKibana<TimelionVisDependencies>();
const [chart, setChart] = useState(() => cloneDeep(seriesList.list));
const [canvasElem, setCanvasElem] = useState();
const [chartElem, setChartElem] = useState();
const [originalColorMap, setOriginalColorMap] = useState(() => new Map<Series, string>());
const [highlightedSeries, setHighlightedSeries] = useState<number | null>(null);
const [focusedSeries, setFocusedSeries] = useState();
const [plot, setPlot] = useState();
// Used to toggle the series, and for displaying values on hover
const [legendValueNumbers, setLegendValueNumbers] = useState();
const [legendCaption, setLegendCaption] = useState();
const canvasRef = useCallback(node => {
if (node !== null) {
setCanvasElem(node);
}
}, []);
const elementRef = useCallback(node => {
if (node !== null) {
setChartElem(node);
}
}, []);
useEffect(
() => () => {
$(chartElem)
.off('plotselected')
.off('plothover')
.off('mouseleave');
},
[chartElem]
);
const highlightSeries = useCallback(
debounce(({ currentTarget }: JQuery.TriggeredEvent) => {
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
if (highlightedSeries === id) {
return;
}
setHighlightedSeries(id);
setChart(chartState =>
chartState.map((series: Series, seriesIndex: number) => {
series.color =
seriesIndex === id
? originalColorMap.get(series) // color it like it was
: 'rgba(128,128,128,0.1)'; // mark as grey
return series;
})
);
}, DEBOUNCE_DELAY),
[originalColorMap, highlightedSeries]
);
const focusSeries = useCallback(
(event: JQuery.TriggeredEvent) => {
const id = Number(event.currentTarget.getAttribute(SERIES_ID_ATTR));
setFocusedSeries(id);
highlightSeries(event);
},
[highlightSeries]
);
const toggleSeries = useCallback(({ currentTarget }: JQuery.TriggeredEvent) => {
const id = Number(currentTarget.getAttribute(SERIES_ID_ATTR));
setChart(chartState =>
chartState.map((series: Series, seriesIndex: number) => {
if (seriesIndex === id) {
series._hide = !series._hide;
}
return series;
})
);
}, []);
const updateCaption = useCallback(
(plotData: any) => {
if (get(plotData, '[0]._global.legend.showTime', true)) {
const caption = $('<caption class="timChart__legendCaption"></caption>');
caption.html(emptyCaption);
setLegendCaption(caption);
const canvasNode = $(canvasElem);
canvasNode.find('div.legend table').append(caption);
setLegendValueNumbers(canvasNode.find('.ngLegendValueNumber'));
const legend = $(canvasElem).find('.ngLegendValue');
if (legend) {
legend.click(toggleSeries);
legend.focus(focusSeries);
legend.mouseover(highlightSeries);
}
// legend has been re-created. Apply focus on legend element when previously set
if (focusedSeries || focusedSeries === 0) {
canvasNode
.find('div.legend table .legendLabel>span')
.get(focusedSeries)
.focus();
}
}
},
[focusedSeries, canvasElem, toggleSeries, focusSeries, highlightSeries]
);
const updatePlot = useCallback(
(chartValue: Series[], grid?: boolean) => {
if (canvasElem && canvasElem.clientWidth > 0 && canvasElem.clientHeight > 0) {
const options = buildOptions(
interval,
kibana.services.timefilter,
kibana.services.uiSettings,
chartElem && chartElem.clientWidth,
grid
);
const updatedSeries = buildSeriesData(chartValue, options);
if (options.yaxes) {
options.yaxes.forEach((yaxis: Axis) => {
if (yaxis && yaxis.units) {
const formatters = tickFormatters();
yaxis.tickFormatter = formatters[yaxis.units.type as keyof typeof formatters];
const byteModes = ['bytes', 'bytes/s'];
if (byteModes.includes(yaxis.units.type)) {
yaxis.tickGenerator = generateTicksProvider();
}
}
});
}
const newPlot = $.plot(canvasElem, updatedSeries, options);
setPlot(newPlot);
renderComplete();
updateCaption(newPlot.getData());
}
},
[canvasElem, chartElem, renderComplete, kibana.services, interval, updateCaption]
);
useEffect(() => {
updatePlot(chart, seriesList.render && seriesList.render.grid);
}, [chart, updatePlot, seriesList.render]);
useEffect(() => {
const colorsSet: Array<[Series, string]> = [];
const newChart = seriesList.list.map((series: Series, seriesIndex: number) => {
const newSeries = { ...series };
if (!newSeries.color) {
const colorIndex = seriesIndex % colors.length;
newSeries.color = colors[colorIndex];
}
colorsSet.push([newSeries, newSeries.color]);
return newSeries;
});
setChart(newChart);
setOriginalColorMap(new Map(colorsSet));
}, [seriesList.list]);
const unhighlightSeries = useCallback(() => {
if (highlightedSeries === null) {
return;
}
setHighlightedSeries(null);
setFocusedSeries(null);
setChart(chartState =>
chartState.map((series: Series) => {
series.color = originalColorMap.get(series); // reset the colors
return series;
})
);
}, [originalColorMap, highlightedSeries]);
// Shamelessly borrowed from the flotCrosshairs example
const setLegendNumbers = useCallback(
(pos: Position) => {
unhighlightSeries();
const axes = plot.getAxes();
if (pos.x < axes.xaxis.min || pos.x > axes.xaxis.max) {
return;
}
const dataset = plot.getData();
if (legendCaption) {
legendCaption.text(
moment(pos.x).format(get(dataset, '[0]._global.legend.timeFormat', DEFAULT_TIME_FORMAT))
);
}
for (let i = 0; i < dataset.length; ++i) {
const series = dataset[i];
const useNearestPoint = series.lines.show && !series.lines.steps;
const precision = get(series, '_meta.precision', 2);
if (series._hide) {
continue;
}
const currentPoint = series.data.find((point: [number, number], index: number) => {
if (index + 1 === series.data.length) {
return true;
}
if (useNearestPoint) {
return pos.x - point[0] < series.data[index + 1][0] - pos.x;
} else {
return pos.x < series.data[index + 1][0];
}
});
const y = currentPoint[1];
if (y != null && legendValueNumbers) {
let label = y.toFixed(precision);
if (series.yaxis.tickFormatter) {
label = series.yaxis.tickFormatter(Number(label), series.yaxis);
}
legendValueNumbers.eq(i).text(`(${label})`);
} else {
legendValueNumbers.eq(i).empty();
}
}
},
[plot, legendValueNumbers, unhighlightSeries, legendCaption]
);
const debouncedSetLegendNumbers = useCallback(
debounce(setLegendNumbers, DEBOUNCE_DELAY, {
maxWait: DEBOUNCE_DELAY,
leading: true,
trailing: false,
}),
[setLegendNumbers]
);
const clearLegendNumbers = useCallback(() => {
if (legendCaption) {
legendCaption.html(emptyCaption);
}
each(legendValueNumbers, (num: Node) => {
$(num).empty();
});
}, [legendCaption, legendValueNumbers]);
const plotHoverHandler = useCallback(
(event: JQuery.TriggeredEvent, pos: Position) => {
if (!plot) {
return;
}
plot.setCrosshair(pos);
debouncedSetLegendNumbers(pos);
},
[plot, debouncedSetLegendNumbers]
);
const mouseLeaveHandler = useCallback(() => {
if (!plot) {
return;
}
plot.clearCrosshair();
clearLegendNumbers();
}, [plot, clearLegendNumbers]);
const plotSelectedHandler = useCallback(
(event: JQuery.TriggeredEvent, ranges: Ranges) => {
kibana.services.timefilter.setTime({
from: moment(ranges.xaxis.from),
to: moment(ranges.xaxis.to),
});
},
[kibana.services.timefilter]
);
useEffect(() => {
if (chartElem) {
$(chartElem)
.off('plotselected')
.on('plotselected', plotSelectedHandler);
}
}, [chartElem, plotSelectedHandler]);
useEffect(() => {
if (chartElem) {
$(chartElem)
.off('mouseleave')
.on('mouseleave', mouseLeaveHandler);
}
}, [chartElem, mouseLeaveHandler]);
useEffect(() => {
if (chartElem) {
$(chartElem)
.off('plothover')
.on('plothover', plotHoverHandler);
}
}, [chartElem, plotHoverHandler]);
const title: string = useMemo(() => last(compact(map(seriesList.list, '_title'))) || '', [
seriesList.list,
]);
return (
<div ref={elementRef} className="timChart">
<div className="chart-top-title">{title}</div>
<div ref={canvasRef} className="chart-canvas" />
</div>
);
}
export { Panel };

View file

@ -25,7 +25,7 @@ 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 '../services/arg_value_suggestions';
import { getArgValueSuggestions } from '../helpers/arg_value_suggestions';
const LANGUAGE_ID = 'timelion_expression';
monacoEditor.languages.register({ id: LANGUAGE_ID });

View file

@ -26,7 +26,7 @@ import grammar from 'raw-loader!../chain.peg';
import { i18n } from '@kbn/i18n';
import { ITimelionFunction, TimelionFunctionArgs } from '../../common/types';
import { ArgValueSuggestions, FunctionArg, Location } from '../services/arg_value_suggestions';
import { ArgValueSuggestions, FunctionArg, Location } from '../helpers/arg_value_suggestions';
const Parser = PEG.generate(grammar);

View file

@ -21,8 +21,8 @@ import React, { useMemo, useCallback } from 'react';
import { EuiFormRow, EuiComboBox, EuiComboBoxOptionProps } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { useValidation } from 'ui/vis/editors/default/controls/agg_utils';
import { isValidEsInterval } from '../../../../core_plugins/data/common';
import { useValidation } from '../legacy_imports';
const intervalOptions = [
{

View file

@ -0,0 +1,49 @@
/*
* 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 React from 'react';
import { IUiSettingsClient } from 'kibana/public';
import { Vis } from '../legacy_imports';
import { ChartComponent } from './chart';
import { VisParams } from '../timelion_vis_fn';
import { TimelionSuccessResponse } from '../helpers/timelion_request_handler';
export interface TimelionVisComponentProp {
config: IUiSettingsClient;
renderComplete(): void;
updateStatus: object;
vis: Vis;
visData: TimelionSuccessResponse;
visParams: VisParams;
}
function TimelionVisComponent(props: TimelionVisComponentProp) {
return (
<div className="timVis">
<ChartComponent
seriesList={props.visData.sheet[0]}
renderComplete={props.renderComplete}
interval={props.vis.getState().params.interval}
/>
</div>
);
}
export { TimelionVisComponent };

View file

@ -0,0 +1,187 @@
/*
* 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 { cloneDeep, defaults, merge, compact } from 'lodash';
import moment, { Moment } from 'moment-timezone';
import { TimefilterContract } from 'src/plugins/data/public';
import { IUiSettingsClient } from 'kibana/public';
import { calculateInterval } from '../../common/lib';
import { xaxisFormatterProvider } from './xaxis_formatter';
import { Series } from './timelion_request_handler';
export interface Axis {
delta?: number;
max?: number;
min?: number;
mode: string;
options?: {
units: { prefix: string; suffix: string };
};
tickSize?: number;
ticks: number;
tickLength: number;
timezone: string;
tickDecimals?: number;
tickFormatter: ((val: number) => string) | ((val: number, axis: Axis) => string);
tickGenerator?(axis: Axis): number[];
units?: { type: string };
}
interface TimeRangeBounds {
min: Moment | undefined;
max: Moment | undefined;
}
const colors = [
'#01A4A4',
'#C66',
'#D0D102',
'#616161',
'#00A1CB',
'#32742C',
'#F18D05',
'#113F8C',
'#61AE24',
'#D70060',
];
const SERIES_ID_ATTR = 'data-series-id';
function buildSeriesData(chart: Series[], options: jquery.flot.plotOptions) {
const seriesData = chart.map((series: Series, seriesIndex: number) => {
const newSeries: Series = cloneDeep(
defaults(series, {
shadowSize: 0,
lines: {
lineWidth: 3,
},
})
);
newSeries._id = seriesIndex;
if (series.color) {
const span = document.createElement('span');
span.style.color = series.color;
newSeries.color = span.style.color;
}
if (series._hide) {
newSeries.data = [];
newSeries.stack = false;
newSeries.label = `(hidden) ${series.label}`;
}
if (series._global) {
merge(options, series._global, (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;
}
if (srcVal == null) {
return objVal;
}
});
}
return newSeries;
});
return compact(seriesData);
}
function buildOptions(
intervalValue: string,
timefilter: TimefilterContract,
uiSettings: IUiSettingsClient,
clientWidth = 0,
showGrid?: boolean
) {
// Get the X-axis tick format
const time: TimeRangeBounds = timefilter.getBounds();
const interval = calculateInterval(
(time.min && time.min.valueOf()) || 0,
(time.max && time.max.valueOf()) || 0,
uiSettings.get('timelion:target_buckets') || 200,
intervalValue,
uiSettings.get('timelion:min_interval') || '1ms'
);
const format = xaxisFormatterProvider(uiSettings)(interval);
const tickLetterWidth = 7;
const tickPadding = 45;
const options = {
xaxis: {
mode: 'time',
tickLength: 5,
timezone: 'browser',
// Calculate how many ticks can fit on the axis
ticks: Math.floor(clientWidth / (format.length * tickLetterWidth + tickPadding)),
// Use moment to format ticks so we get timezone correction
tickFormatter: (val: number) => moment(val).format(format),
},
selection: {
mode: 'x',
color: '#ccc',
},
crosshair: {
mode: 'x',
color: '#C66',
lineWidth: 2,
},
colors,
grid: {
show: showGrid,
borderWidth: 0,
borderColor: null,
margin: 10,
hoverable: true,
autoHighlight: false,
},
legend: {
backgroundColor: 'rgb(255,255,255,0)',
position: 'nw',
labelBoxBorderColor: 'rgb(255,255,255,0)',
labelFormatter(label: string, series: { _id: number }) {
const wrapperSpan = document.createElement('span');
const labelSpan = document.createElement('span');
const numberSpan = document.createElement('span');
wrapperSpan.setAttribute('class', 'ngLegendValue');
wrapperSpan.setAttribute(SERIES_ID_ATTR, `${series._id}`);
labelSpan.appendChild(document.createTextNode(label));
numberSpan.setAttribute('class', 'ngLegendValueNumber');
wrapperSpan.appendChild(labelSpan);
wrapperSpan.appendChild(numberSpan);
return wrapperSpan.outerHTML;
},
},
} as jquery.flot.plotOptions & { yaxes?: Axis[] };
return options;
}
export { buildSeriesData, buildOptions, SERIES_ID_ATTR, colors };

View file

@ -17,124 +17,123 @@
* under the License.
*/
import expect from '@kbn/expect';
import { tickFormatters } from '../../services/tick_formatters';
import { tickFormatters } from './tick_formatters';
describe('Tick Formatters', function() {
let formatters;
let formatters: any;
beforeEach(function() {
formatters = tickFormatters();
});
describe('Bits mode', function() {
let bitFormatter;
let bitFormatter: any;
beforeEach(function() {
bitFormatter = formatters.bits;
});
it('is a function', function() {
expect(bitFormatter).to.be.a('function');
expect(bitFormatter).toEqual(expect.any(Function));
});
it('formats with b/kb/mb/gb', function() {
expect(bitFormatter(7)).to.equal('7b');
expect(bitFormatter(4 * 1000)).to.equal('4kb');
expect(bitFormatter(4.1 * 1000 * 1000)).to.equal('4.1mb');
expect(bitFormatter(3 * 1000 * 1000 * 1000)).to.equal('3gb');
expect(bitFormatter(7)).toEqual('7b');
expect(bitFormatter(4 * 1000)).toEqual('4kb');
expect(bitFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb');
expect(bitFormatter(3 * 1000 * 1000 * 1000)).toEqual('3gb');
});
it('formats negative values with b/kb/mb/gb', () => {
expect(bitFormatter(-7)).to.equal('-7b');
expect(bitFormatter(-4 * 1000)).to.equal('-4kb');
expect(bitFormatter(-4.1 * 1000 * 1000)).to.equal('-4.1mb');
expect(bitFormatter(-3 * 1000 * 1000 * 1000)).to.equal('-3gb');
expect(bitFormatter(-7)).toEqual('-7b');
expect(bitFormatter(-4 * 1000)).toEqual('-4kb');
expect(bitFormatter(-4.1 * 1000 * 1000)).toEqual('-4.1mb');
expect(bitFormatter(-3 * 1000 * 1000 * 1000)).toEqual('-3gb');
});
});
describe('Bits/s mode', function() {
let bitsFormatter;
let bitsFormatter: any;
beforeEach(function() {
bitsFormatter = formatters['bits/s'];
});
it('is a function', function() {
expect(bitsFormatter).to.be.a('function');
expect(bitsFormatter).toEqual(expect.any(Function));
});
it('formats with b/kb/mb/gb', function() {
expect(bitsFormatter(7)).to.equal('7b/s');
expect(bitsFormatter(4 * 1000)).to.equal('4kb/s');
expect(bitsFormatter(4.1 * 1000 * 1000)).to.equal('4.1mb/s');
expect(bitsFormatter(3 * 1000 * 1000 * 1000)).to.equal('3gb/s');
expect(bitsFormatter(7)).toEqual('7b/s');
expect(bitsFormatter(4 * 1000)).toEqual('4kb/s');
expect(bitsFormatter(4.1 * 1000 * 1000)).toEqual('4.1mb/s');
expect(bitsFormatter(3 * 1000 * 1000 * 1000)).toEqual('3gb/s');
});
it('formats negative values with b/kb/mb/gb', function() {
expect(bitsFormatter(-7)).to.equal('-7b/s');
expect(bitsFormatter(-4 * 1000)).to.equal('-4kb/s');
expect(bitsFormatter(-4.1 * 1000 * 1000)).to.equal('-4.1mb/s');
expect(bitsFormatter(-3 * 1000 * 1000 * 1000)).to.equal('-3gb/s');
expect(bitsFormatter(-7)).toEqual('-7b/s');
expect(bitsFormatter(-4 * 1000)).toEqual('-4kb/s');
expect(bitsFormatter(-4.1 * 1000 * 1000)).toEqual('-4.1mb/s');
expect(bitsFormatter(-3 * 1000 * 1000 * 1000)).toEqual('-3gb/s');
});
});
describe('Bytes mode', function() {
let byteFormatter;
let byteFormatter: any;
beforeEach(function() {
byteFormatter = formatters.bytes;
});
it('is a function', function() {
expect(byteFormatter).to.be.a('function');
expect(byteFormatter).toEqual(expect.any(Function));
});
it('formats with B/KB/MB/GB', function() {
expect(byteFormatter(10)).to.equal('10B');
expect(byteFormatter(10 * 1024)).to.equal('10KB');
expect(byteFormatter(10.2 * 1024 * 1024)).to.equal('10.2MB');
expect(byteFormatter(3 * 1024 * 1024 * 1024)).to.equal('3GB');
expect(byteFormatter(10)).toEqual('10B');
expect(byteFormatter(10 * 1024)).toEqual('10KB');
expect(byteFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB');
expect(byteFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB');
});
it('formats negative values with B/KB/MB/GB', function() {
expect(byteFormatter(-10)).to.equal('-10B');
expect(byteFormatter(-10 * 1024)).to.equal('-10KB');
expect(byteFormatter(-10.2 * 1024 * 1024)).to.equal('-10.2MB');
expect(byteFormatter(-3 * 1024 * 1024 * 1024)).to.equal('-3GB');
expect(byteFormatter(-10)).toEqual('-10B');
expect(byteFormatter(-10 * 1024)).toEqual('-10KB');
expect(byteFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB');
expect(byteFormatter(-3 * 1024 * 1024 * 1024)).toEqual('-3GB');
});
});
describe('Bytes/s mode', function() {
let bytesFormatter;
let bytesFormatter: any;
beforeEach(function() {
bytesFormatter = formatters['bytes/s'];
});
it('is a function', function() {
expect(bytesFormatter).to.be.a('function');
expect(bytesFormatter).toEqual(expect.any(Function));
});
it('formats with B/KB/MB/GB', function() {
expect(bytesFormatter(10)).to.equal('10B/s');
expect(bytesFormatter(10 * 1024)).to.equal('10KB/s');
expect(bytesFormatter(10.2 * 1024 * 1024)).to.equal('10.2MB/s');
expect(bytesFormatter(3 * 1024 * 1024 * 1024)).to.equal('3GB/s');
expect(bytesFormatter(10)).toEqual('10B/s');
expect(bytesFormatter(10 * 1024)).toEqual('10KB/s');
expect(bytesFormatter(10.2 * 1024 * 1024)).toEqual('10.2MB/s');
expect(bytesFormatter(3 * 1024 * 1024 * 1024)).toEqual('3GB/s');
});
it('formats negative values with B/KB/MB/GB', function() {
expect(bytesFormatter(-10)).to.equal('-10B/s');
expect(bytesFormatter(-10 * 1024)).to.equal('-10KB/s');
expect(bytesFormatter(-10.2 * 1024 * 1024)).to.equal('-10.2MB/s');
expect(bytesFormatter(-3 * 1024 * 1024 * 1024)).to.equal('-3GB/s');
expect(bytesFormatter(-10)).toEqual('-10B/s');
expect(bytesFormatter(-10 * 1024)).toEqual('-10KB/s');
expect(bytesFormatter(-10.2 * 1024 * 1024)).toEqual('-10.2MB/s');
expect(bytesFormatter(-3 * 1024 * 1024 * 1024)).toEqual('-3GB/s');
});
});
describe('Currency mode', function() {
let currencyFormatter;
let currencyFormatter: any;
beforeEach(function() {
currencyFormatter = formatters.currency;
});
it('is a function', function() {
expect(currencyFormatter).to.be.a('function');
expect(currencyFormatter).toEqual(expect.any(Function));
});
it('formats with $ by default', function() {
@ -143,7 +142,7 @@ describe('Tick Formatters', function() {
units: {},
},
};
expect(currencyFormatter(10.2, axis)).to.equal('$10.20');
expect(currencyFormatter(10.2, axis)).toEqual('$10.20');
});
it('accepts currency in ISO 4217', function() {
@ -155,18 +154,18 @@ describe('Tick Formatters', function() {
},
};
expect(currencyFormatter(10.2, axis)).to.equal('CN¥10.20');
expect(currencyFormatter(10.2, axis)).toEqual('CN¥10.20');
});
});
describe('Percent mode', function() {
let percentFormatter;
let percentFormatter: any;
beforeEach(function() {
percentFormatter = formatters.percent;
});
it('is a function', function() {
expect(percentFormatter).to.be.a('function');
expect(percentFormatter).toEqual(expect.any(Function));
});
it('formats with %', function() {
@ -175,7 +174,7 @@ describe('Tick Formatters', function() {
units: {},
},
};
expect(percentFormatter(0.1234, axis)).to.equal('12%');
expect(percentFormatter(0.1234, axis)).toEqual('12%');
});
it('formats with % with decimal precision', function() {
@ -189,18 +188,18 @@ describe('Tick Formatters', function() {
},
},
};
expect(percentFormatter(0.12345, axis)).to.equal('12.345%');
expect(percentFormatter(0.12345, axis)).toEqual('12.345%');
});
});
describe('Custom mode', function() {
let customFormatter;
let customFormatter: any;
beforeEach(function() {
customFormatter = formatters.custom;
});
it('is a function', function() {
expect(customFormatter).to.be.a('function');
expect(customFormatter).toEqual(expect.any(Function));
});
it('accepts prefix and suffix', function() {
@ -214,7 +213,7 @@ describe('Tick Formatters', function() {
tickDecimals: 1,
};
expect(customFormatter(10.2, axis)).to.equal('prefix10.2suffix');
expect(customFormatter(10.2, axis)).toEqual('prefix10.2suffix');
});
it('correctly renders small values', function() {
@ -228,7 +227,7 @@ describe('Tick Formatters', function() {
tickDecimals: 3,
};
expect(customFormatter(0.00499999999999999, axis)).to.equal('prefix0.005suffix');
expect(customFormatter(0.00499999999999999, axis)).toEqual('prefix0.005suffix');
});
});
});

View file

@ -17,9 +17,11 @@
* under the License.
*/
import _ from 'lodash';
import { get } from 'lodash';
function baseTickFormatter(value: any, axis: any) {
import { Axis } from './panel_utils';
function baseTickFormatter(value: number, axis: Axis) {
const factor = axis.tickDecimals ? Math.pow(10, axis.tickDecimals) : 1;
const formatted = '' + Math.round(value * factor) / factor;
@ -40,8 +42,8 @@ function baseTickFormatter(value: any, axis: any) {
return formatted;
}
function unitFormatter(divisor: any, units: any) {
return (val: any) => {
function unitFormatter(divisor: number, units: string[]) {
return (val: number) => {
let index = 0;
const isNegative = val < 0;
val = Math.abs(val);
@ -55,20 +57,20 @@ function unitFormatter(divisor: any, units: any) {
}
export function tickFormatters() {
const formatters = {
return {
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/s': unitFormatter(1024, ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s']),
currency(val: any, axis: any) {
currency(val: number, axis: Axis) {
return val.toLocaleString('en', {
style: 'currency',
currency: axis.options.units.prefix || 'USD',
currency: (axis && axis.options && axis.options.units.prefix) || 'USD',
});
},
percent(val: any, axis: any) {
percent(val: number, axis: Axis) {
let precision =
_.get(axis, 'tickDecimals', 0) - _.get(axis, 'options.units.tickDecimalsShift', 0);
get(axis, 'tickDecimals', 0) - get(axis, 'options.units.tickDecimalsShift', 0);
// toFixed only accepts values between 0 and 20
if (precision < 0) {
precision = 0;
@ -78,13 +80,11 @@ export function tickFormatters() {
return (val * 100).toFixed(precision) + '%';
},
custom(val: any, axis: any) {
custom(val: number, axis: Axis) {
const formattedVal = baseTickFormatter(val, axis);
const prefix = axis.options.units.prefix;
const suffix = axis.options.units.suffix;
const prefix = axis && axis.options && axis.options.units.prefix;
const suffix = axis && axis.options && axis.options.units.suffix;
return prefix + formattedVal + suffix;
},
};
return formatters;
}

View file

@ -17,11 +17,10 @@
* under the License.
*/
import expect from '@kbn/expect';
import { generateTicksProvider } from '../panels/timechart/tick_generator';
import { generateTicksProvider } from './tick_generator';
describe('Tick Generator', function() {
let generateTicks;
let generateTicks: any;
beforeEach(function() {
generateTicks = generateTicksProvider();
@ -29,7 +28,7 @@ describe('Tick Generator', function() {
describe('generateTicksProvider()', function() {
it('should return a function', function() {
expect(generateTicks).to.be.a('function');
expect(generateTicks).toEqual(expect.any(Function));
});
});
@ -58,14 +57,14 @@ describe('Tick Generator', function() {
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)
const expectedNr = Math.floor((axis.max - axis.min) / expectedDelta) + 2;
expect(ticks instanceof Array).toBeTruthy();
expect(ticks.length).toBe(expectedNr);
expect(ticks[0]).toEqual(axis.min);
expect(ticks[Math.floor(ticks.length / 2)]).toEqual(
axis.min + expectedDelta * Math.floor(ticks.length / 2)
);
expect(ticks[ticks.length - 1]).to.equal(axis.min + expectedDelta * (ticks.length - 1));
expect(ticks[ticks.length - 1]).toEqual(axis.min + expectedDelta * (ticks.length - 1));
});
});
});

View file

@ -17,15 +17,17 @@
* under the License.
*/
import { Axis } from './panel_utils';
export function generateTicksProvider() {
function floorInBase(n: any, base: any) {
function floorInBase(n: number, base: number) {
return base * Math.floor(n / base);
}
function generateTicks(axis: any) {
function generateTicks(axis: Axis) {
const returnTicks = [];
let tickSize = 2;
let delta = axis.delta;
let delta = axis.delta || 0;
let steps = 0;
let tickVal;
let tickCount = 0;
@ -46,16 +48,14 @@ export function generateTicksProvider() {
axis.tickSize = tickSize * Math.pow(1024, steps);
// Calculate the new ticks
const tickMin = floorInBase(axis.min, axis.tickSize);
const tickMin = floorInBase(axis.min || 0, axis.tickSize);
do {
tickVal = tickMin + tickCount++ * axis.tickSize;
returnTicks.push(tickVal);
} while (tickVal < axis.max);
} while (tickVal < (axis.max || 0));
return returnTicks;
}
return function(axis: any) {
return generateTicks(axis);
};
return (axis: Axis) => generateTicks(axis);
}

View file

@ -17,13 +17,11 @@
* under the License.
*/
// @ts-ignore
import { timezoneProvider } from 'ui/vis/lib/timezone';
import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public';
import { VisParams } from 'ui/vis';
import { i18n } from '@kbn/i18n';
import { TimelionVisualizationDependencies } from '../plugin';
import { TimeRange, esFilters, esQuery, Query } from '../../../../../plugins/data/public';
import { timezoneProvider, VisParams } from '../legacy_imports';
import { TimelionVisDependencies } from '../plugin';
interface Stats {
cacheCount: number;
@ -33,9 +31,25 @@ interface Stats {
sheetTime: number;
}
interface Sheet {
list: Array<Record<string, unknown>>;
render: Record<string, unknown>;
export interface Series {
_global?: boolean;
_hide?: boolean;
_id?: number;
_title?: string;
color?: string;
data: Array<Record<number, number>>;
fit: string;
label: string;
split: string;
stack?: boolean;
type: string;
}
export interface Sheet {
list: Series[];
render?: {
grid?: boolean;
};
type: string;
}
@ -46,8 +60,11 @@ export interface TimelionSuccessResponse {
type: KIBANA_CONTEXT_NAME;
}
export function getTimelionRequestHandler(dependencies: TimelionVisualizationDependencies) {
const { uiSettings, http, timefilter } = dependencies;
export function getTimelionRequestHandler({
uiSettings,
http,
timefilter,
}: TimelionVisDependencies) {
const timezone = timezoneProvider(uiSettings)();
return async function({

View file

@ -20,12 +20,13 @@
import moment from 'moment';
import { i18n } from '@kbn/i18n';
import { IUiSettingsClient } from 'kibana/public';
export function xaxisFormatterProvider(config: any) {
export function xaxisFormatterProvider(config: IUiSettingsClient) {
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) {
if (parts === null || parts[1] === null || parts[2] === null) {
throw new Error(
i18n.translate('timelion.panels.timechart.unknownIntervalErrorMessage', {
defaultMessage: 'Unknown interval',
@ -48,7 +49,5 @@ export function xaxisFormatterProvider(config: any) {
return config.get('dateFormat');
}
return function(esInterval: any) {
return getFormat(esInterval);
};
return (esInterval: any) => getFormat(esInterval);
}

View file

@ -0,0 +1,5 @@
@import 'src/legacy/ui/public/styles/styling_constants';
@import './timelion_vis';
@import './timelion_editor';
@import './components/index';

View file

@ -17,5 +17,9 @@
* under the License.
*/
import './_tick_generator.js';
describe('Timelion', function() {});
import { PluginInitializerContext } from '../../../../core/public';
import { TimelionVisPlugin as Plugin } from './plugin';
export function plugin(initializerContext: PluginInitializerContext) {
return new Plugin(initializerContext);
}

View file

@ -0,0 +1,37 @@
/*
* 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 './legacy_imports';
import { setup as visualizationsSetup } from '../../visualizations/public/np_ready/public/legacy';
import { TimelionVisSetupDependencies } from './plugin';
import { plugin } from '.';
const setupPlugins: Readonly<TimelionVisSetupDependencies> = {
expressions: npSetup.plugins.expressions,
data: npSetup.plugins.data,
visualizations: visualizationsSetup,
};
const pluginInstance = plugin({} as PluginInitializerContext);
export const setup = pluginInstance.setup(npSetup.core, setupPlugins);
export const start = pluginInstance.start(npStart.core, npStart.plugins);

View file

@ -0,0 +1,29 @@
/*
* 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.
*/
export { npSetup, npStart } from 'ui/new_platform';
export { PluginsStart } from 'ui/new_platform/new_platform';
// @ts-ignore
export { DefaultEditorSize } from 'ui/vis/editor_size';
// @ts-ignore
export { timezoneProvider } from 'ui/vis/lib/timezone';
export { VisParams, Vis } from 'ui/vis';
export { VisOptionsProps } from 'ui/vis/editors/default';
export { useValidation } from 'ui/vis/editors/default/controls/agg_utils';

View file

@ -0,0 +1,76 @@
/*
* 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,
Plugin,
PluginInitializerContext,
IUiSettingsClient,
HttpSetup,
} from 'kibana/public';
import { Plugin as ExpressionsPlugin } from 'src/plugins/expressions/public';
import { DataPublicPluginSetup, TimefilterContract } from 'src/plugins/data/public';
import { PluginsStart } from './legacy_imports';
import { VisualizationsSetup } from '../../visualizations/public/np_ready/public';
import { getTimelionVisualizationConfig } from './timelion_vis_fn';
import { getTimelionVisDefinition } from './timelion_vis_type';
import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services';
type TimelionVisCoreSetup = CoreSetup<TimelionVisSetupDependencies>;
/** @internal */
export interface TimelionVisDependencies extends Partial<CoreStart> {
uiSettings: IUiSettingsClient;
http: HttpSetup;
timefilter: TimefilterContract;
}
/** @internal */
export interface TimelionVisSetupDependencies {
expressions: ReturnType<ExpressionsPlugin['setup']>;
visualizations: VisualizationsSetup;
data: DataPublicPluginSetup;
}
/** @internal */
export class TimelionVisPlugin implements Plugin<void, void> {
constructor(public initializerContext: PluginInitializerContext) {}
public async setup(
core: TimelionVisCoreSetup,
{ expressions, visualizations, data }: TimelionVisSetupDependencies
) {
const dependencies: TimelionVisDependencies = {
uiSettings: core.uiSettings,
http: core.http,
timefilter: data.query.timefilter.timefilter,
};
expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies));
visualizations.types.createReactVisualization(getTimelionVisDefinition(dependencies));
}
public start(core: CoreStart, plugins: PluginsStart) {
setIndexPatterns(plugins.data.indexPatterns);
setSavedObjectsClient(core.savedObjects.client);
}
}

View file

@ -20,9 +20,9 @@
import React, { useCallback } from 'react';
import { EuiPanel } from '@elastic/eui';
import { VisOptionsProps } from 'ui/vis/editors/default';
import { VisParams } from '../timelion_vis_fn';
import { TimelionInterval, TimelionExpressionInput } from '../components';
import { VisOptionsProps } from './legacy_imports';
import { VisParams } from './timelion_vis_fn';
import { TimelionInterval, TimelionExpressionInput } from './components';
function TimelionOptions({ stateParams, setValue, setValidity }: VisOptionsProps<VisParams>) {
const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [

View file

@ -20,9 +20,9 @@
import { get } from 'lodash';
import { i18n } from '@kbn/i18n';
import { ExpressionFunction, KibanaContext, Render } from 'src/plugins/expressions/public';
import { getTimelionRequestHandler } from './vis/timelion_request_handler';
import { TimelionVisualizationDependencies } from './plugin';
import { TIMELION_VIS_NAME } from './vis';
import { getTimelionRequestHandler } from './helpers/timelion_request_handler';
import { TIMELION_VIS_NAME } from './timelion_vis_type';
import { TimelionVisDependencies } from './plugin';
const name = 'timelion_vis';
@ -42,7 +42,7 @@ export type VisParams = Arguments;
type Return = Promise<Render<RenderValue>>;
export const getTimelionVisualizationConfig = (
dependencies: TimelionVisualizationDependencies
dependencies: TimelionVisDependencies
): ExpressionFunction<typeof name, Context, Arguments, Return> => ({
name,
type: 'render',

View file

@ -20,20 +20,17 @@
import React from 'react';
import { i18n } from '@kbn/i18n';
import { VisOptionsProps } from 'ui/vis/editors/default';
import { DefaultEditorSize } from '../../../visualizations/public';
import { KibanaContextProvider } from '../../../../../plugins/kibana_react/public';
import { getTimelionRequestHandler } from './timelion_request_handler';
import visConfigTemplate from './timelion_vis.html';
import { TimelionVisualizationDependencies } from '../plugin';
// @ts-ignore
import { AngularVisController } from '../../../../ui/public/vis/vis_types/angular_vis_type';
import { KibanaContextProvider } from '../../../../plugins/kibana_react/public';
import { DefaultEditorSize, VisOptionsProps } from './legacy_imports';
import { getTimelionRequestHandler } from './helpers/timelion_request_handler';
import { TimelionVisComponent, TimelionVisComponentProp } from './components';
import { TimelionOptions } from './timelion_options';
import { VisParams } from '../timelion_vis_fn';
import { VisParams } from './timelion_vis_fn';
import { TimelionVisDependencies } from './plugin';
export const TIMELION_VIS_NAME = 'timelion';
export function getTimelionVisualization(dependencies: TimelionVisualizationDependencies) {
export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) {
const { http, uiSettings } = dependencies;
const timelionRequestHandler = getTimelionRequestHandler(dependencies);
@ -46,13 +43,16 @@ export function getTimelionVisualization(dependencies: TimelionVisualizationDepe
description: i18n.translate('timelion.timelionDescription', {
defaultMessage: 'Build time-series using functional expressions',
}),
visualization: AngularVisController,
visConfig: {
defaults: {
expression: '.es(*)',
interval: 'auto',
},
template: visConfigTemplate,
component: (props: TimelionVisComponentProp) => (
<KibanaContextProvider services={{ ...dependencies }}>
<TimelionVisComponent {...props} />
</KibanaContextProvider>
),
},
editorConfig: {
optionsTemplate: (props: VisOptionsProps<VisParams>) => (

View file

@ -51,7 +51,8 @@
"types": [
"node",
"jest",
"react"
"react",
"flot"
]
},
"include": [

View file

@ -4398,6 +4398,13 @@
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.0.tgz#cbb49815a5e1129d5f23836a98d65d93822409af"
integrity sha512-dxdRrUov2HVTbSRFX+7xwUPlbGYVEZK6PrSqClg2QPos3PNe0bCajkDDkDeeC1znjSH03KOEqVbXpnJuWa2wgQ==
"@types/flot@^0.0.31":
version "0.0.31"
resolved "https://registry.yarnpkg.com/@types/flot/-/flot-0.0.31.tgz#0daca37c6c855b69a0a7e2e37dd0f84b3db8c8c1"
integrity sha512-X+RcMQCqPlQo8zPT6cUFTd/PoYBShMQlHUeOXf05jWlfYnvLuRmluB9z+2EsOKFgUzqzZve5brx+gnFxBaHEUw==
dependencies:
"@types/jquery" "*"
"@types/geojson@*":
version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
@ -4601,7 +4608,7 @@
resolved "https://registry.yarnpkg.com/@types/joi/-/joi-13.6.1.tgz#325486a397504f8e22c8c551dc8b0e1d41d5d5ae"
integrity sha512-JxZ0NP8NuB0BJOXi1KvAA6rySLTPmhOy4n2gzSFq/IFM3LNFm0h+2Vn/bPPgEYlWqzS2NPeLgKqfm75baX+Hog==
"@types/jquery@^3.3.31":
"@types/jquery@*", "@types/jquery@^3.3.31":
version "3.3.31"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.31.tgz#27c706e4bf488474e1cb54a71d8303f37c93451b"
integrity sha512-Lz4BAJihoFw5nRzKvg4nawXPzutkv7wmfQ5121avptaSIXlDNJCUuxZxX/G+9EVidZGuO0UBlk+YjKbwRKJigg==