diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index c26b5a6ad811421..d970287253e3f21 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -111,6 +111,7 @@ import { toAbsoluteDates, boundsDescendingRaw, getResponseInspectorStats, + calcAutoIntervalLessThan, // tabify tabifyAggResponse, tabifyGetColumns, @@ -217,6 +218,7 @@ export const search = { termsAggFilter, toAbsoluteDates, boundsDescendingRaw, + calcAutoIntervalLessThan, }, getResponseInspectorStats, tabifyAggResponse, diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts index c0ce9d5f8804e18..435335fe9dd25ec 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.test.ts @@ -5,33 +5,9 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import type { DataView } from '@kbn/data-plugin/common'; -import type { Panel, Series } from '../../common/types'; -import { convertTSVBtoLensConfiguration } from '.'; - -const dataViewsMap: Record = { - test1: { id: 'test1', title: 'test1', timeFieldName: 'timeField1' } as DataView, - test2: { - id: 'test2', - title: 'test2', - timeFieldName: 'timeField2', - } as DataView, - test3: { id: 'test3', title: 'test3', timeFieldName: 'timeField3' } as DataView, -}; -const getDataview = (id: string): DataView | undefined => dataViewsMap[id]; -jest.mock('../services', () => { - return { - getDataViewsStart: jest.fn(() => { - return { - getDefault: jest.fn(() => { - return { id: '12345', title: 'default', timeFieldName: '@timestamp' }; - }), - get: getDataview, - }; - }), - }; -}); +import type { Panel } from '../../common/types'; +import { convertTSVBtoLensConfiguration } from '.'; const model = { axis_position: 'left', @@ -61,7 +37,7 @@ const model = { } as Panel; describe('convertTSVBtoLensConfiguration', () => { - test('should return null for a non timeseries chart', async () => { + test('should return null for a not supported chart', async () => { const metricModel = { ...model, type: 'metric', @@ -78,279 +54,4 @@ describe('convertTSVBtoLensConfiguration', () => { const triggerOptions = await convertTSVBtoLensConfiguration(stringIndexPatternModel); expect(triggerOptions).toBeNull(); }); - - test('should return null for a non supported aggregation', async () => { - const nonSupportedAggModel = { - ...model, - series: [ - { - ...model.series[0], - metrics: [ - { - type: 'std_deviation', - }, - ] as Series['metrics'], - }, - ], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(nonSupportedAggModel); - expect(triggerOptions).toBeNull(); - }); - - test('should return options for a supported aggregation', async () => { - const triggerOptions = await convertTSVBtoLensConfiguration(model); - expect(triggerOptions).toStrictEqual({ - configuration: { - extents: { yLeftExtent: { mode: 'full' }, yRightExtent: { mode: 'full' } }, - fill: '0', - gridLinesVisibility: { x: false, yLeft: false, yRight: false }, - legend: { - isVisible: false, - maxLines: 1, - position: 'right', - shouldTruncate: false, - showSingleSeries: false, - }, - }, - type: 'lnsXY', - layers: { - '0': { - axisPosition: 'left', - chartType: 'line', - collapseFn: undefined, - indexPatternId: 'test2', - metrics: [ - { - agg: 'count', - color: '#000000', - fieldName: 'document', - isFullReference: false, - params: {}, - }, - ], - palette: { - name: 'default', - type: 'palette', - }, - splitWithDateHistogram: false, - timeFieldName: 'timeField2', - timeInterval: 'auto', - dropPartialBuckets: false, - }, - }, - }); - }); - - test('should return area for timeseries line chart with fill > 0', async () => { - const modelWithFill = { - ...model, - series: [ - { - ...model.series[0], - fill: '0.3', - stacked: 'none', - }, - ], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithFill); - expect(triggerOptions?.layers[0].chartType).toBe('area'); - }); - - test('should return timeShift in the params if it is provided', async () => { - const modelWithFill = { - ...model, - series: [ - { - ...model.series[0], - offset_time: '1h', - }, - ], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithFill); - expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.shift).toBe('1h'); - }); - - test('should return filter in the params if it is provided', async () => { - const modelWithFill = { - ...model, - series: [ - { - ...model.series[0], - filter: { - language: 'kuery', - query: 'test', - }, - }, - ], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithFill); - expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.kql).toBe('test'); - }); - - test('should return splitFilters information if the chart is broken down by filters', async () => { - const modelWithSplitFilters = { - ...model, - series: [ - { - ...model.series[0], - split_mode: 'filters', - split_filters: [ - { - color: 'rgba(188,0,85,1)', - filter: { - language: 'kuery', - query: '', - }, - id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', - }, - ], - }, - ], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithSplitFilters); - expect(triggerOptions?.layers[0]?.splitFilters).toStrictEqual([ - { - color: 'rgba(188,0,85,1)', - filter: { - language: 'kuery', - query: '', - }, - id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', - }, - ]); - }); - - test('should return termsParams information if the chart is broken down by terms including series agg collapse fn', async () => { - const modelWithTerms = { - ...model, - series: [ - { - ...model.series[0], - metrics: [ - ...model.series[0].metrics, - { - type: 'series_agg', - function: 'sum', - }, - ], - split_mode: 'terms', - terms_size: 6, - terms_direction: 'desc', - terms_order_by: '_key', - }, - ] as unknown as Series[], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithTerms); - expect(triggerOptions?.layers[0]?.collapseFn).toStrictEqual('sum'); - expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ - size: 6, - otherBucket: false, - orderDirection: 'desc', - orderBy: { type: 'alphabetical' }, - includeIsRegex: false, - excludeIsRegex: false, - parentFormat: { - id: 'terms', - }, - }); - }); - - test('should return include exclude information if the chart is broken down by terms', async () => { - const modelWithTerms = { - ...model, - series: [ - { - ...model.series[0], - split_mode: 'terms', - terms_size: 6, - terms_direction: 'desc', - terms_order_by: '_key', - terms_include: 't.*', - }, - ] as unknown as Series[], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithTerms); - expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ - size: 6, - otherBucket: false, - orderDirection: 'desc', - orderBy: { type: 'alphabetical' }, - includeIsRegex: true, - include: ['t.*'], - excludeIsRegex: false, - parentFormat: { - id: 'terms', - }, - }); - }); - - test('should return custom time interval if it is given', async () => { - const modelWithTerms = { - ...model, - interval: '1h', - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithTerms); - expect(triggerOptions?.layers[0]?.timeInterval).toBe('1h'); - }); - - test('should return dropPartialbuckets if enabled', async () => { - const modelWithDropBuckets = { - ...model, - drop_last_bucket: 1, - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithDropBuckets); - expect(triggerOptions?.layers[0]?.dropPartialBuckets).toBe(true); - }); - - test('should return the correct chart configuration', async () => { - const modelWithConfig = { - ...model, - show_legend: 1, - legend_position: 'bottom', - truncate_legend: 0, - show_grid: 1, - series: [{ ...model.series[0], fill: '0.3', separate_axis: 1, axis_position: 'right' }], - }; - const triggerOptions = await convertTSVBtoLensConfiguration(modelWithConfig); - expect(triggerOptions).toStrictEqual({ - configuration: { - extents: { yLeftExtent: { mode: 'full' }, yRightExtent: { mode: 'full' } }, - fill: '0.3', - gridLinesVisibility: { x: true, yLeft: true, yRight: true }, - legend: { - isVisible: true, - maxLines: 1, - position: 'bottom', - shouldTruncate: false, - showSingleSeries: true, - }, - }, - type: 'lnsXY', - layers: { - '0': { - axisPosition: 'right', - chartType: 'area_stacked', - collapseFn: undefined, - indexPatternId: 'test2', - metrics: [ - { - agg: 'count', - color: '#000000', - fieldName: 'document', - isFullReference: false, - params: {}, - }, - ], - palette: { - name: 'default', - type: 'palette', - }, - splitWithDateHistogram: false, - timeFieldName: 'timeField2', - timeInterval: 'auto', - dropPartialBuckets: false, - }, - }, - }); - }); }); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts index 071001381f0f163..08806f42e197638 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/index.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { TimeRange } from '@kbn/data-plugin/common'; import type { Panel } from '../../common/types'; import { PANEL_TYPES } from '../../common/enums'; import { ConvertTsvbToLensVisualization } from './types'; @@ -18,6 +19,10 @@ const getConvertFnByType = ( const { convertToLens } = await import('./timeseries'); return convertToLens; }, + [PANEL_TYPES.TOP_N]: async () => { + const { convertToLens } = await import('./top_n'); + return convertToLens; + }, }; return convertionFns[type]?.(); @@ -28,12 +33,17 @@ const getConvertFnByType = ( * Returns the Lens model, only if it is supported. If not, it returns null. * In case of null, the menu item is disabled and the user can't navigate to Lens. */ -export const convertTSVBtoLensConfiguration = async (model: Panel) => { - // Disables the option for not timeseries charts, for the string mode and for series with annotations +export const convertTSVBtoLensConfiguration = async (model: Panel, timeRange?: TimeRange) => { + // Disables the option for not supported charts, for the string mode and for series with annotations if (!model.use_kibana_indexes || (model.annotations && model.annotations.length > 0)) { return null; } + // Disables if model is invalid + if (model.isModelInvalid) { + return null; + } + const convertFn = await getConvertFnByType(model.type); - return (await convertFn?.(model)) ?? null; + return (await convertFn?.(model, timeRange)) ?? null; }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/layers/get_layer.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/layers/get_layer.ts index 8483c6f2c31918a..de4c61e54837ef5 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/layers/get_layer.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/layers/get_layer.ts @@ -41,18 +41,21 @@ export const getLayerConfiguration = ( model: Panel, series: VisSeries, splitFields: string[], - timeField?: string, - splitWithDateHistogram?: boolean + xFieldName?: string, + xMode?: string, + splitWithDateHistogram?: boolean, + window?: string ): VisualizeEditorLayersContext => { const layer = model.series[layerIdx]; const palette = layer.palette as PaletteOutput; const splitFilters = convertSplitFilters(layer); const { metrics: metricsArray, seriesAgg } = series; const filter = convertFilter(layer); - const metrics = convertMetrics(layer, metricsArray, filter); + const metrics = convertMetrics(layer, metricsArray, filter, window); return { indexPatternId, - timeFieldName: timeField, + xFieldName, + xMode, chartType, axisPosition: layer.separate_axis ? layer.axis_position : model.axis_position, ...(layer.terms_field && { splitFields }), diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts index a132b861889fa85..c341351c6641820 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/filter_ratio_formula.ts @@ -7,6 +7,7 @@ */ import type { Query } from '@kbn/es-query'; +import { addTimeRangeToFormula } from '.'; import type { Metric } from '../../../../common/types'; import { SUPPORTED_METRICS } from './supported_metrics'; @@ -14,15 +15,15 @@ const escapeQuotes = (str: string) => { return str?.replace(/'/g, "\\'"); }; -const constructFilterRationFormula = (operation: string, metric?: Query) => { +const constructFilterRationFormula = (operation: string, metric?: Query, window?: string) => { return `${operation}${metric?.language === 'lucene' ? 'lucene' : 'kql'}='${ metric?.query && typeof metric?.query === 'string' ? escapeQuotes(metric?.query) : metric?.query ?? '*' - }')`; + }'${addTimeRangeToFormula(window)})`; }; -export const getFilterRatioFormula = (currentMetric: Metric) => { +export const getFilterRatioFormula = (currentMetric: Metric, window?: string) => { // eslint-disable-next-line @typescript-eslint/naming-convention const { numerator, denominator, metric_agg, field } = currentMetric; let aggregation = SUPPORTED_METRICS.count; @@ -38,16 +39,18 @@ export const getFilterRatioFormula = (currentMetric: Metric) => { if (aggregation.name === 'counter_rate') { const numeratorFormula = constructFilterRationFormula( `${aggregation.name}(max('${field}',`, - numerator + numerator, + window ); const denominatorFormula = constructFilterRationFormula( `${aggregation.name}(max('${field}',`, - denominator + denominator, + window ); return `${numeratorFormula}) / ${denominatorFormula})`; } else { - const numeratorFormula = constructFilterRationFormula(operation, numerator); - const denominatorFormula = constructFilterRationFormula(operation, denominator); + const numeratorFormula = constructFilterRationFormula(operation, numerator, window); + const denominatorFormula = constructFilterRationFormula(operation, denominator, window); return `${numeratorFormula} / ${denominatorFormula}`; } }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/index.ts index 9a79fb804e3ba50..08a7599ba3f8dfd 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/index.ts @@ -13,3 +13,4 @@ export * from './parent_pipeline_formula'; export * from './sibling_pipeline_formula'; export * from './filter_ratio_formula'; export * from './parent_pipeline_series'; +export * from './validate_metrics'; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_converter.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_converter.ts index 65427bfa2a2689c..6ea305fdface3c2 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_converter.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_converter.ts @@ -27,7 +27,8 @@ export const convertFilter = (series: Series): Filter | void => { const convertMetric = ( series: Series, metric: VisualizeEditorLayersContext['metrics'][number], - filter: Filter | void + filter: Filter | void, + interval?: string ) => ({ ...metric, color: metric.color ?? series.color, @@ -35,11 +36,13 @@ const convertMetric = ( ...metric.params, ...(series.offset_time && { shift: series.offset_time }), ...(filter && filter), + ...(interval && { window: interval }), }, }); export const convertMetrics = ( series: Series, metrics: VisualizeEditorLayersContext['metrics'], - filter: Filter | void -) => metrics.map((metric) => convertMetric(series, metric, filter)); + filter: Filter | void, + interval?: string +) => metrics.map((metric) => convertMetric(series, metric, filter, interval)); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.test.ts index 83922a54aa1113b..b07bcecb7f79011 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.test.ts @@ -44,7 +44,7 @@ describe('getPercentilesSeries', () => { value: '90', }, ] as Metric['percentiles']; - const config = getPercentilesSeries(percentiles, 'bytes'); + const config = getPercentilesSeries(percentiles, 'everything', '', 'bytes'); expect(config).toStrictEqual([ { agg: 'percentile', @@ -82,7 +82,7 @@ describe('getPercentileRankSeries', () => { test('should return correct config for multiple percentile ranks', () => { const values = ['1', '5', '7'] as Metric['values']; const colors = ['#68BC00', 'rgba(0,63,188,1)', 'rgba(188,38,0,1)'] as Metric['colors']; - const config = getPercentileRankSeries(values, colors, 'day_of_week_i'); + const config = getPercentileRankSeries(values, colors, 'everything', '', 'day_of_week_i'); expect(config).toStrictEqual([ { agg: 'percentile_rank', diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts index fdc7f4ca2f6d018..400adb4571fb7cc 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/metrics_helpers.ts @@ -6,18 +6,28 @@ * Side Public License, v 1. */ +import { utc } from 'moment'; +import { search } from '@kbn/data-plugin/public'; +import dateMath from '@kbn/datemath'; +import { TimeRange, UI_SETTINGS } from '@kbn/data-plugin/common'; +import { getUISettings } from '../../../services'; import type { Metric } from '../../../../common/types'; import { SUPPORTED_METRICS } from './supported_metrics'; import { getFilterRatioFormula } from './filter_ratio_formula'; import { getParentPipelineSeriesFormula } from './parent_pipeline_formula'; import { getSiblingPipelineSeriesFormula } from './sibling_pipeline_formula'; -export const getPercentilesSeries = (percentiles: Metric['percentiles'], fieldName?: string) => { +export const getPercentilesSeries = ( + percentiles: Metric['percentiles'], + splitMode: string, + layerColor: string, + fieldName?: string +) => { return percentiles?.map((percentile) => { return { agg: 'percentile', isFullReference: false, - color: percentile.color, + color: splitMode === 'everything' ? percentile.color : layerColor, fieldName: fieldName ?? 'document', params: { percentile: percentile.value }, }; @@ -27,19 +37,42 @@ export const getPercentilesSeries = (percentiles: Metric['percentiles'], fieldNa export const getPercentileRankSeries = ( values: Metric['values'], colors: Metric['colors'], + splitMode: string, + layerColor: string, fieldName?: string ) => { return values?.map((value, index) => { return { agg: 'percentile_rank', isFullReference: false, - color: colors?.[index], + color: splitMode === 'everything' ? colors?.[index] : layerColor, fieldName: fieldName ?? 'document', params: { value }, }; }); }; +export const getWindow = (interval?: string, timeRange?: TimeRange) => { + let window = interval || '1h'; + + if (timeRange && !interval) { + const { from, to } = timeRange; + const timerange = utc(to).valueOf() - utc(from).valueOf(); + const maxBars = getUISettings().get(UI_SETTINGS.HISTOGRAM_BAR_TARGET); + + const duration = search.aggs.calcAutoIntervalLessThan(maxBars, timerange); + const unit = + dateMath.units.find((u) => { + const value = duration.as(u); + return Number.isInteger(value); + }) || 'ms'; + + window = `${duration.as(unit)}${unit}`; + } + + return window; +}; + export const getTimeScale = (metric: Metric) => { const supportedTimeScales = ['1s', '1m', '1h', '1d']; let timeScale; @@ -60,6 +93,10 @@ export const getFormulaSeries = (script: string) => { ]; }; +export const addTimeRangeToFormula = (window?: string) => { + return window ? `, timeRange='${window}'` : ''; +}; + export const getPipelineAgg = (subFunctionMetric: Metric) => { const pipelineAggMap = SUPPORTED_METRICS[subFunctionMetric.type]; if (!pipelineAggMap) { @@ -71,7 +108,8 @@ export const getPipelineAgg = (subFunctionMetric: Metric) => { export const getFormulaEquivalent = ( currentMetric: Metric, metrics: Metric[], - metaValue?: number + metaValue?: number, + window?: string ) => { const aggregation = SUPPORTED_METRICS[currentMetric.type]?.name; switch (currentMetric.type) { @@ -80,7 +118,7 @@ export const getFormulaEquivalent = ( case 'min_bucket': case 'sum_bucket': case 'positive_only': { - return getSiblingPipelineSeriesFormula(currentMetric.type, currentMetric, metrics); + return getSiblingPipelineSeriesFormula(currentMetric.type, currentMetric, metrics, window); } case 'count': { return `${aggregation}()`; @@ -88,10 +126,12 @@ export const getFormulaEquivalent = ( case 'percentile': { return `${aggregation}(${currentMetric.field}${ metaValue ? `, percentile=${metaValue}` : '' - })`; + }${addTimeRangeToFormula(window)})`; } case 'percentile_rank': { - return `${aggregation}(${currentMetric.field}${metaValue ? `, value=${metaValue}` : ''})`; + return `${aggregation}(${currentMetric.field}${ + metaValue ? `, value=${metaValue}` : '' + }${addTimeRangeToFormula(window)})`; } case 'cumulative_sum': case 'derivative': @@ -110,20 +150,34 @@ export const getFormulaEquivalent = ( subFunctionMetric, pipelineAgg, currentMetric.type, - metaValue + metaValue, + window ); } case 'positive_rate': { - return `${aggregation}(max(${currentMetric.field}))`; + return `${aggregation}(max(${currentMetric.field}${addTimeRangeToFormula(window)}))`; } case 'filter_ratio': { - return getFilterRatioFormula(currentMetric); + return getFilterRatioFormula(currentMetric, window); } case 'static': { return `${currentMetric.value}`; } - default: { + case 'std_deviation': { + if (currentMetric.mode === 'lower') { + return `average(${currentMetric.field}${addTimeRangeToFormula(window)}) - ${ + currentMetric.sigma || 1.5 + } * ${aggregation}(${currentMetric.field}${addTimeRangeToFormula(window)})`; + } + if (currentMetric.mode === 'upper') { + return `average(${currentMetric.field}${addTimeRangeToFormula(window)}) + ${ + currentMetric.sigma || 1.5 + } * ${aggregation}(${currentMetric.field}${addTimeRangeToFormula(window)})`; + } return `${aggregation}(${currentMetric.field})`; } + default: { + return `${aggregation}(${currentMetric.field}${addTimeRangeToFormula(window)})`; + } } }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_formula.ts index e00fe505df1fdfe..7d12e77fdb8bc63 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_formula.ts @@ -8,14 +8,15 @@ import type { Metric, MetricType } from '../../../../common/types'; import { SUPPORTED_METRICS } from './supported_metrics'; -import { getFilterRatioFormula } from './filter_ratio_formula'; +import { getFormulaEquivalent } from './metrics_helpers'; export const getParentPipelineSeriesFormula = ( metrics: Metric[], subFunctionMetric: Metric, pipelineAgg: string, aggregation: MetricType, - percentileValue?: number + percentileValue?: number, + window?: string ) => { let formula = ''; const aggregationMap = SUPPORTED_METRICS[aggregation]; @@ -42,28 +43,13 @@ export const getParentPipelineSeriesFormula = ( additionalSubFunction.field ?? '' }${additionalFunctionArgs ?? ''})))`; } else { - let additionalFunctionArgs; - if (pipelineAgg === 'percentile' && percentileValue) { - additionalFunctionArgs = `, percentile=${percentileValue}`; - } - if (pipelineAgg === 'percentile_rank' && percentileValue) { - additionalFunctionArgs = `, value=${percentileValue}`; - } - if (pipelineAgg === 'filter_ratio') { - const script = getFilterRatioFormula(subFunctionMetric); - if (!script) { - return null; - } - formula = `${aggregationMap.name}(${script}${additionalFunctionArgs ?? ''})`; - } else if (pipelineAgg === 'counter_rate') { - formula = `${aggregationMap.name}(${pipelineAgg}(max(${subFunctionMetric.field}${ - additionalFunctionArgs ? `${additionalFunctionArgs}` : '' - })))`; - } else { - formula = `${aggregationMap.name}(${pipelineAgg}(${subFunctionMetric.field}${ - additionalFunctionArgs ? `${additionalFunctionArgs}` : '' - }))`; + const subFormula = getFormulaEquivalent(subFunctionMetric, metrics, percentileValue, window); + + if (!subFormula) { + return null; } + + formula = `${aggregationMap.name}(${subFormula})`; } return formula; }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.test.ts index 40264b89491b778..db8faccf5976f7c 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.test.ts @@ -91,7 +91,7 @@ describe('getParentPipelineSeries', () => { { field: 'AvgTicketPrice', id: '04558549-f19f-4a87-9923-27df8b81af3e', - type: 'std_deviation', + type: 'sum_of_squares_bucket', }, { field: '04558549-f19f-4a87-9923-27df8b81af3e', diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.ts index 6a263ca8bb44de2..6f0caf3837b5ca4 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/parent_pipeline_series.ts @@ -17,11 +17,12 @@ export const computeParentSeries = ( currentMetric: Metric, subFunctionMetric: Metric, pipelineAgg: string, - meta?: number + meta?: number, + window?: string ) => { const aggregationMap = SUPPORTED_METRICS[aggregation]; if (subFunctionMetric.type === 'filter_ratio') { - const script = getFilterRatioFormula(subFunctionMetric); + const script = getFilterRatioFormula(subFunctionMetric, window); if (!script) { return null; } @@ -49,7 +50,8 @@ export const computeParentSeries = ( export const getParentPipelineSeries = ( aggregation: MetricType, currentMetricIdx: number, - metrics: Metric[] + metrics: Metric[], + window?: string ) => { const currentMetric = metrics[currentMetricIdx]; // percentile value is derived from the field Id. It has the format xxx-xxx-xxx-xxx[percentile] @@ -73,7 +75,8 @@ export const getParentPipelineSeries = ( subFunctionMetric, pipelineAgg, aggregation, - metaValue + metaValue, + window ); if (!formula) { return null; @@ -85,7 +88,8 @@ export const getParentPipelineSeries = ( currentMetric, subFunctionMetric, pipelineAgg, - metaValue + metaValue, + window ); } }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/sibling_pipeline_formula.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/sibling_pipeline_formula.ts index b3e141b11e598e1..4243d6c0dd27cf5 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/sibling_pipeline_formula.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/sibling_pipeline_formula.ts @@ -7,12 +7,14 @@ */ import type { Metric, MetricType } from '../../../../common/types'; +import { getFormulaEquivalent } from './metrics_helpers'; import { SUPPORTED_METRICS } from './supported_metrics'; export const getSiblingPipelineSeriesFormula = ( aggregation: MetricType, currentMetric: Metric, - metrics: Metric[] + metrics: Metric[], + window?: string ) => { const [nestedFieldId, nestedMeta] = currentMetric.field?.split('[') ?? []; const subFunctionMetric = metrics.find((metric) => metric.id === nestedFieldId); @@ -43,18 +45,15 @@ export const getSiblingPipelineSeriesFormula = ( additionalSubFunctionField ?? '' }))${minimumValue})`; } else { - let additionalFunctionArgs; - // handle percentile and percentile_rank const nestedMetaValue = Number(nestedMeta?.replace(']', '')); - if (pipelineAggMap.name === 'percentile' && nestedMetaValue) { - additionalFunctionArgs = `, percentile=${nestedMetaValue}`; - } - if (pipelineAggMap.name === 'percentile_rank' && nestedMetaValue) { - additionalFunctionArgs = `, value=${nestedMetaValue}`; + + const subFormula = getFormulaEquivalent(subFunctionMetric, metrics, nestedMetaValue, window); + + if (!subFormula) { + return null; } - formula += `${pipelineAggMap.name}(${subMetricField ?? ''}${ - additionalFunctionArgs ? `${additionalFunctionArgs}` : '' - })${minimumValue})`; + + formula += `${subFormula}${minimumValue})`; } return formula; }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts index 29bf2008e208d28..a5a50f5f032cf89 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/supported_metrics.ts @@ -6,9 +6,13 @@ * Side Public License, v 1. */ +import { PANEL_TYPES, TIME_RANGE_DATA_MODES } from '../../../../common/enums'; interface AggOptions { name: string; isFullReference: boolean; + isFieldRequired: boolean; + supportedPanelTypes: string[]; + supportedTimeRangeModes: string[]; } // list of supported TSVB aggregation types in Lens @@ -19,85 +23,207 @@ export const SUPPORTED_METRICS: { [key: string]: AggOptions } = { avg: { name: 'average', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, cardinality: { name: 'unique_count', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, count: { name: 'count', isFullReference: false, + isFieldRequired: false, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, positive_rate: { name: 'counter_rate', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, moving_average: { name: 'moving_average', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, derivative: { name: 'differences', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, cumulative_sum: { name: 'cumulative_sum', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, avg_bucket: { name: 'overall_average', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, max_bucket: { name: 'overall_max', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, min_bucket: { name: 'overall_min', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, sum_bucket: { name: 'overall_sum', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, max: { name: 'max', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, min: { name: 'min', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, percentile: { name: 'percentile', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, percentile_rank: { name: 'percentile_rank', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, sum: { name: 'sum', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, filter_ratio: { name: 'filter_ratio', isFullReference: false, + isFieldRequired: false, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, top_hit: { name: 'last_value', isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, math: { name: 'formula', isFullReference: true, + isFieldRequired: false, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, positive_only: { name: 'pick_max', isFullReference: true, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES], + supportedTimeRangeModes: [TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE], }, static: { name: 'static_value', isFullReference: true, + isFieldRequired: false, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], + }, + value_count: { + name: 'count', + isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], + }, + std_deviation: { + name: 'standard_deviation', + isFullReference: false, + isFieldRequired: true, + supportedPanelTypes: [PANEL_TYPES.TIMESERIES, PANEL_TYPES.TOP_N], + supportedTimeRangeModes: [ + TIME_RANGE_DATA_MODES.ENTIRE_TIME_RANGE, + TIME_RANGE_DATA_MODES.LAST_VALUE, + ], }, }; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/validate_metrics.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/validate_metrics.ts new file mode 100644 index 000000000000000..adcc2e9a94664c0 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/metrics/validate_metrics.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Metric } from '../../../../common/types'; +import { SUPPORTED_METRICS } from '.'; + +const isMetricValid = ( + metricType: string, + panelType: string, + field?: string, + timeRangeMode?: string +) => { + const isMetricSupported = SUPPORTED_METRICS[metricType]; + if (!isMetricSupported) { + return false; + } + const isPanelTypeSupported = + SUPPORTED_METRICS[metricType].supportedPanelTypes.includes(panelType); + const isTimeRangeModeSupported = + !timeRangeMode || SUPPORTED_METRICS[metricType].supportedTimeRangeModes.includes(timeRangeMode); + return ( + isPanelTypeSupported && + isTimeRangeModeSupported && + (!SUPPORTED_METRICS[metricType].isFieldRequired || field) + ); +}; + +export const isValidMetrics = (metrics: Metric[], panelType: string, timeRangeMode?: string) => { + return metrics.every((metric) => { + const isMetricAggValid = + metric.type !== 'filter_ratio' || + isMetricValid(metric.metric_agg || 'count', panelType, metric.field, timeRangeMode); + return ( + metric.type === 'series_agg' || + (isMetricValid(metric.type, panelType, metric.field, timeRangeMode) && isMetricAggValid) + ); + }); +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.test.ts index aeb401e86dd0128..80b7ba29b24c7bb 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.test.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.test.ts @@ -5,6 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + import type { Metric } from '../../../../common/types'; import { getSeries } from './get_series'; @@ -17,7 +18,7 @@ describe('getSeries', () => { field: 'day_of_week_i', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'average', @@ -44,7 +45,7 @@ describe('getSeries', () => { }, }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -71,7 +72,7 @@ describe('getSeries', () => { field: '123456', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -97,7 +98,7 @@ describe('getSeries', () => { field: '123456', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -122,7 +123,7 @@ describe('getSeries', () => { field: '123456', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'cumulative_sum', @@ -147,7 +148,7 @@ describe('getSeries', () => { field: '123456', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -174,7 +175,7 @@ describe('getSeries', () => { unit: '1m', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'differences', @@ -202,7 +203,7 @@ describe('getSeries', () => { window: 6, }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'moving_average', @@ -246,7 +247,7 @@ describe('getSeries', () => { window: 6, }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -293,7 +294,7 @@ describe('getSeries', () => { ], }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'percentile', @@ -335,7 +336,7 @@ describe('getSeries', () => { colors: ['rgba(211,96,134,1)', 'rgba(155,33,230,1)', '#68BC00'], }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'percentile_rank', @@ -377,7 +378,7 @@ describe('getSeries', () => { order_by: 'timestamp', }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'last_value', @@ -405,7 +406,7 @@ describe('getSeries', () => { function: 'mean', }, ] as Metric[]; - const config = getSeries(metric, 1)!; + const config = getSeries(metric, 1, 'everything', '')!; expect(config).toStrictEqual({ metrics: [ { @@ -430,7 +431,7 @@ describe('getSeries', () => { size: 2, }, ] as Metric[]; - const config = getSeries(metric, 1); + const config = getSeries(metric, 1, 'everything', ''); expect(config).toBeNull(); }); @@ -442,7 +443,7 @@ describe('getSeries', () => { value: '10', }, ] as Metric[]; - const config = getSeries(metric, 1); + const config = getSeries(metric, 1, 'everything', ''); expect(config).toBeNull(); }); @@ -454,7 +455,7 @@ describe('getSeries', () => { value: '10', }, ] as Metric[]; - const config = getSeries(metric, 2)!.metrics; + const config = getSeries(metric, 2, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'static_value', @@ -521,7 +522,7 @@ describe('getSeries', () => { ], }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', @@ -572,7 +573,7 @@ describe('getSeries', () => { ], }, ] as Metric[]; - const config = getSeries(metric, 1)!.metrics; + const config = getSeries(metric, 1, 'everything', '')!.metrics; expect(config).toStrictEqual([ { agg: 'formula', diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.ts index 0db270b465719a3..a5c4bbfbc548d5a 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/lib/series/get_series.ts @@ -28,7 +28,13 @@ export interface VisSeries { seriesAgg?: string; } -export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): VisSeries | null => { +export const getSeries = ( + initialMetrics: Metric[], + totalSeriesNum: number, + splitMode: string, + layerColor: string, + window?: string +): VisSeries | null => { const { metrics, seriesAgg } = getSeriesAgg(initialMetrics); const metricIdx = metrics.length - 1; const aggregation = metrics[metricIdx].type; @@ -44,6 +50,8 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis if (percentiles?.length) { const percentilesSeries = getPercentilesSeries( percentiles, + splitMode, + layerColor, fieldName ) as VisualizeEditorLayersContext['metrics']; metricsArray = [...metricsArray, ...percentilesSeries]; @@ -57,6 +65,8 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis const percentileRanksSeries = getPercentileRankSeries( values, colors, + splitMode, + layerColor, fieldName ) as VisualizeEditorLayersContext['metrics']; metricsArray = [...metricsArray, ...percentileRanksSeries]; @@ -92,12 +102,17 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis const [_, meta] = variable?.field?.split('[') ?? []; const metaValue = Number(meta?.replace(']', '')); if (!metaValue) return; - const script = getFormulaEquivalent(currentMetric, layerMetricsArray, metaValue); + const script = getFormulaEquivalent( + currentMetric, + layerMetricsArray, + metaValue, + window + ); if (!script) return; finalScript = finalScript?.replace(`params.${variable.name}`, script); }); } else { - const script = getFormulaEquivalent(currentMetric, layerMetricsArray); + const script = getFormulaEquivalent(currentMetric, layerMetricsArray, undefined, window); if (!script) return null; const variable = variables.find((v) => v.field === currentMetric.id); finalScript = finalScript?.replaceAll(`params.${variable?.name}`, script); @@ -113,7 +128,8 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis metricsArray = getParentPipelineSeries( aggregation, metricIdx, - metrics + metrics, + window ) as VisualizeEditorLayersContext['metrics']; break; } @@ -137,7 +153,8 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis subFunctionMetric, pipelineAgg, aggregation, - metaValue + metaValue, + window ); if (!formula) return null; metricsArray = getFormulaSeries(formula); @@ -146,7 +163,9 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis aggregation, metrics[metricIdx], subFunctionMetric, - pipelineAgg + pipelineAgg, + undefined, + window ); if (!series) return null; metricsArray = series; @@ -154,7 +173,12 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis break; } case 'positive_only': { - const formula = getSiblingPipelineSeriesFormula(aggregation, metrics[metricIdx], metrics); + const formula = getSiblingPipelineSeriesFormula( + aggregation, + metrics[metricIdx], + metrics, + window + ); if (!formula) { return null; } @@ -165,7 +189,12 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis case 'max_bucket': case 'min_bucket': case 'sum_bucket': { - const formula = getSiblingPipelineSeriesFormula(aggregation, metrics[metricIdx], metrics); + const formula = getSiblingPipelineSeriesFormula( + aggregation, + metrics[metricIdx], + metrics, + window + ); if (!formula) { return null; } @@ -173,7 +202,7 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis break; } case 'filter_ratio': { - const formula = getFilterRatioFormula(metrics[metricIdx]); + const formula = getFilterRatioFormula(metrics[metricIdx], window); if (!formula) { return null; } @@ -221,6 +250,25 @@ export const getSeries = (initialMetrics: Metric[], totalSeriesNum: number): Vis ]; break; } + case 'std_deviation': { + const currentMetric = metrics[metricIdx]; + if (currentMetric.mode === 'upper' || currentMetric.mode === 'lower') { + const script = getFormulaEquivalent(currentMetric, metrics, undefined, window); + if (!script) return null; + metricsArray = getFormulaSeries(script); + break; + } else if (currentMetric.mode === 'band') { + [ + { ...currentMetric, mode: 'upper' }, + { ...currentMetric, mode: 'lower' }, + ].forEach((metric) => { + const script = getFormulaEquivalent(metric, metrics, undefined, window); + if (!script) return null; + metricsArray.push(...getFormulaSeries(script)); + }); + break; + } + } default: { const timeScale = getTimeScale(metrics[metricIdx]); metricsArray = [ diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts new file mode 100644 index 000000000000000..7593018a22593cf --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.test.ts @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { DataView } from '@kbn/data-plugin/common'; +import type { Panel, Series } from '../../../common/types'; +import { convertToLens } from '.'; + +const dataViewsMap: Record = { + test1: { id: 'test1', title: 'test1', timeFieldName: 'timeField1' } as DataView, + test2: { + id: 'test2', + title: 'test2', + timeFieldName: 'timeField2', + } as DataView, + test3: { id: 'test3', title: 'test3', timeFieldName: 'timeField3' } as DataView, +}; + +const getDataview = (id: string): DataView | undefined => dataViewsMap[id]; +jest.mock('../../services', () => { + return { + getDataViewsStart: jest.fn(() => { + return { + getDefault: jest.fn(() => { + return { id: '12345', title: 'default', timeFieldName: '@timestamp' }; + }), + get: getDataview, + }; + }), + }; +}); + +const model = { + axis_position: 'left', + type: 'timeseries', + index_pattern: { id: 'test2' }, + use_kibana_indexes: true, + series: [ + { + color: '#000000', + chart_type: 'line', + fill: '0', + id: '85147356-c185-4636-9182-d55f3ab2b6fa', + palette: { + name: 'default', + type: 'palette', + }, + split_mode: 'everything', + metrics: [ + { + id: '3fa8b32f-5c38-4813-9361-1f2817ae5b18', + type: 'count', + }, + ], + override_index_pattern: 0, + }, + ], +} as Panel; + +describe('convertToLens for TimeSeries', () => { + test('should return null for a non supported aggregation', async () => { + const nonSupportedAggModel = { + ...model, + series: [ + { + ...model.series[0], + metrics: [ + { + type: 'sum_of_squares_bucket', + }, + ] as Series['metrics'], + }, + ], + }; + const triggerOptions = await convertToLens(nonSupportedAggModel); + expect(triggerOptions).toBeNull(); + }); + + test('should return options for a supported aggregation', async () => { + const triggerOptions = await convertToLens(model); + expect(triggerOptions).toStrictEqual({ + configuration: { + extents: { yLeftExtent: { mode: 'full' }, yRightExtent: { mode: 'full' } }, + fill: '0', + gridLinesVisibility: { x: false, yLeft: false, yRight: false }, + legend: { + isVisible: false, + maxLines: 1, + position: 'right', + shouldTruncate: false, + showSingleSeries: false, + }, + }, + type: 'lnsXY', + layers: { + '0': { + axisPosition: 'left', + chartType: 'line', + collapseFn: undefined, + indexPatternId: 'test2', + metrics: [ + { + agg: 'count', + color: '#000000', + fieldName: 'document', + isFullReference: false, + params: {}, + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + splitWithDateHistogram: false, + xFieldName: 'timeField2', + xMode: 'date_histogram', + timeInterval: 'auto', + dropPartialBuckets: false, + }, + }, + }); + }); + + test('should return area for timeseries line chart with fill > 0', async () => { + const modelWithFill = { + ...model, + series: [ + { + ...model.series[0], + fill: '0.3', + stacked: 'none', + }, + ], + }; + const triggerOptions = await convertToLens(modelWithFill); + expect(triggerOptions?.layers[0].chartType).toBe('area'); + }); + + test('should return timeShift in the params if it is provided', async () => { + const modelWithFill = { + ...model, + series: [ + { + ...model.series[0], + offset_time: '1h', + }, + ], + }; + const triggerOptions = await convertToLens(modelWithFill); + expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.shift).toBe('1h'); + }); + + test('should return filter in the params if it is provided', async () => { + const modelWithFill = { + ...model, + series: [ + { + ...model.series[0], + filter: { + language: 'kuery', + query: 'test', + }, + }, + ], + }; + const triggerOptions = await convertToLens(modelWithFill); + expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.kql).toBe('test'); + }); + + test('should return splitFilters information if the chart is broken down by filters', async () => { + const modelWithSplitFilters = { + ...model, + series: [ + { + ...model.series[0], + split_mode: 'filters', + split_filters: [ + { + color: 'rgba(188,0,85,1)', + filter: { + language: 'kuery', + query: '', + }, + id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', + }, + ], + }, + ], + }; + const triggerOptions = await convertToLens(modelWithSplitFilters); + expect(triggerOptions?.layers[0]?.splitFilters).toStrictEqual([ + { + color: 'rgba(188,0,85,1)', + filter: { + language: 'kuery', + query: '', + }, + id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', + }, + ]); + }); + + test('should return termsParams information if the chart is broken down by terms including series agg collapse fn', async () => { + const modelWithTerms = { + ...model, + series: [ + { + ...model.series[0], + metrics: [ + ...model.series[0].metrics, + { + type: 'series_agg', + function: 'sum', + }, + ], + split_mode: 'terms', + terms_size: 6, + terms_direction: 'desc', + terms_order_by: '_key', + }, + ] as unknown as Series[], + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.collapseFn).toStrictEqual('sum'); + expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ + size: 6, + otherBucket: false, + orderDirection: 'desc', + orderBy: { type: 'alphabetical' }, + includeIsRegex: false, + excludeIsRegex: false, + parentFormat: { + id: 'terms', + }, + }); + }); + + test('should return include exclude information if the chart is broken down by terms', async () => { + const modelWithTerms = { + ...model, + series: [ + { + ...model.series[0], + split_mode: 'terms', + terms_size: 6, + terms_direction: 'desc', + terms_order_by: '_key', + terms_include: 't.*', + }, + ] as unknown as Series[], + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ + size: 6, + otherBucket: false, + orderDirection: 'desc', + orderBy: { type: 'alphabetical' }, + includeIsRegex: true, + include: ['t.*'], + excludeIsRegex: false, + parentFormat: { + id: 'terms', + }, + }); + }); + + test('should return custom time interval if it is given', async () => { + const modelWithTerms = { + ...model, + interval: '1h', + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.timeInterval).toBe('1h'); + }); + + test('should return dropPartialbuckets if enabled', async () => { + const modelWithDropBuckets = { + ...model, + drop_last_bucket: 1, + }; + const triggerOptions = await convertToLens(modelWithDropBuckets); + expect(triggerOptions?.layers[0]?.dropPartialBuckets).toBe(true); + }); + + test('should return the correct chart configuration', async () => { + const modelWithConfig = { + ...model, + show_legend: 1, + legend_position: 'bottom', + truncate_legend: 0, + show_grid: 1, + series: [{ ...model.series[0], fill: '0.3', separate_axis: 1, axis_position: 'right' }], + }; + const triggerOptions = await convertToLens(modelWithConfig); + expect(triggerOptions).toStrictEqual({ + configuration: { + extents: { yLeftExtent: { mode: 'full' }, yRightExtent: { mode: 'full' } }, + fill: '0.3', + gridLinesVisibility: { x: true, yLeft: true, yRight: true }, + legend: { + isVisible: true, + maxLines: 1, + position: 'bottom', + shouldTruncate: false, + showSingleSeries: true, + }, + }, + type: 'lnsXY', + layers: { + '0': { + axisPosition: 'right', + chartType: 'area_stacked', + collapseFn: undefined, + indexPatternId: 'test2', + metrics: [ + { + agg: 'count', + color: '#000000', + fieldName: 'document', + isFullReference: false, + params: {}, + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + splitWithDateHistogram: false, + xFieldName: 'timeField2', + xMode: 'date_histogram', + timeInterval: 'auto', + dropPartialBuckets: false, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts index 2996ad19c42d922..1aa81b950a28ca5 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/timeseries/index.ts @@ -7,6 +7,7 @@ */ import { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/public'; +import { PANEL_TYPES } from '../../../common/enums'; import { getDataViewsStart } from '../../services'; import { getDataSourceInfo } from '../lib/datasource'; import { getSeries } from '../lib/series'; @@ -15,6 +16,7 @@ import { ConvertTsvbToLensVisualization } from '../types'; import { convertChartType, getYExtents } from '../lib/xy'; import { getLayerConfiguration } from '../lib/layers'; import { isSplitWithDateHistogram } from '../lib/split_chart'; +import { isValidMetrics } from '../lib/metrics'; export const convertToLens: ConvertTsvbToLensVisualization = async (model) => { const layersConfiguration: { [key: string]: VisualizeEditorLayersContext } = {}; @@ -30,6 +32,10 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model) => { continue; } + if (!isValidMetrics(layer.metrics, PANEL_TYPES.TIMESERIES)) { + return null; + } + const { indexPatternId, timeField } = await getDataSourceInfo( model.index_pattern, model.time_field, @@ -39,7 +45,7 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model) => { ); // handle multiple metrics - const series = getSeries(layer.metrics, seriesNum); + const series = getSeries(layer.metrics, seriesNum, layer.split_mode, layer.color); if (!series || !series.metrics) { return null; } @@ -68,6 +74,7 @@ export const convertToLens: ConvertTsvbToLensVisualization = async (model) => { series, splitFields, timeField, + 'date_histogram', splitWithDateHistogram ); } diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts new file mode 100644 index 000000000000000..1f408bf3dfa891b --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.test.ts @@ -0,0 +1,329 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { DataView } from '@kbn/data-plugin/common'; +import type { Panel, Series } from '../../../common/types'; +import { convertToLens } from '.'; + +const dataViewsMap: Record = { + test1: { id: 'test1', title: 'test1', timeFieldName: 'timeField1' } as DataView, + test2: { + id: 'test2', + title: 'test2', + timeFieldName: 'timeField2', + } as DataView, + test3: { id: 'test3', title: 'test3', timeFieldName: 'timeField3' } as DataView, +}; + +const getDataview = (id: string): DataView | undefined => dataViewsMap[id]; +jest.mock('../../services', () => { + return { + getDataViewsStart: jest.fn(() => { + return { + getDefault: jest.fn(() => { + return { id: '12345', title: 'default', timeFieldName: '@timestamp' }; + }), + get: getDataview, + }; + }), + }; +}); + +const model = { + axis_position: 'left', + type: 'timeseries', + index_pattern: { id: 'test2' }, + use_kibana_indexes: true, + series: [ + { + color: '#000000', + chart_type: 'line', + fill: '0', + id: '85147356-c185-4636-9182-d55f3ab2b6fa', + palette: { + name: 'default', + type: 'palette', + }, + split_mode: 'everything', + metrics: [ + { + id: '3fa8b32f-5c38-4813-9361-1f2817ae5b18', + type: 'count', + }, + ], + override_index_pattern: 0, + }, + ], +} as Panel; + +describe('convertToLens for Top N', () => { + test('should return null for a non supported aggregation', async () => { + const nonSupportedAggModel = { + ...model, + series: [ + { + ...model.series[0], + metrics: [ + { + type: 'sum_of_squares_bucket', + }, + ] as Series['metrics'], + }, + ], + }; + const triggerOptions = await convertToLens(nonSupportedAggModel); + expect(triggerOptions).toBeNull(); + }); + + test('should return options for a supported aggregation', async () => { + const triggerOptions = await convertToLens(model); + expect(triggerOptions).toStrictEqual({ + configuration: { + fill: '0', + gridLinesVisibility: { x: false, yLeft: false, yRight: false }, + legend: { + isVisible: false, + maxLines: 1, + position: 'right', + shouldTruncate: false, + showSingleSeries: false, + }, + valueLabels: true, + tickLabelsVisibility: { x: true, yLeft: false, yRight: false }, + axisTitlesVisibility: { x: false, yLeft: false, yRight: false }, + }, + type: 'lnsXY', + layers: { + '0': { + axisPosition: 'left', + chartType: 'bar_horizontal', + collapseFn: undefined, + indexPatternId: 'test2', + metrics: [ + { + agg: 'count', + color: '#000000', + fieldName: 'document', + isFullReference: false, + params: {}, + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + splitWithDateHistogram: false, + xFieldName: undefined, + xMode: undefined, + timeInterval: 'auto', + dropPartialBuckets: false, + }, + }, + }); + }); + + test('should return timeShift in the params if it is provided', async () => { + const modelWithFill = { + ...model, + series: [ + { + ...model.series[0], + offset_time: '1h', + }, + ], + }; + const triggerOptions = await convertToLens(modelWithFill); + expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.shift).toBe('1h'); + }); + + test('should return filter in the params if it is provided', async () => { + const modelWithFill = { + ...model, + series: [ + { + ...model.series[0], + filter: { + language: 'kuery', + query: 'test', + }, + }, + ], + }; + const triggerOptions = await convertToLens(modelWithFill); + expect(triggerOptions?.layers[0]?.metrics?.[0]?.params?.kql).toBe('test'); + }); + + test('should return splitFilters information if the chart is broken down by filters', async () => { + const modelWithSplitFilters = { + ...model, + series: [ + { + ...model.series[0], + split_mode: 'filters', + split_filters: [ + { + color: 'rgba(188,0,85,1)', + filter: { + language: 'kuery', + query: '', + }, + id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', + }, + ], + }, + ], + }; + const triggerOptions = await convertToLens(modelWithSplitFilters); + expect(triggerOptions?.layers[0]?.splitFilters).toStrictEqual([ + { + color: 'rgba(188,0,85,1)', + filter: { + language: 'kuery', + query: '', + }, + id: '89afac60-7d2b-11ec-917c-c18cd38d60b5', + }, + ]); + }); + + test('should return termsParams information if the chart is broken down by terms including series agg collapse fn', async () => { + const modelWithTerms = { + ...model, + series: [ + { + ...model.series[0], + metrics: [ + ...model.series[0].metrics, + { + type: 'series_agg', + function: 'sum', + }, + ], + split_mode: 'terms', + terms_size: 6, + terms_direction: 'desc', + terms_order_by: '_key', + }, + ] as unknown as Series[], + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.collapseFn).toStrictEqual('sum'); + expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ + size: 6, + otherBucket: false, + orderDirection: 'desc', + orderBy: { type: 'alphabetical' }, + includeIsRegex: false, + excludeIsRegex: false, + parentFormat: { + id: 'terms', + }, + }); + }); + + test('should return include exclude information if the chart is broken down by terms', async () => { + const modelWithTerms = { + ...model, + series: [ + { + ...model.series[0], + split_mode: 'terms', + terms_size: 6, + terms_direction: 'desc', + terms_order_by: '_key', + terms_include: 't.*', + }, + ] as unknown as Series[], + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.termsParams).toStrictEqual({ + size: 6, + otherBucket: false, + orderDirection: 'desc', + orderBy: { type: 'alphabetical' }, + includeIsRegex: true, + include: ['t.*'], + excludeIsRegex: false, + parentFormat: { + id: 'terms', + }, + }); + }); + + test('should return custom time interval if it is given', async () => { + const modelWithTerms = { + ...model, + interval: '1h', + }; + const triggerOptions = await convertToLens(modelWithTerms); + expect(triggerOptions?.layers[0]?.timeInterval).toBe('1h'); + }); + + test('should return dropPartialbuckets if enabled', async () => { + const modelWithDropBuckets = { + ...model, + drop_last_bucket: 1, + }; + const triggerOptions = await convertToLens(modelWithDropBuckets); + expect(triggerOptions?.layers[0]?.dropPartialBuckets).toBe(true); + }); + + test('should return the correct chart configuration', async () => { + const modelWithConfig = { + ...model, + show_legend: 1, + legend_position: 'bottom', + truncate_legend: 0, + show_grid: 1, + series: [{ ...model.series[0], fill: '0.3', separate_axis: 1, axis_position: 'right' }], + }; + const triggerOptions = await convertToLens(modelWithConfig); + expect(triggerOptions).toStrictEqual({ + configuration: { + fill: '0.3', + gridLinesVisibility: { x: false, yLeft: false, yRight: false }, + legend: { + isVisible: true, + maxLines: 1, + position: 'bottom', + shouldTruncate: false, + showSingleSeries: true, + }, + valueLabels: true, + tickLabelsVisibility: { x: true, yLeft: false, yRight: false }, + axisTitlesVisibility: { x: false, yLeft: false, yRight: false }, + }, + type: 'lnsXY', + layers: { + '0': { + axisPosition: 'right', + chartType: 'bar_horizontal', + collapseFn: undefined, + indexPatternId: 'test2', + metrics: [ + { + agg: 'count', + color: '#000000', + fieldName: 'document', + isFullReference: false, + params: {}, + }, + ], + palette: { + name: 'default', + type: 'palette', + }, + splitWithDateHistogram: false, + xFieldName: undefined, + xMode: undefined, + timeInterval: 'auto', + dropPartialBuckets: false, + }, + }, + }); + }); +}); diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts new file mode 100644 index 000000000000000..292a44aaf98d9f3 --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/top_n/index.ts @@ -0,0 +1,115 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { VisualizeEditorLayersContext } from '@kbn/visualizations-plugin/public'; +import { PANEL_TYPES, TIME_RANGE_DATA_MODES } from '../../../common/enums'; +import { getDataViewsStart } from '../../services'; +import { getDataSourceInfo } from '../lib/datasource'; +import { getSeries } from '../lib/series'; +import { getFieldsForTerms } from '../../../common/fields_utils'; +import { ConvertTsvbToLensVisualization } from '../types'; +import { isSplitWithDateHistogram } from '../lib/split_chart'; +import { getLayerConfiguration } from '../lib/layers'; +import { getWindow, isValidMetrics } from '../lib/metrics'; + +export const convertToLens: ConvertTsvbToLensVisualization = async (model, timeRange) => { + const layersConfiguration: { [key: string]: VisualizeEditorLayersContext } = {}; + + // get the active series number + const seriesNum = model.series.filter((series) => !series.hidden).length; + const dataViews = getDataViewsStart(); + + // handle multiple layers/series + for (let layerIdx = 0; layerIdx < model.series.length; layerIdx++) { + const layer = model.series[layerIdx]; + if (layer.hidden) { + continue; + } + + if (!isValidMetrics(layer.metrics, PANEL_TYPES.TOP_N, layer.time_range_mode)) { + return null; + } + + const { indexPatternId } = await getDataSourceInfo( + model.index_pattern, + model.time_field, + Boolean(layer.override_index_pattern), + layer.series_index_pattern, + dataViews + ); + + const window = + model.time_range_mode === TIME_RANGE_DATA_MODES.LAST_VALUE + ? getWindow(model.interval, timeRange) + : undefined; + + // handle multiple metrics + const series = getSeries(layer.metrics, seriesNum, layer.split_mode, layer.color, window); + if (!series || !series.metrics) { + return null; + } + + const splitFields = getFieldsForTerms(layer.terms_field); + + // in case of terms in a date field, we want to apply the date_histogram + const splitWithDateHistogram = await isSplitWithDateHistogram( + layer, + splitFields, + indexPatternId, + dataViews + ); + + if (splitWithDateHistogram === null) { + return null; + } + + layersConfiguration[layerIdx] = getLayerConfiguration( + indexPatternId, + layerIdx, + 'bar_horizontal', + model, + series, + splitFields, + undefined, + undefined, + splitWithDateHistogram, + window + ); + } + + return { + layers: layersConfiguration, + type: 'lnsXY', + configuration: { + fill: model.series[0].fill ?? 0.3, + legend: { + isVisible: Boolean(model.show_legend), + showSingleSeries: Boolean(model.show_legend), + position: model.legend_position ?? 'right', + shouldTruncate: Boolean(model.truncate_legend), + maxLines: model.max_lines_legend ?? 1, + }, + gridLinesVisibility: { + x: false, + yLeft: false, + yRight: false, + }, + tickLabelsVisibility: { + x: true, + yLeft: false, + yRight: false, + }, + axisTitlesVisibility: { + x: false, + yLeft: false, + yRight: false, + }, + valueLabels: true, + }, + }; +}; diff --git a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts index 8c17b9ec6ce5770..cf91d9835bac731 100644 --- a/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts +++ b/src/plugins/vis_types/timeseries/public/convert_to_lens/types.ts @@ -6,11 +6,13 @@ * Side Public License, v 1. */ +import { TimeRange } from '@kbn/data-plugin/common'; import { NavigateToLensContext } from '@kbn/visualizations-plugin/public'; import type { Panel } from '../../common/types'; export type ConvertTsvbToLensVisualization = ( - model: Panel + model: Panel, + timeRange?: TimeRange ) => Promise; export interface Filter { diff --git a/src/plugins/vis_types/timeseries/public/metrics_type.ts b/src/plugins/vis_types/timeseries/public/metrics_type.ts index 10658b264c4a396..edd4ac0297b3c2f 100644 --- a/src/plugins/vis_types/timeseries/public/metrics_type.ts +++ b/src/plugins/vis_types/timeseries/public/metrics_type.ts @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { TimeRange } from '@kbn/data-plugin/common'; import { Vis, VIS_EVENT_TO_TRIGGER, @@ -168,8 +169,8 @@ export const metricsVisDefinition: VisTypeDefinition< } return []; }, - navigateToLens: async (params?: VisParams) => - params ? await convertTSVBtoLensConfiguration(params as Panel) : null, + navigateToLens: async (params?: VisParams, timeRange?: TimeRange) => + params ? await convertTSVBtoLensConfiguration(params as Panel, timeRange) : null, inspectorAdapters: () => ({ requests: new RequestAdapter(), diff --git a/src/plugins/visualizations/public/vis_types/types.ts b/src/plugins/visualizations/public/vis_types/types.ts index 4267e6a0d8aef46..ebcdcf594fdfda1 100644 --- a/src/plugins/visualizations/public/vis_types/types.ts +++ b/src/plugins/visualizations/public/vis_types/types.ts @@ -10,6 +10,7 @@ import type { IconType } from '@elastic/eui'; import type { ReactNode } from 'react'; import type { PaletteOutput } from '@kbn/coloring'; import type { Adapters } from '@kbn/inspector-plugin/common'; +import { TimeRange } from '@kbn/data-plugin/common'; import type { Query } from '@kbn/es-query'; import type { AggGroupNames, AggParam, AggGroupName } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -92,7 +93,8 @@ interface VisualizeEditorMetricContext { export interface VisualizeEditorLayersContext { indexPatternId: string; splitWithDateHistogram?: boolean; - timeFieldName?: string; + xFieldName?: string; + xMode?: string; chartType?: string; axisPosition?: string; termsParams?: Record; @@ -134,7 +136,18 @@ export interface NavigateToLensContext { yLeft: boolean; yRight: boolean; }; - extents: { + tickLabelsVisibility?: { + x: boolean; + yLeft: boolean; + yRight: boolean; + }; + axisTitlesVisibility?: { + x: boolean; + yLeft: boolean; + yRight: boolean; + }; + valueLabels?: boolean; + extents?: { yLeftExtent: AxisExtents; yRightExtent: AxisExtents; }; @@ -173,7 +186,8 @@ export interface VisTypeDefinition { * in order to be displayed in the Lens editor. */ readonly navigateToLens?: ( - params?: VisParams + params?: VisParams, + timeRange?: TimeRange ) => Promise | undefined; /** diff --git a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx index ca373b31b60207c..c80492b45c5cb56 100644 --- a/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx +++ b/src/plugins/visualizations/public/visualize_app/components/visualize_top_nav.tsx @@ -99,12 +99,15 @@ const TopNav = ({ useEffect(() => { const asyncGetTriggerContext = async () => { if (vis.type.navigateToLens) { - const triggerConfig = await vis.type.navigateToLens(vis.params); + const triggerConfig = await vis.type.navigateToLens( + vis.params, + services.data.query.timefilter.timefilter.getAbsoluteTime() + ); setEditInLensConfig(triggerConfig); } }; asyncGetTriggerContext(); - }, [vis.params, vis.type]); + }, [services.data.query.timefilter.timefilter, vis.params, vis.type]); const displayEditInLensItem = Boolean(vis.type.navigateToLens && editInLensConfig); const config = useMemo(() => { diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts new file mode 100644 index 000000000000000..6eaa5b3e95cf29a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.test.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mockHttpValues } from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test-jest-helpers'; + +import { deleteIndex } from './delete_index_api_logic'; + +describe('deleteIndexApiLogic', () => { + const { http } = mockHttpValues; + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('deleteIndex', () => { + it('calls correct api', async () => { + const promise = Promise.resolve(); + http.post.mockReturnValue(promise); + const result = deleteIndex({ indexName: 'deleteIndex' }); + await nextTick(); + expect(http.delete).toHaveBeenCalledWith('/internal/enterprise_search/indices/deleteIndex'); + await expect(result).resolves; + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts new file mode 100644 index 000000000000000..ff92e4ecef6bc55 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/delete_index_api_logic.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createApiLogic } from '../../../shared/api_logic/create_api_logic'; +import { HttpLogic } from '../../../shared/http'; + +export interface DeleteIndexApiLogicArgs { + indexName: string; +} + +export const deleteIndex = async ({ indexName }: DeleteIndexApiLogicArgs): Promise => { + const route = `/internal/enterprise_search/indices/${indexName}`; + await HttpLogic.values.http.delete(route); + return; +}; + +export const DeleteIndexApiLogic = createApiLogic(['delete_index_api_logic'], deleteIndex); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts index 13b75e7c534be6e..42d78b65df449ae 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.test.ts @@ -28,7 +28,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 1, return_hidden_indices: false, search_query: null, size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: true, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: true, + result: 'result', + returnHiddenIndices: false, + searchQuery: undefined, + }); }); it('sets initialRequest to false if page is not the first page', async () => { const promise = Promise.resolve({ result: 'result' }); @@ -41,7 +46,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 2, return_hidden_indices: false, search_query: null, size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: false, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: false, + result: 'result', + returnHiddenIndices: false, + searchQuery: undefined, + }); }); it('sets initialRequest to false if searchQuery is not empty', async () => { const promise = Promise.resolve({ result: 'result' }); @@ -55,7 +65,12 @@ describe('FetchIndicesApiLogic', () => { expect(http.get).toHaveBeenCalledWith('/internal/enterprise_search/indices', { query: { page: 1, return_hidden_indices: false, search_query: 'a', size: 20 }, }); - await expect(result).resolves.toEqual({ isInitialRequest: false, result: 'result' }); + await expect(result).resolves.toEqual({ + isInitialRequest: false, + result: 'result', + returnHiddenIndices: false, + searchQuery: 'a', + }); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts index a1ca3a9c3141f50..709c47ed919c0f2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/api/index/fetch_indices_api_logic.ts @@ -38,7 +38,7 @@ export const fetchIndices = async ({ // We need this to determine whether to show the empty state on the indices page const isInitialRequest = meta.page.current === 1 && !searchQuery; - return { ...response, isInitialRequest }; + return { ...response, isInitialRequest, returnHiddenIndices, searchQuery }; }; export const FetchIndicesAPILogic = createApiLogic(['content', 'indices_api_logic'], fetchIndices); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx new file mode 100644 index 000000000000000..99f0e35c352e123 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/delete_index_modal.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiConfirmModal } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { IndicesLogic } from './indices_logic'; + +export const DeleteIndexModal: React.FC = () => { + const { closeDeleteModal, deleteIndex } = useActions(IndicesLogic); + const { deleteModalIndexName: indexName, isDeleteModalVisible } = useValues(IndicesLogic); + return isDeleteModalVisible ? ( + { + closeDeleteModal(); + }} + onConfirm={() => { + deleteIndex({ indexName }); + }} + cancelButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.cancelButton.title', + { + defaultMessage: 'Cancel', + } + )} + confirmButtonText={i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.confirmButton.title', + { + defaultMessage: 'Delete index', + } + )} + defaultFocusedButton="confirm" + buttonColor="danger" + > +

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.delete.description', + { + defaultMessage: + 'You are about to delete the index {indexName}. This will also delete any associated connector documents or crawlers.', + values: { + indexName, + }, + } + )} +

+

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.searchEngine.description', + { + defaultMessage: + 'Any associated search engines will no longer be able to access any data stored in this index.', + } + )} +

+

+ {i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.deleteModal.irrevokable.description', + { + defaultMessage: + "You can't recover a deleted index, connector or crawler configuration. Make sure you have appropriate backups.", + } + )} +

+
+ ) : ( + <> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts index 36157301d3bae3d..2574af68ce50c03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.test.ts @@ -28,11 +28,14 @@ import { IndicesLogic } from './indices_logic'; const DEFAULT_VALUES = { data: undefined, + deleteModalIndexName: '', hasNoIndices: false, indices: [], + isDeleteModalVisible: false, isFirstRequest: true, isLoading: true, meta: DEFAULT_META, + searchParams: { meta: DEFAULT_META, returnHiddenIndices: false }, status: Status.IDLE, }; @@ -64,11 +67,75 @@ describe('IndicesLogic', () => { current: 3, }, }, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta: { page: { ...DEFAULT_META.page, current: 3 } }, + }, }); }); }); + describe('openDeleteModal', () => { + it('should set deleteIndexName and set isDeleteModalVisible to true', () => { + IndicesLogic.actions.openDeleteModal('delete'); + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + deleteModalIndexName: 'delete', + isDeleteModalVisible: true, + }); + }); + }); + describe('closeDeleteModal', () => { + it('should set deleteIndexName to empty and set isDeleteModalVisible to false', () => { + IndicesLogic.actions.openDeleteModal('delete'); + IndicesLogic.actions.closeDeleteModal(); + expect(IndicesLogic.values).toEqual(DEFAULT_VALUES); + }); + }); }); describe('reducers', () => { + describe('isFirstRequest', () => { + it('should update to true on setIsFirstRequest', () => { + IndicesLogic.actions.setIsFirstRequest(); + expect(IndicesLogic.values).toEqual({ ...DEFAULT_VALUES, isFirstRequest: true }); + }); + it('should update to false on apiError', () => { + IndicesLogic.actions.setIsFirstRequest(); + IndicesLogic.actions.apiError({} as HttpError); + + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + hasNoIndices: false, + indices: [], + isFirstRequest: false, + isLoading: false, + status: Status.ERROR, + }); + }); + it('should update to false on apiSuccess', () => { + IndicesLogic.actions.setIsFirstRequest(); + IndicesLogic.actions.apiSuccess({ + indices: [], + isInitialRequest: false, + meta: DEFAULT_VALUES.meta, + returnHiddenIndices: false, + }); + + expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, + data: { + indices: [], + isInitialRequest: false, + meta: DEFAULT_VALUES.meta, + returnHiddenIndices: false, + }, + hasNoIndices: false, + indices: [], + isFirstRequest: false, + isLoading: false, + status: Status.SUCCESS, + }); + }); + }); describe('meta', () => { it('updates when apiSuccess listener triggered', () => { const newMeta = { @@ -84,18 +151,28 @@ describe('IndicesLogic', () => { indices, isInitialRequest: true, meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices, isInitialRequest: true, meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', }, hasNoIndices: false, indices: elasticsearchViewIndices, isFirstRequest: false, isLoading: false, meta: newMeta, + searchParams: { + meta: newMeta, + returnHiddenIndices: true, + searchQuery: 'a', + }, status: Status.SUCCESS, }); }); @@ -115,18 +192,25 @@ describe('IndicesLogic', () => { indices: [], isInitialRequest: true, meta, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [], isInitialRequest: true, meta, + returnHiddenIndices: false, }, hasNoIndices: true, indices: [], isFirstRequest: false, isLoading: false, meta, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta, + }, status: Status.SUCCESS, }); }); @@ -144,18 +228,25 @@ describe('IndicesLogic', () => { indices: [], isInitialRequest: false, meta, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [], isInitialRequest: false, meta, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [], isFirstRequest: false, isLoading: false, meta, + searchParams: { + ...DEFAULT_VALUES.searchParams, + meta, + }, status: Status.SUCCESS, }); }); @@ -172,6 +263,21 @@ describe('IndicesLogic', () => { expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); }); + it('calls flashAPIErrors on deleteError', () => { + IndicesLogic.actions.deleteError({} as HttpError); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledTimes(1); + expect(mockFlashMessageHelpers.flashAPIErrors).toHaveBeenCalledWith({}); + }); + it('calls flashSuccessToast, closeDeleteModal and fetchIndices on deleteSuccess', () => { + IndicesLogic.actions.fetchIndices = jest.fn(); + IndicesLogic.actions.closeDeleteModal = jest.fn(); + IndicesLogic.actions.deleteSuccess(); + expect(mockFlashMessageHelpers.flashSuccessToast).toHaveBeenCalledTimes(1); + expect(IndicesLogic.actions.fetchIndices).toHaveBeenCalledWith( + IndicesLogic.values.searchParams + ); + expect(IndicesLogic.actions.closeDeleteModal).toHaveBeenCalled(); + }); it('calls makeRequest on fetchIndices', async () => { jest.useFakeTimers(); IndicesLogic.actions.makeRequest = jest.fn(); @@ -223,13 +329,16 @@ describe('IndicesLogic', () => { indices: elasticsearchViewIndices, isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: elasticsearchViewIndices, isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: elasticsearchViewIndices, @@ -256,9 +365,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -272,6 +383,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -302,9 +414,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -314,6 +428,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -343,9 +458,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -355,6 +472,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ @@ -385,9 +503,11 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }); expect(IndicesLogic.values).toEqual({ + ...DEFAULT_VALUES, data: { indices: [ { @@ -401,6 +521,7 @@ describe('IndicesLogic', () => { ], isInitialRequest: true, meta: DEFAULT_META, + returnHiddenIndices: false, }, hasNoIndices: false, indices: [ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts index 59640a948ddbc54..eb09425c1facad2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_logic.ts @@ -7,12 +7,23 @@ import { kea, MakeLogicType } from 'kea'; +import { i18n } from '@kbn/i18n'; + import { Meta } from '../../../../../common/types'; import { HttpError, Status } from '../../../../../common/types/api'; import { ElasticsearchIndexWithIngestion } from '../../../../../common/types/indices'; +import { Actions } from '../../../shared/api_logic/create_api_logic'; import { DEFAULT_META } from '../../../shared/constants'; -import { flashAPIErrors, clearFlashMessages } from '../../../shared/flash_messages'; +import { + flashAPIErrors, + clearFlashMessages, + flashSuccessToast, +} from '../../../shared/flash_messages'; import { updateMetaPageIndex } from '../../../shared/table_pagination'; +import { + DeleteIndexApiLogic, + DeleteIndexApiLogicArgs, +} from '../../api/index/delete_index_api_logic'; import { FetchIndicesAPILogic } from '../../api/index/fetch_indices_api_logic'; import { ElasticsearchViewIndex } from '../../types'; import { indexToViewIndex } from '../../utils/indices'; @@ -23,15 +34,25 @@ export interface IndicesActions { indices, isInitialRequest, meta, + returnHiddenIndices, + searchQuery, }: { indices: ElasticsearchIndexWithIngestion[]; isInitialRequest: boolean; meta: Meta; + returnHiddenIndices: boolean; + searchQuery?: string; }): { indices: ElasticsearchIndexWithIngestion[]; isInitialRequest: boolean; meta: Meta; + returnHiddenIndices: boolean; + searchQuery?: string; }; + closeDeleteModal(): void; + deleteError: Actions['apiError']; + deleteIndex: Actions['makeRequest']; + deleteSuccess: Actions['apiSuccess']; fetchIndices({ meta, returnHiddenIndices, @@ -43,34 +64,59 @@ export interface IndicesActions { }): { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string }; makeRequest: typeof FetchIndicesAPILogic.actions.makeRequest; onPaginate(newPageIndex: number): { newPageIndex: number }; - setIsFirstRequest(): boolean; + openDeleteModal(indexName: string): { indexName: string }; + setIsFirstRequest(): void; } export interface IndicesValues { data: typeof FetchIndicesAPILogic.values.data; + deleteModalIndexName: string; hasNoIndices: boolean; indices: ElasticsearchViewIndex[]; + isDeleteModalVisible: boolean; isFirstRequest: boolean; isLoading: boolean; meta: Meta; + searchParams: { meta: Meta; returnHiddenIndices: boolean; searchQuery?: string }; status: typeof FetchIndicesAPILogic.values.status; } export const IndicesLogic = kea>({ actions: { + closeDeleteModal: true, fetchIndices: ({ meta, returnHiddenIndices, searchQuery }) => ({ meta, returnHiddenIndices, searchQuery, }), onPaginate: (newPageIndex) => ({ newPageIndex }), - setIsFirstRequest: () => true, + openDeleteModal: (indexName) => ({ indexName }), + setIsFirstRequest: true, }, connect: { - actions: [FetchIndicesAPILogic, ['makeRequest', 'apiSuccess', 'apiError']], + actions: [ + FetchIndicesAPILogic, + ['makeRequest', 'apiSuccess', 'apiError'], + DeleteIndexApiLogic, + ['apiError as deleteError', 'apiSuccess as deleteSuccess', 'makeRequest as deleteIndex'], + ], values: [FetchIndicesAPILogic, ['data', 'status']], }, - listeners: ({ actions }) => ({ + listeners: ({ actions, values }) => ({ apiError: (e) => flashAPIErrors(e), + deleteError: (e) => flashAPIErrors(e), + deleteSuccess: () => { + flashSuccessToast( + i18n.translate('xpack.enterpriseSearch.content.indices.deleteIndex.successToast.title', { + defaultMessage: + 'Your index {indexName} and any associated connectors or crawlers were successfully deleted', + values: { + indexName: values.deleteModalIndexName, + }, + }) + ); + actions.closeDeleteModal(); + actions.fetchIndices(values.searchParams); + }, fetchIndices: async (input, breakpoint) => { await breakpoint(150); actions.makeRequest(input); @@ -79,6 +125,20 @@ export const IndicesLogic = kea>({ }), path: ['enterprise_search', 'content', 'indices_logic'], reducers: () => ({ + deleteModalIndexName: [ + '', + { + closeDeleteModal: () => '', + openDeleteModal: (_, { indexName }) => indexName, + }, + ], + isDeleteModalVisible: [ + false, + { + closeDeleteModal: () => false, + openDeleteModal: () => true, + }, + ], isFirstRequest: [ true, { @@ -87,11 +147,18 @@ export const IndicesLogic = kea>({ setIsFirstRequest: () => true, }, ], - meta: [ - DEFAULT_META, + searchParams: [ + { meta: DEFAULT_META, returnHiddenIndices: false }, { - apiSuccess: (_, { meta }) => meta, - onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + apiSuccess: (_, { meta, returnHiddenIndices, searchQuery }) => ({ + meta, + returnHiddenIndices, + searchQuery, + }), + onPaginate: (state, { newPageIndex }) => ({ + ...state, + meta: updateMetaPageIndex(state.meta, newPageIndex), + }), }, ], }), @@ -110,5 +177,6 @@ export const IndicesLogic = kea>({ () => [selectors.status, selectors.isFirstRequest], (status, isFirstRequest) => [Status.LOADING, Status.IDLE].includes(status) && isFirstRequest, ], + meta: [() => [selectors.searchParams], (searchParams) => searchParams.meta], }), }); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx index d68ce14b1b18388..0462613b247d85f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/indices_table.tsx @@ -11,6 +11,7 @@ import { CriteriaWithPagination, EuiBasicTable, EuiBasicTableColumn, + EuiButtonIcon, EuiIcon, EuiText, } from '@elastic/eui'; @@ -37,122 +38,12 @@ const healthColorsMap = { yellow: 'warning', }; -const columns: Array> = [ - { - field: 'name', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', { - defaultMessage: 'Index name', - }), - render: (name: string) => ( - - {name} - - ), - sortable: true, - truncateText: true, - width: '40%', - }, - { - field: 'health', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.health.columnTitle', { - defaultMessage: 'Index health', - }), - render: (health: 'red' | 'green' | 'yellow' | 'unavailable') => ( - - -  {health ?? '-'} - - ), - sortable: true, - truncateText: true, - width: '10%', - }, - { - field: 'count', - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', { - defaultMessage: 'Docs count', - }), - sortable: true, - truncateText: true, - width: '10%', - }, - { - field: 'ingestionMethod', - name: i18n.translate( - 'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle', - { - defaultMessage: 'Ingestion method', - } - ), - render: (ingestionMethod: IngestionMethod) => ( - {ingestionMethodToText(ingestionMethod)} - ), - truncateText: true, - width: '10%', - }, - { - name: i18n.translate( - 'xpack.enterpriseSearch.content.searchIndices.ingestionStatus.columnTitle', - { - defaultMessage: 'Ingestion status', - } - ), - render: (index: ElasticsearchViewIndex) => { - const overviewPath = generateEncodedPath(SEARCH_INDEX_PATH, { indexName: index.name }); - if (isCrawlerIndex(index)) { - const label = crawlerStatusToText(index.crawler?.most_recent_crawl_request_status); - - return ( - - ); - } else { - const label = ingestionStatusToText(index.ingestionStatus); - return ( - - ); - } - }, - truncateText: true, - width: '10%', - }, - { - actions: [ - { - render: ({ name }) => ( - - ), - }, - ], - name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', { - defaultMessage: 'Actions', - }), - width: '5%', - }, -]; - interface IndicesTableProps { indices: ElasticsearchViewIndex[]; isLoading?: boolean; meta: Meta; onChange: (criteria: CriteriaWithPagination) => void; + onDelete: (indexName: string) => void; } export const IndicesTable: React.FC = ({ @@ -160,13 +51,141 @@ export const IndicesTable: React.FC = ({ isLoading, meta, onChange, -}) => ( - -); + onDelete, +}) => { + const columns: Array> = [ + { + field: 'name', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.name.columnTitle', { + defaultMessage: 'Index name', + }), + render: (name: string) => ( + + {name} + + ), + sortable: true, + truncateText: true, + width: '40%', + }, + { + field: 'health', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.health.columnTitle', { + defaultMessage: 'Index health', + }), + render: (health: 'red' | 'green' | 'yellow' | 'unavailable') => ( + + +  {health ?? '-'} + + ), + sortable: true, + truncateText: true, + width: '10%', + }, + { + field: 'count', + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.docsCount.columnTitle', { + defaultMessage: 'Docs count', + }), + sortable: true, + truncateText: true, + width: '10%', + }, + { + field: 'ingestionMethod', + name: i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.ingestionMethod.columnTitle', + { + defaultMessage: 'Ingestion method', + } + ), + render: (ingestionMethod: IngestionMethod) => ( + {ingestionMethodToText(ingestionMethod)} + ), + truncateText: true, + width: '10%', + }, + { + name: i18n.translate( + 'xpack.enterpriseSearch.content.searchIndices.ingestionStatus.columnTitle', + { + defaultMessage: 'Ingestion status', + } + ), + render: (index: ElasticsearchViewIndex) => { + const overviewPath = generateEncodedPath(SEARCH_INDEX_PATH, { indexName: index.name }); + if (isCrawlerIndex(index)) { + const label = crawlerStatusToText(index.crawler?.most_recent_crawl_request_status); + + return ( + + ); + } else { + const label = ingestionStatusToText(index.ingestionStatus); + return ( + + ); + } + }, + truncateText: true, + width: '10%', + }, + { + actions: [ + { + render: ({ name }) => ( + + ), + }, + { + render: (index) => + // We don't have a way to delete crawlers yet + isCrawlerIndex(index) ? ( + <> + ) : ( + onDelete(index.name)} + /> + ), + }, + ], + name: i18n.translate('xpack.enterpriseSearch.content.searchIndices.actions.columnTitle', { + defaultMessage: 'Actions', + }), + width: '5%', + }, + ]; + return ( + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx index 96019c8139c978b..bc9aa175e1c713f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search_content/components/search_indices/search_indices.tsx @@ -34,6 +34,7 @@ import { useLocalStorage } from '../../../shared/use_local_storage'; import { NEW_INDEX_PATH } from '../../routes'; import { EnterpriseSearchContentPageTemplate } from '../layout/page_template'; +import { DeleteIndexModal } from './delete_index_modal'; import { IndicesLogic } from './indices_logic'; import { IndicesTable } from './indices_table'; @@ -49,7 +50,7 @@ export const baseBreadcrumbs = [ ]; export const SearchIndices: React.FC = () => { - const { fetchIndices, onPaginate, setIsFirstRequest } = useActions(IndicesLogic); + const { fetchIndices, onPaginate, openDeleteModal, setIsFirstRequest } = useActions(IndicesLogic); const { meta, indices, hasNoIndices, isLoading } = useValues(IndicesLogic); const [showHiddenIndices, setShowHiddenIndices] = useState(false); const [searchQuery, setSearchValue] = useState(''); @@ -85,6 +86,7 @@ export const SearchIndices: React.FC = () => { return ( <> + { - + ) : ( diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts index e157df1df16f66c..6fb4e55dd636151 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.test.ts @@ -9,7 +9,10 @@ import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { ANALYTICS_COLLECTIONS_INDEX } from '../..'; -import { fetchAnalyticsCollectionByName } from './fetch_analytics_collection'; +import { + fetchAnalyticsCollectionByName, + fetchAnalyticsCollections, +} from './fetch_analytics_collection'; import { setupAnalyticsCollectionIndex } from './setup_indices'; jest.mock('./setup_indices', () => ({ @@ -28,6 +31,75 @@ describe('fetch analytics collection lib function', () => { jest.clearAllMocks(); }); + describe('fetch collections', () => { + it('should return a list of analytics collections', async () => { + mockClient.asCurrentUser.search.mockImplementationOnce(() => + Promise.resolve({ + hits: { + hits: [ + { _id: '2', _source: { name: 'example' } }, + { _id: '1', _source: { name: 'example2' } }, + ], + }, + }) + ); + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).resolves.toEqual([ + { id: '2', name: 'example' }, + { id: '1', name: 'example2' }, + ]); + }); + + it('should setup the indexes if none exist and return an empty array', async () => { + mockClient.asCurrentUser.search.mockImplementationOnce(() => + Promise.reject({ + meta: { + body: { + error: { + type: 'index_not_found_exception', + }, + }, + }, + }) + ); + + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).resolves.toEqual([]); + + expect(setupAnalyticsCollectionIndex as jest.Mock).toHaveBeenCalledWith( + mockClient.asCurrentUser + ); + }); + + it('should not call setup analytics index on other errors and return error', async () => { + const error = { + meta: { + body: { + error: { + type: 'other error', + }, + }, + }, + }; + mockClient.asCurrentUser.search.mockImplementationOnce(() => Promise.reject(error)); + await expect( + fetchAnalyticsCollections(mockClient as unknown as IScopedClusterClient) + ).rejects.toMatchObject(error); + + expect(mockClient.asCurrentUser.search).toHaveBeenCalledWith({ + from: 0, + index: ANALYTICS_COLLECTIONS_INDEX, + query: { + match_all: {}, + }, + size: 1000, + }); + expect(setupAnalyticsCollectionIndex as jest.Mock).not.toHaveBeenCalled(); + }); + }); + describe('fetch collection by name', () => { it('should fetch analytics collection by name', async () => { mockClient.asCurrentUser.search.mockImplementationOnce(() => diff --git a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts index 46f718d23976d0b..ef356ae2bca2cce 100644 --- a/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts +++ b/x-pack/plugins/enterprise_search/server/lib/analytics/fetch_analytics_collection.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IScopedClusterClient } from '@kbn/core-elasticsearch-server'; import { ANALYTICS_COLLECTIONS_INDEX } from '../..'; import { AnalyticsCollection } from '../../../common/types/analytics'; import { isIndexNotFoundException } from '../../utils/identify_exceptions'; +import { fetchAll } from '../fetch_all'; import { setupAnalyticsCollectionIndex } from './setup_indices'; @@ -36,3 +38,19 @@ export const fetchAnalyticsCollectionByName = async ( return undefined; } }; + +export const fetchAnalyticsCollections = async ( + client: IScopedClusterClient +): Promise => { + const query: QueryDslQueryContainer = { match_all: {} }; + + try { + return await fetchAll(client, ANALYTICS_COLLECTIONS_INDEX, query); + } catch (error) { + if (isIndexNotFoundException(error)) { + await setupAnalyticsCollectionIndex(client.asCurrentUser); + return []; + } + throw error; + } +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts index be53b797239a13a..7035329e4d31410 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/analytics.ts @@ -11,11 +11,24 @@ import { i18n } from '@kbn/i18n'; import { ErrorCode } from '../../../common/types/error_codes'; import { addAnalyticsCollection } from '../../lib/analytics/add_analytics_collection'; +import { fetchAnalyticsCollections } from '../../lib/analytics/fetch_analytics_collection'; import { RouteDependencies } from '../../plugin'; import { createError } from '../../utils/create_error'; import { elasticsearchErrorHandler } from '../../utils/elasticsearch_error_handler'; export function registerAnalyticsRoutes({ router, log }: RouteDependencies) { + router.get( + { + path: '/internal/enterprise_search/analytics/collections', + validate: {}, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + const collections = await fetchAnalyticsCollections(client); + return response.ok({ body: collections }); + }) + ); + router.post( { path: '/internal/enterprise_search/analytics/collections', diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts index 5d5f72c29772706..6741721053b8632 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/indices.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { ErrorCode } from '../../../common/types/error_codes'; +import { deleteConnectorById } from '../../lib/connectors/delete_connector'; import { fetchConnectorByIndexName, fetchConnectors } from '../../lib/connectors/fetch_connectors'; import { fetchCrawlerByIndexName, fetchCrawlers } from '../../lib/crawler/fetch_crawlers'; @@ -124,6 +125,52 @@ export function registerIndexRoutes({ router, log }: RouteDependencies) { }) ); + router.delete( + { + path: '/internal/enterprise_search/indices/{indexName}', + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, + }, + elasticsearchErrorHandler(log, async (context, request, response) => { + const indexName = decodeURIComponent(request.params.indexName); + const { client } = (await context.core).elasticsearch; + + try { + const connector = await fetchConnectorByIndexName(client, indexName); + const crawler = await fetchCrawlerByIndexName(client, indexName); + + if (connector) { + await deleteConnectorById(client, connector.id); + } + + if (crawler) { + // do nothing for now because we don't have a way to delete a crawler yet + } + + await client.asCurrentUser.indices.delete({ index: indexName }); + + return response.ok({ + body: {}, + headers: { 'content-type': 'application/json' }, + }); + } catch (error) { + if (isIndexNotFoundException(error)) { + return createError({ + errorCode: ErrorCode.INDEX_NOT_FOUND, + message: 'Could not find index', + response, + statusCode: 404, + }); + } + + throw error; + } + }) + ); + router.get( { path: '/internal/enterprise_search/indices/{indexName}/exists', diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 03cf38f141f0dc0..7f166cb54e299e5 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -1415,7 +1415,8 @@ describe('Lens App', () => { layers: [ { indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - timeFieldName: 'order_date', + xFieldName: 'order_date', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', palette: { diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts index 5cc62012a509498..e566443640d8576 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_helpers.test.ts @@ -288,7 +288,8 @@ describe('suggestion helpers', () => { layers: [ { indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - timeFieldName: 'order_date', + xFieldName: 'order_date', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', palette: { @@ -369,7 +370,8 @@ describe('suggestion helpers', () => { layers: [ { indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - timeFieldName: 'order_date', + xFieldName: 'order_date', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', palette: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index 90f59f5470e3778..e90ed2c781d5a26 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1504,7 +1504,8 @@ describe('IndexPattern Data Source suggestions', () => { const context = [ { indexPatternId: '1', - timeFieldName: 'timestamp', + xFieldName: 'timestamp', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', palette: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index 319c309cac03635..382df0c9b53dd88 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -178,10 +178,8 @@ function getEmptyLayersSuggestionsForVisualizeCharts( if (!indexPattern) return []; const newId = generateId(); - let newLayer: IndexPatternLayer | undefined; - if (indexPattern.timeFieldName) { - newLayer = createNewTimeseriesLayerWithMetricAggregationFromVizEditor(indexPattern, layer); - } + const newLayer: IndexPatternLayer | undefined = + createNewLayerWithMetricAggregationFromVizEditor(indexPattern, layer); if (newLayer) { const suggestion = buildSuggestion({ state, @@ -197,13 +195,13 @@ function getEmptyLayersSuggestionsForVisualizeCharts( return suggestions; } -function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( +function createNewLayerWithMetricAggregationFromVizEditor( indexPattern: IndexPattern, layer: VisualizeEditorLayersContext ): IndexPatternLayer | undefined { - const { timeFieldName, splitMode, splitFilters, metrics, timeInterval, dropPartialBuckets } = + const { splitMode, splitFilters, metrics, timeInterval, dropPartialBuckets, xMode, xFieldName } = layer; - const dateField = indexPattern.getFieldByName(timeFieldName!); + const xField = xFieldName ? indexPattern.getFieldByName(xFieldName) : undefined; const splitFields = layer.splitFields ? (layer.splitFields @@ -213,10 +211,10 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( // generate the layer for split by terms if (splitMode === 'terms' && splitFields?.length) { - return getSplitByTermsLayer(indexPattern, splitFields, dateField, layer); + return getSplitByTermsLayer(indexPattern, splitFields, xField, xMode, layer); // generate the layer for split by filters } else if (splitMode?.includes('filter') && splitFilters && splitFilters.length) { - return getSplitByFiltersLayer(indexPattern, dateField, layer); + return getSplitByFiltersLayer(indexPattern, xField, xMode, layer); } else { const copyMetricsArray = [...metrics]; const computedLayer = computeLayerFromContext( @@ -231,11 +229,15 @@ function createNewTimeseriesLayerWithMetricAggregationFromVizEditor( return computedLayer; } + if (!xField || !xMode) { + return computedLayer; + } + return insertNewColumn({ - op: 'date_histogram', + op: xMode, layer: computedLayer, columnId: generateId(), - field: dateField, + field: xField, indexPattern, visualizationGroups: [], columnParams: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index 3a85a317eec0f3f..80f6eaecd3a8a78 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -148,7 +148,8 @@ describe('loader', () => { layers: [ { indexPatternId: '1', - timeFieldName: 'timestamp', + xFieldName: 'timestamp', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', metrics: [], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index 3d079584e32f9a6..634ed490e11cc33 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -1651,7 +1651,9 @@ export function computeLayerFromContext( FormulaIndexPatternColumn, 'managedReference' >; - const tempLayer = { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] }; + const tempLayer = isLast + ? { indexPatternId: indexPattern.id, columns: {}, columnOrder: [] } + : computeLayerFromContext(metricsArray.length === 1, metricsArray, indexPattern); let newColumn = operationDefinition.buildColumn({ indexPattern, layer: tempLayer, @@ -1740,7 +1742,8 @@ export function computeLayerFromContext( export function getSplitByTermsLayer( indexPattern: IndexPattern, splitFields: IndexPatternField[], - dateField: IndexPatternField | undefined, + xField: IndexPatternField | undefined, + xMode: string | undefined, layer: VisualizeEditorLayersContext ): IndexPatternLayer { const { termsParams, metrics, timeInterval, splitWithDateHistogram, dropPartialBuckets } = layer; @@ -1759,18 +1762,21 @@ export function getSplitByTermsLayer( let termsLayer = insertNewColumn({ op: splitWithDateHistogram ? 'date_histogram' : 'terms', - layer: insertNewColumn({ - op: 'date_histogram', - layer: computedLayer, - columnId: generateId(), - field: dateField, - indexPattern, - visualizationGroups: [], - columnParams: { - interval: timeInterval, - dropPartials: dropPartialBuckets, - }, - }), + layer: + xField && xMode + ? insertNewColumn({ + op: xMode, + layer: computedLayer, + columnId: generateId(), + field: xField, + indexPattern, + visualizationGroups: [], + columnParams: { + interval: timeInterval, + dropPartials: dropPartialBuckets, + }, + }) + : computedLayer, columnId, field: baseField, indexPattern, @@ -1821,7 +1827,8 @@ export function getSplitByTermsLayer( export function getSplitByFiltersLayer( indexPattern: IndexPattern, - dateField: IndexPatternField | undefined, + xField: IndexPatternField | undefined, + xMode: string | undefined, layer: VisualizeEditorLayersContext ): IndexPatternLayer { const { splitFilters, metrics, timeInterval, dropPartialBuckets } = layer; @@ -1847,18 +1854,21 @@ export function getSplitByFiltersLayer( const columnId = generateId(); let filtersLayer = insertNewColumn({ op: 'filters', - layer: insertNewColumn({ - op: 'date_histogram', - layer: computedLayer, - columnId: generateId(), - field: dateField, - indexPattern, - visualizationGroups: [], - columnParams: { - interval: timeInterval, - dropPartials: dropPartialBuckets, - }, - }), + layer: + xField && xMode + ? insertNewColumn({ + op: xMode, + layer: computedLayer, + columnId: generateId(), + field: xField, + indexPattern, + visualizationGroups: [], + columnParams: { + interval: timeInterval, + dropPartials: dropPartialBuckets, + }, + }) + : computedLayer, columnId, field: undefined, indexPattern, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index d79a9b757953691..97df24648f4d4f6 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -229,6 +229,9 @@ interface ChartSettings { fill?: string; legend?: Record; gridLinesVisibility?: Record; + tickLabelsVisibility?: Record; + axisTitlesVisibility?: Record; + valueLabels?: boolean; extents?: { yLeftExtent: AxisExtents; yRightExtent: AxisExtents; diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts index 68170468647834f..ad26ea92762bf71 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.test.ts @@ -971,7 +971,8 @@ describe('xy_visualization', () => { layers: [ { indexPatternId: 'ff959d40-b880-11e8-a6d9-e546fe2bba5f', - timeFieldName: 'order_date', + xFieldName: 'order_date', + xMode: 'date_histogram', chartType: 'area', axisPosition: 'left', palette: { diff --git a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx index e0d0a19cebec878..24710c3261cdbe2 100644 --- a/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/xy/visualization.tsx @@ -452,7 +452,10 @@ export const getXyVisualization = ({ yLeftExtent: context.configuration.extents?.yLeftExtent, legend: context.configuration.legend, gridlinesVisibilitySettings: context.configuration.gridLinesVisibility, + tickLabelsVisibilitySettings: context.configuration.tickLabelsVisibility, + axisTitlesVisibilitySettings: context.configuration.axisTitlesVisibility, valuesInLegend: true, + valueLabels: context.configuration.valueLabels ? 'show' : 'hide', layers: visualizationStateLayers, }, }; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx new file mode 100644 index 000000000000000..666fc6318c5775c --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/mock_security_context.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { SecuritySolutionPluginContext } from '../..'; + +export const getSecuritySolutionContextMock = (): SecuritySolutionPluginContext => ({ + getFiltersGlobalComponent: + () => + ({ children }) => +
{children}
, + licenseService: { + isEnterprise() { + return true; + }, + }, + sourcererDataView: { + browserFields: {}, + selectedPatterns: [], + indexPattern: { fields: [], title: '' }, + }, +}); diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx new file mode 100644 index 000000000000000..752e93b756ddbfe --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/story_providers.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { ReactNode, VFC } from 'react'; +import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { CoreStart, IUiSettingsClient } from '@kbn/core/public'; +import { SecuritySolutionContext } from '../../containers/security_solution_context'; +import { getSecuritySolutionContextMock } from './mock_security_context'; + +export interface KibanaContextMock { + /** + * For the data plugin (see {@link DataPublicPluginStart}) + */ + data?: DataPublicPluginStart; + /** + * For the core ui-settings package (see {@link IUiSettingsClient}) + */ + uiSettings?: IUiSettingsClient; +} + +export interface StoryProvidersComponentProps { + /** + * Used to generate a new KibanaReactContext (using {@link createKibanaReactContext}) + */ + kibana: KibanaContextMock; + /** + * Component(s) to be displayed inside + */ + children: ReactNode; +} + +/** + * Helper functional component used in Storybook stories. + * Wraps the story with our {@link SecuritySolutionContext} and KibanaReactContext. + */ +export const StoryProvidersComponent: VFC = ({ + children, + kibana, +}) => { + const KibanaReactContext = createKibanaReactContext(kibana as CoreStart); + const securitySolutionContextMock = getSecuritySolutionContextMock(); + + return ( + + {children} + + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx index afd526342689b5b..a2c0318c482aaf2 100644 --- a/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx +++ b/x-pack/plugins/threat_intelligence/public/common/mocks/test_providers.tsx @@ -14,6 +14,7 @@ import type { IStorage } from '@kbn/kibana-utils-plugin/public'; import { Storage } from '@kbn/kibana-utils-plugin/public'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { BehaviorSubject } from 'rxjs'; +import { getSecuritySolutionContextMock } from './mock_security_context'; import { mockUiSetting } from './mock_kibana_ui_setting'; import { KibanaContext } from '../../hooks/use_kibana'; import { SecuritySolutionPluginContext } from '../../types'; @@ -95,22 +96,7 @@ const coreServiceMock = { uiSettings: { get: jest.fn().mockImplementation(mockUiSetting) }, }; -const mockSecurityContext: SecuritySolutionPluginContext = { - getFiltersGlobalComponent: - () => - ({ children }) => -
{children}
, - licenseService: { - isEnterprise() { - return true; - }, - }, - sourcererDataView: { - browserFields: {}, - selectedPatterns: [], - indexPattern: { fields: [], title: '' }, - }, -}; +const mockSecurityContext: SecuritySolutionPluginContext = getSecuritySolutionContextMock(); export const mockedServices = { ...coreServiceMock, diff --git a/x-pack/plugins/threat_intelligence/public/components/paywall/index.tsx b/x-pack/plugins/threat_intelligence/public/components/paywall/index.tsx new file mode 100644 index 000000000000000..46d2df81a1c8cb5 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/components/paywall/index.tsx @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './paywall'; diff --git a/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.stories.tsx b/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.stories.tsx new file mode 100644 index 000000000000000..14de7d5b907fba3 --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.stories.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Paywall } from './paywall'; + +export default { + component: BasicPaywall, + title: 'Paywall', +}; + +export function BasicPaywall() { + return ; +} diff --git a/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.tsx b/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.tsx new file mode 100644 index 000000000000000..8f095a2fd9baabd --- /dev/null +++ b/x-pack/plugins/threat_intelligence/public/components/paywall/paywall.tsx @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { VFC } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +interface PaywallProps { + /** + * Can be obtained using `http.basePath.prepend('/app/management/stack/license_management')` + */ + licenseManagementHref: string; +} + +export const Paywall: VFC = ({ licenseManagementHref }) => { + return ( + } + color="subdued" + data-test-subj="tiPaywall" + title={ +

+ +

+ } + body={ +

+ +

+ } + actions={ + + +
+ + + +
+
+ +
+ + + +
+
+
+ } + /> + ); +}; diff --git a/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.test.tsx b/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.test.tsx index 4c0abd97f154d12..a01c3b94e1cd41b 100644 --- a/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.test.tsx @@ -7,6 +7,7 @@ import { render, screen } from '@testing-library/react'; import React from 'react'; +import { TestProvidersComponent } from '../../common/mocks/test_providers'; import { SecuritySolutionPluginContext } from '../../types'; import { SecuritySolutionContext } from '../security_solution_context'; import { EnterpriseGuard } from './enterprise_guard'; @@ -15,17 +16,19 @@ describe('', () => { describe('when on enterprise plan', () => { it('should render specified children', () => { render( - - -
enterprise only content
-
-
+ + + +
enterprise only content
+
+
+
); expect(screen.queryByText('enterprise only content')).toBeInTheDocument(); @@ -35,21 +38,23 @@ describe('', () => { describe('when not on enterprise plan', () => { it('should render specified children', () => { render( - - fallback for non enterprise}> -
enterprise only content
-
-
+ + + +
enterprise only content
+
+
+
); expect(screen.queryByText('enterprise only content')).not.toBeInTheDocument(); - expect(screen.queryByText('fallback for non enterprise')).toBeInTheDocument(); + expect(screen.queryByTestId('tiPaywall')).toBeInTheDocument(); }); }); }); diff --git a/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.tsx b/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.tsx index 67351c1e3d149fd..06ca98973af0933 100644 --- a/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.tsx +++ b/x-pack/plugins/threat_intelligence/public/containers/enterprise_guard/enterprise_guard.tsx @@ -5,20 +5,25 @@ * 2.0. */ -import React, { ReactElement } from 'react'; +import React from 'react'; import { FC } from 'react'; +import { Paywall } from '../../components/paywall'; +import { useKibana } from '../../hooks/use_kibana'; import { useSecurityContext } from '../../hooks/use_security_context'; -interface EnterpriseGuardProps { - fallback?: ReactElement; -} - -export const EnterpriseGuard: FC = ({ children, fallback = null }) => { +export const EnterpriseGuard: FC = ({ children }) => { const { licenseService } = useSecurityContext(); + const { + services: { http }, + } = useKibana(); if (licenseService.isEnterprise()) { return <>{children}; } - return fallback; + return ( + + ); }; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx index 1f4cbce1c76a2e6..478b2ec398b9797 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.stories.tsx @@ -7,12 +7,14 @@ import moment from 'moment'; import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { of } from 'rxjs'; import { Story } from '@storybook/react'; import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; import { TimeRange } from '@kbn/es-query'; +import { DataPublicPluginStart } from '@kbn/data-plugin/public'; +import { IUiSettingsClient } from '@kbn/core/public'; +import { StoryProvidersComponent } from '../../../../common/mocks/story_providers'; import { Aggregation, AGGREGATION_NAME } from '../../hooks/use_aggregated_indicators'; import { DEFAULT_TIME_RANGE } from '../../hooks/use_filters/utils'; import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper'; @@ -74,38 +76,43 @@ const aggregation2: Aggregation = { doc_count: 0, key: '[Filebeat] AbuseCH MalwareBazaar', }; -const KibanaReactContext = createKibanaReactContext({ - data: { - search: { - search: () => - of({ - rawResponse: { - aggregations: { - [AGGREGATION_NAME]: { - buckets: [aggregation1, aggregation2], - }, +const mockData = { + search: { + search: () => + of({ + rawResponse: { + aggregations: { + [AGGREGATION_NAME]: { + buckets: [aggregation1, aggregation2], }, }, - }), - }, - query: { - timefilter: { - timefilter: { - calculateBounds: () => ({ - min: moment(validDate), - max: moment(validDate).add(numberOfDays, 'days'), - }), }, + }), + }, + query: { + timefilter: { + timefilter: { + calculateBounds: () => ({ + min: moment(validDate), + max: moment(validDate).add(numberOfDays, 'days'), + }), }, }, + filterManager: { + getFilters: () => {}, + setFilters: () => {}, + getUpdates$: () => of(), + }, }, - uiSettings: { get: () => {} }, -} as unknown as Partial); +} as unknown as DataPublicPluginStart; + +const mockUiSettings = { get: () => {} } as unknown as IUiSettingsClient; export const Default: Story = () => { return ( - + - + ); }; +Default.decorators = [(story) => {story()}]; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx index b8f06c06aa6f5e8..9deeb4e5bcb8adb 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_barchart_wrapper/indicators_barchart_wrapper.test.tsx @@ -12,8 +12,11 @@ import { DataView, DataViewField } from '@kbn/data-views-plugin/common'; import { TestProvidersComponent } from '../../../../common/mocks/test_providers'; import { IndicatorsBarChartWrapper } from './indicators_barchart_wrapper'; import { DEFAULT_TIME_RANGE } from '../../hooks/use_filters/utils'; +import { useFilters } from '../../hooks/use_filters'; -const mockIndexPatterns: DataView = { +jest.mock('../../hooks/use_filters'); + +const mockIndexPattern: DataView = { fields: [ { name: '@timestamp', @@ -25,13 +28,26 @@ const mockIndexPatterns: DataView = { } as DataViewField, ], } as DataView; + const mockTimeRange: TimeRange = DEFAULT_TIME_RANGE; +const stub = () => {}; + describe('', () => { + beforeEach(() => { + (useFilters as jest.MockedFunction).mockReturnValue({ + filters: [], + filterQuery: { language: 'kuery', query: '' }, + filterManager: {} as any, + handleSavedQuery: stub, + handleSubmitQuery: stub, + handleSubmitTimeRange: stub, + }); + }); it('should render barchart and field selector dropdown', () => { const component = render( - + ); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx index b8bc912e6208eee..0e94e873728202f 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/components/indicators_table/indicators_table.stories.tsx @@ -20,7 +20,7 @@ export default { }; const indicatorsFixture: Indicator[] = Array(10).fill(generateMockIndicator()); -const mockIndexPattern: DataView = {} as DataView; +const mockIndexPattern: DataView = undefined as unknown as DataView; const stub = () => void 0; diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx index 90bffdb6464842b..7a671f923f796e2 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.test.tsx @@ -21,6 +21,9 @@ import { mockedSearchService, mockedTimefilterService, } from '../../../common/mocks/test_providers'; +import { useFilters } from './use_filters'; + +jest.mock('./use_filters/use_filters'); const aggregationResponse = { rawResponse: { aggregations: { [AGGREGATION_NAME]: { buckets: [] } } }, @@ -35,6 +38,8 @@ const useAggregatedIndicatorsParams: UseAggregatedIndicatorsParam = { timeRange: DEFAULT_TIME_RANGE, }; +const stub = () => {}; + describe('useAggregatedIndicators()', () => { beforeEach(jest.clearAllMocks); @@ -45,6 +50,15 @@ describe('useAggregatedIndicators()', () => { describe('when mounted', () => { beforeEach(() => { + (useFilters as jest.MockedFunction).mockReturnValue({ + filters: [], + filterQuery: { language: 'kuery', query: '' }, + filterManager: {} as any, + handleSavedQuery: stub, + handleSubmitQuery: stub, + handleSubmitTimeRange: stub, + }); + renderHook(() => useAggregatedIndicators(useAggregatedIndicatorsParams), { wrapper: TestProvidersComponent, }); diff --git a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts index 0520991e6593d80..9322e84d78c4fd2 100644 --- a/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts +++ b/x-pack/plugins/threat_intelligence/public/modules/indicators/hooks/use_aggregated_indicators.ts @@ -15,6 +15,7 @@ import { isErrorResponse, TimeRangeBounds, } from '@kbn/data-plugin/common'; +import { useFilters } from './use_filters'; import { convertAggregationToChartSeries } from '../../../common/utils/barchart'; import { RawIndicatorFieldId } from '../../../../common/types/indicator'; import { THREAT_QUERY_BASE } from '../../../../common/constants'; @@ -87,6 +88,8 @@ export const useAggregatedIndicators = ({ [queryService, timeRange] ); + const { filters, filterQuery } = useFilters(); + const loadData = useCallback(async () => { const dateFrom: number = (dateRange.min as moment.Moment).toDate().getTime(); const dateTo: number = (dateRange.max as moment.Moment).toDate().getTime(); @@ -101,8 +104,13 @@ export const useAggregatedIndicators = ({ query: THREAT_QUERY_BASE, language: 'kuery', }, + { + query: filterQuery.query as string, + language: 'kuery', + }, ], [ + ...filters, { query: { range: { @@ -175,6 +183,8 @@ export const useAggregatedIndicators = ({ dateRange.max, dateRange.min, field, + filterQuery, + filters, searchService, selectedPatterns, timeRange.from,