From 293dc95f8a54d234877a836191af4fac4e04649c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 23 Jun 2021 19:07:57 +0200 Subject: [PATCH] [Exploratory view] Refactor code for multi series (#101157) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app/RumDashboard/ActionMenu/index.tsx | 5 +- .../PageLoadDistribution/index.tsx | 2 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 2 +- .../components/empty_view.tsx | 8 +- .../components/filter_label.test.tsx | 4 + .../components/filter_label.tsx | 6 +- .../configurations/constants/constants.ts | 22 +- .../configurations/default_configs.ts | 14 +- .../configurations/lens_attributes.test.ts | 305 +++++----- .../configurations/lens_attributes.ts | 528 +++++++++++------- .../mobile/device_distribution_config.ts | 2 +- .../synthetics/data_distribution_config.ts | 4 +- .../test_data/sample_attribute.ts | 28 +- .../exploratory_view/configurations/utils.ts | 40 +- .../exploratory_view.test.tsx | 3 +- .../exploratory_view/exploratory_view.tsx | 105 +++- .../exploratory_view/header/header.test.tsx | 2 +- .../shared/exploratory_view/header/header.tsx | 7 +- .../hooks/use_app_index_pattern.tsx | 35 +- .../hooks/use_lens_attributes.ts | 81 ++- .../hooks/use_series_storage.tsx | 29 +- .../shared/exploratory_view/index.tsx | 4 +- .../shared/exploratory_view/rtl_helpers.tsx | 11 +- .../series_builder/columns/chart_types.tsx | 8 +- .../columns/data_types_col.test.tsx | 11 +- .../series_builder/columns/data_types_col.tsx | 6 +- .../columns/date_picker_col.tsx | 11 +- .../columns/operation_type_select.test.tsx | 8 +- .../columns/report_breakdowns.test.tsx | 6 +- .../columns/report_definition_col.test.tsx | 6 +- .../columns/report_definition_col.tsx | 32 +- .../columns/report_definition_field.tsx | 26 +- .../columns/report_filters.test.tsx | 2 +- .../columns/report_types_col.test.tsx | 11 +- .../columns/report_types_col.tsx | 28 +- .../series_builder/last_updated.tsx | 37 ++ .../series_builder/series_builder.tsx | 273 ++++++--- .../series_date_picker/date_range_picker.tsx | 113 ++++ .../series_date_picker/index.tsx | 2 +- .../series_date_picker.test.tsx | 10 +- .../series_editor/columns/breakdowns.test.tsx | 4 +- .../series_editor/columns/date_picker_col.tsx | 11 +- .../series_editor/columns/filter_expanded.tsx | 8 +- .../columns/filter_value_btn.test.tsx | 4 +- .../columns/filter_value_btn.tsx | 4 +- .../series_editor/columns/remove_series.tsx | 4 +- .../series_editor/columns/series_actions.tsx | 92 ++- .../series_editor/selected_filters.test.tsx | 2 +- .../series_editor/selected_filters.tsx | 5 +- .../series_editor/series_editor.tsx | 128 ++--- .../shared/exploratory_view/types.ts | 7 +- .../utils/stringify_kueries.test.ts | 148 +++++ .../utils/stringify_kueries.ts | 37 ++ .../observability/public/routes/index.tsx | 14 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../common/charts/ping_histogram.tsx | 2 +- .../common/header/action_menu_content.tsx | 5 +- .../monitor_duration_container.tsx | 2 +- 59 files changed, 1551 insertions(+), 765 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 20d930d28599..63ba7047696c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -47,10 +47,11 @@ export function UXActionMenu({ const uxExploratoryViewLink = createExploratoryViewUrl( { - 'ux-series': { + 'ux-series': ({ dataType: 'ux', + isNew: true, time: { from: rangeFrom, to: rangeTo }, - } as SeriesUrl, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index e0486af6cd6e..5c63cc24b6fd 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -89,7 +89,7 @@ export function PageLoadDistribution() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index c45637e5d3c8..667d0b5e4b4d 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -64,7 +64,7 @@ export function PageViewsTrend() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index ea69a371ceda..3566835b1701 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -38,6 +38,12 @@ export function EmptyView({ emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT; } + if (!series) { + emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found. Please add a series.', + }); + } + return ( {loading && ( @@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui export const CHOOSE_REPORT_DEFINITION = i18n.translate( 'xpack.observability.expView.seriesBuilder.emptyReportDefinition', { - defaultMessage: 'Select a report type to create a visualization.', + defaultMessage: 'Select a report definition to create a visualization.', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index af64e74bca89..fe2953edd36d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -29,6 +29,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); @@ -52,6 +53,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -74,6 +76,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -99,6 +102,7 @@ describe('FilterLabel', function () { negate={true} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index 3d4ba6dc08c3..a08e777c5ea7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; @@ -17,6 +17,7 @@ interface Props { seriesId: string; negate: boolean; definitionFilter?: boolean; + indexPattern: IndexPattern; removeFilter: (field: string, value: string, notVal: boolean) => void; } @@ -26,11 +27,10 @@ export function FilterLabel({ field, value, negate, + indexPattern, removeFilter, definitionFilter, }: Props) { - const { indexPattern } = useAppIndexPatternContext(); - const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e119507860c5..01e8d023ae96 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,8 +5,15 @@ * 2.0. */ -import { ReportViewTypeId } from '../../types'; -import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; +import { ReportViewType } from '../../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, BROWSER_FAMILY_LABEL, @@ -58,6 +65,7 @@ export const FieldLabels: Record = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, + [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, @@ -77,11 +85,11 @@ export const FieldLabels: Record = { 'http.request.method': REQUEST_METHOD, }; -export const DataViewLabels: Record = { - dist: PERF_DIST_LABEL, - kpi: KPI_OVER_TIME_LABEL, - cwv: CORE_WEB_VITALS_LABEL, - mdd: DEVICE_DISTRIBUTION_LABEL, +export const DataViewLabels: Record = { + 'data-distribution': PERF_DIST_LABEL, + 'kpi-over-time': KPI_OVER_TIME_LABEL, + 'core-web-vitals': CORE_WEB_VITALS_LABEL, + 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 07342d976cbe..574a9f6a2bc1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AppDataType, ReportViewTypes } from '../types'; +import { AppDataType, ReportViewType } from '../types'; import { getRumDistributionConfig } from './rum/data_distribution_config'; import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; @@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; interface Props { - reportType: keyof typeof ReportViewTypes; + reportType: ReportViewType; indexPattern: IndexPattern; dataType: AppDataType; } @@ -25,23 +25,23 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { case 'ux': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'cwv') { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); case 'synthetics': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); case 'mobile': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'mdd') { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 8b21df64a3c9..5189a529bda8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -5,25 +5,37 @@ * 2.0. */ -import { LensAttributes } from './lens_attributes'; +import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { buildExistsFilter, buildPhrasesFilter } from './utils'; describe('Lens Attribute', () => { mockAppIndexPattern(); const reportViewConfig = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); + reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + let lnsAttr: LensAttributes; + const layerConfig: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: {}, + time: { from: 'now-15m', to: 'now' }, + }; + beforeEach(() => { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + lnsAttr = new LensAttributes([layerConfig]); }); it('should return expected json', function () { @@ -31,7 +43,7 @@ describe('Lens Attribute', () => { }); it('should return main y axis', function () { - expect(lnsAttr.getMainYAxis()).toEqual({ + expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', isBucketed: false, label: 'Pages loaded', @@ -42,7 +54,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -60,7 +72,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -79,11 +91,18 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with passed value', function () { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { - 'performance.metric': [LCP_FIELD], - }); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + lnsAttr = new LensAttributes([layerConfig1]); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -102,7 +121,7 @@ describe('Lens Attribute', () => { }); it('should return expected number range column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -124,7 +143,7 @@ describe('Lens Attribute', () => { }); it('should return expected number operation column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -160,7 +179,7 @@ describe('Lens Attribute', () => { }); it('should return main x axis', function () { - expect(lnsAttr.getXAxis()).toEqual({ + expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -182,38 +201,45 @@ describe('Lens Attribute', () => { }); it('should return first layer', function () { - expect(lnsAttr.getLayer()).toEqual({ - columnOrder: ['x-axis-column', 'y-axis-column'], - columns: { - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], - type: 'histogram', + expect(lnsAttr.getLayers()).toEqual({ + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', }, + incompleteColumns: {}, }, - incompleteColumns: {}, }); }); @@ -225,12 +251,12 @@ describe('Lens Attribute', () => { gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -240,108 +266,52 @@ describe('Lens Attribute', () => { }); }); - describe('ParseFilters function', function () { - it('should parse default filters', function () { - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ]); - }); - - it('should parse default and ui filters', function () { - lnsAttr = new LensAttributes( - mockIndexPattern, - reportViewConfig, - 'line', - [ - { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, - { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, - ], - 'count', - {} - ); - - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - { - meta: { - index: 'apm-*', - key: 'service.name', - params: ['elastic-co', 'kibana-front'], - type: 'phrases', - value: 'elastic-co, kibana-front', - }, - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'service.name': 'elastic-co', - }, - }, - { - match_phrase: { - 'service.name': 'kibana-front', - }, - }, - ], - }, - }, - }, - { - meta: { - index: 'apm-*', - }, - query: { - match_phrase: { - 'user_agent.name': 'Firefox', - }, - }, - }, - { - meta: { - index: 'apm-*', - negate: true, - }, - query: { - match_phrase: { - 'user_agent.name': 'Chrome', - }, - }, - }, - ]); - }); - }); - describe('Layer breakdowns', function () { - it('should add breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); + it('should return breakdown column', function () { + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + breakdown: USER_AGENT_NAME, + time: { from: 'now-15m', to: 'now' }, + }; + + lnsAttr = new LensAttributes([layerConfig1]); + + lnsAttr.getBreakdownColumn({ + sourceField: USER_AGENT_NAME, + layerId: 'layer0', + indexPattern: mockIndexPattern, + }); expect(lnsAttr.visualization.layers).toEqual([ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - splitAccessor: 'break-down-column', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + splitAccessor: 'breakdown-column-layer0', + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); - expect(lnsAttr.layers.layer1).toEqual({ - columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + expect(lnsAttr.layers.layer0).toEqual({ + columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'], columns: { - 'break-down-column': { + 'breakdown-column-layer0': { dataType: 'string', isBucketed: true, label: 'Top values of Browser family', operationType: 'terms', params: { missingBucket: false, - orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, orderDirection: 'desc', otherBucket: true, size: 10, @@ -349,10 +319,10 @@ describe('Lens Attribute', () => { scale: 'ordinal', sourceField: 'user_agent.name', }, - 'x-axis-column': { + 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Largest contentful paint', operationType: 'range', params: { maxBars: 'auto', @@ -360,62 +330,47 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.duration.us', + sourceField: 'transaction.marks.agent.largestContentfulPaint', }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, }); }); + }); - it('should remove breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); + describe('Layer Filters', function () { + it('should return expected filters', function () { + reportViewConfig.filters?.push( + ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) + ); - lnsAttr.removeBreakdown(); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.visualization.layers).toEqual([ - { - accessors: ['y-axis-column'], - layerId: 'layer1', - palette: undefined, - seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], - }, - ]); + const filters = lnsAttr.getLayerFilters(layerConfig1, 2); - expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); - - expect(lnsAttr.layers.layer1.columns).toEqual({ - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [{ from: 0, label: '', to: 1000 }], - type: 'histogram', - }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', - }, - }); + expect(filters).toEqual( + '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)' + ); }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 22ad18c663b3..208e8d8ba43c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -27,13 +27,12 @@ import { TermsIndexPatternColumn, CardinalityIndexPatternColumn, } from '../../../../../../lens/public'; -import { - buildPhraseFilter, - buildPhrasesFilter, - IndexPattern, -} from '../../../../../../../../src/plugins/data/common'; +import { urlFiltersToKueryString } from '../utils/stringify_kueries'; +import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { PersistableFilter } from '../../../../../../lens/common'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; @@ -87,46 +86,50 @@ export const parseCustomFieldName = ( return { fieldName, columnType, columnFilters, timeScale, columnLabel }; }; -export class LensAttributes { +export interface LayerConfig { + filters?: UrlFilter[]; + reportConfig: DataSeries; + breakdown?: string; + seriesType?: SeriesType; + operationType?: OperationType; + reportDefinitions: URLReportDefinition; + time: { to: string; from: string }; indexPattern: IndexPattern; +} + +export class LensAttributes { layers: Record; visualization: XYState; - filters: UrlFilter[]; - seriesType: SeriesType; - reportViewConfig: DataSeries; - reportDefinitions: URLReportDefinition; - breakdownSource?: string; + layerConfigs: LayerConfig[]; - constructor( - indexPattern: IndexPattern, - reportViewConfig: DataSeries, - seriesType?: SeriesType, - filters?: UrlFilter[], - operationType?: OperationType, - reportDefinitions?: URLReportDefinition, - breakdownSource?: string - ) { - this.indexPattern = indexPattern; + constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - this.filters = filters ?? []; - this.reportDefinitions = reportDefinitions ?? {}; - this.breakdownSource = breakdownSource; - if (operationType) { - reportViewConfig.yAxisColumns.forEach((yAxisColumn) => { - if (typeof yAxisColumn.operationType !== undefined) { - yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; - } - }); - } - this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; - this.reportViewConfig = reportViewConfig; - this.layers.layer1 = this.getLayer(); + layerConfigs.forEach(({ reportConfig, operationType }) => { + if (operationType) { + reportConfig.yAxisColumns.forEach((yAxisColumn) => { + if (typeof yAxisColumn.operationType !== undefined) { + yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; + } + }); + } + }); + + this.layerConfigs = layerConfigs; + this.layers = this.getLayers(); this.visualization = this.getXyState(); } - getBreakdownColumn(sourceField: string): TermsIndexPatternColumn { - const fieldMeta = this.indexPattern.getFieldByName(sourceField); + getBreakdownColumn({ + sourceField, + layerId, + indexPattern, + }: { + sourceField: string; + layerId: string; + indexPattern: IndexPattern; + }): TermsIndexPatternColumn { + const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, @@ -136,8 +139,8 @@ export class LensAttributes { scale: 'ordinal', isBucketed: true, params: { + orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` }, size: 10, - orderBy: { type: 'column', columnId: 'y-axis-column' }, orderDirection: 'desc', otherBucket: true, missingBucket: false, @@ -145,36 +148,14 @@ export class LensAttributes { }; } - addBreakdown(sourceField: string) { - const { xAxisColumn } = this.reportViewConfig; - if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - // do nothing since this will be used a x axis source - return; - } - this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField); - - this.layers.layer1.columnOrder = [ - 'x-axis-column', - 'break-down-column', - 'y-axis-column', - ...Object.keys(this.getChildYAxises()), - ]; - - this.visualization.layers[0].splitAccessor = 'break-down-column'; - } - - removeBreakdown() { - delete this.layers.layer1.columns['break-down-column']; - - this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; - - this.visualization.layers[0].splitAccessor = undefined; - } - - getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn { + getNumberRangeColumn( + sourceField: string, + reportViewConfig: DataSeries, + label?: string + ): RangeIndexPatternColumn { return { sourceField, - label: this.reportViewConfig.labels[sourceField] ?? label, + label: reportViewConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -187,16 +168,36 @@ export class LensAttributes { }; } - getCardinalityColumn(sourceField: string, label?: string) { - return this.getNumberOperationColumn(sourceField, 'unique_count', label); + getCardinalityColumn({ + sourceField, + label, + reportViewConfig, + }: { + sourceField: string; + label?: string; + reportViewConfig: DataSeries; + }) { + return this.getNumberOperationColumn({ + sourceField, + operationType: 'unique_count', + label, + reportViewConfig, + }); } - getNumberColumn( - sourceField: string, - columnType?: string, - operationType?: string, - label?: string - ) { + getNumberColumn({ + reportViewConfig, + label, + sourceField, + columnType, + operationType, + }: { + sourceField: string; + columnType?: string; + operationType?: string; + label?: string; + reportViewConfig: DataSeries; + }) { if (columnType === 'operation' || operationType) { if ( operationType === 'median' || @@ -204,48 +205,58 @@ export class LensAttributes { operationType === 'sum' || operationType === 'unique_count' ) { - return this.getNumberOperationColumn(sourceField, operationType, label); + return this.getNumberOperationColumn({ + sourceField, + operationType, + label, + reportViewConfig, + }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType); + return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); } } - return this.getNumberRangeColumn(sourceField, label); + return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); } - getNumberOperationColumn( - sourceField: string, - operationType: 'average' | 'median' | 'sum' | 'unique_count', - label?: string - ): + getNumberOperationColumn({ + sourceField, + label, + reportViewConfig, + operationType, + }: { + sourceField: string; + operationType: 'average' | 'median' | 'sum' | 'unique_count'; + label?: string; + reportViewConfig: DataSeries; + }): | AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), - label: - label || - i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: this.reportViewConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: label || reportViewConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), operationType, }; } getPercentileNumberColumn( sourceField: string, - percentileValue: string + percentileValue: string, + reportViewConfig: DataSeries ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -268,7 +279,7 @@ export class LensAttributes { return { operationType: 'terms', sourceField, - label: label || 'Top values of ' + sourceField, + label: 'Top values of ' + label || sourceField, dataType: 'string', isBucketed: true, scale: 'ordinal', @@ -283,30 +294,45 @@ export class LensAttributes { }; } - getXAxis() { - const { xAxisColumn } = this.reportViewConfig; + getXAxis(layerConfig: LayerConfig, layerId: string) { + const { xAxisColumn } = layerConfig.reportConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]); + return this.getBreakdownColumn({ + layerId, + indexPattern: layerConfig.indexPattern, + sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + }); } - return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label); + return this.getColumnBasedOnType({ + layerConfig, + label: xAxisColumn.label, + sourceField: xAxisColumn.sourceField!, + }); } - getColumnBasedOnType( - sourceField: string, - operationType?: OperationType, - label?: string, - colIndex?: number - ) { + getColumnBasedOnType({ + sourceField, + label, + layerConfig, + operationType, + colIndex, + }: { + sourceField: string; + operationType?: OperationType; + label?: string; + layerConfig: LayerConfig; + colIndex?: number; + }) { const { fieldMeta, columnType, fieldName, - columnFilters, - timeScale, columnLabel, - } = this.getFieldMeta(sourceField); + timeScale, + columnFilters, + } = this.getFieldMeta(sourceField, layerConfig); const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -325,47 +351,76 @@ export class LensAttributes { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); + return this.getNumberColumn({ + sourceField: fieldName, + columnType, + operationType, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } if (operationType === 'unique_count') { - return this.getCardinalityColumn(fieldName, columnLabel || label); + return this.getCardinalityColumn({ + sourceField: fieldName, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); } - getCustomFieldName(sourceField: string) { - return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions); + getCustomFieldName({ + sourceField, + layerConfig, + }: { + sourceField: string; + layerConfig: LayerConfig; + }) { + return parseCustomFieldName( + sourceField, + layerConfig.reportConfig, + layerConfig.reportDefinitions + ); } - getFieldMeta(sourceField: string) { + getFieldMeta(sourceField: string, layerConfig: LayerConfig) { const { fieldName, columnType, + columnLabel, columnFilters, timeScale, - columnLabel, - } = this.getCustomFieldName(sourceField); + } = this.getCustomFieldName({ + sourceField, + layerConfig, + }); - const fieldMeta = this.indexPattern.getFieldByName(fieldName); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; } - getMainYAxis() { - const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0]; + getMainYAxis(layerConfig: LayerConfig) { + const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); } - return this.getColumnBasedOnType(sourceField!, operationType, label, 0); + return this.getColumnBasedOnType({ + sourceField, + operationType, + label, + layerConfig, + colIndex: 0, + }); } - getChildYAxises() { + getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record = {}; - const yAxisColumns = this.reportViewConfig.yAxisColumns; + const yAxisColumns = layerConfig.reportConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -373,12 +428,13 @@ export class LensAttributes { for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; - lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType( - sourceField!, + lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({ + sourceField: sourceField!, operationType, label, - i - ); + layerConfig, + colIndex: i, + }); } return lensColumns; } @@ -396,20 +452,139 @@ export class LensAttributes { scale: 'ratio', sourceField: 'Records', filter: columnFilter, - timeScale, + ...(timeScale ? { timeScale } : {}), } as CountIndexPatternColumn; } - getLayer() { - return { - columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())], - columns: { - 'x-axis-column': this.getXAxis(), - 'y-axis-column': this.getMainYAxis(), - ...this.getChildYAxises(), - }, - incompleteColumns: {}, - }; + getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { + const { + filters, + time: { from, to }, + reportConfig: { filters: layerFilters, reportType }, + } = layerConfig; + let baseFilters = ''; + if (reportType !== 'kpi-over-time' && totalLayers > 1) { + // for kpi over time, we don't need to add time range filters + // since those are essentially plotted along the x-axis + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + } + + layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { + const qFilter = filter as PersistableFilter; + if (qFilter.query?.match_phrase) { + const fieldName = Object.keys(qFilter.query.match_phrase)[0]; + const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + if (qFilter.query?.bool?.should) { + const values: string[] = []; + let fieldName = ''; + qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => { + if (ft.match_phrase) { + fieldName = Object.keys(ft.match_phrase)[0]; + values.push(ft.match_phrase[fieldName]); + } + }); + + const kueryString = `${fieldName}: (${values.join(' or ')})`; + + if (baseFilters.length > 0) { + baseFilters += ` and ${kueryString}`; + } else { + baseFilters += kueryString; + } + } + const existFilter = filter as ExistsFilter; + + if (existFilter.exists) { + const fieldName = existFilter.exists.field; + const kql = `${fieldName} : *`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + }); + + const rFilters = urlFiltersToKueryString(filters ?? []); + if (!baseFilters) { + return rFilters; + } + if (!rFilters) { + return baseFilters; + } + return `${rFilters} and ${baseFilters}`; + } + + getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { + if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + return null; + } + + const { + time: { from: mainFrom }, + } = mainLayerConfig; + + const { + time: { from }, + } = layerConfig; + + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + if (inDays > 1) { + return inDays + 'd'; + } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + return inHours + 'h'; + } + + getLayers() { + const layers: Record = {}; + const layerConfigs = this.layerConfigs; + + layerConfigs.forEach((layerConfig, index) => { + const { breakdown } = layerConfig; + + const layerId = `layer${index}`; + const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); + const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); + const mainYAxis = this.getMainYAxis(layerConfig); + layers[layerId] = { + columnOrder: [ + `x-axis-column-${layerId}`, + ...(breakdown ? [`breakdown-column-${layerId}`] : []), + `y-axis-column-${layerId}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + columns: { + [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), + [`y-axis-column-${layerId}`]: { + ...mainYAxis, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + filter: { query: columnFilter, language: 'kuery' }, + ...(timeShift ? { timeShift } : {}), + }, + ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ? // do nothing since this will be used a x axis source + { + [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ + layerId, + sourceField: breakdown, + indexPattern: layerConfig.indexPattern, + }), + } + : {}), + ...this.getChildYAxises(layerConfig), + }, + incompleteColumns: {}, + }; + }); + + return layers; } getXyState(): XYState { @@ -422,71 +597,48 @@ export class LensAttributes { tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, preferredSeriesType: 'line', - layers: [ - { - accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())], - layerId: 'layer1', - seriesType: this.seriesType ?? 'line', - palette: this.reportViewConfig.palette, - yConfig: this.reportViewConfig.yConfig || [ - { forAccessor: 'y-axis-column', color: 'green' }, - ], - xAccessor: 'x-axis-column', - }, - ], - ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}), + layers: this.layerConfigs.map((layerConfig, index) => ({ + accessors: [ + `y-axis-column-layer${index}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + layerId: `layer${index}`, + seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, + palette: layerConfig.reportConfig.palette, + yConfig: layerConfig.reportConfig.yConfig || [ + { forAccessor: `y-axis-column-layer${index}` }, + ], + xAccessor: `x-axis-column-layer${index}`, + ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + })), + ...(this.layerConfigs[0].reportConfig.yTitle + ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + : {}), }; } - parseFilters() { - const defaultFilters = this.reportViewConfig.filters ?? []; - const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; - - this.filters.forEach(({ field, values = [], notValues = [] }) => { - const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; - - if (values?.length > 0) { - if (values?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); - parsedFilters.push(filter); - } - } - - if (notValues?.length > 0) { - if (notValues?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); - multiFilter.meta.negate = true; - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); - filter.meta.negate = true; - parsedFilters.push(filter); - } - } - }); - - return parsedFilters; - } + parseFilters() {} getJSON(): TypedLensByValueInput['attributes'] { + const uniqueIndexPatternsIds = Array.from( + new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) + ); + return { title: 'Prefilled from exploratory view app', description: '', visualizationType: 'lnsXY', references: [ - { - id: this.indexPattern.id!, + ...uniqueIndexPatternsIds.map((patternId) => ({ + id: patternId!, name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', - }, - { - id: this.indexPattern.id!, - name: getLayerReferenceName('layer1'), + })), + ...this.layerConfigs.map(({ indexPattern }, index) => ({ + id: indexPattern.id!, + name: getLayerReferenceName(`layer${index}`), type: 'index-pattern', - }, + })), ], state: { datasourceStates: { @@ -496,7 +648,7 @@ export class LensAttributes { }, visualization: this.visualization, query: { query: '', language: 'kuery' }, - filters: this.parseFilters(), + filters: [], }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 6f9806660e48..e1cb5a0370fb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - reportType: 'mobile-device-distribution', + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 854f844db047..b958c0dd7152 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { return { reportType: 'data-distribution', - defaultSeriesType: 'line', + defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { sourceField: 'performance.metric', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bc..edf2a4241582 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -10,16 +10,16 @@ export const sampleAttribute = { visualizationType: 'lnsXY', references: [ { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { indexpattern: { layers: { - layer1: { - columnOrder: ['x-axis-column', 'y-axis-column'], + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { - 'x-axis-column': { + 'x-axis-column-layer0': { sourceField: 'transaction.duration.us', label: 'Page load time', dataType: 'number', @@ -32,13 +32,18 @@ export const sampleAttribute = { maxBars: 'auto', }, }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, @@ -57,18 +62,15 @@ export const sampleAttribute = { preferredSeriesType: 'line', layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], - xAccessor: 'x-axis-column', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', }, ], }, query: { query: '', language: 'kuery' }, - filters: [ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ], + filters: [], }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index fc60800bc440..9b1e7ec141ca 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,12 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; -import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; import { URL_KEYS } from './constants/url_constants'; +import { PersistableFilter } from '../../../../../../lens/common'; export function convertToShortUrl(series: SeriesUrl) { const { @@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { } export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)]; } @@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II } export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)]; } @@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern: } export function buildExistsFilter(field: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildExistsFilter(fieldMeta, indexPattern)]; } return []; } + +type FiltersType = PersistableFilter[] | ExistsFilter[]; + +export function urlFilterToPersistedFilter({ + urlFilters, + initFilters, + indexPattern, +}: { + urlFilters: UrlFilter[]; + initFilters: FiltersType; + indexPattern: IIndexPattern; +}) { + const parsedFilters: FiltersType = initFilters ? [...initFilters] : []; + + urlFilters.forEach(({ field, values = [], notValues = [] }) => { + if (values?.length > 0) { + const filter = buildPhrasesFilter(field, values, indexPattern); + parsedFilters.push(...filter); + } + + if (notValues?.length > 0) { + const filter = buildPhrasesFilter(field, notValues, indexPattern)[0]; + filter.meta.negate = true; + parsedFilters.push(filter); + } + }); + + return parsedFilters; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 779049601bd6..989ebf17c206 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -51,8 +51,9 @@ describe('ExploratoryView', () => { const initSeries = { data: { 'ux-series': { + isNew: true, dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, breakdown: 'user_agent .name', reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 329ed20ffed3..ad85ecab968b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -5,9 +5,10 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; + +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -33,6 +61,8 @@ export function ExploratoryView({ const [height, setHeight] = useState('100vh'); const [seriesId, setSeriesId] = useState(''); + const [lastUpdated, setLastUpdated] = useState(); + const [lensAttributes, setLensAttributes] = useState( null ); @@ -47,9 +77,7 @@ export function ExploratoryView({ setSeriesId(firstSeriesId); }, [allSeries, firstSeriesId]); - const lensAttributesT = useLensAttributes({ - seriesId, - }); + const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -60,10 +88,12 @@ export function ExploratoryView({ }; useEffect(() => { - if (series?.dataType) { - loadIndexPattern({ dataType: series?.dataType }); - } - }, [series?.dataType, loadIndexPattern]); + Object.values(allSeries).forEach((seriesT) => { + loadIndexPattern({ + dataType: seriesT.dataType, + }); + }); + }, [allSeries, loadIndexPattern]); useEffect(() => { setLensAttributes(lensAttributesT); @@ -72,47 +102,62 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); + const timeRange = combineTimeRanges(allSeries, series); + + const onLensLoad = useCallback(() => { + setLastUpdated(Date.now()); + }, []); + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + if (series?.reportType !== 'data-distribution') { + setSeries(seriesId, { + ...series, + time: { + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }, + }); + } else { + notifications?.toasts.add( + i18n.translate('xpack.observability.exploratoryView.noBrusing', { + defaultMessage: 'Zoom by brush selection is only available on time series charts.', + }) + ); + } + }, + [notifications?.toasts, series, seriesId, setSeries] + ); + return ( {lens ? ( <> - {lensAttributes && seriesId && series?.reportType && series?.time ? ( + {lensAttributes && timeRange.to && timeRange.from ? ( { - if (series?.reportType !== 'dist') { - setSeries(seriesId, { - ...series, - time: { - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }, - }); - } else { - notifications?.toasts.add( - i18n.translate('xpack.observability.exploratoryView.noBrusing', { - defaultMessage: - 'Zoom by brush selection is only available on time series charts.', - }) - ); - } - }} + onLoad={onLensLoad} + onBrushEnd={onBrushEnd} /> ) : ( )} - + ) : ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1dedc4142f17..8cd8977fcf74 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 3e02207e2627..dbe9cd163451 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { DataViewLabels } from '../configurations/constants'; import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { seriesId: string; @@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const { lens } = kServices; - const { getSeries } = useSeriesStorage(); + const { getSeries, allSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const LensSaveModalComponent = lens.SaveModalComponent; + const timeRange = combineTimeRanges(allSeries, series); + return ( <> @@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { lens.navigateToPrefilledEditor( { id: '', - timeRange: series.time, + timeRange, attributes: lensAttributes, }, true diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 4259bb778e51..7a5f12a72b1f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler'; export interface IIndexPatternContext { loading: boolean; - selectedApp: AppDataType; indexPatterns: IndexPatternState; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; @@ -29,10 +28,10 @@ interface ProviderProps { type HasAppDataState = Record; type IndexPatternState = Record; +type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { - const [loading, setLoading] = useState(false); - const [selectedApp, setSelectedApp] = useState(); + const [loading, setLoading] = useState({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); const [hasAppData, setHasAppData] = useState({ infra_metrics: null, @@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { - setSelectedApp(dataType); + if (hasAppData[dataType] === null && !loading[dataType]) { + setLoading((prevState) => ({ ...prevState, [dataType]: true })); - if (hasAppData[dataType] === null) { - setLoading(true); try { let hasDataT = false; let indices: string | undefined = ''; @@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern })); } - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } }, - [data, hasAppData] + [data, hasAppData, loading] ); return ( loadingT), }} > {children} @@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { ); } -export const useAppIndexPatternContext = () => { - const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( +export const useAppIndexPatternContext = (dataType?: AppDataType) => { + const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( (IndexPatternContext as unknown) as Context ); + if (dataType && !indexPatterns?.[dataType] && !loading) { + loadIndexPattern({ dataType }); + } + return useMemo(() => { return { hasAppData, - selectedApp, loading, - indexPattern: indexPatterns?.[selectedApp], - hasData: hasAppData?.[selectedApp], + indexPatterns, + indexPattern: dataType ? indexPatterns?.[dataType] : undefined, + hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]); + }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c85bc5089b2..11487afe28e9 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,17 +8,13 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { LensAttributes } from '../configurations/lens_attributes'; +import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DataSeries, SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -interface Props { - seriesId: string; -} - export const getFiltersFromDefs = ( reportDefinitions: SeriesUrl['reportDefinitions'], dataViewConfig: DataSeries @@ -37,54 +33,51 @@ export const getFiltersFromDefs = ( }); }; -export const useLensAttributes = ({ - seriesId, -}: Props): TypedLensByValueInput['attributes'] | null => { - const { getSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } = - series ?? {}; +export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { + const { allSeriesIds, allSeries } = useSeriesStorage(); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPatterns } = useAppIndexPatternContext(); return useMemo(() => { - if (!indexPattern || !reportType || isEmpty(reportDefinitions)) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const dataViewConfig = getDefaultConfigs({ - reportType, - dataType, - indexPattern, + const layerConfigs: LayerConfig[] = []; + + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + const reportViewConfig = getDefaultConfigs({ + reportType: seriesT.reportType, + dataType: seriesT.dataType, + indexPattern, + }); + + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + ); + + layerConfigs.push({ + filters, + indexPattern, + reportConfig: reportViewConfig, + breakdown: seriesT.breakdown, + operationType: seriesT.operationType, + seriesType: seriesT.seriesType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + time: seriesT.time, + }); + } }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(reportDefinitions, dataViewConfig) - ); - - const lensAttributes = new LensAttributes( - indexPattern, - dataViewConfig, - seriesType, - filters, - operationType, - reportDefinitions, - breakdown - ); - - if (breakdown) { - lensAttributes.addBreakdown(breakdown); + if (layerConfigs.length < 1) { + return null; } + const lensAttributes = new LensAttributes(layerConfigs); + return lensAttributes.getJSON(); - }, [ - indexPattern, - reportType, - reportDefinitions, - dataType, - series.filters, - seriesType, - operationType, - breakdown, - ]); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index fac75f910a93..e9ae43950d47 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -12,7 +12,7 @@ import { } from '../../../../../../../../src/plugins/kibana_utils/public'; import type { AppDataType, - ReportViewTypeId, + ReportViewType, SeriesUrl, UrlFilter, URLReportDefinition, @@ -36,6 +36,16 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + return allSeriesN; +} + export function UrlStorageContextProvider({ children, storage, @@ -45,15 +55,14 @@ export function UrlStorageContextProvider({ const [allShortSeries, setAllShortSeries] = useState( () => storage.get(allSeriesKey) ?? {} ); - const [allSeries, setAllSeries] = useState({}); + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); const [firstSeriesId, setFirstSeriesId] = useState(''); useEffect(() => { const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); setAllSeries(allSeriesN); setFirstSeriesId(allSeriesIds?.[0]); @@ -68,8 +77,10 @@ export function UrlStorageContextProvider({ }; const removeSeries = (seriesIdN: string) => { - delete allShortSeries[seriesIdN]; - delete allSeries[seriesIdN]; + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); }; const allSeriesIds = Object.keys(allShortSeries); @@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e..e55752ceb62b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 8e54ab7629d2..972e3beb4b72 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { UrlFilter } from './types'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; @@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => { const loadIndexPattern = jest.fn(); const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({ indexPattern: mockIndexPattern, - selectedApp: 'ux', hasData: true, loading: false, hasAppData: { ux: true } as any, loadIndexPattern, + indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record, }); return { spy, loadIndexPattern }; }; @@ -260,7 +263,7 @@ function mockSeriesStorageContext({ }) { const mockDataSeries = data || { 'performance-distribution': { - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', breakdown: breakdown || 'user_agent.name', time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 9ae8b68bf3e8..50c2f91e6067 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({ seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { getSeries, setSeries, allSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; const onChange = (value: SeriesType) => { - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, seriesType: value }); - }); + setSeries(seriesId, { ...series, seriesType: value }); }; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index e3c1666c533e..b10702ebded5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -29,7 +29,14 @@ describe('DataTypesCol', function () { fireEvent.click(screen.getByText(/user experience \(rum\)/i)); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' }); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); }); it('should set series on change on already selected', function () { @@ -37,7 +44,7 @@ describe('DataTypesCol', function () { data: { [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 985afdf88886..f386f62d9ed7 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { if (!dataType) { removeSeries(seriesId); } else { - setSeries(seriesId || `${dataType}-series`, { dataType } as any); + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); } }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx index 175fbea9445c..6be78084ae19 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -8,14 +8,23 @@ import React from 'react'; import styled from 'styled-components'; import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; } export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + return ( - + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index c262a94f968b..516f04e3812b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () { data: { 'performance-distribution': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () { data: { 'series-id': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); @@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 805186e877d5..203382afc162 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Series Builder ReportBreakdowns', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: USER_AGENT_OS, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); @@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: undefined, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index e947961fb430..2e5c674b9fad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); @@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () { data: { [seriesId]: { dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, time: { from: 'now-30d', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, }, @@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', reportDefinitions: {}, - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-30d', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 338f5d52c26f..47962af0d4bc 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { CustomReportField } from '../custom_report_field'; import { DataSeries, URLReportDefinition } from '../../types'; @@ -36,8 +35,6 @@ export function ReportDefinitionCol({ dataViewSeries: DataSeries; seriesId: string; }) { - const { indexPattern } = useAppIndexPatternContext(); - const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -69,21 +66,20 @@ export function ReportDefinitionCol({ - {indexPattern && - reportDefinitions.map(({ field, custom, options }) => ( - - {!custom ? ( - - ) : ( - - )} - - ))} + {reportDefinitions.map(({ field, custom, options }) => ( + + {!custom ? ( + + ) : ( + + )} + + ))} {(hasOperationType || columnType === 'operation') && ( { - if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - /> + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + /> + )} ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index 7ca947fed0bc..f35639388aac 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f36d64ca5bbb..f7cfe06c0d92 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers'; import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; import { ReportTypes } from '../series_builder'; import { DEFAULT_TIME } from '../../configurations/constants'; -import { NEW_SERIES_KEY } from '../../hooks/use_series_storage'; describe('ReportTypesCol', function () { - const seriesId = 'test-series-id'; + const seriesId = 'performance-distribution'; mockAppIndexPattern(); @@ -40,7 +39,7 @@ describe('ReportTypesCol', function () { breakdown: 'user_agent.name', dataType: 'ux', reportDefinitions: {}, - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); @@ -49,11 +48,12 @@ describe('ReportTypesCol', function () { it('should set selected as filled', function () { const initSeries = { data: { - [NEW_SERIES_KEY]: { + [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, + isNew: true, }, }, }; @@ -74,6 +74,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'synthetics', time: DEFAULT_TIME, + isNew: true, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9fff8dae14a4..64c7b48c668b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -7,27 +7,33 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import styled from 'styled-components'; -import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; interface Props { seriesId: string; - reportTypes: Array<{ id: ReportViewTypeId; label: string }>; + reportTypes: ReportTypeItem[]; } export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - const { loading, hasData, selectedApp } = useAppIndexPatternContext(); + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - if (!loading && !hasData && selectedApp) { + if (!restSeries.dataType) { + return {SELECT_DATA_TYPE}; + } + + if (!loading && !hasData) { return ( firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + return reportTypes?.length > 0 ? ( - {reportTypes.map(({ id: reportType, label }) => ( + {reportTypes.map(({ reportType, label }) => (