From fd5dacf17e3ad4b8b5319bc64bd9dcfab621dfe0 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 24 Sep 2020 16:26:06 +0300 Subject: [PATCH 01/13] Update styles --- .../public/_timelion_editor.scss | 15 ------------- .../public/_timelion_vis.scss | 12 ---------- .../{_panel.scss => _timelion_vis.scss} | 22 ++++++++++++++++--- .../components/{_index.scss => index.scss} | 2 +- .../vis_type_timelion/public/index.scss | 3 --- .../vis_type_timelion/public/plugin.ts | 2 +- 6 files changed, 21 insertions(+), 35 deletions(-) delete mode 100644 src/plugins/vis_type_timelion/public/_timelion_editor.scss delete mode 100644 src/plugins/vis_type_timelion/public/_timelion_vis.scss rename src/plugins/vis_type_timelion/public/components/{_panel.scss => _timelion_vis.scss} (78%) rename src/plugins/vis_type_timelion/public/components/{_index.scss => index.scss} (60%) delete mode 100644 src/plugins/vis_type_timelion/public/index.scss diff --git a/src/plugins/vis_type_timelion/public/_timelion_editor.scss b/src/plugins/vis_type_timelion/public/_timelion_editor.scss deleted file mode 100644 index a9331930a86ff..0000000000000 --- a/src/plugins/vis_type_timelion/public/_timelion_editor.scss +++ /dev/null @@ -1,15 +0,0 @@ -.visEditor--timelion { - vis-options-react-wrapper, - .visEditorSidebar__options, - .visEditorSidebar__timelionOptions { - flex: 1 1 auto; - display: flex; - flex-direction: column; - } - - .visEditor__sidebar { - @include euiBreakpoint('xs', 's', 'm') { - width: 100%; - } - } -} diff --git a/src/plugins/vis_type_timelion/public/_timelion_vis.scss b/src/plugins/vis_type_timelion/public/_timelion_vis.scss deleted file mode 100644 index e7175bf3c0c2a..0000000000000 --- a/src/plugins/vis_type_timelion/public/_timelion_vis.scss +++ /dev/null @@ -1,12 +0,0 @@ -.timVis { - min-width: 100%; - display: flex; - flex-direction: column; - - .timChart { - min-width: 100%; - flex: 1; - display: flex; - flex-direction: column; - } -} diff --git a/src/plugins/vis_type_timelion/public/components/_panel.scss b/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss similarity index 78% rename from src/plugins/vis_type_timelion/public/components/_panel.scss rename to src/plugins/vis_type_timelion/public/components/_timelion_vis.scss index c4d591bc82cad..7d8ceb6ee50b5 100644 --- a/src/plugins/vis_type_timelion/public/components/_panel.scss +++ b/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss @@ -1,9 +1,25 @@ -.timChart { - height: 100%; - width: 100%; +.timVis { + min-width: 100%; display: flex; flex-direction: column; + .timChart { + min-width: 100%; + flex: 1; + display: flex; + flex-direction: column; + } +} + +.visEditor--timelion { + .visEditorSidebar__timelionOptions { + flex: 1 1 auto; + display: flex; + flex-direction: column; + } +} + +.timChart { // Custom Jquery FLOT / schema selectors // Cannot change at the moment diff --git a/src/plugins/vis_type_timelion/public/components/_index.scss b/src/plugins/vis_type_timelion/public/components/index.scss similarity index 60% rename from src/plugins/vis_type_timelion/public/components/_index.scss rename to src/plugins/vis_type_timelion/public/components/index.scss index 707c9dafebe2b..a541c66e6e913 100644 --- a/src/plugins/vis_type_timelion/public/components/_index.scss +++ b/src/plugins/vis_type_timelion/public/components/index.scss @@ -1,2 +1,2 @@ -@import 'panel'; +@import 'timelion_vis'; @import 'timelion_expression_input'; diff --git a/src/plugins/vis_type_timelion/public/index.scss b/src/plugins/vis_type_timelion/public/index.scss deleted file mode 100644 index 00e9a88520961..0000000000000 --- a/src/plugins/vis_type_timelion/public/index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './timelion_vis'; -@import './timelion_editor'; -@import './components/index'; diff --git a/src/plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts index e2c7efec34c7f..bcae70344106f 100644 --- a/src/plugins/vis_type_timelion/public/plugin.ts +++ b/src/plugins/vis_type_timelion/public/plugin.ts @@ -39,7 +39,7 @@ import { getTimelionVisDefinition } from './timelion_vis_type'; import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services'; import { ConfigSchema } from '../config'; -import './index.scss'; +import './components/index.scss'; import { getArgValueSuggestions } from './helpers/arg_value_suggestions'; /** @internal */ From e443f61b58c9f50323760a3a4338d211b93208f8 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Thu, 24 Sep 2020 16:26:59 +0300 Subject: [PATCH 02/13] Implement toExpressionAst fn --- .../public/timelion_vis_fn.ts | 9 ++++- .../public/timelion_vis_type.tsx | 2 + .../vis_type_timelion/public/to_ast.ts | 37 +++++++++++++++++++ .../public/legacy/build_pipeline.ts | 5 --- 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 src/plugins/vis_type_timelion/public/to_ast.ts diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index d3c6ca5d90371..9935f9f76fa56 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -44,9 +44,16 @@ interface RenderValue { export type VisParams = Arguments; +export type TimelionExpressionFunctionDefinition = ExpressionFunctionDefinition< + 'timelion_vis', + Input, + Arguments, + Output +>; + export const getTimelionVisualizationConfig = ( dependencies: TimelionVisDependencies -): ExpressionFunctionDefinition<'timelion_vis', Input, Arguments, Output> => ({ +): TimelionExpressionFunctionDefinition => ({ name: 'timelion_vis', type: 'render', inputTypes: ['kibana_context', 'null'], diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 8fdde175708e0..345835ab79c1a 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -26,6 +26,7 @@ import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; import { TimelionVisComponent, TimelionVisComponentProp } from './components'; import { TimelionOptions, TimelionOptionsProps } from './timelion_options'; import { TimelionVisDependencies } from './plugin'; +import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; @@ -63,6 +64,7 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) defaultSize: DefaultEditorSize.MEDIUM, }, requestHandler: timelionRequestHandler, + toExpressionAst, responseHandler: 'none', inspectorAdapters: {}, getSupportedTriggers: () => { diff --git a/src/plugins/vis_type_timelion/public/to_ast.ts b/src/plugins/vis_type_timelion/public/to_ast.ts new file mode 100644 index 0000000000000..d07cb759e190f --- /dev/null +++ b/src/plugins/vis_type_timelion/public/to_ast.ts @@ -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 { buildExpression, buildExpressionFunction } from '../../expressions/public'; +import { Vis } from '../../visualizations/public'; +import { TimelionExpressionFunctionDefinition } from './timelion_vis_fn'; + +const escapeString = (data: string): string => { + return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); +}; + +export const toExpressionAst = (vis: Vis) => { + const timelion = buildExpressionFunction('timelion_vis', { + expression: escapeString(vis.params.expression), + interval: escapeString(vis.params.interval), + }); + + const ast = buildExpression([timelion]); + + return ast.toAst(); +}; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.ts b/src/plugins/visualizations/public/legacy/build_pipeline.ts index 438a6d2337724..92ff29e9bc6d4 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.ts @@ -264,11 +264,6 @@ export const buildPipelineVisFunction: BuildPipelineVisFunction = { const paramsArray = [paramsJson, uiStateJson].filter((param) => Boolean(param)); return `tsvb ${paramsArray.join(' ')}`; }, - timelion: (params) => { - const expression = prepareString('expression', params.expression); - const interval = prepareString('interval', params.interval); - return `timelion_vis ${expression}${interval}`; - }, table: (params, schemas) => { const visConfig = { ...params, From f91560b97d29eed0ddabae275a8f611c1ee3fc40 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Fri, 25 Sep 2020 15:32:18 +0300 Subject: [PATCH 03/13] Implement renderer --- src/plugins/timelion/public/index.scss | 6 + .../public/components/_timelion_vis.scss | 30 +- .../public/components/chart.tsx | 41 -- .../public/components/index.ts | 1 - .../public/components/panel.tsx | 399 ----------------- .../public/components/timelion_vis.tsx | 405 +++++++++++++++++- .../vis_type_timelion/public/plugin.ts | 5 +- .../public/timelion_vis_fn.ts | 13 +- .../public/timelion_vis_renderer.tsx | 63 +++ .../public/timelion_vis_type.tsx | 6 - 10 files changed, 475 insertions(+), 494 deletions(-) delete mode 100644 src/plugins/vis_type_timelion/public/components/chart.tsx delete mode 100644 src/plugins/vis_type_timelion/public/components/panel.tsx create mode 100644 src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx diff --git a/src/plugins/timelion/public/index.scss b/src/plugins/timelion/public/index.scss index 6bf7133287c51..f39e0c18a2870 100644 --- a/src/plugins/timelion/public/index.scss +++ b/src/plugins/timelion/public/index.scss @@ -10,3 +10,9 @@ @import './app'; @import './base'; @import './directives/index'; + +// these styles is needed to be loaded here explicitly if the timelion visualization was not opened in browser +// styles for timelion visualization are lazy loaded only while a vis is opened +// this will duplicate styles only if both Timelion app and timelion visualization are loaded +// could be left here as it is since the Timelion app is deprecated +@import '../../vis_type_timelion/public/components/index.scss'; diff --git a/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss b/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss index 7d8ceb6ee50b5..6729d400523cd 100644 --- a/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss +++ b/src/plugins/vis_type_timelion/public/components/_timelion_vis.scss @@ -1,25 +1,9 @@ -.timVis { - min-width: 100%; +.timChart { + height: 100%; + width: 100%; display: flex; flex-direction: column; - .timChart { - min-width: 100%; - flex: 1; - display: flex; - flex-direction: column; - } -} - -.visEditor--timelion { - .visEditorSidebar__timelionOptions { - flex: 1 1 auto; - display: flex; - flex-direction: column; - } -} - -.timChart { // Custom Jquery FLOT / schema selectors // Cannot change at the moment @@ -74,3 +58,11 @@ white-space: nowrap; font-weight: $euiFontWeightBold; } + +.visEditor--timelion { + .visEditorSidebar__timelionOptions { + flex: 1 1 auto; + display: flex; + flex-direction: column; + } +} diff --git a/src/plugins/vis_type_timelion/public/components/chart.tsx b/src/plugins/vis_type_timelion/public/components/chart.tsx deleted file mode 100644 index 15a376d4e9638..0000000000000 --- a/src/plugins/vis_type_timelion/public/components/chart.tsx +++ /dev/null @@ -1,41 +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 React from 'react'; - -import { Sheet } from '../helpers/timelion_request_handler'; -import { Panel } from './panel'; -import { ExprVisAPIEvents } from '../../../visualizations/public'; - -interface ChartComponentProp { - applyFilter: ExprVisAPIEvents['applyFilter']; - interval: string; - renderComplete(): void; - seriesList: Sheet; -} - -function ChartComponent(props: ChartComponentProp) { - if (!props.seriesList) { - return null; - } - - return ; -} - -export { ChartComponent }; diff --git a/src/plugins/vis_type_timelion/public/components/index.ts b/src/plugins/vis_type_timelion/public/components/index.ts index c70caab8dd70c..8d7d32a3ba262 100644 --- a/src/plugins/vis_type_timelion/public/components/index.ts +++ b/src/plugins/vis_type_timelion/public/components/index.ts @@ -19,4 +19,3 @@ export * from './timelion_expression_input'; export * from './timelion_interval'; -export * from './timelion_vis'; diff --git a/src/plugins/vis_type_timelion/public/components/panel.tsx b/src/plugins/vis_type_timelion/public/components/panel.tsx deleted file mode 100644 index 9c30a6b75d6db..0000000000000 --- a/src/plugins/vis_type_timelion/public/components/panel.tsx +++ /dev/null @@ -1,399 +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 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 '../../../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'; -import { ExprVisAPIEvents } from '../../../visualizations/public'; - -interface CrosshairPlot extends jquery.flot.plot { - setCrosshair: (pos: Position) => void; - clearCrosshair: () => void; -} - -interface PanelProps { - applyFilter: ExprVisAPIEvents['applyFilter']; - 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 = '
'; - -function Panel({ interval, seriesList, renderComplete, applyFilter }: PanelProps) { - const kibana = useKibana(); - const [chart, setChart] = useState(() => cloneDeep(seriesList.list)); - const [canvasElem, setCanvasElem] = useState(); - const [chartElem, setChartElem] = useState(); - - const [originalColorMap, setOriginalColorMap] = useState(() => new Map()); - - const [highlightedSeries, setHighlightedSeries] = useState(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: HTMLDivElement | null) => { - if (node !== null) { - setCanvasElem(node); - } - }, []); - - const elementRef = useCallback((node: HTMLDivElement | null) => { - if (node !== null) { - setChartElem(node); - } - }, []); - - useEffect( - () => () => { - if (chartElem) { - $(chartElem).off('plotselected').off('plothover').off('mouseleave'); - } - }, - [chartElem] - ); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - 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 (canvasElem && get(plotData, '[0]._global.legend.showTime', true)) { - const 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); - - // We're setting this flag on top on the series object belonging to the flot library, so we're simply casting here. - if ((series as { _hide?: boolean })._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 (legendValueNumbers) { - if (y == null) { - legendValueNumbers.eq(i).empty(); - } else { - let label = y.toFixed(precision); - const formatter = ((series.yaxis as unknown) as Axis).tickFormatter; - if (formatter) { - label = formatter(Number(label), (series.yaxis as unknown) as Axis); - } - legendValueNumbers.eq(i).text(`(${label})`); - } - } - } - }, - [plot, legendValueNumbers, unhighlightSeries, legendCaption] - ); - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - 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 as CrosshairPlot).setCrosshair(pos); - debouncedSetLegendNumbers(pos); - }, - [plot, debouncedSetLegendNumbers] - ); - const mouseLeaveHandler = useCallback(() => { - if (!plot) { - return; - } - (plot as CrosshairPlot).clearCrosshair(); - clearLegendNumbers(); - }, [plot, clearLegendNumbers]); - - const plotSelectedHandler = useCallback( - (event: JQuery.TriggeredEvent, ranges: Ranges) => { - applyFilter({ - timeFieldName: '*', - filters: [ - { - range: { - '*': { - gte: ranges.xaxis.from, - lte: ranges.xaxis.to, - }, - }, - }, - ], - }); - }, - [applyFilter] - ); - - 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 ( -
-
{title}
-
-
- ); -} - -export { Panel }; diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx index aa594c749b600..e3e66699e659b 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx @@ -17,34 +17,397 @@ * under the License. */ -import React from 'react'; +import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +import $ from 'jquery'; +import moment from 'moment-timezone'; +import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; +import { useResizeObserver } from '@elastic/eui'; -import { IUiSettingsClient } from 'kibana/public'; -import { ChartComponent } from './chart'; -import { VisParams } from '../timelion_vis_fn'; -import { TimelionSuccessResponse } from '../helpers/timelion_request_handler'; -import { ExprVis } from '../../../visualizations/public'; +import { useKibana } from '../../../kibana_react/public'; +import '../flot'; +import { DEFAULT_TIME_FORMAT } from '../../common/lib'; -export interface TimelionVisComponentProp { - config: IUiSettingsClient; +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'; + +import './index.scss'; + +interface CrosshairPlot extends jquery.flot.plot { + setCrosshair: (pos: Position) => void; + clearCrosshair: () => void; +} + +interface TimelionVisComponentProps { + fireEvent(event: any): void; + interval: string; + seriesList: Sheet; renderComplete(): void; - updateStatus: object; - vis: ExprVis; - visData: TimelionSuccessResponse; - visParams: VisParams; } -function TimelionVisComponent(props: TimelionVisComponentProp) { +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 = '
'; + +function TimelionVisComponent({ + interval, + seriesList, + renderComplete, + fireEvent, +}: TimelionVisComponentProps) { + const kibana = useKibana(); + const [chart, setChart] = useState(() => cloneDeep(seriesList.list)); + const [canvasElem, setCanvasElem] = useState(); + const [chartElem, setChartElem] = useState(null); + + const [originalColorMap, setOriginalColorMap] = useState(() => new Map()); + + const [highlightedSeries, setHighlightedSeries] = useState(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: HTMLDivElement | null) => { + if (node !== null) { + setCanvasElem(node); + } + }, []); + + const elementRef = useCallback((node: HTMLDivElement | null) => { + if (node !== null) { + setChartElem(node); + } + }, []); + + useEffect( + () => () => { + if (chartElem) { + $(chartElem).off('plotselected').off('plothover').off('mouseleave'); + } + }, + [chartElem] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + 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 (canvasElem && get(plotData, '[0]._global.legend.showTime', true)) { + const 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?.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?.clientWidth, renderComplete, kibana.services, interval, updateCaption] + ); + + const dimensions = useResizeObserver(chartElem); + + useEffect(() => { + updatePlot(chart, seriesList.render && seriesList.render.grid); + }, [chart, updatePlot, seriesList.render, dimensions]); + + 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); + + // We're setting this flag on top on the series object belonging to the flot library, so we're simply casting here. + if ((series as { _hide?: boolean })._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 (legendValueNumbers) { + if (y == null) { + legendValueNumbers.eq(i).empty(); + } else { + let label = y.toFixed(precision); + const formatter = ((series.yaxis as unknown) as Axis).tickFormatter; + if (formatter) { + label = formatter(Number(label), (series.yaxis as unknown) as Axis); + } + legendValueNumbers.eq(i).text(`(${label})`); + } + } + } + }, + [plot, legendValueNumbers, unhighlightSeries, legendCaption] + ); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + 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 as CrosshairPlot).setCrosshair(pos); + debouncedSetLegendNumbers(pos); + }, + [plot, debouncedSetLegendNumbers] + ); + const mouseLeaveHandler = useCallback(() => { + if (!plot) { + return; + } + (plot as CrosshairPlot).clearCrosshair(); + clearLegendNumbers(); + }, [plot, clearLegendNumbers]); + + const plotSelectedHandler = useCallback( + (event: JQuery.TriggeredEvent, ranges: Ranges) => { + fireEvent({ + name: 'applyFilter', + data: { + timeFieldName: '*', + filters: [ + { + range: { + '*': { + gte: ranges.xaxis.from, + lte: ranges.xaxis.to, + }, + }, + }, + ], + }, + }); + }, + [fireEvent] + ); + + 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 ( -
- +
+
{title}
+
); } -export { TimelionVisComponent }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TimelionVisComponent as default }; diff --git a/src/plugins/vis_type_timelion/public/plugin.ts b/src/plugins/vis_type_timelion/public/plugin.ts index bcae70344106f..bb8fb6b298a07 100644 --- a/src/plugins/vis_type_timelion/public/plugin.ts +++ b/src/plugins/vis_type_timelion/public/plugin.ts @@ -39,8 +39,8 @@ import { getTimelionVisDefinition } from './timelion_vis_type'; import { setIndexPatterns, setSavedObjectsClient } from './helpers/plugin_services'; import { ConfigSchema } from '../config'; -import './components/index.scss'; import { getArgValueSuggestions } from './helpers/arg_value_suggestions'; +import { getTimelionVisRenderer } from './timelion_vis_renderer'; /** @internal */ export interface TimelionVisDependencies extends Partial { @@ -93,7 +93,8 @@ export class TimelionVisPlugin }; expressions.registerFunction(() => getTimelionVisualizationConfig(dependencies)); - visualizations.createReactVisualization(getTimelionVisDefinition(dependencies)); + expressions.registerRenderer(getTimelionVisRenderer(dependencies)); + visualizations.createBaseVisualization(getTimelionVisDefinition(dependencies)); return { isUiEnabled: this.initializerContext.config.get().ui.enabled, diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index 9935f9f76fa56..2984ea63ded4f 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -24,20 +24,23 @@ import { KibanaContext, Render, } from 'src/plugins/expressions/public'; -import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; +import { + getTimelionRequestHandler, + TimelionSuccessResponse, +} from './helpers/timelion_request_handler'; import { TIMELION_VIS_NAME } from './timelion_vis_type'; import { TimelionVisDependencies } from './plugin'; import { Filter, Query, TimeRange } from '../../data/common'; type Input = KibanaContext | null; -type Output = Promise>; +type Output = Promise>; interface Arguments { expression: string; interval: string; } -interface RenderValue { - visData: Input; +export interface TimelionRenderValue { + visData: TimelionSuccessResponse; visType: 'timelion'; visParams: VisParams; } @@ -89,7 +92,7 @@ export const getTimelionVisualizationConfig = ( return { type: 'render', - as: 'visualization', + as: 'timelion_vis', value: { visParams, visType: TIMELION_VIS_NAME, diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx new file mode 100644 index 0000000000000..12b193f263321 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -0,0 +1,63 @@ +/* + * 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, { lazy, Suspense } from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { KibanaContextProvider } from '../../kibana_react/public'; +import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; +import { VisualizationContainer } from '../../visualizations/public'; +import { TimelionVisDependencies } from './plugin'; +import { TimelionRenderValue } from './timelion_vis_fn'; +// @ts-ignore +const TimelionVisComponent = lazy(() => import('./components/timelion_vis')); + +export const getTimelionVisRenderer: ( + deps: TimelionVisDependencies +) => ExpressionRenderDefinition = (deps) => ({ + name: 'timelion_vis', + displayName: 'Nimelion visualization', + reuseDomNode: true, + render: async (domNode, { visData, visParams }, handlers) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); + + const [seriesList] = visData.sheet; + + render( + + }> + + {seriesList && ( + + )} + + + , + domNode + ); + }, +}); diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 345835ab79c1a..5a2a09609fcd2 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -23,7 +23,6 @@ import { i18n } from '@kbn/i18n'; import { KibanaContextProvider } from '../../kibana_react/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; -import { TimelionVisComponent, TimelionVisComponentProp } from './components'; import { TimelionOptions, TimelionOptionsProps } from './timelion_options'; import { TimelionVisDependencies } from './plugin'; import { toExpressionAst } from './to_ast'; @@ -49,11 +48,6 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) expression: '.es(*)', interval: 'auto', }, - component: (props: TimelionVisComponentProp) => ( - - - - ), }, editorConfig: { optionsTemplate: (props: TimelionOptionsProps) => ( From b337e8dcb1a4d3d7bb75a4b67d64c1e46e6c9c18 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 28 Sep 2020 11:18:33 +0300 Subject: [PATCH 04/13] Update unit tests --- .../vis_type_timelion/public/components/timelion_vis.tsx | 2 +- .../public/legacy/__snapshots__/build_pipeline.test.ts.snap | 2 -- .../visualizations/public/legacy/build_pipeline.test.ts | 6 ------ 3 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx index e3e66699e659b..af94a5c370bc6 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx @@ -17,7 +17,7 @@ * under the License. */ -import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; +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'; diff --git a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap index fae777b98ef63..fef249fd4ca2c 100644 --- a/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap +++ b/src/plugins/visualizations/public/legacy/__snapshots__/build_pipeline.test.ts.snap @@ -30,6 +30,4 @@ exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunct exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles tile_map function 1`] = `"tilemap visConfig='{\\"metric\\":{},\\"dimensions\\":{\\"metric\\":{\\"accessor\\":0,\\"label\\":\\"\\",\\"format\\":{},\\"params\\":{},\\"aggType\\":\\"\\"},\\"geohash\\":1,\\"geocentroid\\":3}}' "`; -exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles timelion function 1`] = `"timelion_vis expression='foo' interval='bar' "`; - exports[`visualize loader pipeline helpers: build pipeline buildPipelineVisFunction handles vega function 1`] = `"vega spec='this is a test' "`; diff --git a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts index 2d92b386253b0..7422f44015312 100644 --- a/src/plugins/visualizations/public/legacy/build_pipeline.test.ts +++ b/src/plugins/visualizations/public/legacy/build_pipeline.test.ts @@ -117,12 +117,6 @@ describe('visualize loader pipeline helpers: build pipeline', () => { expect(actual).toMatchSnapshot(); }); - it('handles timelion function', () => { - const params = { expression: 'foo', interval: 'bar' }; - const actual = buildPipelineVisFunction.timelion(params, schemasDef, uiState); - expect(actual).toMatchSnapshot(); - }); - describe('handles table function', () => { it('without splits or buckets', () => { const params = { foo: 'bar' }; From 5662e4bb9143a1301ff4b0375f67919f78dd5aa5 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Mon, 28 Sep 2020 11:45:21 +0300 Subject: [PATCH 05/13] Add unit tests --- .../public/__snapshots__/to_ast.test.ts.snap | 21 ++++++++++ .../vis_type_timelion/public/to_ast.test.ts | 39 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/plugins/vis_type_timelion/public/__snapshots__/to_ast.test.ts.snap create mode 100644 src/plugins/vis_type_timelion/public/to_ast.test.ts diff --git a/src/plugins/vis_type_timelion/public/__snapshots__/to_ast.test.ts.snap b/src/plugins/vis_type_timelion/public/__snapshots__/to_ast.test.ts.snap new file mode 100644 index 0000000000000..9e32a6c4ae17c --- /dev/null +++ b/src/plugins/vis_type_timelion/public/__snapshots__/to_ast.test.ts.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`timelion vis toExpressionAst function should match basic snapshot 1`] = ` +Object { + "chain": Array [ + Object { + "arguments": Object { + "expression": Array [ + ".es(*)", + ], + "interval": Array [ + "auto", + ], + }, + "function": "timelion_vis", + "type": "function", + }, + ], + "type": "expression", +} +`; diff --git a/src/plugins/vis_type_timelion/public/to_ast.test.ts b/src/plugins/vis_type_timelion/public/to_ast.test.ts new file mode 100644 index 0000000000000..7ca5f704733b8 --- /dev/null +++ b/src/plugins/vis_type_timelion/public/to_ast.test.ts @@ -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 { Vis } from 'src/plugins/visualizations/public'; +import { toExpressionAst } from './to_ast'; + +describe('timelion vis toExpressionAst function', () => { + let vis: Vis; + + beforeEach(() => { + vis = { + params: { + expression: '.es(*)', + interval: 'auto', + }, + } as any; + }); + + it('should match basic snapshot', () => { + const actual = toExpressionAst(vis); + expect(actual).toMatchSnapshot(); + }); +}); From aa0ed044fffefbd8ddac62cfa4b0a5027d4a718d Mon Sep 17 00:00:00 2001 From: sulemanof Date: Tue, 29 Sep 2020 11:27:26 +0300 Subject: [PATCH 06/13] Update types --- .../helpers/timelion_request_handler.ts | 4 ++-- .../public/timelion_options.tsx | 13 ++++++----- .../public/timelion_vis_fn.ts | 4 ++-- .../public/timelion_vis_renderer.tsx | 22 ++++++++----------- .../vis_type_timelion/public/to_ast.ts | 4 ++-- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts index 3442f84599fb8..975d12a152d89 100644 --- a/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts +++ b/src/plugins/vis_type_timelion/public/helpers/timelion_request_handler.ts @@ -19,10 +19,10 @@ import { i18n } from '@kbn/i18n'; import { KIBANA_CONTEXT_NAME } from 'src/plugins/expressions/public'; -import { VisParams } from '../../../visualizations/public'; import { TimeRange, Filter, esQuery, Query } from '../../../data/public'; import { TimelionVisDependencies } from '../plugin'; import { getTimezone } from './get_timezone'; +import { TimelionVisParams } from '../timelion_vis_fn'; interface Stats { cacheCount: number; @@ -77,7 +77,7 @@ export function getTimelionRequestHandler({ timeRange: TimeRange; filters: Filter[]; query: Query; - visParams: VisParams; + visParams: TimelionVisParams; }): Promise { const expression = visParams.expression; diff --git a/src/plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx index dfe017d3a273f..dc2703194c35c 100644 --- a/src/plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -21,17 +21,18 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; -import { VisParams } from './timelion_vis_fn'; +import { TimelionVisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; -export type TimelionOptionsProps = VisOptionsProps; +export type TimelionOptionsProps = VisOptionsProps; function TimelionOptions({ stateParams, setValue, setValidity }: TimelionOptionsProps) { - const setInterval = useCallback((value: VisParams['interval']) => setValue('interval', value), [ - setValue, - ]); + const setInterval = useCallback( + (value: TimelionVisParams['interval']) => setValue('interval', value), + [setValue] + ); const setExpressionInput = useCallback( - (value: VisParams['expression']) => setValue('expression', value), + (value: TimelionVisParams['expression']) => setValue('expression', value), [setValue] ); diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts index 2984ea63ded4f..a0cd410e197ff 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts +++ b/src/plugins/vis_type_timelion/public/timelion_vis_fn.ts @@ -42,10 +42,10 @@ interface Arguments { export interface TimelionRenderValue { visData: TimelionSuccessResponse; visType: 'timelion'; - visParams: VisParams; + visParams: TimelionVisParams; } -export type VisParams = Arguments; +export type TimelionVisParams = Arguments; export type TimelionExpressionFunctionDefinition = ExpressionFunctionDefinition< 'timelion_vis', diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 12b193f263321..61fc5cb08f319 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -43,19 +43,15 @@ export const getTimelionVisRenderer: ( const [seriesList] = visData.sheet; render( - - }> - - {seriesList && ( - - )} - - + + + + , domNode ); diff --git a/src/plugins/vis_type_timelion/public/to_ast.ts b/src/plugins/vis_type_timelion/public/to_ast.ts index d07cb759e190f..7044bbf4e5831 100644 --- a/src/plugins/vis_type_timelion/public/to_ast.ts +++ b/src/plugins/vis_type_timelion/public/to_ast.ts @@ -19,13 +19,13 @@ import { buildExpression, buildExpressionFunction } from '../../expressions/public'; import { Vis } from '../../visualizations/public'; -import { TimelionExpressionFunctionDefinition } from './timelion_vis_fn'; +import { TimelionExpressionFunctionDefinition, TimelionVisParams } from './timelion_vis_fn'; const escapeString = (data: string): string => { return data.replace(/\\/g, `\\\\`).replace(/'/g, `\\'`); }; -export const toExpressionAst = (vis: Vis) => { +export const toExpressionAst = (vis: Vis) => { const timelion = buildExpressionFunction('timelion_vis', { expression: escapeString(vis.params.expression), interval: escapeString(vis.params.interval), From f3d3e31c48a46818786925e5332245ccfe78d46f Mon Sep 17 00:00:00 2001 From: sulemanof Date: Tue, 29 Sep 2020 11:37:48 +0300 Subject: [PATCH 07/13] Remove unused vars --- .../{timelion_vis.tsx => timelion_vis_component.tsx} | 0 .../vis_type_timelion/public/timelion_vis_renderer.tsx | 7 +++---- 2 files changed, 3 insertions(+), 4 deletions(-) rename src/plugins/vis_type_timelion/public/components/{timelion_vis.tsx => timelion_vis_component.tsx} (100%) diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx similarity index 100% rename from src/plugins/vis_type_timelion/public/components/timelion_vis.tsx rename to src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 61fc5cb08f319..ac595b21f65db 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -17,9 +17,8 @@ * under the License. */ -import React, { lazy, Suspense } from 'react'; +import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { EuiLoadingSpinner } from '@elastic/eui'; import { KibanaContextProvider } from '../../kibana_react/public'; import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; @@ -27,13 +26,13 @@ import { VisualizationContainer } from '../../visualizations/public'; import { TimelionVisDependencies } from './plugin'; import { TimelionRenderValue } from './timelion_vis_fn'; // @ts-ignore -const TimelionVisComponent = lazy(() => import('./components/timelion_vis')); +const TimelionVisComponent = lazy(() => import('./components/timelion_vis_component')); export const getTimelionVisRenderer: ( deps: TimelionVisDependencies ) => ExpressionRenderDefinition = (deps) => ({ name: 'timelion_vis', - displayName: 'Nimelion visualization', + displayName: 'Timelion visualization', reuseDomNode: true, render: async (domNode, { visData, visParams }, handlers) => { handlers.onDestroy(() => { From 6052295efcba4db6ceffb77ade95729ce9eff2fe Mon Sep 17 00:00:00 2001 From: sulemanof Date: Tue, 29 Sep 2020 13:07:33 +0300 Subject: [PATCH 08/13] Fix types --- src/plugins/vis_type_timelion/public/to_ast.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timelion/public/to_ast.test.ts b/src/plugins/vis_type_timelion/public/to_ast.test.ts index 7ca5f704733b8..8a9d4b83f94d2 100644 --- a/src/plugins/vis_type_timelion/public/to_ast.test.ts +++ b/src/plugins/vis_type_timelion/public/to_ast.test.ts @@ -18,10 +18,11 @@ */ import { Vis } from 'src/plugins/visualizations/public'; +import { TimelionVisParams } from './timelion_vis_fn'; import { toExpressionAst } from './to_ast'; describe('timelion vis toExpressionAst function', () => { - let vis: Vis; + let vis: Vis; beforeEach(() => { vis = { From c50d1f0e1834c6036931afeb3f3595b1633e110a Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 30 Sep 2020 12:08:13 +0300 Subject: [PATCH 09/13] Update types --- src/plugins/expressions/common/expression_renderers/types.ts | 2 +- .../public/components/timelion_vis_component.tsx | 5 +++-- .../vis_type_timelion/public/timelion_vis_renderer.tsx | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/plugins/expressions/common/expression_renderers/types.ts b/src/plugins/expressions/common/expression_renderers/types.ts index 7b3e812eafedd..b760e7b32a7d2 100644 --- a/src/plugins/expressions/common/expression_renderers/types.ts +++ b/src/plugins/expressions/common/expression_renderers/types.ts @@ -28,7 +28,7 @@ export interface ExpressionRenderDefinition { /** * A user friendly name of the renderer as will be displayed to user in UI. */ - displayName: string; + displayName?: string; /** * Help text as will be displayed to user. A sentence or few about what this diff --git a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx index af94a5c370bc6..a7b623ac8680c 100644 --- a/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_type_timelion/public/components/timelion_vis_component.tsx @@ -23,6 +23,7 @@ import moment from 'moment-timezone'; import { debounce, compact, get, each, cloneDeep, last, map } from 'lodash'; import { useResizeObserver } from '@elastic/eui'; +import { IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { useKibana } from '../../../kibana_react/public'; import '../flot'; import { DEFAULT_TIME_FORMAT } from '../../common/lib'; @@ -48,10 +49,10 @@ interface CrosshairPlot extends jquery.flot.plot { } interface TimelionVisComponentProps { - fireEvent(event: any): void; + fireEvent: IInterpreterRenderHandlers['event']; interval: string; seriesList: Sheet; - renderComplete(): void; + renderComplete: IInterpreterRenderHandlers['done']; } interface Position { diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index ac595b21f65db..cd48ac049f1aa 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -20,8 +20,8 @@ import React, { lazy } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { ExpressionRenderDefinition } from 'src/plugins/expressions'; import { KibanaContextProvider } from '../../kibana_react/public'; -import { ExpressionRenderDefinition } from '../../expressions/common/expression_renderers'; import { VisualizationContainer } from '../../visualizations/public'; import { TimelionVisDependencies } from './plugin'; import { TimelionRenderValue } from './timelion_vis_fn'; @@ -34,7 +34,7 @@ export const getTimelionVisRenderer: ( name: 'timelion_vis', displayName: 'Timelion visualization', reuseDomNode: true, - render: async (domNode, { visData, visParams }, handlers) => { + render: (domNode, { visData, visParams }, handlers) => { handlers.onDestroy(() => { unmountComponentAtNode(domNode); }); From 84efe768384b789e48c4038d77e9a285a53e86de Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 30 Sep 2020 12:42:16 +0300 Subject: [PATCH 10/13] Show error message when no data --- src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index cd48ac049f1aa..6d132176e9953 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -42,7 +42,7 @@ export const getTimelionVisRenderer: ( const [seriesList] = visData.sheet; render( - + Date: Wed, 30 Sep 2020 12:52:53 +0300 Subject: [PATCH 11/13] Update ExpressionRenderDefinition api --- ...expressions-public.expressionrenderdefinition.displayname.md | 2 +- ...expressions-server.expressionrenderdefinition.displayname.md | 2 +- src/plugins/expressions/public/public.api.md | 2 +- src/plugins/expressions/server/server.api.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.displayname.md b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.displayname.md index 9d5f7609ee6cd..a957ecd63f043 100644 --- a/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.displayname.md +++ b/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.displayname.md @@ -9,5 +9,5 @@ A user friendly name of the renderer as will be displayed to user in UI. Signature: ```typescript -displayName: string; +displayName?: string; ``` diff --git a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionrenderdefinition.displayname.md b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionrenderdefinition.displayname.md index e936e25cee6ca..8ae5aa2f1790e 100644 --- a/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionrenderdefinition.displayname.md +++ b/docs/development/plugins/expressions/server/kibana-plugin-plugins-expressions-server.expressionrenderdefinition.displayname.md @@ -9,5 +9,5 @@ A user friendly name of the renderer as will be displayed to user in UI. Signature: ```typescript -displayName: string; +displayName?: string; ``` diff --git a/src/plugins/expressions/public/public.api.md b/src/plugins/expressions/public/public.api.md index 162f0ef6824f5..5c0fd8ab1a572 100644 --- a/src/plugins/expressions/public/public.api.md +++ b/src/plugins/expressions/public/public.api.md @@ -429,7 +429,7 @@ export interface ExpressionImage { // // @public (undocumented) export interface ExpressionRenderDefinition { - displayName: string; + displayName?: string; help?: string; name: string; render: (domNode: HTMLElement, config: Config, handlers: IInterpreterRenderHandlers) => void | Promise; diff --git a/src/plugins/expressions/server/server.api.md b/src/plugins/expressions/server/server.api.md index 6ac251ea005b4..d8872ee416017 100644 --- a/src/plugins/expressions/server/server.api.md +++ b/src/plugins/expressions/server/server.api.md @@ -401,7 +401,7 @@ export interface ExpressionImage { // // @public (undocumented) export interface ExpressionRenderDefinition { - displayName: string; + displayName?: string; help?: string; name: string; render: (domNode: HTMLElement, config: Config, handlers: IInterpreterRenderHandlers) => void | Promise; From fb1d99b93a059341f5cceae05182093e325dd972 Mon Sep 17 00:00:00 2001 From: sulemanof Date: Wed, 30 Sep 2020 13:59:39 +0300 Subject: [PATCH 12/13] Update renderer when there is no data --- .../vis_type_timelion/public/timelion_vis_renderer.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx index 6d132176e9953..13a279138a8e4 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_renderer.tsx @@ -40,9 +40,16 @@ export const getTimelionVisRenderer: ( }); const [seriesList] = visData.sheet; + const showNoResult = !seriesList || !seriesList.list.length; + + if (showNoResult) { + // send the render complete event when there is no data to show + // to notify that a chart is updated + handlers.done(); + } render( - + Date: Thu, 1 Oct 2020 12:07:06 +0300 Subject: [PATCH 13/13] Make options component lazy --- .../public/tag_cloud_vis_renderer.tsx | 1 - .../public/timelion_options.tsx | 34 +++++++++++++------ .../public/timelion_vis_type.tsx | 11 +++--- 3 files changed, 29 insertions(+), 17 deletions(-) diff --git a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx index d37aa5f6fe409..b433ed9cbed21 100644 --- a/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx +++ b/src/plugins/vis_type_tagcloud/public/tag_cloud_vis_renderer.tsx @@ -25,7 +25,6 @@ import { ExpressionRenderDefinition } from '../../expressions/common/expression_ import { TagCloudVisDependencies } from './plugin'; import { TagCloudVisRenderValue } from './tag_cloud_fn'; -// @ts-ignore const TagCloudChart = lazy(() => import('./components/tag_cloud_chart')); export const getTagCloudVisRenderer: ( diff --git a/src/plugins/vis_type_timelion/public/timelion_options.tsx b/src/plugins/vis_type_timelion/public/timelion_options.tsx index dc2703194c35c..1ef8088c7a714 100644 --- a/src/plugins/vis_type_timelion/public/timelion_options.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_options.tsx @@ -21,12 +21,22 @@ import React, { useCallback } from 'react'; import { EuiPanel } from '@elastic/eui'; import { VisOptionsProps } from 'src/plugins/vis_default_editor/public'; +import { KibanaContextProvider } from '../../kibana_react/public'; + import { TimelionVisParams } from './timelion_vis_fn'; import { TimelionInterval, TimelionExpressionInput } from './components'; +import { TimelionVisDependencies } from './plugin'; export type TimelionOptionsProps = VisOptionsProps; -function TimelionOptions({ stateParams, setValue, setValidity }: TimelionOptionsProps) { +function TimelionOptions({ + services, + stateParams, + setValue, + setValidity, +}: TimelionOptionsProps & { + services: TimelionVisDependencies; +}) { const setInterval = useCallback( (value: TimelionVisParams['interval']) => setValue('interval', value), [setValue] @@ -37,15 +47,19 @@ function TimelionOptions({ stateParams, setValue, setValidity }: TimelionOptions ); return ( - - - - + + + + + + ); } -export { TimelionOptions }; +// default export required for React.Lazy +// eslint-disable-next-line import/no-default-export +export { TimelionOptions as default }; diff --git a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx index 5a2a09609fcd2..a5425478e46ac 100644 --- a/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx +++ b/src/plugins/vis_type_timelion/public/timelion_vis_type.tsx @@ -17,18 +17,19 @@ * under the License. */ -import React from 'react'; +import React, { lazy } from 'react'; import { i18n } from '@kbn/i18n'; -import { KibanaContextProvider } from '../../kibana_react/public'; import { DefaultEditorSize } from '../../vis_default_editor/public'; import { getTimelionRequestHandler } from './helpers/timelion_request_handler'; -import { TimelionOptions, TimelionOptionsProps } from './timelion_options'; +import { TimelionOptionsProps } from './timelion_options'; import { TimelionVisDependencies } from './plugin'; import { toExpressionAst } from './to_ast'; import { VIS_EVENT_TO_TRIGGER } from '../../visualizations/public'; +const TimelionOptions = lazy(() => import('./timelion_options')); + export const TIMELION_VIS_NAME = 'timelion'; export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) { @@ -51,9 +52,7 @@ export function getTimelionVisDefinition(dependencies: TimelionVisDependencies) }, editorConfig: { optionsTemplate: (props: TimelionOptionsProps) => ( - - - + ), defaultSize: DefaultEditorSize.MEDIUM, },