diff --git a/packages/kbn-test/src/jest/utils/get_url.ts b/packages/kbn-test/src/jest/utils/get_url.ts index e08695b334e1b..734e26c5199d7 100644 --- a/packages/kbn-test/src/jest/utils/get_url.ts +++ b/packages/kbn-test/src/jest/utils/get_url.ts @@ -22,6 +22,11 @@ interface UrlParam { username?: string; } +interface App { + pathname?: string; + hash?: string; +} + /** * Converts a config and a pathname to a url * @param {object} config A url config @@ -41,11 +46,11 @@ interface UrlParam { * @return {string} */ -function getUrl(config: UrlParam, app: UrlParam) { +function getUrl(config: UrlParam, app: App) { return url.format(_.assign({}, config, app)); } -getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: UrlParam) { +getUrl.noAuth = function getUrlNoAuth(config: UrlParam, app: App) { config = _.pickBy(config, function (val, param) { return param !== 'auth'; }); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 8a2a66c41d426..49d56d6f43784 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -202,13 +202,7 @@ export class CommonPageObject extends FtrService { async navigateToApp( appName: string, - { - basePath = '', - shouldLoginIfPrompted = true, - hash = '', - search = '', - insertTimestamp = true, - } = {} + { basePath = '', shouldLoginIfPrompted = true, hash = '', insertTimestamp = true } = {} ) { let appUrl: string; if (this.config.has(['apps', appName])) { @@ -217,13 +211,11 @@ export class CommonPageObject extends FtrService { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}${appConfig.pathname}`, hash: hash || appConfig.hash, - search, }); } else { appUrl = getUrl.noAuth(this.config.get('servers.kibana'), { pathname: `${basePath}/app/${appName}`, hash, - search, }); } 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 0bd873bd7064b..4e6544a20f301 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 @@ -11,11 +11,11 @@ import { i18n } from '@kbn/i18n'; import { createExploratoryViewUrl, HeaderMenuPortal, + SeriesUrl, } from '../../../../../../observability/public'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { AppMountParameters } from '../../../../../../../../src/core/public'; -import { SERVICE_NAME } from '../../../../../common/elasticsearch_fieldnames'; const ANALYZE_DATA = i18n.translate('xpack.apm.analyzeDataButtonLabel', { defaultMessage: 'Analyze data', @@ -38,22 +38,15 @@ export function UXActionMenu({ services: { http }, } = useKibana(); const { urlParams } = useUrlParams(); - const { rangeTo, rangeFrom, serviceName } = urlParams; + const { rangeTo, rangeFrom } = urlParams; const uxExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'ux', - name: `${serviceName}-page-views`, - time: { from: rangeFrom!, to: rangeTo! }, - reportDefinitions: { - [SERVICE_NAME]: serviceName ? [serviceName] : [], - }, - selectedMetricField: 'Records', - }, - ], + 'ux-series': ({ + dataType: 'ux', + isNew: true, + time: { from: rangeFrom, to: rangeTo }, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); @@ -67,7 +60,6 @@ export function UXActionMenu({ {ANALYZE_MESSAGE}

}> { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:ux,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:ux,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -48,7 +48,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(testEnvironment),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -58,7 +58,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -68,7 +68,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ENVIRONMENT_NOT_DEFINED),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); @@ -78,7 +78,7 @@ describe('AnalyzeDataButton', () => { render(); expect((screen.getByRole('link') as HTMLAnchorElement).href).toEqual( - 'http://localhost/app/observability/exploratory-view/configure#?reportType=kpi-over-time&sr=!((dt:mobile,mt:transaction.duration.us,n:testServiceName-response-latency,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),time:(from:now-15m,to:now)))' + 'http://localhost/app/observability/exploratory-view#?sr=(apm-series:(dt:mobile,isNew:!t,op:average,rdf:(service.environment:!(ALL_VALUES),service.name:!(testServiceName)),rt:kpi-over-time,time:(from:now-15m,to:now)))' ); }); }); diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx index 5af6ea6cdc777..d8ff7fdf47c58 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/analyze_data_button.tsx @@ -9,7 +9,10 @@ import { EuiButtonEmpty, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; -import { createExploratoryViewUrl } from '../../../../../../observability/public'; +import { + createExploratoryViewUrl, + SeriesUrl, +} from '../../../../../../observability/public'; import { ALL_VALUES_SELECTED } from '../../../../../../observability/public'; import { isIosAgentName, @@ -18,7 +21,6 @@ import { import { SERVICE_ENVIRONMENT, SERVICE_NAME, - TRANSACTION_DURATION, } from '../../../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, @@ -27,11 +29,13 @@ import { import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { useUrlParams } from '../../../../context/url_params_context/use_url_params'; -function getEnvironmentDefinition(environment: string) { +function getEnvironmentDefinition(environment?: string) { switch (environment) { case ENVIRONMENT_ALL.value: return { [SERVICE_ENVIRONMENT]: [ALL_VALUES_SELECTED] }; case ENVIRONMENT_NOT_DEFINED.value: + case undefined: + return {}; default: return { [SERVICE_ENVIRONMENT]: [environment] }; } @@ -47,26 +51,21 @@ export function AnalyzeDataButton() { if ( (isRumAgentName(agentName) || isIosAgentName(agentName)) && - rangeFrom && - canShowDashboard && - rangeTo + canShowDashboard ) { const href = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${serviceName}-response-latency`, - selectedMetricField: TRANSACTION_DURATION, - dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', - time: { from: rangeFrom, to: rangeTo }, - reportDefinitions: { - [SERVICE_NAME]: [serviceName], - ...(environment ? getEnvironmentDefinition(environment) : {}), - }, - operationType: 'average', + 'apm-series': { + dataType: isRumAgentName(agentName) ? 'ux' : 'mobile', + time: { from: rangeFrom, to: rangeTo }, + reportType: 'kpi-over-time', + reportDefinitions: { + [SERVICE_NAME]: [serviceName], + ...getEnvironmentDefinition(environment), }, - ], + operationType: 'average', + isNew: true, + } as SeriesUrl, }, basepath ); diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 70585f9a9bf28..28fab3369b1eb 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,7 +5,6 @@ * 2.0. */ -import moment from 'moment'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { SERVICE_NAME, @@ -21,10 +20,7 @@ export async function hasRumData({ setup: Setup & Partial; }) { try { - const { - start = moment().subtract(24, 'h').valueOf(), - end = moment().valueOf(), - } = setup; + const { start, end } = setup; const params = { apm: { diff --git a/x-pack/plugins/observability/kibana.json b/x-pack/plugins/observability/kibana.json index b794f91231505..4273252850da4 100644 --- a/x-pack/plugins/observability/kibana.json +++ b/x-pack/plugins/observability/kibana.json @@ -6,8 +6,16 @@ }, "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": ["xpack", "observability"], - "optionalPlugins": ["home", "discover", "lens", "licensing", "usageCollection"], + "configPath": [ + "xpack", + "observability" + ], + "optionalPlugins": [ + "home", + "lens", + "licensing", + "usageCollection" + ], "requiredPlugins": [ "alerting", "cases", diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx deleted file mode 100644 index 0e17c6277618b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/mobile_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function MobileAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.mobile.addDataButtonLabel', { - defaultMessage: 'Add Mobile data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx deleted file mode 100644 index af91624769e6b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function SyntheticsAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability..synthetics.addDataButtonLabel', { - defaultMessage: 'Add synthetics data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx deleted file mode 100644 index c6aa0742466f1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/ux_add_data.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * 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 { EuiHeaderLink } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useKibana } from '../../../utils/kibana_react'; - -export function UXAddData() { - const kibana = useKibana(); - - return ( - - {ADD_DATA_LABEL} - - ); -} - -const ADD_DATA_LABEL = i18n.translate('xpack.observability.ux.addDataButtonLabel', { - defaultMessage: 'Add UX data', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx deleted file mode 100644 index 329192abc99d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { render } from '../../rtl_helpers'; -import { fireEvent, screen } from '@testing-library/dom'; -import React from 'react'; -import { sampleAttribute } from '../../configurations/test_data/sample_attribute'; -import * as pluginHook from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ExpViewActionMenuContent } from './action_menu'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); - -describe('Action Menu', function () { - it('should be able to click open in lens', async function () { - const { findByText, core } = render( - - ); - - expect(await screen.findByText('Open in Lens')).toBeInTheDocument(); - - fireEvent.click(await findByText('Open in Lens')); - - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); - expect(core.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( - { - id: '', - attributes: sampleAttribute, - timeRange: { to: 'now', from: 'now-10m' }, - }, - true - ); - }); - - it('should be able to click save', async function () { - const { findByText } = render( - - ); - - expect(await screen.findByText('Save')).toBeInTheDocument(); - - fireEvent.click(await findByText('Save')); - - expect(await screen.findByText('Lens Save Modal Component')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx deleted file mode 100644 index 38011eb5f8ffb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/action_menu.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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, { useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { LensEmbeddableInput, TypedLensByValueInput } from '../../../../../../../lens/public'; -import { ObservabilityAppServices } from '../../../../../application/types'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; - -export function ExpViewActionMenuContent({ - timeRange, - lensAttributes, -}: { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -}) { - const kServices = useKibana().services; - - const { lens } = kServices; - - const [isSaveOpen, setIsSaveOpen] = useState(false); - - const LensSaveModalComponent = lens.SaveModalComponent; - - return ( - <> - - - { - if (lensAttributes) { - lens.navigateToPrefilledEditor( - { - id: '', - timeRange, - attributes: lensAttributes, - }, - true - ); - } - }} - > - {i18n.translate('xpack.observability.expView.heading.openInLens', { - defaultMessage: 'Open in Lens', - })} - - - - { - if (lensAttributes) { - setIsSaveOpen(true); - } - }} - size="s" - > - {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { - defaultMessage: 'Save', - })} - - - - - {isSaveOpen && lensAttributes && ( - setIsSaveOpen(false)} - // if we want to do anything after the viz is saved - // right now there is no action, so an empty function - onSave={() => {}} - /> - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx deleted file mode 100644 index 23500b63e900a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/action_menu/index.tsx +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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 { ExpViewActionMenuContent } from './action_menu'; -import HeaderMenuPortal from '../../../header_menu_portal'; -import { usePluginContext } from '../../../../../hooks/use_plugin_context'; -import { TypedLensByValueInput } from '../../../../../../../lens/public'; - -interface Props { - timeRange?: { from: string; to: string }; - lensAttributes: TypedLensByValueInput['attributes'] | null; -} -export function ExpViewActionMenu(props: Props) { - const { appMountParameters } = usePluginContext(); - - return ( - - - - ); -} 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 d17e451ef702c..3566835b1701c 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 @@ -10,19 +10,19 @@ import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; -import { LOADING_VIEW } from '../series_editor/series_editor'; -import { ReportViewType, SeriesUrl } from '../types'; +import { LOADING_VIEW } from '../series_builder/series_builder'; +import { SeriesUrl } from '../types'; export function EmptyView({ loading, + height, series, - reportType, }: { loading: boolean; - series?: SeriesUrl; - reportType: ReportViewType; + height: string; + series: SeriesUrl; }) { - const { dataType, reportDefinitions } = series ?? {}; + const { dataType, reportType, reportDefinitions } = series ?? {}; let emptyMessage = EMPTY_LABEL; @@ -45,7 +45,7 @@ export function EmptyView({ } return ( - + {loading && ( ` text-align: center; + height: ${(props) => props.height}; position: relative; `; 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 03fd23631f755..fe2953edd36d6 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 @@ -7,7 +7,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { FilterLabel } from './filter_label'; import * as useSeriesHook from '../hooks/use_series_filters'; import { buildFilterLabel } from '../../filter_value_label/filter_value_label'; @@ -27,10 +27,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -52,10 +51,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -76,10 +74,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={false} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={removeFilter} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); @@ -103,10 +100,9 @@ describe('FilterLabel', function () { value={'elastic-co'} label={'Web Application'} negate={true} - seriesId={0} + seriesId={'kpi-over-time'} removeFilter={jest.fn()} indexPattern={mockIndexPattern} - series={mockUxSeries} /> ); 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 c6254a85de9ac..a08e777c5ea71 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 @@ -9,24 +9,21 @@ import React from 'react'; import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; -import { SeriesUrl } from '../types'; interface Props { field: string; label: string; - value: string | string[]; - seriesId: number; - series: SeriesUrl; + value: string; + seriesId: string; negate: boolean; definitionFilter?: boolean; indexPattern: IndexPattern; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; } export function FilterLabel({ label, seriesId, - series, field, value, negate, @@ -34,7 +31,7 @@ export function FilterLabel({ removeFilter, definitionFilter, }: Props) { - const { invertFilter } = useSeriesFilters({ seriesId, series }); + const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( { - setSeries(seriesId, { ...series, color: colorN }); - }; - - const color = - series.color ?? ((theme.eui as unknown) as Record)[`euiColorVis${seriesId}`]; - - const button = ( - - setIsOpen((prevState) => !prevState)} hasArrow={false}> - - - - ); - - return ( - setIsOpen(false)}> - - - - - ); -} - -const PICK_A_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.pickColor', - { - defaultMessage: 'Pick a color', - } -); - -const EDIT_SERIES_COLOR_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.editSeriesColor', - { - defaultMessage: 'Edit color for series', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx deleted file mode 100644 index 23d6589fecbcb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/index.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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 moment from 'moment'; -import { EuiSuperDatePicker, EuiText } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; - -import { useHasData } from '../../../../../hooks/use_has_data'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { useQuickTimeRanges } from '../../../../../hooks/use_quick_time_ranges'; -import { parseTimeParts } from '../../series_viewer/columns/utils'; -import { useUiSetting } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; - -export interface TimePickerTime { - from: string; - to: string; -} - -export interface TimePickerQuickRange extends TimePickerTime { - display: string; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - readonly?: boolean; -} -const readableUnit: Record = { - m: i18n.translate('xpack.observability.overview.exploratoryView.minutes', { - defaultMessage: 'Minutes', - }), - h: i18n.translate('xpack.observability.overview.exploratoryView.hour', { - defaultMessage: 'Hour', - }), - d: i18n.translate('xpack.observability.overview.exploratoryView.day', { - defaultMessage: 'Day', - }), -}; - -export function SeriesDatePicker({ series, seriesId, readonly = true }: Props) { - const { onRefreshTimeRange } = useHasData(); - - const commonlyUsedRanges = useQuickTimeRanges(); - - const { setSeries, reportType, allSeries, firstSeries } = useSeriesStorage(); - - function onTimeChange({ start, end }: { start: string; end: string }) { - onRefreshTimeRange(); - if (reportType === ReportTypes.KPI) { - allSeries.forEach((currSeries, seriesIndex) => { - setSeries(seriesIndex, { ...currSeries, time: { from: start, to: end } }); - }); - } else { - setSeries(seriesId, { ...series, time: { from: start, to: end } }); - } - } - - const seriesTime = series.time ?? firstSeries!.time; - - const dateFormat = useUiSetting('dateFormat').replace('ss.SSS', 'ss'); - - if (readonly) { - const timeParts = parseTimeParts(seriesTime?.from, seriesTime?.to); - - if (timeParts) { - const { - timeTense: timeTenseDefault, - timeUnits: timeUnitsDefault, - timeValue: timeValueDefault, - } = timeParts; - - return ( - {`${timeTenseDefault} ${timeValueDefault} ${ - readableUnit?.[timeUnitsDefault] ?? timeUnitsDefault - }`} - ); - } else { - return ( - - {i18n.translate('xpack.observability.overview.exploratoryView.dateRangeReadonly', { - defaultMessage: '{start} to {end}', - values: { - start: moment(seriesTime.from).format(dateFormat), - end: moment(seriesTime.to).format(dateFormat), - }, - })} - - ); - } - } - - return ( - - ); -} 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 bf5feb7d5863c..ba1f2214223e3 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 @@ -94,19 +94,6 @@ export const DataViewLabels: Record = { 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; -export enum ReportTypes { - KPI = 'kpi-over-time', - DISTRIBUTION = 'data-distribution', - CORE_WEB_VITAL = 'core-web-vitals', - DEVICE_DISTRIBUTION = 'device-data-distribution', -} - -export enum DataTypes { - SYNTHETICS = 'synthetics', - UX = 'ux', - MOBILE = 'mobile', -} - export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts index 55ac75b47c056..6f990015fbc62 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/url_constants.ts @@ -8,12 +8,10 @@ export enum URL_KEYS { DATA_TYPE = 'dt', OPERATION_TYPE = 'op', + REPORT_TYPE = 'rt', SERIES_TYPE = 'st', BREAK_DOWN = 'bd', FILTERS = 'ft', REPORT_DEFINITIONS = 'rdf', SELECTED_METRIC = 'mt', - HIDDEN = 'h', - NAME = 'n', - COLOR = 'c', } 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 3f6551986527c..574a9f6a2bc10 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 @@ -15,7 +15,6 @@ import { getCoreWebVitalsConfig } from './rum/core_web_vitals_config'; import { getMobileKPIConfig } from './mobile/kpi_over_time_config'; import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; -import { DataTypes, ReportTypes } from './constants'; interface Props { reportType: ReportViewType; @@ -25,24 +24,24 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { - case DataTypes.UX: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'ux': + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.CORE_WEB_VITAL) { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); - case DataTypes.SYNTHETICS: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'synthetics': + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); - case DataTypes.MOBILE: - if (reportType === ReportTypes.DISTRIBUTION) { + case 'mobile': + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { + 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 08d2da4714e47..ae70bbdcfa3b8 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 @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; +import { REPORT_METRIC_FIELD } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -38,9 +38,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: {}, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; beforeEach(() => { @@ -53,7 +50,7 @@ describe('Lens Attribute', () => { it('should return expected json for kpi report type', function () { const seriesConfigKpi = getDefaultConfigs({ - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -66,9 +63,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: RECORDS_FIELD, }, ]); @@ -141,9 +135,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -286,7 +277,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -392,7 +383,7 @@ describe('Lens Attribute', () => { palette: undefined, seriesType: 'line', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -412,9 +403,6 @@ describe('Lens Attribute', () => { reportDefinitions: { 'performance.metric': [LCP_FIELD] }, breakdown: USER_AGENT_NAME, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; lnsAttr = new LensAttributes([layerConfig1]); @@ -434,7 +422,7 @@ describe('Lens Attribute', () => { seriesType: 'line', splitAccessor: 'breakdown-column-layer0', xAccessor: 'x-axis-column-layer0', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); @@ -495,7 +483,7 @@ describe('Lens Attribute', () => { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -601,9 +589,6 @@ describe('Lens Attribute', () => { indexPattern: mockIndexPattern, reportDefinitions: { 'performance.metric': [LCP_FIELD] }, time: { from: 'now-15m', to: 'now' }, - color: 'green', - name: 'test-series', - selectedMetricField: TRANSACTION_DURATION, }; const filters = lnsAttr.getLayerFilters(layerConfig1, 2); 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 5426d3bcd4233..dfb17ee470d35 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 @@ -7,7 +7,6 @@ import { i18n } from '@kbn/i18n'; import { capitalize } from 'lodash'; - import { CountIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -37,11 +36,10 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, - ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../components/date_range_picker'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -75,6 +73,14 @@ export const parseCustomFieldName = (seriesConfig: SeriesConfig, selectedMetricF timeScale = currField?.timeScale; columnLabel = currField?.label; } + } else if (metricOptions?.[0].field || metricOptions?.[0].id) { + const firstMetricOption = metricOptions?.[0]; + + selectedMetricField = firstMetricOption.field || firstMetricOption.id; + columnType = firstMetricOption.columnType; + columnFilters = firstMetricOption.columnFilters; + timeScale = firstMetricOption.timeScale; + columnLabel = firstMetricOption.label; } return { fieldName: selectedMetricField!, columnType, columnFilters, timeScale, columnLabel }; @@ -89,9 +95,7 @@ export interface LayerConfig { reportDefinitions: URLReportDefinition; time: { to: string; from: string }; indexPattern: IndexPattern; - selectedMetricField: string; - color: string; - name: string; + selectedMetricField?: string; } export class LensAttributes { @@ -467,15 +471,14 @@ export class LensAttributes { getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { const { filters, - time, + time: { from, to }, seriesConfig: { baseFilters: layerFilters, reportType }, } = layerConfig; let baseFilters = ''; - - if (reportType !== ReportTypes.KPI && totalLayers > 1 && time) { + 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 >= ${time.from} and @timestamp <= ${time.to}`; + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; } layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { @@ -531,11 +534,7 @@ export class LensAttributes { } getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { - if ( - index === 0 || - mainLayerConfig.seriesConfig.reportType !== ReportTypes.KPI || - !layerConfig.time - ) { + if (index === 0 || mainLayerConfig.seriesConfig.reportType !== 'kpi-over-time') { return null; } @@ -547,14 +546,11 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); if (inDays > 1) { return inDays + 'd'; } - const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); - if (inHours === 0) { - return null; - } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); return inHours + 'h'; } @@ -572,12 +568,6 @@ export class LensAttributes { const { sourceField } = seriesConfig.xAxisColumn; - let label = timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label; - - if (layerConfig.seriesConfig.reportType !== ReportTypes.CORE_WEB_VITAL && layerConfig.name) { - label = layerConfig.name; - } - layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, @@ -591,7 +581,7 @@ export class LensAttributes { [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), [`y-axis-column-${layerId}`]: { ...mainYAxis, - label, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, @@ -634,7 +624,7 @@ export class LensAttributes { seriesType: layerConfig.seriesType || layerConfig.seriesConfig.defaultSeriesType, palette: layerConfig.seriesConfig.palette, yConfig: layerConfig.seriesConfig.yConfig || [ - { forAccessor: `y-axis-column-layer${index}`, color: layerConfig.color }, + { forAccessor: `y-axis-column-layer${index}` }, ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && @@ -648,7 +638,7 @@ export class LensAttributes { }; } - getJSON(refresh?: number): TypedLensByValueInput['attributes'] { + getJSON(): TypedLensByValueInput['attributes'] { const uniqueIndexPatternsIds = Array.from( new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) ); @@ -657,7 +647,7 @@ export class LensAttributes { return { title: 'Prefilled from exploratory view app', - description: String(refresh), + description: '', visualizationType: 'lnsXY', references: [ ...uniqueIndexPatternsIds.map((patternId) => ({ 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 4e178bba7e02a..d1612a08f5551 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 @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, USE_BREAK_DOWN_COLUMN } from '../constants'; import { buildPhraseFilter } from '../utils'; import { SERVICE_NAME } from '../constants/elasticsearch_fieldnames'; import { MOBILE_APP, NUMBER_OF_DEVICES } from '../constants/labels'; @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DEVICE_DISTRIBUTION, + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { @@ -38,13 +38,13 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) ...MobileFields, [SERVICE_NAME]: MOBILE_APP, }, - definitionFields: [SERVICE_NAME], metricOptions: [ { - field: 'labels.device_id', id: 'labels.device_id', + field: 'labels.device_id', label: NUMBER_OF_DEVICES, }, ], + definitionFields: [SERVICE_NAME], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts index 1da27be4fcc95..9b1c4c8da3e9b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/distribution_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -21,7 +21,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'bar', seriesTypes: ['line', 'bar'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts index 3ee5b3125fcda..945a631078a33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -32,7 +26,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'line', seriesTypes: ['line', 'bar', 'area'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts index 35e094996f6f2..07bb13f957e45 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.test.ts @@ -9,7 +9,7 @@ import { mockAppIndexPattern, mockIndexPattern } from '../../rtl_helpers'; import { getDefaultConfigs } from '../default_configs'; import { LayerConfig, LensAttributes } from '../lens_attributes'; import { sampleAttributeCoreWebVital } from '../test_data/sample_attribute_cwv'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; +import { SERVICE_NAME, USER_AGENT_OS } from '../constants/elasticsearch_fieldnames'; describe('Core web vital config test', function () { mockAppIndexPattern(); @@ -24,13 +24,10 @@ describe('Core web vital config test', function () { const layerConfig: LayerConfig = { seriesConfig, - color: 'green', - name: 'test-series', - breakdown: USER_AGENT_OS, indexPattern: mockIndexPattern, - time: { from: 'now-15m', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, - selectedMetricField: LCP_FIELD, + time: { from: 'now-15m', to: 'now' }, + breakdown: USER_AGENT_OS, }; beforeEach(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts index e8d620388a89e..62455df248085 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/core_web_vitals_config.ts @@ -11,7 +11,6 @@ import { FieldLabels, FILTER_RECORDS, REPORT_METRIC_FIELD, - ReportTypes, USE_BREAK_DOWN_COLUMN, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -39,7 +38,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_horizontal_percentage_stacked', - reportType: ReportTypes.CORE_WEB_VITAL, + reportType: 'core-web-vitals', seriesTypes: ['bar_horizontal_percentage_stacked'], xAxisColumn: { sourceField: USE_BREAK_DOWN_COLUMN, @@ -154,6 +153,5 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon { color: statusPallete[1], forAccessor: 'y-axis-column-1' }, { color: statusPallete[2], forAccessor: 'y-axis-column-2' }, ], - query: { query: 'transaction.type: "page-load"', language: 'kuery' }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts index de6f2c67b2aeb..f34c8db6c197d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/data_distribution_config.ts @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -46,7 +41,7 @@ import { export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index 9112778eadaa7..5899b16d12b4f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -6,13 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - OPERATION_COLUMN, - RECORDS_FIELD, - REPORT_METRIC_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD } from '../constants'; import { buildPhraseFilter } from '../utils'; import { CLIENT_GEO_COUNTRY_NAME, @@ -49,7 +43,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon return { defaultSeriesType: 'bar_stacked', seriesTypes: [], - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', xAxisColumn: { sourceField: '@timestamp', }, 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 da90f45d15201..730e742f9d8c5 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 @@ -6,12 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { - FieldLabels, - REPORT_METRIC_FIELD, - RECORDS_PERCENTAGE_FIELD, - ReportTypes, -} from '../constants'; +import { FieldLabels, REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -35,7 +30,7 @@ export function getSyntheticsDistributionConfig({ indexPattern, }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.DISTRIBUTION, + reportType: 'data-distribution', defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 65b43a83a8fb5..4ee22181d4334 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,7 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD } from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -30,7 +30,7 @@ const SUMMARY_DOWN = 'summary.down'; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { - reportType: ReportTypes.KPI, + reportType: 'kpi-over-time', defaultSeriesType: 'bar_stacked', seriesTypes: [], xAxisColumn: { 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 a5898f33e0ec0..569d68ad4ebff 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 @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttribute = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -34,23 +28,17 @@ export const sampleAttribute = { ], columns: { 'x-axis-column-layer0': { - dataType: 'number', - isBucketed: true, + sourceField: 'transaction.duration.us', label: 'Page load time', + dataType: 'number', operationType: 'range', + isBucketed: true, + scale: 'interval', params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], type: 'histogram', + ranges: [{ from: 0, to: 1000, label: '' }], + maxBars: 'auto', }, - scale: 'interval', - sourceField: 'transaction.duration.us', }, 'y-axis-column-layer0': { dataType: 'number', @@ -60,7 +48,7 @@ export const sampleAttribute = { 'transaction.type: page-load and processor.event: transaction and transaction.type : *', }, isBucketed: false, - label: 'test-series', + label: 'Pages loaded', operationType: 'formula', params: { format: { @@ -93,16 +81,16 @@ export const sampleAttribute = { 'y-axis-column-layer0X1': { customLabel: true, dataType: 'number', - filter: { - language: 'kuery', - query: - 'transaction.type: page-load and processor.event: transaction and transaction.type : *', - }, isBucketed: false, label: 'Part of count() / overall_sum(count())', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, 'y-axis-column-layer0X2': { customLabel: true, @@ -153,51 +141,26 @@ export const sampleAttribute = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: 'transaction.duration.us < 60000000', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: 'transaction.duration.us < 60000000', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index 425bf069cc87f..2087b85b81886 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -5,7 +5,7 @@ * 2.0. */ export const sampleAttributeCoreWebVital = { - description: 'undefined', + description: '', references: [ { id: 'apm-*', @@ -94,7 +94,7 @@ export const sampleAttributeCoreWebVital = { filters: [], query: { language: 'kuery', - query: 'transaction.type: "page-load"', + query: '', }, visualization: { axisTitlesVisibilitySettings: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 85bafdecabde0..7f066caf66bf1 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -5,18 +5,12 @@ * 2.0. */ export const sampleAttributeKpi = { - description: 'undefined', + title: 'Prefilled from exploratory view app', + description: '', + visualizationType: 'lnsXY', references: [ - { - id: 'apm-*', - name: 'indexpattern-datasource-current-indexpattern', - type: 'index-pattern', - }, - { - id: 'apm-*', - name: 'indexpattern-datasource-layer-layer0', - type: 'index-pattern', - }, + { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { @@ -26,27 +20,25 @@ export const sampleAttributeKpi = { columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { 'x-axis-column-layer0': { + sourceField: '@timestamp', dataType: 'date', isBucketed: true, label: '@timestamp', operationType: 'date_histogram', - params: { - interval: 'auto', - }, + params: { interval: 'auto' }, scale: 'interval', - sourceField: '@timestamp', }, 'y-axis-column-layer0': { dataType: 'number', - filter: { - language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', - }, isBucketed: false, - label: 'test-series', + label: 'Page views', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + query: 'transaction.type: page-load and processor.event: transaction', + language: 'kuery', + }, }, }, incompleteColumns: {}, @@ -54,51 +46,26 @@ export const sampleAttributeKpi = { }, }, }, - filters: [], - query: { - language: 'kuery', - query: '', - }, visualization: { - axisTitlesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - curveType: 'CURVE_MONOTONE_X', + legend: { isVisible: true, position: 'right' }, + valueLabels: 'hide', fittingFunction: 'Linear', - gridlinesVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, + curveType: 'CURVE_MONOTONE_X', + axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, + gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, + preferredSeriesType: 'line', layers: [ { accessors: ['y-axis-column-layer0'], layerId: 'layer0', seriesType: 'line', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], xAccessor: 'x-axis-column-layer0', - yConfig: [ - { - color: 'green', - forAccessor: 'y-axis-column-layer0', - }, - ], }, ], - legend: { - isVisible: true, - position: 'right', - }, - preferredSeriesType: 'line', - tickLabelsVisibilitySettings: { - x: true, - yLeft: true, - yRight: true, - }, - valueLabels: 'hide', }, + query: { query: '', language: 'kuery' }, + filters: [], }, - title: 'Prefilled from exploratory view app', - visualizationType: 'lnsXY', }; 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 694250e5749cb..f7df2939d9909 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,7 +5,7 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; -import type { ReportViewType, SeriesUrl, UrlFilter } from '../types'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; @@ -16,43 +16,40 @@ export function convertToShortUrl(series: SeriesUrl) { const { operationType, seriesType, + reportType, breakdown, filters, reportDefinitions, dataType, selectedMetricField, - hidden, - name, - color, ...restSeries } = series; return { [URL_KEYS.OPERATION_TYPE]: operationType, + [URL_KEYS.REPORT_TYPE]: reportType, [URL_KEYS.SERIES_TYPE]: seriesType, [URL_KEYS.BREAK_DOWN]: breakdown, [URL_KEYS.FILTERS]: filters, [URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions, [URL_KEYS.DATA_TYPE]: dataType, [URL_KEYS.SELECTED_METRIC]: selectedMetricField, - [URL_KEYS.HIDDEN]: hidden, - [URL_KEYS.NAME]: name, - [URL_KEYS.COLOR]: color, ...restSeries, }; } -export function createExploratoryViewUrl( - { reportType, allSeries }: { reportType: ReportViewType; allSeries: AllSeries }, - baseHref = '' -) { - const allShortSeries: AllShortSeries = allSeries.map((series) => convertToShortUrl(series)); +export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { + const allSeriesIds = Object.keys(allSeries); + + const allShortSeries: AllShortSeries = {}; + + allSeriesIds.forEach((seriesKey) => { + allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]); + }); return ( baseHref + - `/app/observability/exploratory-view/configure#?reportType=${reportType}&sr=${rison.encode( - (allShortSeries as unknown) as RisonValue - )}` + `/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}` ); } 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 21c749258bebe..989ebf17c2062 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 @@ -11,13 +11,6 @@ import { render, mockCore, mockAppIndexPattern } from './rtl_helpers'; import { ExploratoryView } from './exploratory_view'; import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils'; import * as obsvInd from './utils/observability_index_patterns'; -import * as pluginHook from '../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); describe('ExploratoryView', () => { mockAppIndexPattern(); @@ -48,18 +41,29 @@ describe('ExploratoryView', () => { it('renders exploratory view', async () => { render(); - expect(await screen.findByText(/Preview/i)).toBeInTheDocument(); - expect(await screen.findByText(/Configure series/i)).toBeInTheDocument(); - expect(await screen.findByText(/Hide chart/i)).toBeInTheDocument(); - expect(await screen.findByText(/Refresh/i)).toBeInTheDocument(); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect( await screen.findByRole('heading', { name: /Performance Distribution/i }) ).toBeInTheDocument(); }); it('renders lens component when there is series', async () => { - render(); + const initSeries = { + data: { + 'ux-series': { + isNew: true, + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + breakdown: 'user_agent .name', + reportDefinitions: { 'service.name': ['elastic-co'] }, + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + expect(await screen.findByText(/open in lens/i)).toBeInTheDocument(); expect((await screen.findAllByText('Performance distribution'))[0]).toBeInTheDocument(); expect(await screen.findByText(/Lens Embeddable Component/i)).toBeInTheDocument(); 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 cb901b8b588f3..af04108c56790 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 @@ -4,13 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { i18n } from '@kbn/i18n'; import React, { useEffect, useRef, useState } from 'react'; -import { EuiButtonEmpty, EuiPanel, EuiResizableContainer, EuiTitle } from '@elastic/eui'; +import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; -import { useRouteMatch } from 'react-router-dom'; -import { PanelDirection } from '@elastic/eui/src/components/resizable_container/types'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -18,15 +16,40 @@ import { useSeriesStorage } from './hooks/use_series_storage'; import { useLensAttributes } from './hooks/use_lens_attributes'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; -import { SeriesViews } from './views/series_views'; +import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; import { LensEmbeddable } from './lens_embeddable'; import { EmptyView } from './components/empty_view'; -export type PanelId = 'seriesPanel' | 'chartPanel'; +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 { @@ -46,19 +69,20 @@ export function ExploratoryView({ const { loadIndexPattern, loading } = useAppIndexPatternContext(); - const { firstSeries, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { firstSeries, firstSeriesId, allSeries } = useSeriesStorage(); const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { const headerOffset = wrapperRef.current.getBoundingClientRect().top; - setHeight(`calc(100vh - ${headerOffset + 40}px)`); + const seriesOffset = seriesBuilderRef.current.getBoundingClientRect().height; + setHeight(`calc(100vh - ${seriesOffset + headerOffset + 40}px)`); } }; useEffect(() => { - allSeries.forEach((seriesT) => { + Object.values(allSeries).forEach((seriesT) => { loadIndexPattern({ dataType: seriesT.dataType, }); @@ -72,104 +96,38 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), lastRefresh]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); - const collapseFn = useRef<(id: PanelId, direction: PanelDirection) => void>(); - - const [hiddenPanel, setHiddenPanel] = useState(''); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const onCollapse = (panelId: string) => { - setHiddenPanel((prevState) => (panelId === prevState ? '' : panelId)); - }; - - const onChange = (panelId: PanelId) => { - onCollapse(panelId); - if (collapseFn.current) { - collapseFn.current(panelId, panelId === 'seriesPanel' ? 'right' : 'left'); - } - }; - return ( {lens ? ( <> - + - - {(EuiResizablePanel, EuiResizableButton, { togglePanel }) => { - collapseFn.current = (id, direction) => togglePanel?.(id, { direction }); - - return ( - <> - - {lensAttributes ? ( - - ) : ( - - )} - - - - {!isPreview && - (hiddenPanel === 'chartPanel' ? ( - onChange('chartPanel')} iconType="arrowDown"> - {SHOW_CHART_LABEL} - - ) : ( - onChange('chartPanel')} - iconType="arrowUp" - color="text" - > - {HIDE_CHART_LABEL} - - ))} - - - - ); - }} - - {hiddenPanel === 'seriesPanel' && ( - onChange('seriesPanel')} iconType="arrowUp"> - {PREVIEW_LABEL} - + {lensAttributes ? ( + + ) : ( + )} + ) : ( -

{LENS_NOT_AVAILABLE}

+

+ {i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', { + defaultMessage: + 'Lens app is not available, please enable Lens to use exploratory view.', + })} +

)}
@@ -189,39 +147,4 @@ const Wrapper = styled(EuiPanel)` margin: 0 auto; width: 100%; overflow-x: auto; - position: relative; -`; - -const ShowPreview = styled(EuiButtonEmpty)` - position: absolute; - bottom: 34px; -`; -const HideChart = styled(EuiButtonEmpty)` - position: absolute; - top: -35px; - right: 50px; `; -const ShowChart = styled(EuiButtonEmpty)` - position: absolute; - top: -10px; - right: 50px; -`; - -const HIDE_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.hideChart', { - defaultMessage: 'Hide chart', -}); - -const SHOW_CHART_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.showChart', { - defaultMessage: 'Show chart', -}); - -const PREVIEW_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', -}); - -const LENS_NOT_AVAILABLE = i18n.translate( - 'xpack.observability.overview.exploratoryView.lensDisabled', - { - defaultMessage: 'Lens app is not available, please enable Lens to use exploratory view.', - } -); 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 1f910b946deb3..8cd8977fcf741 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 @@ -8,22 +8,51 @@ import React from 'react'; import { render } from '../rtl_helpers'; import { ExploratoryViewHeader } from './header'; -import * as pluginHook from '../../../../hooks/use_plugin_context'; - -jest.spyOn(pluginHook, 'usePluginContext').mockReturnValue({ - appMountParameters: { - setHeaderActionMenu: jest.fn(), - }, -} as any); +import { fireEvent } from '@testing-library/dom'; describe('ExploratoryViewHeader', function () { it('should render properly', function () { const { getByText } = render( ); - getByText('Refresh'); + getByText('Open in Lens'); + }); + + it('should be able to click open in lens', function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + const { getByText, core } = render( + , + { initSeries } + ); + fireEvent.click(getByText('Open in Lens')); + + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1); + expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith( + { + attributes: { title: 'Performance distribution' }, + id: '', + timeRange: { + from: 'now-15m', + to: 'now', + }, + }, + true + ); }); }); 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 bec8673f88b4e..ded56ec9e817f 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 @@ -5,37 +5,43 @@ * 2.0. */ -import React from 'react'; +import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { TypedLensByValueInput } from '../../../../../../lens/public'; +import { TypedLensByValueInput, LensEmbeddableInput } from '../../../../../../lens/public'; +import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { DataViewLabels } from '../configurations/constants'; +import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; -import { LastUpdated } from './last_updated'; -import { combineTimeRanges } from '../lens_embeddable'; -import { ExpViewActionMenu } from '../components/action_menu'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { - seriesId?: number; - lastUpdated?: number; + seriesId: string; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { - const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); +export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { + const kServices = useKibana().services; - const series = seriesId ? getSeries(seriesId) : undefined; + const { lens } = kServices; - const timeRange = combineTimeRanges(reportType, allSeries, series); + const { getSeries, allSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const [isSaveOpen, setIsSaveOpen] = useState(false); + + const LensSaveModalComponent = lens.SaveModalComponent; + + const timeRange = combineTimeRanges(allSeries, series); return ( <> -

- {DataViewLabels[reportType] ?? + {DataViewLabels[series.reportType] ?? i18n.translate('xpack.observability.expView.heading.label', { defaultMessage: 'Analyze data', })}{' '} @@ -51,18 +57,53 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: - + { + if (lensAttributes) { + lens.navigateToPrefilledEditor( + { + id: '', + timeRange, + attributes: lensAttributes, + }, + true + ); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.openInLens', { + defaultMessage: 'Open in Lens', + })} + - setLastRefresh(Date.now())}> - {REFRESH_LABEL} + { + if (lensAttributes) { + setIsSaveOpen(true); + } + }} + > + {i18n.translate('xpack.observability.expView.heading.saveLensVisualization', { + defaultMessage: 'Save', + })} + + {isSaveOpen && lensAttributes && ( + setIsSaveOpen(false)} + onSave={() => {}} + /> + )} ); } - -const REFRESH_LABEL = i18n.translate('xpack.observability.overview.exploratoryView.refresh', { - defaultMessage: 'Refresh', -}); 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 d65917093d129..7a5f12a72b1f0 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 @@ -27,7 +27,7 @@ interface ProviderProps { } type HasAppDataState = Record; -export type IndexPatternState = Record; +type IndexPatternState = Record; type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx deleted file mode 100644 index e86144c124949..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_discover_link.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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 { useCallback, useEffect, useState } from 'react'; -import { useKibana } from '../../../../utils/kibana_react'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from './use_app_index_pattern'; -import { buildExistsFilter, buildPhraseFilter, buildPhrasesFilter } from '../configurations/utils'; -import { getFiltersFromDefs } from './use_lens_attributes'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface UseDiscoverLink { - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export const useDiscoverLink = ({ series, seriesConfig }: UseDiscoverLink) => { - const kServices = useKibana().services; - const { - application: { navigateToUrl }, - } = kServices; - - const { indexPatterns } = useAppIndexPatternContext(); - - const urlGenerator = kServices.discover?.urlGenerator; - const [discoverUrl, setDiscoverUrl] = useState(''); - - useEffect(() => { - const indexPattern = indexPatterns?.[series.dataType]; - - const definitions = series.reportDefinitions ?? {}; - const filters = [...(seriesConfig?.baseFilters ?? [])]; - - const definitionFilters = getFiltersFromDefs(definitions); - - definitionFilters.forEach(({ field, values = [] }) => { - if (values.length > 1) { - filters.push(buildPhrasesFilter(field, values, indexPattern)[0]); - } else { - filters.push(buildPhraseFilter(field, values[0], indexPattern)[0]); - } - }); - - const selectedMetricField = series.selectedMetricField; - - if ( - selectedMetricField && - selectedMetricField !== RECORDS_FIELD && - selectedMetricField !== RECORDS_PERCENTAGE_FIELD - ) { - filters.push(buildExistsFilter(selectedMetricField, indexPattern)[0]); - } - - const getDiscoverUrl = async () => { - if (!urlGenerator?.createUrl) return; - - const newUrl = await urlGenerator.createUrl({ - filters, - indexPatternId: indexPattern?.id, - }); - setDiscoverUrl(newUrl); - }; - getDiscoverUrl(); - }, [ - indexPatterns, - series.dataType, - series.reportDefinitions, - series.selectedMetricField, - seriesConfig?.baseFilters, - urlGenerator, - ]); - - const onClick = useCallback( - (event: React.MouseEvent) => { - if (discoverUrl) { - event.preventDefault(); - - return navigateToUrl(discoverUrl); - } - }, - [discoverUrl, navigateToUrl] - ); - - return { - href: discoverUrl, - onClick, - }; -}; 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 71945734eeabc..8bb265b4f6d89 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 @@ -9,18 +9,12 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; -import { - AllSeries, - allSeriesKey, - convertAllShortSeries, - useSeriesStorage, -} from './use_series_storage'; +import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; -import { useTheme } from '../../../../hooks/use_theme'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -34,56 +28,41 @@ export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitio }; export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { - const { storage, autoApply, allSeries, lastRefresh, reportType } = useSeriesStorage(); + const { allSeriesIds, allSeries } = useSeriesStorage(); const { indexPatterns } = useAppIndexPatternContext(); - const theme = useTheme(); - return useMemo(() => { - if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const allSeriesT: AllSeries = autoApply - ? allSeries - : convertAllShortSeries(storage.get(allSeriesKey) ?? []); - const layerConfigs: LayerConfig[] = []; - allSeriesT.forEach((series, seriesIndex) => { - const indexPattern = indexPatterns?.[series?.dataType]; - - if ( - indexPattern && - !isEmpty(series.reportDefinitions) && - !series.hidden && - series.selectedMetricField - ) { + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { const seriesConfig = getDefaultConfigs({ - reportType, + reportType: seriesT.reportType, + dataType: seriesT.dataType, indexPattern, - dataType: series.dataType, }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(series.reportDefinitions) + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions) ); - const color = `euiColorVis${seriesIndex}`; - layerConfigs.push({ filters, indexPattern, seriesConfig, - time: series.time, - name: series.name, - breakdown: series.breakdown, - seriesType: series.seriesType, - operationType: series.operationType, - reportDefinitions: series.reportDefinitions ?? {}, - selectedMetricField: series.selectedMetricField, - color: series.color ?? ((theme.eui as unknown) as Record)[color], + time: seriesT.time, + breakdown: seriesT.breakdown, + seriesType: seriesT.seriesType, + operationType: seriesT.operationType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + selectedMetricField: seriesT.selectedMetricField, }); } }); @@ -94,6 +73,6 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null const lensAttributes = new LensAttributes(layerConfigs); - return lensAttributes.getJSON(lastRefresh); - }, [indexPatterns, allSeries, reportType, autoApply, storage, theme, lastRefresh]); + return lensAttributes.getJSON(); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts index f2a6130cdc59d..2d2618bc46152 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_filters.ts @@ -6,16 +6,18 @@ */ import { useSeriesStorage } from './use_series_storage'; -import { SeriesUrl, UrlFilter } from '../types'; +import { UrlFilter } from '../types'; export interface UpdateFilter { field: string; - value: string | string[]; + value: string; negate?: boolean; } -export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; series: SeriesUrl }) => { - const { setSeries } = useSeriesStorage(); +export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const filters = series.filters ?? []; @@ -24,14 +26,10 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie .map((filter) => { if (filter.field === field) { if (negate) { - const notValuesN = filter.notValues?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValuesN = filter.notValues?.filter((val) => val !== value); return { ...filter, notValues: notValuesN }; } else { - const valuesN = filter.values?.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const valuesN = filter.values?.filter((val) => val !== value); return { ...filter, values: valuesN }; } } @@ -45,9 +43,9 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const addFilter = ({ field, value, negate }: UpdateFilter) => { const currFilter: UrlFilter = { field }; if (negate) { - currFilter.notValues = value instanceof Array ? value : [value]; + currFilter.notValues = [value]; } else { - currFilter.values = value instanceof Array ? value : [value]; + currFilter.values = [value]; } if (filters.length === 0) { setSeries(seriesId, { ...series, filters: [currFilter] }); @@ -67,26 +65,13 @@ export const useSeriesFilters = ({ seriesId, series }: { seriesId: number; serie const currNotValues = currFilter.notValues ?? []; const currValues = currFilter.values ?? []; - const notValues = currNotValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); - - const values = currValues.filter((val) => - value instanceof Array ? !value.includes(val) : val !== value - ); + const notValues = currNotValues.filter((val) => val !== value); + const values = currValues.filter((val) => val !== value); if (negate) { - if (value instanceof Array) { - notValues.push(...value); - } else { - notValues.push(value); - } + notValues.push(value); } else { - if (value instanceof Array) { - values.push(...value); - } else { - values.push(value); - } + values.push(value); } currFilter.notValues = notValues.length > 0 ? notValues : undefined; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx index ce6d7bd94d8e4..c32acc47abd1b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.test.tsx @@ -6,39 +6,37 @@ */ import React, { useEffect } from 'react'; -import { Route, Router } from 'react-router-dom'; -import { render } from '@testing-library/react'; + import { UrlStorageContextProvider, useSeriesStorage } from './use_series_storage'; -import { getHistoryFromUrl } from '../rtl_helpers'; +import { render } from '@testing-library/react'; -const mockSingleSeries = [ - { - name: 'performance-distribution', +const mockSingleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -const mockMultipleSeries = [ - { - name: 'performance-distribution', +const mockMultipleSeries = { + 'performance-distribution': { + reportType: 'data-distribution', dataType: 'ux', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, -]; +}; -describe('userSeriesStorage', function () { +describe('userSeries', function () { function setupTestComponent(seriesData: any) { const setData = jest.fn(); - function TestComponent() { const data = useSeriesStorage(); @@ -50,20 +48,11 @@ describe('userSeriesStorage', function () { } render( - - - (key === 'sr' ? seriesData : null)), - set: jest.fn(), - }} - > - - - - + + + ); return setData; @@ -74,20 +63,22 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); @@ -98,38 +89,42 @@ describe('userSeriesStorage', function () { expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [ - { - name: 'performance-distribution', - dataType: 'ux', + allSeries: { + 'performance-distribution': { breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, - { - name: 'kpi-over-time', + 'kpi-over-time': { + reportType: 'kpi-over-time', dataType: 'synthetics', breakdown: 'user_agent.name', time: { from: 'now-15m', to: 'now' }, }, - ], + }, + allSeriesIds: ['performance-distribution', 'kpi-over-time'], firstSeries: { - name: 'performance-distribution', - dataType: 'ux', breakdown: 'user_agent.name', + dataType: 'ux', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }, + firstSeriesId: 'performance-distribution', }) ); }); it('should return expected result when there are no series', function () { - const setData = setupTestComponent([]); + const setData = setupTestComponent({}); - expect(setData).toHaveBeenCalledTimes(1); + expect(setData).toHaveBeenCalledTimes(2); expect(setData).toHaveBeenLastCalledWith( expect.objectContaining({ - allSeries: [], + allSeries: {}, + allSeriesIds: [], firstSeries: undefined, + firstSeriesId: undefined, }) ); }); 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 04f8751e2a0b6..a47a124d14b4d 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 @@ -6,7 +6,6 @@ */ import React, { createContext, useContext, useState, useEffect, useCallback } from 'react'; -import { useRouteMatch } from 'react-router-dom'; import { IKbnUrlStateStorage, ISessionStorageStateStorage, @@ -23,19 +22,13 @@ import { OperationType, SeriesType } from '../../../../../../lens/public'; import { URL_KEYS } from '../configurations/constants/url_constants'; export interface SeriesContextValue { - firstSeries?: SeriesUrl; - autoApply: boolean; - lastRefresh: number; - setLastRefresh: (val: number) => void; - setAutoApply: (val: boolean) => void; - applyChanges: () => void; + firstSeries: SeriesUrl; + firstSeriesId: string; + allSeriesIds: string[]; allSeries: AllSeries; - setSeries: (seriesIndex: number, newValue: SeriesUrl) => void; - getSeries: (seriesIndex: number) => SeriesUrl | undefined; - removeSeries: (seriesIndex: number) => void; - setReportType: (reportType: string) => void; - storage: IKbnUrlStateStorage | ISessionStorageStateStorage; - reportType: ReportViewType; + setSeries: (seriesIdN: string, newValue: SeriesUrl) => void; + getSeries: (seriesId: string) => SeriesUrl; + removeSeries: (seriesId: string) => void; } export const UrlStorageContext = createContext({} as SeriesContextValue); @@ -43,112 +36,72 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } -export function convertAllShortSeries(allShortSeries: AllShortSeries) { - return (allShortSeries ?? []).map((shortSeries) => convertFromShortUrl(shortSeries)); -} +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); -export const allSeriesKey = 'sr'; -const autoApplyKey = 'autoApply'; -const reportTypeKey = 'reportType'; + return allSeriesN; +} export function UrlStorageContextProvider({ children, storage, }: ProviderProps & { children: JSX.Element }) { - const [allSeries, setAllSeries] = useState(() => - convertAllShortSeries(storage.get(allSeriesKey) ?? []) - ); - - const [autoApply, setAutoApply] = useState(() => storage.get(autoApplyKey) ?? true); - const [lastRefresh, setLastRefresh] = useState(() => Date.now()); + const allSeriesKey = 'sr'; - const [reportType, setReportType] = useState( - () => (storage as IKbnUrlStateStorage).get(reportTypeKey) ?? '' + const [allShortSeries, setAllShortSeries] = useState( + () => storage.get(allSeriesKey) ?? {} ); - + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); + const [firstSeriesId, setFirstSeriesId] = useState(''); const [firstSeries, setFirstSeries] = useState(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - useEffect(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - const firstSeriesT = allSeries?.[0]; - - setFirstSeries(firstSeriesT); - - if (autoApply) { - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - } - }, [allSeries, autoApply, storage]); useEffect(() => { - // needed for tab change - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); + setAllSeries(allSeriesN); + setFirstSeriesId(allSeriesIds?.[0]); + setFirstSeries(allSeriesN?.[allSeriesIds?.[0]]); (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - // this is only needed for tab change, so we will not add allSeries into dependencies - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isPreview, storage]); - - const setSeries = useCallback((seriesIndex: number, newValue: SeriesUrl) => { - setAllSeries((prevAllSeries) => { - const newStateRest = prevAllSeries.map((series, index) => { - if (index === seriesIndex) { - return newValue; - } - return series; - }); - - if (prevAllSeries.length === seriesIndex) { - return [...newStateRest, newValue]; - } - - return [...newStateRest]; + }, [allShortSeries, storage]); + + const setSeries = (seriesIdN: string, newValue: SeriesUrl) => { + setAllShortSeries((prevState) => { + prevState[seriesIdN] = convertToShortUrl(newValue); + return { ...prevState }; }); - }, []); + }; - useEffect(() => { - (storage as IKbnUrlStateStorage).set(reportTypeKey, reportType); - }, [reportType, storage]); + const removeSeries = (seriesIdN: string) => { + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); + }; - const removeSeries = useCallback((seriesIndex: number) => { - setAllSeries((prevAllSeries) => - prevAllSeries.filter((seriesT, index) => index !== seriesIndex) - ); - }, []); + const allSeriesIds = Object.keys(allShortSeries); const getSeries = useCallback( - (seriesIndex: number) => { - return allSeries[seriesIndex]; + (seriesId?: string) => { + return seriesId ? allSeries?.[seriesId] ?? {} : ({} as SeriesUrl); }, [allSeries] ); - const applyChanges = useCallback(() => { - const allShortSeries = allSeries.map((series) => convertToShortUrl(series)); - - (storage as IKbnUrlStateStorage).set(allSeriesKey, allShortSeries); - setLastRefresh(Date.now()); - }, [allSeries, storage]); - - useEffect(() => { - (storage as IKbnUrlStateStorage).set(autoApplyKey, autoApply); - }, [autoApply, storage]); - const value = { - autoApply, - setAutoApply, - applyChanges, storage, getSeries, setSeries, removeSeries, + firstSeriesId, allSeries, - lastRefresh, - setLastRefresh, - setReportType, - reportType: storage.get(reportTypeKey) as ReportViewType, + allSeriesIds, firstSeries: firstSeries!, }; return {children}; @@ -159,9 +112,10 @@ export function useSeriesStorage() { } function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { - const { dt, op, st, bd, ft, time, rdf, mt, h, n, c, ...restSeries } = newValue; + const { dt, op, st, rt, bd, ft, time, rdf, mt, ...restSeries } = newValue; return { operationType: op, + reportType: rt!, seriesType: st, breakdown: bd, filters: ft!, @@ -169,31 +123,26 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { reportDefinitions: rdf, dataType: dt!, selectedMetricField: mt, - hidden: h, - name: n, - color: c, ...restSeries, }; } interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; [URL_KEYS.FILTERS]?: UrlFilter[]; [URL_KEYS.REPORT_DEFINITIONS]?: URLReportDefinition; [URL_KEYS.SELECTED_METRIC]?: string; - [URL_KEYS.HIDDEN]?: boolean; - [URL_KEYS.NAME]: string; - [URL_KEYS.COLOR]?: string; time?: { to: string; from: string; }; } -export type AllShortSeries = ShortUrlSeries[]; -export type AllSeries = SeriesUrl[]; +export type AllShortSeries = Record; +export type AllSeries = Record; -export const NEW_SERIES_KEY = 'new-series'; +export const NEW_SERIES_KEY = 'new-series-key'; 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 3de29b02853e8..e55752ceb62ba 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/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 9e4d9486dc155..4cb586fe94ceb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -7,51 +7,16 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash'; +import { combineTimeRanges } from './exploratory_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ReportViewType, SeriesUrl } from './types'; -import { ReportTypes } from './configurations/constants'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } -export const combineTimeRanges = ( - reportType: ReportViewType, - allSeries: SeriesUrl[], - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - - if (reportType === ReportTypes.KPI) { - return firstSeries?.time; - } - - allSeries.forEach((series) => { - if ( - series.dataType && - series.selectedMetricField && - !isEmpty(series.reportDefinitions) && - series.time - ) { - 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 LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -62,11 +27,9 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); + const { firstSeriesId, firstSeries: series, setSeries, allSeries } = useSeriesStorage(); - const firstSeriesId = 0; - - const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; + const timeRange = combineTimeRanges(allSeries, series); const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -74,9 +37,9 @@ export function LensEmbeddable(props: Props) { const onBrushEnd = useCallback( ({ range }: { range: number[] }) => { - if (reportType !== 'data-distribution' && firstSeries) { + if (series?.reportType !== 'data-distribution') { setSeries(firstSeriesId, { - ...firstSeries, + ...series, time: { from: new Date(range[0]).toISOString(), to: new Date(range[1]).toISOString(), @@ -90,30 +53,16 @@ export function LensEmbeddable(props: Props) { ); } }, - [reportType, setSeries, firstSeries, notifications?.toasts] + [notifications?.toasts, series, firstSeriesId, setSeries] ); - if (timeRange === null || !firstSeries) { - return null; - } - return ( - - - + ); } - -const LensWrapper = styled.div` - height: 100%; - - &&& > div { - height: 100%; - } -`; 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 0e609cbe6c9e5..972e3beb4b722 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 @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { stringify } from 'query-string'; // eslint-disable-next-line import/no-extraneous-dependencies import { render as reactTestLibRender, RenderOptions } from '@testing-library/react'; -import { Route, Router } from 'react-router-dom'; +import { Router } from 'react-router-dom'; import { createMemoryHistory, History } from 'history'; import { CoreStart } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n/react'; @@ -24,7 +24,7 @@ import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/ import { lensPluginMock } from '../../../../../lens/public/mocks'; import * as useAppIndexPatternHook from './hooks/use_app_index_pattern'; import { IndexPatternContextProvider } from './hooks/use_app_index_pattern'; -import { AllSeries, SeriesContextValue, UrlStorageContext } from './hooks/use_series_storage'; +import { AllSeries, UrlStorageContext } from './hooks/use_series_storage'; import * as fetcherHook from '../../../hooks/use_fetcher'; import * as useSeriesFilterHook from './hooks/use_series_filters'; @@ -39,10 +39,9 @@ import { IndexPattern, IndexPatternsContract, } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { AppDataType, SeriesUrl, UrlFilter } from './types'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; -import { TRANSACTION_DURATION } from './configurations/constants/elasticsearch_fieldnames'; interface KibanaProps { services?: KibanaServices; @@ -159,11 +158,9 @@ export function MockRouter({ }: MockRouterProps) { return ( - - - {children} - - + + {children} + ); } @@ -176,7 +173,7 @@ export function render( core: customCore, kibanaProps, renderOptions, - url = '/app/observability/exploratory-view/configure#?autoApply=!t', + url, initSeries = {}, }: RenderRouterOptions = {} ) { @@ -206,7 +203,7 @@ export function render( }; } -export const getHistoryFromUrl = (url: Url) => { +const getHistoryFromUrl = (url: Url) => { if (typeof url === 'string') { return createMemoryHistory({ initialEntries: [url], @@ -255,15 +252,6 @@ export const mockUseValuesList = (values?: ListItem[]) => { return { spy, onRefreshTimeRange }; }; -export const mockUxSeries = { - name: 'performance-distribution', - dataType: 'ux', - breakdown: 'user_agent.name', - time: { from: 'now-15m', to: 'now' }, - reportDefinitions: { 'service.name': ['elastic-co'] }, - selectedMetricField: TRANSACTION_DURATION, -} as SeriesUrl; - function mockSeriesStorageContext({ data, filters, @@ -273,34 +261,34 @@ function mockSeriesStorageContext({ filters?: UrlFilter[]; breakdown?: string; }) { - const testSeries = { - ...mockUxSeries, - breakdown: breakdown || 'user_agent.name', - ...(filters ? { filters } : {}), + const mockDataSeries = data || { + 'performance-distribution': { + reportType: 'data-distribution', + dataType: 'ux', + breakdown: breakdown || 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + ...(filters ? { filters } : {}), + }, }; + const allSeriesIds = Object.keys(mockDataSeries); + const firstSeriesId = allSeriesIds?.[0]; - const mockDataSeries = data || [testSeries]; + const series = mockDataSeries[firstSeriesId]; const removeSeries = jest.fn(); const setSeries = jest.fn(); - const getSeries = jest.fn().mockReturnValue(testSeries); + const getSeries = jest.fn().mockReturnValue(series); return { + firstSeriesId, + allSeriesIds, removeSeries, setSeries, getSeries, - autoApply: true, - reportType: 'data-distribution', - lastRefresh: Date.now(), - setLastRefresh: jest.fn(), - setAutoApply: jest.fn(), - applyChanges: jest.fn(), - firstSeries: mockDataSeries[0], + firstSeries: mockDataSeries[firstSeriesId], allSeries: mockDataSeries, - setReportType: jest.fn(), - storage: { get: jest.fn() } as any, - } as SeriesContextValue; + }; } export function mockUseSeriesFilter() { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx similarity index 85% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx index 8f196b8a05dda..c054853d9c877 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.test.tsx @@ -7,12 +7,12 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { SeriesChartTypesSelect, XYChartTypesSelect } from './chart_types'; describe.skip('SeriesChartTypesSelect', function () { it('should render properly', async function () { - render(); + render(); await waitFor(() => { screen.getByText(/chart type/i); @@ -21,7 +21,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should call set series on change', async function () { const { setSeries } = render( - + ); await waitFor(() => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx similarity index 77% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 27d846502dbe6..50c2f91e6067d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -6,11 +6,11 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; +import { useFetcher } from '../../../../..'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesType } from '../../../../../../../lens/public'; @@ -20,14 +20,16 @@ const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes. export function SeriesChartTypesSelect({ seriesId, - series, + seriesTypes, defaultChartType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; + seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; @@ -40,15 +42,17 @@ export function SeriesChartTypesSelect({ onChange={onChange} value={seriesType} excludeChartTypes={['bar_percentage_stacked']} - includeChartTypes={[ - 'bar', - 'bar_horizontal', - 'line', - 'area', - 'bar_stacked', - 'area_stacked', - 'bar_horizontal_percentage_stacked', - ]} + includeChartTypes={ + seriesTypes || [ + 'bar', + 'bar_horizontal', + 'line', + 'area', + 'bar_stacked', + 'area_stacked', + 'bar_horizontal_percentage_stacked', + ] + } label={CHART_TYPE_LABEL} /> ); @@ -101,14 +105,14 @@ export function XYChartTypesSelect({ }); 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 new file mode 100644 index 0000000000000..b10702ebded57 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -0,0 +1,62 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +import { mockAppIndexPattern, render } from '../../rtl_helpers'; +import { dataTypes, DataTypesCol } from './data_types_col'; + +describe('DataTypesCol', function () { + const seriesId = 'test-series-id'; + + mockAppIndexPattern(); + + it('should render properly', function () { + const { getByText } = render(); + + dataTypes.forEach(({ label }) => { + getByText(label); + }); + }); + + it('should set series on change', function () { + const { setSeries } = render(); + + fireEvent.click(screen.getByText(/user experience \(rum\)/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); + }); + + it('should set series on change on already selected', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + }, + }, + }; + + render(, { initSeries }); + + const button = screen.getByRole('button', { + name: /Synthetic Monitoring/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + }); +}); 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 new file mode 100644 index 0000000000000..f386f62d9ed73 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.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 from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType } from '../../types'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +export const dataTypes: Array<{ id: AppDataType; label: string }> = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'ux', label: 'User Experience (RUM)' }, + { id: 'mobile', label: 'Mobile Experience' }, + // { id: 'infra_logs', label: 'Logs' }, + // { id: 'infra_metrics', label: 'Metrics' }, + // { id: 'apm', label: 'APM' }, +]; + +export function DataTypesCol({ seriesId }: { seriesId: string }) { + const { getSeries, setSeries, removeSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { loading } = useAppIndexPatternContext(); + + const onDataTypeChange = (dataType?: AppDataType) => { + if (!dataType) { + removeSeries(seriesId); + } else { + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); + } + }; + + const selectedDataType = series.dataType; + + return ( + + {dataTypes.map(({ id: dataTypeId, label }) => ( + + + + ))} + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; 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 new file mode 100644 index 0000000000000..6be78084ae195 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -0,0 +1,39 @@ +/* + * 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 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' ? ( + + ) : ( + + )} + + ); +} + +const Wrapper = styled.div` + .euiSuperDatePicker__flexWrapper { + width: 100%; + > .euiFlexItem { + margin-right: 0px; + } + } +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index ced4d3af057ff..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -7,66 +7,62 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; -import { mockUxSeries, render } from '../../rtl_helpers'; +import { render } from '../../rtl_helpers'; import { OperationTypeSelect } from './operation_type_select'; describe('OperationTypeSelect', function () { it('should render properly', function () { - render(); + render(); screen.getByText('Select an option: , is selected'); }); it('should display selected value', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'performance-distribution': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - render(, { - initSeries, - }); + render(, { initSeries }); screen.getByText('Median'); }); it('should call set series on change', function () { const initSeries = { - data: [ - { - name: 'performance-distribution', + data: { + 'series-id': { dataType: 'ux' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, - ], + }, }; - const { setSeries } = render(, { - initSeries, - }); + const { setSeries } = render(, { initSeries }); fireEvent.click(screen.getByTestId('operationTypeSelect')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); fireEvent.click(screen.getByText('95th Percentile')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, - name: 'performance-distribution', }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx similarity index 91% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx index 4c10c9311704d..fce1383f30f34 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/operation_type_select.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.tsx @@ -11,18 +11,17 @@ import { EuiSuperSelect } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { OperationType } from '../../../../../../../lens/public'; -import { SeriesUrl } from '../../types'; export function OperationTypeSelect({ seriesId, - series, defaultOperationType, }: { - seriesId: number; - series: SeriesUrl; + seriesId: string; defaultOperationType?: OperationType; }) { - const { setSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const operationType = series?.operationType; @@ -84,7 +83,11 @@ export function OperationTypeSelect({ return ( ); + + screen.getByText('Select an option: , is selected'); + screen.getAllByText('Browser family'); + }); + + it('should set new series breakdown on change', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/operating system/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: USER_AGENT_OS, + dataType: 'ux', + reportType: 'data-distribution', + time: { from: 'now-15m', to: 'now' }, + }); + }); + it('should set undefined on new series on no select breakdown', function () { + const { setSeries } = render( + + ); + + const btn = screen.getByRole('button', { + name: /select an option: Browser family , is selected/i, + hidden: true, + }); + + fireEvent.click(btn); + + fireEvent.click(screen.getByText(/no breakdown/i)); + + expect(setSeries).toHaveBeenCalledTimes(1); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + breakdown: undefined, + dataType: 'ux', + 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_breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx new file mode 100644 index 0000000000000..fa2d01691ce1d --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.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 { Breakdowns } from '../../series_editor/columns/breakdowns'; +import { SeriesConfig } from '../../types'; + +export function ReportBreakdowns({ + seriesId, + seriesConfig, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx similarity index 65% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index 544a294e021e2..3d156e0ee9c2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -12,14 +12,14 @@ import { mockAppIndexPattern, mockIndexPattern, mockUseValuesList, - mockUxSeries, render, } from '../../rtl_helpers'; import { ReportDefinitionCol } from './report_definition_col'; +import { SERVICE_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Series Builder ReportDefinitionCol', function () { mockAppIndexPattern(); - const seriesId = 0; + const seriesId = 'test-series-id'; const seriesConfig = getDefaultConfigs({ reportType: 'data-distribution', @@ -27,24 +27,36 @@ describe('Series Builder ReportDefinitionCol', function () { dataType: 'ux', }); + const initSeries = { + data: { + [seriesId]: { + dataType: 'ux' as const, + reportType: 'data-distribution' as const, + time: { from: 'now-30d', to: 'now' }, + reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, + }, + }, + }; + mockUseValuesList([{ label: 'elastic-co', count: 10 }]); - it('renders', async () => { - render( - - ); + it('should render properly', async function () { + render(, { + initSeries, + }); await waitFor(() => { - expect(screen.getByText('Web Application')).toBeInTheDocument(); - expect(screen.getByText('Environment')).toBeInTheDocument(); - expect(screen.getByText('Search Environment')).toBeInTheDocument(); + screen.getByText('Web Application'); + screen.getByText('Environment'); + screen.getByText('Select an option: Page load time, is selected'); + screen.getByText('Page load time'); }); }); it('should render selected report definitions', async function () { - render( - - ); + render(, { + initSeries, + }); expect(await screen.findByText('elastic-co')).toBeInTheDocument(); @@ -53,7 +65,8 @@ describe('Series Builder ReportDefinitionCol', function () { it('should be able to remove selected definition', async function () { const { setSeries } = render( - + , + { initSeries } ); expect( @@ -67,14 +80,11 @@ describe('Series Builder ReportDefinitionCol', function () { fireEvent.click(removeBtn); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', - name: 'performance-distribution', - breakdown: 'user_agent.name', reportDefinitions: {}, - selectedMetricField: 'transaction.duration.us', - time: { from: 'now-15m', to: 'now' }, + 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 new file mode 100644 index 0000000000000..0c620abf56e8a --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; +import styled from 'styled-components'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { ReportMetricOptions } from '../report_metric_options'; +import { SeriesConfig } from '../../types'; +import { SeriesChartTypesSelect } from './chart_types'; +import { OperationTypeSelect } from './operation_type_select'; +import { DatePickerCol } from './date_picker_col'; +import { parseCustomFieldName } from '../../configurations/lens_attributes'; +import { ReportDefinitionField } from './report_definition_field'; + +function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { + const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); + + return columnType; +} + +export function ReportDefinitionCol({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions: selectedReportDefinitions = {}, selectedMetricField } = series ?? {}; + + const { + definitionFields, + defaultSeriesType, + hasOperationType, + yAxisColumns, + metricOptions, + } = seriesConfig; + + const onChange = (field: string, value?: string[]) => { + if (!value?.[0]) { + delete selectedReportDefinitions[field]; + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions }, + }); + } else { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [field]: value }, + }); + } + }; + + const columnType = getColumnType(seriesConfig, selectedMetricField); + + return ( + + + + + + {definitionFields.map((field) => ( + + + + ))} + {metricOptions && ( + + + + )} + {(hasOperationType || columnType === 'operation') && ( + + + + )} + + + + + ); +} + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx similarity index 69% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx index 3651b4b7f075b..8a83b5c2a8cb0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_field.tsx @@ -6,25 +6,30 @@ */ import React, { useMemo } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { isEmpty } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; import { PersistableFilter } from '../../../../../../../lens/common'; import { buildPhrasesFilter } from '../../configurations/utils'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_value_combobox'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; field: string; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ seriesId, field, seriesConfig, onChange }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPattern } = useAppIndexPatternContext(series.dataType); const { reportDefinitions: selectedReportDefinitions = {} } = series; @@ -59,26 +64,23 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: // eslint-disable-next-line react-hooks/exhaustive-deps }, [JSON.stringify(selectedReportDefinitions), JSON.stringify(baseFilters)]); - if (!indexPattern) { - return null; - } - return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - asCombobox={true} - allowExclusions={false} - allowAllValuesSelection={true} - usePrependLabel={false} - compressed={false} - required={isEmpty(selectedReportDefinitions)} - /> + + + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + allowAllValuesSelection={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 new file mode 100644 index 0000000000000..0b183b5f20c03 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { screen } from '@testing-library/react'; +import { ReportFilters } from './report_filters'; +import { getDefaultConfigs } from '../../configurations/default_configs'; +import { mockIndexPattern, render } from '../../rtl_helpers'; + +describe('Series Builder ReportFilters', function () { + const seriesId = 'test-series-id'; + + const dataViewSeries = getDefaultConfigs({ + reportType: 'data-distribution', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', function () { + render(); + + screen.getByText('Add filter'); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx new file mode 100644 index 0000000000000..d5938c5387e8f --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -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 React from 'react'; +import { SeriesFilter } from '../../series_editor/columns/series_filter'; +import { SeriesConfig } from '../../types'; + +export function ReportFilters({ + seriesConfig, + seriesId, +}: { + seriesConfig: SeriesConfig; + seriesId: string; +}) { + return ( + + ); +} 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 new file mode 100644 index 0000000000000..12ae8560453c9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -0,0 +1,79 @@ +/* + * 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 { fireEvent, screen } from '@testing-library/react'; +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'; + +describe('ReportTypesCol', function () { + const seriesId = 'performance-distribution'; + + mockAppIndexPattern(); + + it('should render properly', function () { + render(); + screen.getByText('Performance distribution'); + screen.getByText('KPI over time'); + }); + + it('should display empty message', function () { + render(); + screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT); + }); + + it('should set series on change', function () { + const { setSeries } = render( + + ); + + fireEvent.click(screen.getByText(/KPI over time/i)); + + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + selectedMetricField: undefined, + reportType: 'kpi-over-time', + time: { from: 'now-15m', to: 'now' }, + }); + expect(setSeries).toHaveBeenCalledTimes(1); + }); + + it('should set selected as filled', function () { + const initSeries = { + data: { + [seriesId]: { + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + breakdown: 'monitor.status', + time: { from: 'now-15m', to: 'now' }, + isNew: true, + }, + }, + }; + + const { setSeries } = render( + , + { initSeries } + ); + + const button = screen.getByRole('button', { + name: /KPI over time/i, + }); + + expect(button.classList).toContain('euiButton--fill'); + fireEvent.click(button); + + // undefined on click selected + 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 new file mode 100644 index 0000000000000..c4eebbfaca3eb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,108 @@ +/* + * 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 { 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 { 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 } from '../series_builder'; + +interface Props { + seriesId: string; + reportTypes: ReportTypeItem[]; +} + +export function ReportTypesCol({ seriesId, reportTypes }: Props) { + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); + + const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); + + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); + + if (!restSeries.dataType) { + return ( + + ); + } + + if (!loading && !hasData) { + return ( + + ); + } + + const disabledReportTypes: ReportViewType[] = map( + reportTypes.filter( + ({ reportType }) => firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + + return reportTypes?.length > 0 ? ( + + {reportTypes.map(({ reportType, label }) => ( + + + + ))} + + ) : ( + {SELECTED_DATA_TYPE_FOR_REPORT} + ); +} + +export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate( + 'xpack.observability.expView.reportType.noDataType', + { defaultMessage: 'No data type selected.' } +); + +const FlexGroup = styled(EuiFlexGroup)` + width: 100%; +`; + +const Button = styled(EuiButton)` + will-change: transform; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx index c352ec0423dd8..874171de123d2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/last_updated.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx @@ -8,7 +8,6 @@ import React, { useEffect, useState } from 'react'; import { EuiIcon, EuiText } from '@elastic/eui'; import moment from 'moment'; -import { FormattedMessage } from '@kbn/i18n/react'; interface Props { lastUpdated?: number; @@ -19,34 +18,20 @@ export function LastUpdated({ lastUpdated }: Props) { useEffect(() => { const interVal = setInterval(() => { setRefresh(Date.now()); - }, 5000); + }, 1000); return () => { clearInterval(interVal); }; }, []); - useEffect(() => { - setRefresh(Date.now()); - }, [lastUpdated]); - if (!lastUpdated) { return null; } - const isWarning = moment().diff(moment(lastUpdated), 'minute') > 5; - const isDanger = moment().diff(moment(lastUpdated), 'minute') > 10; - return ( - - - + + Last Updated: {moment(lastUpdated).from(refresh)} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx new file mode 100644 index 0000000000000..a2a3e34c21834 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/report_metric_options.tsx @@ -0,0 +1,46 @@ +/* + * 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 { EuiSuperSelect } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { SeriesConfig } from '../types'; + +interface Props { + seriesId: string; + defaultValue?: string; + options: SeriesConfig['metricOptions']; +} + +export function ReportMetricOptions({ seriesId, options: opts }: Props) { + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const onChange = (value: string) => { + setSeries(seriesId, { + ...series, + selectedMetricField: value, + }); + }; + + const options = opts ?? []; + + return ( + ({ + value: fd || id, + inputDisplay: label, + }))} + valueOfSelected={series.selectedMetricField || options?.[0].field || options?.[0].id} + onChange={(value) => onChange(value)} + /> + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx new file mode 100644 index 0000000000000..684cf3a210a51 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,303 @@ +/* + * 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, { RefObject, useEffect, useState } from 'react'; +import { isEmpty } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { + EuiBasicTable, + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiSwitch, +} from '@elastic/eui'; +import { rgba } from 'polished'; +import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; +import { DataTypesCol } from './columns/data_types_col'; +import { ReportTypesCol } from './columns/report_types_col'; +import { ReportDefinitionCol } from './columns/report_definition_col'; +import { ReportFilters } from './columns/report_filters'; +import { ReportBreakdowns } from './columns/report_breakdowns'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesEditor } from '../series_editor/series_editor'; +import { SeriesActions } from '../series_editor/columns/series_actions'; +import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LastUpdated } from './last_updated'; +import { + CORE_WEB_VITALS_LABEL, + DEVICE_DISTRIBUTION_LABEL, + KPI_OVER_TIME_LABEL, + PERF_DIST_LABEL, +} from '../configurations/constants/labels'; + +export interface ReportTypeItem { + id: string; + reportType: ReportViewType; + label: string; +} + +export const ReportTypes: Record = { + synthetics: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + ], + ux: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'cwv', reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, + ], + mobile: [ + { id: 'kpi', reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, + { id: 'dist', reportType: 'data-distribution', label: PERF_DIST_LABEL }, + { id: 'mdd', reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, + ], + apm: [], + infra_logs: [], + infra_metrics: [], +}; + +interface BuilderItem { + id: string; + series: SeriesUrl; + seriesConfig?: SeriesConfig; +} + +export function SeriesBuilder({ + seriesBuilderRef, + lastUpdated, + multiSeries, +}: { + seriesBuilderRef: RefObject; + lastUpdated?: number; + multiSeries?: boolean; +}) { + const [editorItems, setEditorItems] = useState([]); + const { getSeries, allSeries, allSeriesIds, setSeries, removeSeries } = useSeriesStorage(); + + const { loading, indexPatterns } = useAppIndexPatternContext(); + + useEffect(() => { + const getDataViewSeries = (dataType: AppDataType, reportType: SeriesUrl['reportType']) => { + if (indexPatterns?.[dataType]) { + return getDefaultConfigs({ + dataType, + indexPattern: indexPatterns[dataType], + reportType: reportType!, + }); + } + }; + + const seriesToEdit: BuilderItem[] = + allSeriesIds + .filter((sId) => { + return allSeries?.[sId]?.isNew; + }) + .map((sId) => { + const series = getSeries(sId); + const seriesConfig = getDataViewSeries(series.dataType, series.reportType); + + return { id: sId, series, seriesConfig }; + }) ?? []; + const initSeries: BuilderItem[] = [{ id: 'series-id', series: {} as SeriesUrl }]; + setEditorItems(multiSeries || seriesToEdit.length > 0 ? seriesToEdit : initSeries); + }, [allSeries, allSeriesIds, getSeries, indexPatterns, loading, multiSeries]); + + const columns = [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', { + defaultMessage: 'Data Type', + }), + field: 'id', + width: '15%', + render: (seriesId: string) => , + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.report', { + defaultMessage: 'Report', + }), + width: '15%', + field: 'id', + render: (seriesId: string, { series: { dataType } }: BuilderItem) => ( + + ), + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', { + defaultMessage: 'Definition', + }), + width: '30%', + field: 'id', + render: ( + seriesId: string, + { series: { dataType, reportType }, seriesConfig }: BuilderItem + ) => { + if (dataType && seriesConfig) { + return loading ? ( + LOADING_VIEW + ) : reportType ? ( + + ) : ( + SELECT_REPORT_TYPE + ); + } + + return null; + }, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', { + defaultMessage: 'Filters', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', { + defaultMessage: 'Breakdowns', + }), + width: '20%', + field: 'id', + render: (seriesId: string, { series: { reportType }, seriesConfig }: BuilderItem) => + reportType && seriesConfig ? ( + + ) : null, + }, + ...(multiSeries + ? [ + { + name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + defaultMessage: 'Actions', + }), + align: 'center' as const, + width: '10%', + field: 'id', + render: (seriesId: string, item: BuilderItem) => ( + + ), + }, + ] + : []), + ]; + + const applySeries = () => { + editorItems.forEach(({ series, id: seriesId }) => { + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + + if (reportType && !isEmpty(reportDefinitions)) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + const newSeriesId = `${reportDefId}-${reportType}`; + + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }); + }; + + const addSeries = () => { + const prevSeries = allSeries?.[allSeriesIds?.[0]]; + setSeries( + `${NEW_SERIES_KEY}-${editorItems.length + 1}`, + prevSeries + ? ({ isNew: true, time: prevSeries.time } as SeriesUrl) + : ({ isNew: true } as SeriesUrl) + ); + }; + + return ( + + {multiSeries && ( + + + + + + {}} + compressed + /> + + + applySeries()} isDisabled={true} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { + defaultMessage: 'Apply changes', + })} + + + + addSeries()} size="s"> + {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { + defaultMessage: 'Add Series', + })} + + + + )} +
+ {multiSeries && } + {editorItems.length > 0 && ( + + )} + +
+
+ ); +} + +const Wrapper = euiStyled.div` + max-height: 50vh; + overflow-y: scroll; + overflow-x: clip; + &::-webkit-scrollbar { + height: ${({ theme }) => theme.eui.euiScrollBar}; + width: ${({ theme }) => theme.eui.euiScrollBar}; + } + &::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; + border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; + } + &::-webkit-scrollbar-corner, + &::-webkit-scrollbar-track { + background-color: transparent; + } +`; + +export const LOADING_VIEW = i18n.translate( + 'xpack.observability.expView.seriesBuilder.loadingView', + { + defaultMessage: 'Loading view ...', + } +); + +export const SELECT_REPORT_TYPE = i18n.translate( + 'xpack.observability.expView.seriesBuilder.selectReportType', + { + defaultMessage: 'No report type selected', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx similarity index 58% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx index 0b8e1c1785c7f..c30863585b3b0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx @@ -6,48 +6,48 @@ */ import React from 'react'; +import { i18n } from '@kbn/i18n'; import { EuiDatePicker, EuiDatePickerRange } from '@elastic/eui'; -import { Moment } from 'moment'; import DateMath from '@elastic/datemath'; -import { i18n } from '@kbn/i18n'; +import { Moment } from 'moment'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public'; -import { SeriesUrl } from '../types'; -import { ReportTypes } from '../configurations/constants'; export const parseAbsoluteDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; -export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { - const { firstSeries, setSeries, reportType } = useSeriesStorage(); +export function DateRangePicker({ seriesId }: { seriesId: string }) { + const { firstSeriesId, getSeries, setSeries } = useSeriesStorage(); const dateFormat = useUiSetting('dateFormat'); - const seriesFrom = series.time?.from; - const seriesTo = series.time?.to; + const { + time: { from, to }, + reportType, + } = getSeries(firstSeriesId); - const { from: mainFrom, to: mainTo } = firstSeries!.time; + const series = getSeries(seriesId); - const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; - const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; + const { + time: { from: seriesFrom, to: seriesTo }, + } = series; - const getTotalDuration = () => { - const mainStartDate = parseAbsoluteDate(mainTo)!; - const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; - return mainEndDate.diff(mainStartDate, 'millisecond'); - }; + const startDate = parseAbsoluteDate(seriesFrom ?? from)!; + const endDate = parseAbsoluteDate(seriesTo ?? to, { roundUp: true })!; - const onStartChange = (newStartDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newFrom = newStartDate.toISOString(); - const newTo = newStartDate.add(totalDuration, 'millisecond').toISOString(); + const onStartChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newFrom = newDate.toISOString(); + const newTo = newDate.add(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newFrom = newStartDate.toISOString(); + const newFrom = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -55,19 +55,20 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series }); } }; - - const onEndChange = (newEndDate: Moment) => { - if (reportType === ReportTypes.KPI) { - const totalDuration = getTotalDuration(); - const newTo = newEndDate.toISOString(); - const newFrom = newEndDate.subtract(totalDuration, 'millisecond').toISOString(); + const onEndChange = (newDate: Moment) => { + if (reportType === 'kpi-over-time') { + const mainStartDate = parseAbsoluteDate(from)!; + const mainEndDate = parseAbsoluteDate(to, { roundUp: true })!; + const totalDuration = mainEndDate.diff(mainStartDate, 'millisecond'); + const newTo = newDate.toISOString(); + const newFrom = newDate.subtract(totalDuration, 'millisecond').toISOString(); setSeries(seriesId, { ...series, time: { from: newFrom, to: newTo }, }); } else { - const newTo = newEndDate.toISOString(); + const newTo = newDate.toISOString(); setSeries(seriesId, { ...series, @@ -89,7 +90,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.startDate', { defaultMessage: 'Start date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } @@ -103,7 +104,7 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series aria-label={i18n.translate('xpack.observability.expView.dateRanger.endDate', { defaultMessage: 'End date', })} - dateFormat={dateFormat.replace('ss.SSS', 'ss')} + dateFormat={dateFormat} showTimeSelect /> } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx new file mode 100644 index 0000000000000..e21da424b58c8 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/index.tsx @@ -0,0 +1,58 @@ +/* + * 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 { EuiSuperDatePicker } from '@elastic/eui'; +import React, { useEffect } from 'react'; +import { useHasData } from '../../../../hooks/use_has_data'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges'; +import { DEFAULT_TIME } from '../configurations/constants'; + +export interface TimePickerTime { + from: string; + to: string; +} + +export interface TimePickerQuickRange extends TimePickerTime { + display: string; +} + +interface Props { + seriesId: string; +} + +export function SeriesDatePicker({ seriesId }: Props) { + const { onRefreshTimeRange } = useHasData(); + + const commonlyUsedRanges = useQuickTimeRanges(); + + const { getSeries, setSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + function onTimeChange({ start, end }: { start: string; end: string }) { + onRefreshTimeRange(); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); + } + + useEffect(() => { + if (!series || !series.time) { + setSeries(seriesId, { ...series, time: DEFAULT_TIME }); + } + }, [series, seriesId, setSeries]); + + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx similarity index 50% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx index 3517508300e4b..931dfbe07cd23 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/series_date_picker/series_date_picker.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/series_date_picker.test.tsx @@ -6,48 +6,67 @@ */ import React from 'react'; -import { mockUseHasData, render } from '../../rtl_helpers'; +import { mockUseHasData, render } from '../rtl_helpers'; import { fireEvent, waitFor } from '@testing-library/react'; import { SeriesDatePicker } from './index'; +import { DEFAULT_TIME } from '../configurations/constants'; describe('SeriesDatePicker', function () { it('should render properly', function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'data-distribution' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; - const { getByText } = render(, { - initSeries, - }); + const { getByText } = render(, { initSeries }); - getByText('Last 30 Minutes'); + getByText('Last 30 minutes'); + }); + + it('should set defaults', async function () { + const initSeries = { + data: { + 'uptime-pings-histogram': { + reportType: 'kpi-over-time' as const, + dataType: 'synthetics' as const, + breakdown: 'monitor.status', + }, + }, + }; + const { setSeries: setSeries1 } = render( + , + { initSeries: initSeries as any } + ); + expect(setSeries1).toHaveBeenCalledTimes(1); + expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', { + breakdown: 'monitor.status', + dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, + time: DEFAULT_TIME, + }); }); it('should set series data', async function () { const initSeries = { - data: [ - { - name: 'uptime-pings-histogram', + data: { + 'uptime-pings-histogram': { dataType: 'synthetics' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-30m', to: 'now' }, }, - ], + }, }; const { onRefreshTimeRange } = mockUseHasData(); - const { getByTestId, setSeries } = render( - , - { - initSeries, - } - ); + const { getByTestId, setSeries } = render(, { + initSeries, + }); await waitFor(function () { fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton')); @@ -57,10 +76,10 @@ describe('SeriesDatePicker', function () { expect(onRefreshTimeRange).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(0, { - name: 'uptime-pings-histogram', + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'monitor.status', dataType: 'synthetics', + reportType: 'kpi-over-time', time: { from: 'now/d', to: 'now/d' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx new file mode 100644 index 0000000000000..207a53e13f1ad --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/chart_edit_options.tsx @@ -0,0 +1,30 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { Breakdowns } from './columns/breakdowns'; +import { SeriesConfig } from '../types'; +import { ChartOptions } from './columns/chart_options'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; + breakdownFields: string[]; +} +export function ChartEditOptions({ seriesConfig, seriesId, breakdownFields }: Props) { + return ( + + + + + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx similarity index 74% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx index 21b766227a562..84568e1c5068a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; -import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; +import { mockIndexPattern, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; @@ -20,7 +20,13 @@ describe('Breakdowns', function () { }); it('should render properly', async function () { - render(); + render( + + ); screen.getAllByText('Browser family'); }); @@ -30,9 +36,9 @@ describe('Breakdowns', function () { const { setSeries } = render( , { initSeries } ); @@ -43,14 +49,10 @@ describe('Breakdowns', function () { fireEvent.click(screen.getByText('Browser family')); - expect(setSeries).toHaveBeenCalledWith(0, { + expect(setSeries).toHaveBeenCalledWith('series-id', { breakdown: 'user_agent.name', dataType: 'ux', - name: 'performance-distribution', - reportDefinitions: { - 'service.name': ['elastic-co'], - }, - selectedMetricField: 'transaction.duration.us', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index 315f63e33bed0..2237935d466ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -8,20 +8,20 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { useRouteMatch } from 'react-router-dom'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; -import { SeriesConfig, SeriesUrl } from '../../types'; +import { SeriesConfig } from '../../types'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; + breakdowns: string[]; seriesConfig: SeriesConfig; } -export function Breakdowns({ seriesConfig, seriesId, series }: Props) { - const { setSeries } = useSeriesStorage(); - const isPreview = !!useRouteMatch('/exploratory-view/preview'); +export function Breakdowns({ seriesConfig, seriesId, breakdowns = [] }: Props) { + const { setSeries, getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const selectedBreakdown = series.breakdown; const NO_BREAKDOWN = 'no_breakdown'; @@ -40,13 +40,9 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; - if (!seriesConfig) { - return null; - } - const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; - const items = seriesConfig.breakdownFields.map((breakdown) => ({ + const items = breakdowns.map((breakdown) => ({ id: breakdown, label: seriesConfig.labels[breakdown], })); @@ -54,12 +50,14 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { if (!hasUseBreakdownColumn) { items.push({ id: NO_BREAKDOWN, - label: NO_BREAK_DOWN_LABEL, + label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', { + defaultMessage: 'No breakdown', + }), }); } const options = items.map(({ id, label }) => ({ - inputDisplay: label, + inputDisplay: id === NO_BREAKDOWN ? label : {label}, value: id, dropdownDisplay: label, })); @@ -71,7 +69,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
onOptionChange(value)} @@ -80,10 +78,3 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) {
); } - -export const NO_BREAK_DOWN_LABEL = i18n.translate( - 'xpack.observability.exp.breakDownFilter.noBreakdown', - { - defaultMessage: 'No breakdown', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx new file mode 100644 index 0000000000000..f2a6377fd9b71 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/chart_options.tsx @@ -0,0 +1,35 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesConfig } from '../../types'; +import { OperationTypeSelect } from '../../series_builder/columns/operation_type_select'; +import { SeriesChartTypesSelect } from '../../series_builder/columns/chart_types'; + +interface Props { + seriesConfig: SeriesConfig; + seriesId: string; +} + +export function ChartOptions({ seriesConfig, seriesId }: Props) { + return ( + + + + + {seriesConfig.hasOperationType && ( + + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx deleted file mode 100644 index 838631e1f05df..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.test.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 { fireEvent, screen } from '@testing-library/react'; -import { mockAppIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; -import { DataTypesLabels, DataTypesSelect } from './data_type_select'; -import { DataTypes } from '../../configurations/constants'; - -describe('DataTypeSelect', function () { - const seriesId = 0; - - mockAppIndexPattern(); - - it('should render properly', function () { - render(); - }); - - it('should set series on change', async function () { - const { setSeries } = render(); - - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.UX])); - fireEvent.click(await screen.findByText(DataTypesLabels[DataTypes.SYNTHETICS])); - - expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { - dataType: 'synthetics', - name: 'synthetics-series-1', - time: { - from: 'now-15m', - to: 'now', - }, - }); - }); -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx deleted file mode 100644 index b0a6e3b5e26b0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/data_type_select.tsx +++ /dev/null @@ -1,105 +0,0 @@ -/* - * 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 { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { AppDataType, SeriesUrl } from '../../types'; -import { DataTypes, ReportTypes } from '../../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export const DataTypesLabels = { - [DataTypes.UX]: i18n.translate('xpack.observability.overview.exploratoryView.uxLabel', { - defaultMessage: 'User experience (RUM)', - }), - - [DataTypes.SYNTHETICS]: i18n.translate( - 'xpack.observability.overview.exploratoryView.syntheticsLabel', - { - defaultMessage: 'Synthetics monitoring', - } - ), - - [DataTypes.MOBILE]: i18n.translate( - 'xpack.observability.overview.exploratoryView.mobileExperienceLabel', - { - defaultMessage: 'Mobile experience', - } - ), -}; - -export const dataTypes: Array<{ id: AppDataType; label: string }> = [ - { - id: DataTypes.SYNTHETICS, - label: DataTypesLabels[DataTypes.SYNTHETICS], - }, - { - id: DataTypes.UX, - label: DataTypesLabels[DataTypes.UX], - }, - { - id: DataTypes.MOBILE, - label: DataTypesLabels[DataTypes.MOBILE], - }, -]; - -const SELECT_DATA_TYPE = 'SELECT_DATA_TYPE'; - -export function DataTypesSelect({ seriesId, series }: Props) { - const { setSeries, reportType } = useSeriesStorage(); - - const onDataTypeChange = (dataType: AppDataType) => { - if (String(dataType) !== SELECT_DATA_TYPE) { - setSeries(seriesId, { - dataType, - time: series.time, - name: `${dataType}-series-${seriesId + 1}`, - }); - } - }; - - const options = dataTypes - .filter(({ id }) => { - if (reportType === ReportTypes.DEVICE_DISTRIBUTION) { - return id === DataTypes.MOBILE; - } - if (reportType === ReportTypes.CORE_WEB_VITAL) { - return id === DataTypes.UX; - } - return true; - }) - .map(({ id, label }) => ({ - value: id, - inputDisplay: label, - })); - - return ( - onDataTypeChange(value as AppDataType)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.selectDataType', - { - defaultMessage: 'Select data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx index 032eb66dcfa4f..41e83f407af2b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/date_picker_col.tsx @@ -6,84 +6,24 @@ */ import React from 'react'; -import styled from 'styled-components'; -import { i18n } from '@kbn/i18n'; -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SeriesDatePicker } from '../../series_date_picker'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { DateRangePicker } from '../../components/date_range_picker'; -import { SeriesDatePicker } from '../../components/series_date_picker'; -import { AppDataType, SeriesUrl } from '../../types'; -import { ReportTypes } from '../../configurations/constants'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SyntheticsAddData } from '../../../add_data_buttons/synthetics_add_data'; -import { MobileAddData } from '../../../add_data_buttons/mobile_add_data'; -import { UXAddData } from '../../../add_data_buttons/ux_add_data'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; } - -const AddDataComponents: Record = { - mobile: MobileAddData, - ux: UXAddData, - synthetics: SyntheticsAddData, - apm: null, - infra_logs: null, - infra_metrics: null, -}; - -export function DatePickerCol({ seriesId, series }: Props) { - const { reportType } = useSeriesStorage(); - - const { hasAppData } = useAppIndexPatternContext(); - - if (!series.dataType) { - return null; - } - - const AddDataButton = AddDataComponents[series.dataType]; - if (hasAppData[series.dataType] === false && AddDataButton !== null) { - return ( - - - - {i18n.translate('xpack.observability.overview.exploratoryView.noDataAvailable', { - defaultMessage: 'No {dataType} data available.', - values: { - dataType: series.dataType, - }, - })} - - - - - - - ); - } - - if (!series.selectedMetricField) { - return null; - } +export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); return ( - - {seriesId === 0 || reportType !== ReportTypes.KPI ? ( - +
+ {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + ) : ( - + )} - +
); } - -const Wrapper = styled.div` - width: 100%; - .euiSuperDatePicker__flexWrapper { - width: 100%; - > .euiFlexItem { - margin-right: 0; - } - } -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx similarity index 67% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index a88e2eadd10c9..90a039f6b44d0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -8,24 +8,20 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterExpanded } from './filter_expanded'; -import { mockUxSeries, mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockAppIndexPattern, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME } from '../../configurations/constants/elasticsearch_fieldnames'; describe('FilterExpanded', function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - - const mockSeries = { ...mockUxSeries, filters }; - - it('render', async () => { - const initSeries = { filters }; + it('should render properly', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockAppIndexPattern(); render( , { initSeries } @@ -37,14 +33,15 @@ describe('FilterExpanded', function () { }); it('should call go back on click', async function () { - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; + const goBack = jest.fn(); render( , { initSeries } @@ -52,23 +49,28 @@ describe('FilterExpanded', function () { await waitFor(() => { fireEvent.click(screen.getByText('Browser Family')); + + expect(goBack).toHaveBeenCalledTimes(1); + expect(goBack).toHaveBeenCalledWith(); }); }); - it('calls useValuesList on load', async () => { - const initSeries = { filters }; + it('should call useValuesList on load', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; const { spy } = mockUseValuesList([ { label: 'Chrome', count: 10 }, { label: 'Firefox', count: 5 }, ]); + const goBack = jest.fn(); + render( , { initSeries } @@ -85,8 +87,8 @@ describe('FilterExpanded', function () { }); }); - it('filters display values', async () => { - const initSeries = { filters }; + it('should filter display values', async function () { + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; mockUseValuesList([ { label: 'Chrome', count: 10 }, @@ -95,20 +97,18 @@ describe('FilterExpanded', function () { render( , { initSeries } ); - await waitFor(() => { - fireEvent.click(screen.getByText('Browser Family')); - - expect(screen.queryByText('Firefox')).toBeTruthy(); + expect(screen.getByText('Firefox')).toBeTruthy(); + await waitFor(() => { fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } }); expect(screen.queryByText('Firefox')).toBeFalsy(); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx similarity index 55% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index 1ef25722aff5c..4310402a43a08 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -6,14 +6,7 @@ */ import React, { useState, Fragment } from 'react'; -import { - EuiFieldSearch, - EuiSpacer, - EuiFilterGroup, - EuiText, - EuiPopover, - EuiFilterButton, -} from '@elastic/eui'; +import { EuiFieldSearch, EuiSpacer, EuiButtonEmpty, EuiFilterGroup, EuiText } from '@elastic/eui'; import styled from 'styled-components'; import { rgba } from 'polished'; import { i18n } from '@kbn/i18n'; @@ -21,7 +14,8 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { map } from 'lodash'; import { ExistsFilter } from '@kbn/es-query'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesConfig, UrlFilter } from '../../types'; import { FilterValueButton } from './filter_value_btn'; import { useValuesList } from '../../../../../hooks/use_values_list'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; @@ -29,33 +23,31 @@ import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearc import { PersistableFilter } from '../../../../../../../lens/common'; interface Props { - seriesId: number; - series: SeriesUrl; + seriesId: string; label: string; field: string; isNegated?: boolean; + goBack: () => void; nestedField?: string; filters: SeriesConfig['baseFilters']; } -export interface NestedFilterOpen { - value: string; - negate: boolean; -} - export function FilterExpanded({ seriesId, - series, field, label, + goBack, nestedField, isNegated, filters: defaultFilters, }: Props) { const [value, setValue] = useState(''); - const [isOpen, setIsOpen] = useState(false); - const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); + const [isOpen, setIsOpen] = useState({ value: '', negate: false }); + + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); const queryFilters: ESFilter[] = []; @@ -89,71 +81,62 @@ export function FilterExpanded({ ); return ( - setIsOpen((prevState) => !prevState)} iconType="arrowDown"> - {label} - - } - isOpen={isOpen} - closePopover={() => setIsOpen(false)} - > - - { - setValue(evt.target.value); - }} - placeholder={i18n.translate('xpack.observability.filters.expanded.search', { - defaultMessage: 'Search for {label}', - values: { label }, - })} - /> - - - {displayValues.length === 0 && !loading && ( - - {i18n.translate('xpack.observability.filters.expanded.noFilter', { - defaultMessage: 'No filters found.', - })} - - )} - {displayValues.map((opt) => ( - - - {isNegated !== false && ( - - )} + + goBack()}> + {label} + + { + setValue(evt.target.value); + }} + placeholder={i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + })} + /> + + + {displayValues.length === 0 && !loading && ( + + {i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', + })} + + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( - - - - ))} - - - + )} + + + + + ))} + +
); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx similarity index 64% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx index 764a27fd663f5..a9609abc70d69 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; import { FilterValueButton } from './filter_value_btn'; -import { mockUxSeries, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; +import { mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers'; import { USER_AGENT_NAME, USER_AGENT_VERSION, @@ -19,98 +19,84 @@ describe('FilterValueButton', function () { render( ); - await waitFor(() => { - expect(screen.getByText('Chrome')).toBeInTheDocument(); - }); + screen.getByText('Chrome'); }); - describe('when negate is true', () => { - it('displays negate stats', async () => { - render( - - ); + it('should render display negate state', async function () { + render( + + ); - await waitFor(() => { - expect(screen.getByText('Not Chrome')).toBeInTheDocument(); - expect(screen.getByTitle('Not Chrome')).toBeInTheDocument(); - const btn = screen.getByRole('button'); - expect(btn.classList).toContain('euiButtonEmpty--danger'); - }); + await waitFor(() => { + screen.getByText('Not Chrome'); + screen.getByTitle('Not Chrome'); + const btn = screen.getByRole('button'); + expect(btn.classList).toContain('euiButtonEmpty--danger'); }); + }); - it('calls setFilter on click', async () => { - const { setFilter, removeFilter } = mockUseSeriesFilter(); + it('should call set filter on click', async function () { + const { setFilter, removeFilter } = mockUseSeriesFilter(); - render( - - ); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Not Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledTimes(0); - expect(setFilter).toHaveBeenCalledTimes(1); - - expect(setFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: true, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledTimes(0); + expect(setFilter).toHaveBeenCalledTimes(1); + expect(setFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: true, + value: 'Chrome', }); }); }); - describe('when selected', () => { - it('removes the filter on click', async () => { - const { removeFilter } = mockUseSeriesFilter(); - - render( - - ); + it('should remove filter on click if already selected', async function () { + const { removeFilter } = mockUseSeriesFilter(); + render( + + ); + await waitFor(() => { fireEvent.click(screen.getByText('Chrome')); - - await waitFor(() => { - expect(removeFilter).toHaveBeenCalledWith({ - field: 'user_agent.name', - negate: false, - value: 'Chrome', - }); + expect(removeFilter).toHaveBeenCalledWith({ + field: 'user_agent.name', + negate: false, + value: 'Chrome', }); }); }); @@ -121,13 +107,12 @@ describe('FilterValueButton', function () { render( ); @@ -149,14 +134,13 @@ describe('FilterValueButton', function () { render( ); @@ -183,14 +167,13 @@ describe('FilterValueButton', function () { render( ); @@ -220,14 +203,13 @@ describe('FilterValueButton', function () { render( ); @@ -247,14 +229,13 @@ describe('FilterValueButton', function () { render( ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx similarity index 92% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx index 111f915a95f46..bf4ca6eb83d94 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/filter_value_btn.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_value_btn.tsx @@ -8,11 +8,10 @@ import { i18n } from '@kbn/i18n'; import React, { useMemo } from 'react'; import { EuiFilterButton, hexToRgb } from '@elastic/eui'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; import { useSeriesFilters } from '../../hooks/use_series_filters'; import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import FieldValueSuggestions from '../../../field_value_suggestions'; -import { SeriesUrl } from '../../types'; -import { NestedFilterOpen } from './filter_expanded'; interface Props { value: string; @@ -20,13 +19,12 @@ interface Props { allSelectedValues?: string[]; negate: boolean; nestedField?: string; - seriesId: number; - series: SeriesUrl; + seriesId: string; isNestedOpen: { value: string; negate: boolean; }; - setIsNestedOpen: (val: NestedFilterOpen) => void; + setIsNestedOpen: (val: { value: string; negate: boolean }) => void; } export function FilterValueButton({ @@ -36,13 +34,16 @@ export function FilterValueButton({ field, negate, seriesId, - series, nestedField, allSelectedValues, }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + const { indexPatterns } = useAppIndexPatternContext(series.dataType); - const { setFilter, removeFilter } = useSeriesFilters({ seriesId, series }); + const { setFilter, removeFilter } = useSeriesFilters({ seriesId }); const hasActiveFilters = (allSelectedValues ?? []).includes(value); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx new file mode 100644 index 0000000000000..e75f308dab1e5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/remove_series.tsx @@ -0,0 +1,34 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React from 'react'; +import { EuiButtonIcon } from '@elastic/eui'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; +} + +export function RemoveSeries({ seriesId }: Props) { + const { removeSeries } = useSeriesStorage(); + + const onClick = () => { + removeSeries(seriesId); + }; + return ( + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx deleted file mode 100644 index dad2a7da2367b..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { ReportDefinitionField } from './report_definition_field'; - -export function ReportDefinitionCol({ - seriesId, - series, - seriesConfig, -}: { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -}) { - const { setSeries } = useSeriesStorage(); - - const { reportDefinitions: selectedReportDefinitions = {} } = series; - - const { definitionFields } = seriesConfig; - - const onChange = (field: string, value?: string[]) => { - if (!value?.[0]) { - delete selectedReportDefinitions[field]; - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions }, - }); - } else { - setSeries(seriesId, { - ...series, - reportDefinitions: { ...selectedReportDefinitions, [field]: value }, - }); - } - }; - - return ( - - {definitionFields.map((field) => ( - - - - ))} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx deleted file mode 100644 index 01c9fce7637bb..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_type_select.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 { EuiSuperSelect } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { ReportViewType } from '../../types'; -import { - CORE_WEB_VITALS_LABEL, - DEVICE_DISTRIBUTION_LABEL, - KPI_OVER_TIME_LABEL, - PERF_DIST_LABEL, -} from '../../configurations/constants/labels'; - -const SELECT_REPORT_TYPE = 'SELECT_REPORT_TYPE'; - -export const reportTypesList: Array<{ - reportType: ReportViewType | typeof SELECT_REPORT_TYPE; - label: string; -}> = [ - { - reportType: SELECT_REPORT_TYPE, - label: i18n.translate('xpack.observability.expView.reportType.selectLabel', { - defaultMessage: 'Select report type', - }), - }, - { reportType: 'kpi-over-time', label: KPI_OVER_TIME_LABEL }, - { reportType: 'data-distribution', label: PERF_DIST_LABEL }, - { reportType: 'core-web-vitals', label: CORE_WEB_VITALS_LABEL }, - { reportType: 'device-data-distribution', label: DEVICE_DISTRIBUTION_LABEL }, -]; - -export function ReportTypesSelect() { - const { setReportType, reportType: selectedReportType, allSeries } = useSeriesStorage(); - - const onReportTypeChange = (reportType: ReportViewType) => { - setReportType(reportType); - }; - - const options = reportTypesList - .filter(({ reportType }) => (selectedReportType ? reportType !== SELECT_REPORT_TYPE : true)) - .map(({ reportType, label }) => ({ - value: reportType, - inputDisplay: reportType === SELECT_REPORT_TYPE ? label : {label}, - dropdownDisplay: label, - })); - - return ( - onReportTypeChange(value as ReportViewType)} - style={{ minWidth: 200 }} - isInvalid={!selectedReportType && allSeries.length > 0} - disabled={allSeries.length > 0} - /> - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx new file mode 100644 index 0000000000000..51ebe6c6bd9d5 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -0,0 +1,103 @@ +/* + * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { RemoveSeries } from './remove_series'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; +import { SeriesUrl } from '../../types'; + +interface Props { + seriesId: string; + editorMode?: boolean; +} +export function SeriesActions({ seriesId, editorMode = false }: Props) { + const { getSeries, setSeries, allSeriesIds, removeSeries } = useSeriesStorage(); + const series = getSeries(seriesId); + + const onEdit = () => { + setSeries(seriesId, { ...series, isNew: true }); + }; + + const copySeries = () => { + let copySeriesId: string = `${seriesId}-copy`; + if (allSeriesIds.includes(copySeriesId)) { + copySeriesId = copySeriesId + allSeriesIds.length; + } + setSeries(copySeriesId, series); + }; + + const { reportType, reportDefinitions, isNew, ...restSeries } = series; + const isSaveAble = reportType && !isEmpty(reportDefinitions); + + const saveSeries = () => { + if (isSaveAble) { + const reportDefId = Object.values(reportDefinitions ?? {})[0]; + let newSeriesId = `${reportDefId}-${reportType}`; + + if (allSeriesIds.includes(newSeriesId)) { + newSeriesId = `${newSeriesId}-${allSeriesIds.length}`; + } + const newSeriesN: SeriesUrl = { + ...restSeries, + reportType, + reportDefinitions, + }; + + setSeries(newSeriesId, newSeriesN); + removeSeries(seriesId); + } + }; + + return ( + + {!editorMode && ( + + + + )} + {editorMode && ( + + + + )} + {editorMode && ( + + + + )} + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx new file mode 100644 index 0000000000000..02144c6929b38 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_filter.tsx @@ -0,0 +1,155 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import React, { useState, Fragment } from 'react'; +import { + EuiButton, + EuiPopover, + EuiSpacer, + EuiButtonEmpty, + EuiFlexItem, + EuiFlexGroup, +} from '@elastic/eui'; +import { FilterExpanded } from './filter_expanded'; +import { SeriesConfig } from '../../types'; +import { FieldLabels } from '../../configurations/constants/constants'; +import { SelectedFilters } from '../selected_filters'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; + +interface Props { + seriesId: string; + filterFields: SeriesConfig['filterFields']; + baseFilters: SeriesConfig['baseFilters']; + seriesConfig: SeriesConfig; + isNew?: boolean; + labels?: Record; +} + +export interface Field { + label: string; + field: string; + nested?: string; + isNegated?: boolean; +} + +export function SeriesFilter({ + seriesConfig, + isNew, + seriesId, + filterFields = [], + baseFilters, + labels, +}: Props) { + const [isPopoverVisible, setIsPopoverVisible] = useState(false); + + const [selectedField, setSelectedField] = useState(); + + const options: Field[] = filterFields.map((field) => { + if (typeof field === 'string') { + return { label: labels?.[field] ?? FieldLabels[field], field }; + } + + return { + field: field.field, + nested: field.nested, + isNegated: field.isNegated, + label: labels?.[field.field] ?? FieldLabels[field.field], + }; + }); + + const { setSeries, getSeries } = useSeriesStorage(); + const urlSeries = getSeries(seriesId); + + const button = ( + { + setIsPopoverVisible((prevState) => !prevState); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.addFilter', { + defaultMessage: 'Add filter', + })} + + ); + + const mainPanel = ( + <> + + {options.map((opt) => ( + + { + setSelectedField(opt); + }} + > + {opt.label} + + + + ))} + + ); + + const childPanel = selectedField ? ( + { + setSelectedField(undefined); + }} + filters={baseFilters} + /> + ) : null; + + const closePopover = () => { + setIsPopoverVisible(false); + setSelectedField(undefined); + }; + + return ( + + + + + {!selectedField ? mainPanel : childPanel} + + + {(urlSeries.filters ?? []).length > 0 && ( + + { + setSeries(seriesId, { ...urlSeries, filters: undefined }); + }} + size="s" + > + {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { + defaultMessage: 'Clear filters', + })} + + + )} + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx deleted file mode 100644 index 801c885ec9a62..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; - -import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { ReportDefinitionCol } from './columns/report_definition_col'; -import { OperationTypeSelect } from './columns/operation_type_select'; -import { parseCustomFieldName } from '../configurations/lens_attributes'; -import { SeriesFilter } from '../series_viewer/columns/series_filter'; - -function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { - const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); - - return columnType; -} - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function ExpandedSeriesRow({ seriesId, series, seriesConfig }: Props) { - if (!seriesConfig) { - return null; - } - - const { selectedMetricField } = series ?? {}; - - const { hasOperationType, yAxisColumns } = seriesConfig; - - const columnType = getColumnType(seriesConfig, selectedMetricField); - - return ( -
- - - - - - - - - - - - - {(hasOperationType || columnType === 'operation') && ( - - - - - - )} - - -
- ); -} - -const FILTERS_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.selectFilters', { - defaultMessage: 'Filters', -}); - -const OPERATION_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.operation', { - defaultMessage: 'Operation', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx deleted file mode 100644 index 85eb85e0fc30a..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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 { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD } from '../configurations/constants'; - -interface Props { - seriesId: number; - series: SeriesUrl; - defaultValue?: string; - metricOptions: SeriesConfig['metricOptions']; -} - -const SELECT_REPORT_METRIC = 'SELECT_REPORT_METRIC'; - -export function ReportMetricOptions({ seriesId, series, metricOptions }: Props) { - const { setSeries } = useSeriesStorage(); - - const { indexPatterns } = useAppIndexPatternContext(); - - const onChange = (value: string) => { - setSeries(seriesId, { - ...series, - selectedMetricField: value, - }); - }; - - if (!series.dataType) { - return null; - } - - const indexPattern = indexPatterns?.[series.dataType]; - - const options = (metricOptions ?? []).map(({ label, field, id }) => { - let disabled = false; - - if (field !== RECORDS_FIELD && field !== RECORDS_PERCENTAGE_FIELD && field) { - disabled = !Boolean(indexPattern?.getFieldByName(field)); - } - return { - disabled, - value: field || id, - dropdownDisplay: disabled ? ( - {field}, - }} - /> - } - > - {label} - - ) : ( - label - ), - inputDisplay: label, - }; - }); - - return ( - onChange(value)} - style={{ minWidth: 220 }} - /> - ); -} - -const SELECT_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.selectReportMetric', - { - defaultMessage: 'Select report metric', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx similarity index 71% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx index 8fc5ae95fd41b..eb76772a66c7e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { mockAppIndexPattern, mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { mockAppIndexPattern, mockIndexPattern, render } from '../rtl_helpers'; import { SelectedFilters } from './selected_filters'; import { getDefaultConfigs } from '../configurations/default_configs'; import { USER_AGENT_NAME } from '../configurations/constants/elasticsearch_fieldnames'; @@ -22,19 +22,11 @@ describe('SelectedFilters', function () { }); it('should render properly', async function () { - const filters = [{ field: USER_AGENT_NAME, values: ['Chrome'] }]; - const initSeries = { filters }; + const initSeries = { filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] }; - render( - , - { - initSeries, - } - ); + render(, { + initSeries, + }); await waitFor(() => { screen.getByText('Chrome'); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx new file mode 100644 index 0000000000000..5d2ce6ba84951 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/selected_filters.tsx @@ -0,0 +1,101 @@ +/* + * 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, { Fragment } from 'react'; +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useSeriesStorage } from '../hooks/use_series_storage'; +import { FilterLabel } from '../components/filter_label'; +import { SeriesConfig, UrlFilter } from '../types'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { useSeriesFilters } from '../hooks/use_series_filters'; +import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; + +interface Props { + seriesId: string; + seriesConfig: SeriesConfig; + isNew?: boolean; +} +export function SelectedFilters({ seriesId, isNew, seriesConfig }: Props) { + const { getSeries } = useSeriesStorage(); + + const series = getSeries(seriesId); + + const { reportDefinitions = {} } = series; + + const { labels } = seriesConfig; + + const filters: UrlFilter[] = series.filters ?? []; + + let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); + + // we don't want to display report definition filters in new series view + if (isNew) { + definitionFilters = []; + } + + const { removeFilter } = useSeriesFilters({ seriesId }); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? ( + + + {filters.map(({ field, values, notValues }) => ( + + {(values ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: false })} + negate={false} + indexPattern={indexPattern} + /> + + ))} + {(notValues ?? []).map((val) => ( + + removeFilter({ field, value: val, negate: true })} + indexPattern={indexPattern} + /> + + ))} + + ))} + + {definitionFilters.map(({ field, values }) => ( + + {(values ?? []).map((val) => ( + + { + // FIXME handle this use case + }} + negate={false} + definitionFilter={true} + indexPattern={indexPattern} + /> + + ))} + + ))} + + + ) : null; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx index 80fe400830832..c3cc8484d1751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series_editor.tsx @@ -5,399 +5,134 @@ * 2.0. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; -import { - EuiBasicTable, - EuiButtonIcon, - EuiSpacer, - EuiFormRow, - EuiFlexItem, - EuiFlexGroup, - EuiButtonEmpty, -} from '@elastic/eui'; -import { rgba } from 'polished'; -import classNames from 'classnames'; -import { isEmpty } from 'lodash'; -import { euiStyled } from './../../../../../../../../src/plugins/kibana_react/common'; -import { AppDataType, SeriesConfig, ReportViewType, SeriesUrl } from '../types'; -import { SeriesContextValue, useSeriesStorage } from '../hooks/use_series_storage'; -import { IndexPatternState, useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { SeriesFilter } from './columns/series_filter'; +import { SeriesConfig } from '../types'; +import { NEW_SERIES_KEY, useSeriesStorage } from '../hooks/use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; -import { SeriesActions } from '../series_viewer/columns/series_actions'; -import { SeriesInfo } from '../series_viewer/columns/series_info'; -import { DataTypesSelect } from './columns/data_type_select'; import { DatePickerCol } from './columns/date_picker_col'; -import { ExpandedSeriesRow } from './expanded_series_row'; -import { SeriesName } from '../series_viewer/columns/series_name'; -import { ReportTypesSelect } from './columns/report_type_select'; -import { ViewActions } from '../views/view_actions'; -import { ReportMetricOptions } from './report_metric_options'; -import { Breakdowns } from '../series_viewer/columns/breakdowns'; +import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { SeriesActions } from './columns/series_actions'; +import { ChartEditOptions } from './chart_edit_options'; -export interface ReportTypeItem { - id: string; - reportType: ReportViewType; - label: string; -} - -export interface BuilderItem { - id: number; - series: SeriesUrl; +interface EditItem { seriesConfig: SeriesConfig; + id: string; } -type ExpandedRowMap = Record; - -export const getSeriesToEdit = ({ - indexPatterns, - allSeries, - reportType, -}: { - allSeries: SeriesContextValue['allSeries']; - indexPatterns: IndexPatternState; - reportType: ReportViewType; -}): BuilderItem[] => { - const getDataViewSeries = (dataType: AppDataType) => { - if (indexPatterns?.[dataType]) { - return getDefaultConfigs({ - dataType, - reportType, - indexPattern: indexPatterns[dataType], - }); - } - }; - - return allSeries.map((series, seriesIndex) => { - const seriesConfig = getDataViewSeries(series.dataType)!; - - return { id: seriesIndex, series, seriesConfig }; - }); -}; - -export const SeriesEditor = React.memo(function () { - const [editorItems, setEditorItems] = useState([]); - - const { getSeries, allSeries, reportType, removeSeries } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( - {} - ); - - useEffect(() => { - const newExpandRows: ExpandedRowMap = {}; - - setEditorItems((prevState) => { - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach(({ series, id, seriesConfig }) => { - const prevSeriesItem = prevState.find(({ id: prevId }) => prevId === id); - if ( - prevSeriesItem && - series.selectedMetricField && - prevSeriesItem.series.selectedMetricField !== series.selectedMetricField - ) { - newExpandRows[id] = ( - - ); - } - }); - return [...newEditorItems]; - }); - - setItemIdToExpandedRowMap((prevState) => { - return { ...prevState, ...newExpandRows }; - }); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - useEffect(() => { - setItemIdToExpandedRowMap((prevState) => { - const itemIdToExpandedRowMapValues = { ...prevState }; - - const newEditorItems = getSeriesToEdit({ - reportType, - allSeries, - indexPatterns, - }); - - newEditorItems.forEach((item) => { - if (itemIdToExpandedRowMapValues[item.id]) { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - }); - return itemIdToExpandedRowMapValues; - }); - }, [allSeries, editorItems, indexPatterns, reportType]); - - const toggleDetails = (item: BuilderItem) => { - const itemIdToExpandedRowMapValues = { ...itemIdToExpandedRowMap }; - if (itemIdToExpandedRowMapValues[item.id]) { - delete itemIdToExpandedRowMapValues[item.id]; - } else { - itemIdToExpandedRowMapValues[item.id] = ( - - ); - } - setItemIdToExpandedRowMap(itemIdToExpandedRowMapValues); - }; +export function SeriesEditor() { + const { allSeries, allSeriesIds } = useSeriesStorage(); const columns = [ - { - align: 'left' as const, - width: '40px', - isExpander: true, - field: 'id', - name: '', - render: (id: number, item: BuilderItem) => - item.series.dataType && item.series.selectedMetricField ? ( - toggleDetails(item)} - isDisabled={!item.series.dataType || !item.series.selectedMetricField} - aria-label={itemIdToExpandedRowMap[item.id] ? COLLAPSE_LABEL : EXPAND_LABEL} - iconType={itemIdToExpandedRowMap[item.id] ? 'arrowUp' : 'arrowDown'} - /> - ) : null, - }, - { - name: '', - field: 'id', - width: '40px', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - - ), - }, { name: i18n.translate('xpack.observability.expView.seriesEditor.name', { defaultMessage: 'Name', }), field: 'id', - width: '20%', - render: (seriesId: number, { series }: BuilderItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.dataType', { - defaultMessage: 'Data type', - }), - field: 'id', width: '15%', - render: (seriesId: number, { series }: BuilderItem) => ( - + render: (seriesId: string) => ( + + {' '} + {seriesId === NEW_SERIES_KEY ? 'series-preview' : seriesId} + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.reportMetric', { - defaultMessage: 'Report metric', + name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { + defaultMessage: 'Filters', }), - field: 'id', + field: 'defaultFilters', width: '15%', - render: (seriesId: number, { seriesConfig, series }: BuilderItem) => ( - ( + ), }, { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', + name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', { + defaultMessage: 'Breakdowns', }), field: 'id', - width: '27%', - render: (seriesId: number, { series }: BuilderItem) => ( - + width: '25%', + render: (seriesId: string, { seriesConfig, id }: EditItem) => ( + ), }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - width: '10%', - field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - + name: ( +
+ +
), + width: '20%', + field: 'id', + align: 'right' as const, + render: (seriesId: string, item: EditItem) => , }, - { - name: i18n.translate('xpack.observability.expView.seriesBuilder.actions', { + name: i18n.translate('xpack.observability.expView.seriesEditor.actions', { defaultMessage: 'Actions', }), align: 'center' as const, - width: '8%', + width: '10%', field: 'id', - render: (seriesId: number, { series, seriesConfig }: BuilderItem) => ( - - ), + render: (seriesId: string, item: EditItem) => , }, ]; - const getRowProps = (item: BuilderItem) => { - const { dataType, reportDefinitions, selectedMetricField } = item.series; - - return { - className: classNames({ - isExpanded: itemIdToExpandedRowMap[item.id], - isIncomplete: !dataType || isEmpty(reportDefinitions) || !selectedMetricField, - }), - // commenting this for now, since adding on click on row, blocks adding space - // into text field for name column - // ...(dataType && selectedMetricField - // ? { - // onClick: (evt: MouseEvent) => { - // const targetElem = evt.target as HTMLElement; - // - // if ( - // targetElem.classList.contains('euiTableCellContent') && - // targetElem.tagName !== 'BUTTON' - // ) { - // toggleDetails(item); - // } - // evt.stopPropagation(); - // evt.preventDefault(); - // }, - // } - // : {}), - }; - }; - - const resetView = () => { - const totalSeries = allSeries.length; - for (let i = totalSeries; i >= 0; i--) { - removeSeries(i); - } - setEditorItems([]); - setItemIdToExpandedRowMap({}); - }; - - return ( - -
- - - - - - - - {reportType && ( - - resetView()} color="text"> - {RESET_LABEL} - - - )} - - - - - - - - {editorItems.length > 0 && ( - - )} - -
-
- ); -}); - -const Wrapper = euiStyled.div` - max-height: 50vh; - &::-webkit-scrollbar { - height: ${({ theme }) => theme.eui.euiScrollBar}; - width: ${({ theme }) => theme.eui.euiScrollBar}; - } - &::-webkit-scrollbar-thumb { - background-clip: content-box; - background-color: ${({ theme }) => rgba(theme.eui.euiColorDarkShade, 0.5)}; - border: ${({ theme }) => theme.eui.euiScrollBarCorner} solid transparent; - } - &::-webkit-scrollbar-corner, - &::-webkit-scrollbar-track { - background-color: transparent; - } - - &&& { - .euiTableRow-isExpandedRow .euiTableRowCell { - border-top: none; - background-color: #FFFFFF; - border-bottom: 2px solid #d3dae6; - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - } - - .isExpanded { - border-right: 2px solid rgb(211, 218, 230); - border-left: 2px solid rgb(211, 218, 230); - .euiTableRowCell { - border-bottom: none; - } - } - .isIncomplete .euiTableRowCell { - background-color: rgba(254, 197, 20, 0.1); + const { indexPatterns } = useAppIndexPatternContext(); + const items: EditItem[] = []; + + allSeriesIds.forEach((seriesKey) => { + const series = allSeries[seriesKey]; + if (series?.reportType && indexPatterns[series.dataType] && !series.isNew) { + items.push({ + id: seriesKey, + seriesConfig: getDefaultConfigs({ + indexPattern: indexPatterns[series.dataType], + reportType: series.reportType, + dataType: series.dataType, + }), + }); } - } -`; - -export const LOADING_VIEW = i18n.translate( - 'xpack.observability.expView.seriesBuilder.loadingView', - { - defaultMessage: 'Loading view ...', - } -); - -export const SELECT_REPORT_TYPE = i18n.translate( - 'xpack.observability.expView.seriesBuilder.selectReportType', - { - defaultMessage: 'No report type selected', - } -); - -export const RESET_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.reset', { - defaultMessage: 'Reset', -}); + }); -export const REPORT_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesBuilder.reportType', - { - defaultMessage: 'Report type', + if (items.length === 0 && allSeriesIds.length > 0) { + return null; } -); - -const COLLAPSE_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.collapse', { - defaultMessage: 'Collapse', -}); -const EXPAND_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.expand', { - defaultMessage: 'Exapnd', -}); + return ( + <> + + + + + ); +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx deleted file mode 100644 index e6ba505c82091..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/chart_types.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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, { useState } from 'react'; -import { EuiPopover, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { - useKibana, - ToolbarButton, -} from '../../../../../../../../../src/plugins/kibana_react/public'; -import { ObservabilityPublicPluginsStart } from '../../../../../plugin'; -import { SeriesUrl, useFetcher } from '../../../../..'; -import { SeriesConfig } from '../../types'; -import { SeriesChartTypesSelect } from '../../series_editor/columns/chart_types'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesChartTypes({ seriesId, series, seriesConfig }: Props) { - const seriesType = series?.seriesType ?? seriesConfig.defaultSeriesType; - - const { - services: { lens }, - } = useKibana(); - - const { data = [] } = useFetcher(() => lens.getXyVisTypes(), [lens]); - - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - - return ( - setIsPopoverOpen(false)} - button={ - - id === seriesType)?.icon!} - aria-label={CHART_TYPE_LABEL} - onClick={() => setIsPopoverOpen((prevState) => !prevState)} - /> - - } - > - - - ); -} - -const EDIT_CHART_TYPE_LABEL = i18n.translate( - 'xpack.observability.expView.seriesEditor.editChartSeriesLabel', - { - defaultMessage: 'Edit chart type for series', - } -); - -const CHART_TYPE_LABEL = i18n.translate('xpack.observability.expView.chartTypes.label', { - defaultMessage: 'Chart type', -}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx deleted file mode 100644 index 2d38b81e12c9f..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/remove_series.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import React from 'react'; -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; - -interface Props { - seriesId: number; -} - -export function RemoveSeries({ seriesId }: Props) { - const { removeSeries, allSeries } = useSeriesStorage(); - - const onClick = () => { - removeSeries(seriesId); - }; - - const isDisabled = seriesId === 0 && allSeries.length > 1; - - return ( - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx deleted file mode 100644 index 72ae111f002b1..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_actions.tsx +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { RemoveSeries } from './remove_series'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useDiscoverLink } from '../../hooks/use_discover_link'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SeriesActions({ seriesId, series, seriesConfig }: Props) { - const { setSeries, allSeries } = useSeriesStorage(); - - const { href: discoverHref } = useDiscoverLink({ series, seriesConfig }); - - const copySeries = () => { - let copySeriesId: string = `${series.name}-copy`; - if (allSeries.find(({ name }) => name === copySeriesId)) { - copySeriesId = copySeriesId + allSeries.length; - } - setSeries(allSeries.length, { ...series, name: copySeriesId }); - }; - - const toggleSeries = () => { - if (series.hidden) { - setSeries(seriesId, { ...series, hidden: undefined }); - } else { - setSeries(seriesId, { ...series, hidden: true }); - } - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx deleted file mode 100644 index 87c17d03282c3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_filter.tsx +++ /dev/null @@ -1,69 +0,0 @@ -/* - * 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 { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { FilterExpanded } from './filter_expanded'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; -import { SelectedFilters } from '../selected_filters'; - -interface Props { - seriesId: number; - seriesConfig: SeriesConfig; - series: SeriesUrl; -} - -export interface Field { - label: string; - field: string; - nested?: string; - isNegated?: boolean; -} - -export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const options: Field[] = seriesConfig.filterFields.map((field) => { - if (typeof field === 'string') { - return { label: seriesConfig.labels?.[field] ?? FieldLabels[field], field }; - } - - return { - field: field.field, - nested: field.nested, - isNegated: field.isNegated, - label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], - }; - }); - - return ( - <> - {!isPreview && ( - <> - - {options.map((opt) => ( - - ))} - - - - )} - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx deleted file mode 100644 index 3506acbeb528d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_info.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * 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 { isEmpty } from 'lodash'; -import { EuiBadge, EuiBadgeGroup, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesChartTypes } from './chart_types'; -import { SeriesConfig, SeriesUrl } from '../../types'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesColorPicker } from '../../components/series_color_picker'; -import { dataTypes } from '../../series_editor/columns/data_type_select'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig?: SeriesConfig; -} - -export function SeriesInfo({ seriesId, series, seriesConfig }: Props) { - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - const { dataType, reportDefinitions, selectedMetricField } = series; - - const { loading } = useAppIndexPatternContext(); - - const isIncomplete = - (!dataType || isEmpty(reportDefinitions) || !selectedMetricField) && !loading; - - if (!seriesConfig) { - return null; - } - - const { definitionFields, labels } = seriesConfig; - - const incompleteDefinition = isEmpty(reportDefinitions) - ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { - defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, - }) - : ''; - - let incompleteMessage = !selectedMetricField ? MISSING_REPORT_METRIC_LABEL : incompleteDefinition; - - if (!dataType) { - incompleteMessage = MISSING_DATA_TYPE_LABEL; - } - - if (!isIncomplete && seriesConfig && isConfigure) { - return ( - - - - - - - - - ); - } - - return ( - - - {isIncomplete && {incompleteMessage}} - - {!isConfigure && ( - - - {dataTypes.find(({ id }) => id === dataType)!.label} - - - )} - - ); -} - -const MISSING_REPORT_METRIC_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingReportMetric', - { - defaultMessage: 'Missing report metric', - } -); - -const MISSING_DATA_TYPE_LABEL = i18n.translate( - 'xpack.observability.overview.exploratoryView.missingDataType', - { - defaultMessage: 'Missing data type', - } -); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx deleted file mode 100644 index e35966a9fb0d2..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/series_name.tsx +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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, { useState, ChangeEvent, useEffect } from 'react'; -import { EuiFieldText } from '@elastic/eui'; -import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { SeriesUrl } from '../../types'; - -interface Props { - seriesId: number; - series: SeriesUrl; -} - -export function SeriesName({ series, seriesId }: Props) { - const { setSeries } = useSeriesStorage(); - - const [value, setValue] = useState(series.name); - - const onChange = (e: ChangeEvent) => { - setValue(e.target.value); - }; - - const onSave = () => { - if (value !== series.name) { - setSeries(seriesId, { ...series, name: value }); - } - }; - - useEffect(() => { - setValue(series.name); - }, [series.name]); - - return ; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts deleted file mode 100644 index b9ee53a7e8e2d..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/columns/utils.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * 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 moment from 'moment'; -import dateMath from '@elastic/datemath'; -import _isString from 'lodash/isString'; - -const LAST = 'Last'; -const NEXT = 'Next'; - -const isNow = (value: string) => value === 'now'; - -export const isString = (value: any): value is string => _isString(value); -export interface QuickSelect { - timeTense: string; - timeValue: number; - timeUnits: TimeUnitId; -} -export type TimeUnitFromNowId = 's+' | 'm+' | 'h+' | 'd+' | 'w+' | 'M+' | 'y+'; -export type TimeUnitId = 's' | 'm' | 'h' | 'd' | 'w' | 'M' | 'y'; - -export interface RelativeOption { - text: string; - value: TimeUnitId | TimeUnitFromNowId; -} - -export const relativeOptions: RelativeOption[] = [ - { text: 'Seconds ago', value: 's' }, - { text: 'Minutes ago', value: 'm' }, - { text: 'Hours ago', value: 'h' }, - { text: 'Days ago', value: 'd' }, - { text: 'Weeks ago', value: 'w' }, - { text: 'Months ago', value: 'M' }, - { text: 'Years ago', value: 'y' }, - - { text: 'Seconds from now', value: 's+' }, - { text: 'Minutes from now', value: 'm+' }, - { text: 'Hours from now', value: 'h+' }, - { text: 'Days from now', value: 'd+' }, - { text: 'Weeks from now', value: 'w+' }, - { text: 'Months from now', value: 'M+' }, - { text: 'Years from now', value: 'y+' }, -]; - -const timeUnitIds = relativeOptions - .map(({ value }) => value) - .filter((value) => !value.includes('+')) as TimeUnitId[]; - -export const relativeUnitsFromLargestToSmallest = timeUnitIds.reverse(); - -/** - * This function returns time value, time unit and time tense for a given time string. - * - * For example: for `now-40m` it will parse output as time value to `40` time unit to `m` and time unit to `last`. - * - * If given a datetime string it will return a default value. - * - * If the given string is in the format such as `now/d` it will parse the string to moment object and find the time value, time unit and time tense using moment - * - * This function accepts two strings start and end time. I the start value is now then it uses the end value to parse. - */ -export function parseTimeParts(start: string, end: string): QuickSelect | null { - const value = isNow(start) ? end : start; - - const matches = isString(value) && value.match(/now(([-+])(\d+)([smhdwMy])(\/[smhdwMy])?)?/); - - if (!matches) { - return null; - } - - const operator = matches[2]; - const matchedTimeValue = matches[3]; - const timeUnits = matches[4] as TimeUnitId; - - if (matchedTimeValue && timeUnits && operator) { - return { - timeTense: operator === '+' ? NEXT : LAST, - timeUnits, - timeValue: parseInt(matchedTimeValue, 10), - }; - } - - const duration = moment.duration(moment().diff(dateMath.parse(value))); - let unitOp = ''; - for (let i = 0; i < relativeUnitsFromLargestToSmallest.length; i++) { - const as = duration.as(relativeUnitsFromLargestToSmallest[i]); - if (as < 0) { - unitOp = '+'; - } - if (Math.abs(as) > 1) { - return { - timeValue: Math.round(Math.abs(as)), - timeUnits: relativeUnitsFromLargestToSmallest[i], - timeTense: unitOp === '+' ? NEXT : LAST, - }; - } - } - - return null; -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx deleted file mode 100644 index 46adba1dbde55..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/selected_filters.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * 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, { Fragment } from 'react'; -import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { useRouteMatch } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { FilterLabel } from '../components/filter_label'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { useSeriesFilters } from '../hooks/use_series_filters'; -import { getFiltersFromDefs } from '../hooks/use_lens_attributes'; -import { useSeriesStorage } from '../hooks/use_series_storage'; - -interface Props { - seriesId: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} -export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { - const { setSeries } = useSeriesStorage(); - - const isPreview = !!useRouteMatch('/exploratory-view/preview'); - - const { reportDefinitions = {} } = series; - - const { labels } = seriesConfig; - - const filters: UrlFilter[] = series.filters ?? []; - - let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions); - - const isConfigure = !!useRouteMatch('/exploratory-view/configure'); - - // we don't want to display report definition filters in new series view - if (isConfigure) { - definitionFilters = []; - } - - const { removeFilter } = useSeriesFilters({ seriesId, series }); - - const { indexPattern } = useAppIndexPatternContext(series.dataType); - - if ((filters.length === 0 && definitionFilters.length === 0) || !indexPattern) { - return null; - } - - return ( - - {filters.map(({ field, values, notValues }) => ( - - {(values ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - negate={false} - indexPattern={indexPattern} - /> - - )} - {(notValues ?? []).length > 0 && ( - - { - values?.forEach((val) => { - removeFilter({ field, value: val, negate: false }); - }); - }} - indexPattern={indexPattern} - /> - - )} - - ))} - - {definitionFilters.map(({ field, values }) => - values ? ( - - {}} - negate={false} - definitionFilter={true} - indexPattern={indexPattern} - /> - - ) : null - )} - - {(series.filters ?? []).length > 0 && !isPreview && ( - - { - setSeries(seriesId, { ...series, filters: undefined }); - }} - size="s" - > - {i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', { - defaultMessage: 'Clear filters', - })} - - - )} - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx deleted file mode 100644 index 85d65dcac6ac3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_viewer/series_viewer.tsx +++ /dev/null @@ -1,120 +0,0 @@ -/* - * 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 { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { EuiBasicTable, EuiSpacer, EuiText } from '@elastic/eui'; -import { SeriesFilter } from './columns/series_filter'; -import { SeriesConfig, SeriesUrl } from '../types'; -import { useSeriesStorage } from '../hooks/use_series_storage'; -import { getDefaultConfigs } from '../configurations/default_configs'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { SeriesInfo } from './columns/series_info'; -import { SeriesDatePicker } from '../components/series_date_picker'; -import { NO_BREAK_DOWN_LABEL } from './columns/breakdowns'; - -interface EditItem { - id: number; - series: SeriesUrl; - seriesConfig: SeriesConfig; -} - -export function SeriesViewer() { - const { allSeries, reportType } = useSeriesStorage(); - - const columns = [ - { - name: '', - field: 'id', - width: '10%', - render: (seriesId: number, { seriesConfig, series }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.name', { - defaultMessage: 'Name', - }), - field: 'id', - width: '15%', - render: (seriesId: number, { series }: EditItem) => {series.name}, - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.filters', { - defaultMessage: 'Filters', - }), - field: 'id', - width: '35%', - render: (seriesId: number, { series, seriesConfig }: EditItem) => ( - - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.breakdownBy', { - defaultMessage: 'Breakdown by', - }), - field: 'seriesId', - width: '10%', - render: (seriesId: number, { seriesConfig: { labels }, series }: EditItem) => ( - {series.breakdown ? labels[series.breakdown] : NO_BREAK_DOWN_LABEL} - ), - }, - { - name: i18n.translate('xpack.observability.expView.seriesEditor.time', { - defaultMessage: 'Time', - }), - width: '30%', - field: 'id', - render: (seriesId: number, { series }: EditItem) => ( - - ), - }, - ]; - - const { indexPatterns } = useAppIndexPatternContext(); - const items: EditItem[] = []; - - allSeries.forEach((series, seriesIndex) => { - if (indexPatterns[series.dataType] && !isEmpty(series.reportDefinitions)) { - items.push({ - series, - id: seriesIndex, - seriesConfig: getDefaultConfigs({ - reportType, - dataType: series.dataType, - indexPattern: indexPatterns[series.dataType], - }), - }); - } - }); - - if (items.length === 0 && allSeries.length > 0) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 4bba0c221f3c5..fbda2f4ff62e2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -6,7 +6,7 @@ */ import { PaletteOutput } from 'src/plugins/charts/public'; -import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; +import { ExistsFilter } from '@kbn/es-query'; import { LastValueIndexPatternColumn, DateHistogramIndexPatternColumn, @@ -42,7 +42,7 @@ export interface MetricOption { field?: string; label: string; description?: string; - columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN' | 'unique_count'; + columnType?: 'range' | 'operation' | 'FILTER_RECORDS' | 'TERMS_COLUMN'; columnFilters?: ColumnFilter[]; timeScale?: string; } @@ -55,7 +55,7 @@ export interface SeriesConfig { defaultSeriesType: SeriesType; filterFields: Array; seriesTypes: SeriesType[]; - baseFilters?: Array; + baseFilters?: PersistableFilter[] | ExistsFilter[]; definitionFields: string[]; metricOptions?: MetricOption[]; labels: Record; @@ -69,7 +69,6 @@ export interface SeriesConfig { export type URLReportDefinition = Record; export interface SeriesUrl { - name: string; time: { to: string; from: string; @@ -77,12 +76,12 @@ export interface SeriesUrl { breakdown?: string; filters?: UrlFilter[]; seriesType?: SeriesType; + reportType: ReportViewType; operationType?: OperationType; dataType: AppDataType; reportDefinitions?: URLReportDefinition; selectedMetricField?: string; - hidden?: boolean; - color?: string; + isNew?: boolean; } export interface UrlFilter { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx deleted file mode 100644 index e0b46102caba0..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/series_views.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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, { RefObject, useEffect, useState } from 'react'; - -import { EuiTabs, EuiTab, EuiButtonIcon } from '@elastic/eui'; -import { useHistory, useParams } from 'react-router-dom'; -import { i18n } from '@kbn/i18n'; -import { SeriesEditor } from '../series_editor/series_editor'; -import { SeriesViewer } from '../series_viewer/series_viewer'; -import { PanelId } from '../exploratory_view'; - -const tabs = [ - { - id: 'preview' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.preview', { - defaultMessage: 'Preview', - }), - }, - { - id: 'configure' as const, - name: i18n.translate('xpack.observability.overview.exploratoryView.configureSeries', { - defaultMessage: 'Configure series', - }), - }, -]; - -type ViewTab = 'preview' | 'configure'; - -export function SeriesViews({ - seriesBuilderRef, - onSeriesPanelCollapse, -}: { - seriesBuilderRef: RefObject; - onSeriesPanelCollapse: (panel: PanelId) => void; -}) { - const params = useParams<{ mode: ViewTab }>(); - - const history = useHistory(); - - const [selectedTabId, setSelectedTabId] = useState('configure'); - - const onSelectedTabChanged = (id: ViewTab) => { - setSelectedTabId(id); - history.push('/exploratory-view/' + id); - }; - - useEffect(() => { - setSelectedTabId(params.mode); - }, [params.mode]); - - const renderTabs = () => { - return tabs.map((tab, index) => ( - onSelectedTabChanged(tab.id)} - isSelected={tab.id === selectedTabId} - key={index} - > - {tab.id === 'preview' && selectedTabId === 'preview' ? ( - - onSeriesPanelCollapse('seriesPanel')} - /> -  {tab.name} - - ) : ( - tab.name - )} - - )); - }; - - return ( -
- {renderTabs()} - {selectedTabId === 'preview' && } - {selectedTabId === 'configure' && } -
- ); -} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx deleted file mode 100644 index db1f23ad9b6e3..0000000000000 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/views/view_actions.tsx +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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, { useEffect, useState } from 'react'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSwitch, EuiToolTip } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEqual } from 'lodash'; -import { - allSeriesKey, - convertAllShortSeries, - NEW_SERIES_KEY, - useSeriesStorage, -} from '../hooks/use_series_storage'; -import { SeriesUrl } from '../types'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; -import { BuilderItem, getSeriesToEdit } from '../series_editor/series_editor'; -import { DEFAULT_TIME, ReportTypes } from '../configurations/constants'; - -export function ViewActions() { - const [editorItems, setEditorItems] = useState([]); - const { - getSeries, - allSeries, - setSeries, - storage, - reportType, - autoApply, - setAutoApply, - applyChanges, - } = useSeriesStorage(); - - const { loading, indexPatterns } = useAppIndexPatternContext(); - - useEffect(() => { - setEditorItems(getSeriesToEdit({ allSeries, indexPatterns, reportType })); - }, [allSeries, getSeries, indexPatterns, loading, reportType]); - - const addSeries = () => { - const prevSeries = allSeries?.[0]; - const name = `${NEW_SERIES_KEY}-${editorItems.length + 1}`; - const nextSeries = { name } as SeriesUrl; - - const nextSeriesId = allSeries.length; - - if (reportType === 'data-distribution') { - setSeries(nextSeriesId, { - ...nextSeries, - time: prevSeries?.time || DEFAULT_TIME, - } as SeriesUrl); - } else { - setSeries( - nextSeriesId, - prevSeries ? nextSeries : ({ ...nextSeries, time: DEFAULT_TIME } as SeriesUrl) - ); - } - }; - - const noChanges = isEqual(allSeries, convertAllShortSeries(storage.get(allSeriesKey) ?? [])); - - const isAddDisabled = - !reportType || - ((reportType === ReportTypes.CORE_WEB_VITAL || - reportType === ReportTypes.DEVICE_DISTRIBUTION) && - allSeries.length > 0); - - return ( - - - setAutoApply(!autoApply)} - compressed - /> - - {!autoApply && ( - - applyChanges()} isDisabled={autoApply || noChanges} fill> - {i18n.translate('xpack.observability.expView.seriesBuilder.apply', { - defaultMessage: 'Apply changes', - })} - - - )} - - - addSeries()} isDisabled={isAddDisabled}> - {i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', { - defaultMessage: 'Add series', - })} - - - - - ); -} - -const AUTO_APPLY_LABEL = i18n.translate('xpack.observability.expView.seriesBuilder.autoApply', { - defaultMessage: 'Auto apply', -}); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888aa..fc562fa80e26d 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -6,24 +6,15 @@ */ import React, { useEffect, useState } from 'react'; -import { union, isEmpty } from 'lodash'; -import { - EuiComboBox, - EuiFormControlLayout, - EuiComboBoxOptionOption, - EuiFormRow, -} from '@elastic/eui'; +import { union } from 'lodash'; +import { EuiComboBox, EuiFormControlLayout, EuiComboBoxOptionOption } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import styled from 'styled-components'; import { FieldValueSelectionProps } from './types'; export const ALL_VALUES_SELECTED = 'ALL_VALUES'; const formatOptions = (values?: string[], allowAllValuesSelection?: boolean) => { const uniqueValues = Array.from( - new Set( - allowAllValuesSelection && (values ?? []).length > 0 - ? ['ALL_VALUES', ...(values ?? [])] - : values - ) + new Set(allowAllValuesSelection ? ['ALL_VALUES', ...(values ?? [])] : values) ); return (uniqueValues ?? []).map((label) => ({ @@ -39,9 +30,7 @@ export function FieldValueCombobox({ loading, values, setQuery, - usePrependLabel = true, compressed = true, - required = true, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -65,35 +54,29 @@ export function FieldValueCombobox({ onSelectionChange(selectedValuesN.map(({ label: lbl }) => lbl)); }; - const comboBox = ( - { - setQuery(searchVal); - }} - options={options} - selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} - onChange={onChange} - isInvalid={required && isEmpty(selectedValue)} - /> - ); - - return usePrependLabel ? ( + return ( - {comboBox} + { + setQuery(searchVal); + }} + options={options} + selectedOptions={options.filter((opt) => selectedValue?.includes(opt.label))} + onChange={onChange} + /> - ) : ( - - {comboBox} - ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx index cee3ab8aea28b..f713af9768229 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_selection.tsx @@ -70,7 +70,6 @@ export function FieldValueSelection({ values = [], selectedValue, excludedValue, - allowExclusions = true, compressed = true, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -174,8 +173,8 @@ export function FieldValueSelection({ }} options={options} onChange={onChange} - allowExclusions={allowExclusions} isLoading={loading && !query && options.length === 0} + allowExclusions={true} > {(list, search) => (
diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx index 6671c43dd8c7b..556a8e7052347 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.test.tsx @@ -95,7 +95,6 @@ describe('FieldValueSuggestions', () => { selectedValue={[]} filters={[]} asCombobox={false} - allowExclusions={true} /> ); @@ -120,7 +119,6 @@ describe('FieldValueSuggestions', () => { excludedValue={['Pak']} filters={[]} asCombobox={false} - allowExclusions={true} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx index 65e1d0932e4ed..54114c7604644 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/index.tsx @@ -28,10 +28,7 @@ export function FieldValueSuggestions({ singleSelection, compressed, asFilterButton, - usePrependLabel, allowAllValuesSelection, - required, - allowExclusions = true, asCombobox = true, onChange: onSelectionChange, }: FieldValueSuggestionsProps) { @@ -67,10 +64,7 @@ export function FieldValueSuggestions({ width={width} compressed={compressed} asFilterButton={asFilterButton} - usePrependLabel={usePrependLabel} - allowExclusions={allowExclusions} allowAllValuesSelection={allowAllValuesSelection} - required={required} /> ); } diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 73b3d78ce8700..d857b39b074ac 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -23,10 +23,7 @@ interface CommonProps { compressed?: boolean; asFilterButton?: boolean; showCount?: boolean; - usePrependLabel?: boolean; - allowExclusions?: boolean; allowAllValuesSelection?: boolean; - required?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx index 9e7b96b02206f..01d727071770d 100644 --- a/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx @@ -18,25 +18,21 @@ export function buildFilterLabel({ negate, }: { label: string; - value: string | string[]; + value: string; negate: boolean; field: string; indexPattern: IndexPattern; }) { const indexField = indexPattern.getFieldByName(field)!; - const filter = - value instanceof Array && value.length > 1 - ? esFilters.buildPhrasesFilter(indexField, value, indexPattern) - : esFilters.buildPhraseFilter(indexField, value as string, indexPattern); + const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern); - filter.meta.type = value instanceof Array && value.length > 1 ? 'phrases' : 'phrase'; - - filter.meta.value = value as string; + filter.meta.value = value; filter.meta.key = label; filter.meta.alias = null; filter.meta.negate = negate; filter.meta.disabled = false; + filter.meta.type = 'phrase'; return filter; } @@ -44,10 +40,10 @@ export function buildFilterLabel({ interface Props { field: string; label: string; - value: string | string[]; + value: string; negate: boolean; - removeFilter: (field: string, value: string | string[], notVal: boolean) => void; - invertFilter: (val: { field: string; value: string | string[]; negate: boolean }) => void; + removeFilter: (field: string, value: string, notVal: boolean) => void; + invertFilter: (val: { field: string; value: string; negate: boolean }) => void; indexPattern: IndexPattern; allowExclusion?: boolean; } diff --git a/x-pack/plugins/observability/public/components/shared/index.tsx b/x-pack/plugins/observability/public/components/shared/index.tsx index afc053604fcdf..9d557a40b7987 100644 --- a/x-pack/plugins/observability/public/components/shared/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/index.tsx @@ -6,7 +6,6 @@ */ import React, { lazy, Suspense } from 'react'; -import { EuiLoadingSpinner } from '@elastic/eui'; import type { CoreVitalProps, HeaderMenuPortalProps } from './types'; import type { FieldValueSuggestionsProps } from './field_value_suggestions/types'; @@ -27,7 +26,7 @@ const HeaderMenuPortalLazy = lazy(() => import('./header_menu_portal')); export function HeaderMenuPortal(props: HeaderMenuPortalProps) { return ( - }> + ); diff --git a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx index 198b4092b0ed6..82a0fc39b8519 100644 --- a/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx +++ b/x-pack/plugins/observability/public/hooks/use_quick_time_ranges.tsx @@ -7,7 +7,7 @@ import { useUiSetting } from '../../../../../src/plugins/kibana_react/public'; import { UI_SETTINGS } from '../../../../../src/plugins/data/common'; -import { TimePickerQuickRange } from '../components/shared/exploratory_view/components/series_date_picker'; +import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker'; export function useQuickTimeRanges() { const timePickerQuickRanges = useUiSetting( diff --git a/x-pack/plugins/observability/public/plugin.ts b/x-pack/plugins/observability/public/plugin.ts index 334733e363495..71b83b9e05324 100644 --- a/x-pack/plugins/observability/public/plugin.ts +++ b/x-pack/plugins/observability/public/plugin.ts @@ -24,7 +24,6 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../src/plugins/data/public'; -import type { DiscoverStart } from '../../../../src/plugins/discover/public'; import type { HomePublicPluginSetup, HomePublicPluginStart, @@ -57,7 +56,6 @@ export interface ObservabilityPublicPluginsStart { triggersActionsUi: TriggersAndActionsUIPublicPluginStart; data: DataPublicPluginStart; lens: LensPublicStart; - discover: DiscoverStart; } export type ObservabilityPublicStart = ReturnType; diff --git a/x-pack/plugins/observability/public/routes/index.tsx b/x-pack/plugins/observability/public/routes/index.tsx index 09d22496c98ff..f97e3fb996441 100644 --- a/x-pack/plugins/observability/public/routes/index.tsx +++ b/x-pack/plugins/observability/public/routes/index.tsx @@ -7,7 +7,6 @@ import * as t from 'io-ts'; import React from 'react'; -import { Redirect } from 'react-router-dom'; import { alertStatusRt } from '../../common/typings'; import { ExploratoryViewPage } from '../components/shared/exploratory_view'; import { AlertsPage } from '../pages/alerts'; @@ -100,20 +99,7 @@ export const routes = { }), }, }, - '/exploratory-view/': { - handler: () => { - return ; - }, - params: { - query: t.partial({ - rangeFrom: t.string, - rangeTo: t.string, - refreshPaused: jsonRt.pipe(t.boolean), - refreshInterval: jsonRt.pipe(t.number), - }), - }, - }, - '/exploratory-view/:mode': { + '/exploratory-view': { handler: () => { return ; }, @@ -126,4 +112,18 @@ export const routes = { }), }, }, + // enable this to test multi series architecture + // '/exploratory-view/multi': { + // handler: () => { + // return ; + // }, + // params: { + // query: t.partial({ + // rangeFrom: t.string, + // rangeTo: t.string, + // refreshPaused: jsonRt.pipe(t.boolean), + // refreshInterval: jsonRt.pipe(t.number), + // }), + // }, + // }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 0d4432d8dac56..c8e3526005963 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -18514,10 +18514,20 @@ "xpack.observability.expView.operationType.99thPercentile": "99パーセンタイル", "xpack.observability.expView.operationType.average": "平均", "xpack.observability.expView.operationType.median": "中央", + "xpack.observability.expView.reportType.noDataType": "データ型を選択すると、系列の構築を開始します。", + "xpack.observability.expView.seriesBuilder.breakdown": "内訳", + "xpack.observability.expView.seriesBuilder.dataType": "データ型", + "xpack.observability.expView.seriesBuilder.definition": "定義", + "xpack.observability.expView.seriesBuilder.filters": "フィルター", + "xpack.observability.expView.seriesBuilder.report": "レポート", "xpack.observability.expView.seriesBuilder.selectReportType": "レポートタイプを選択すると、ビジュアライゼーションを定義します。", + "xpack.observability.expView.seriesEditor.actions": "アクション", + "xpack.observability.expView.seriesEditor.addFilter": "フィルターを追加します", + "xpack.observability.expView.seriesEditor.breakdowns": "内訳", "xpack.observability.expView.seriesEditor.clearFilter": "フィルターを消去", "xpack.observability.expView.seriesEditor.filters": "フィルター", "xpack.observability.expView.seriesEditor.name": "名前", + "xpack.observability.expView.seriesEditor.removeSeries": "クリックすると、系列を削除します", "xpack.observability.expView.seriesEditor.time": "時間", "xpack.observability.featureCatalogueDescription": "専用UIで、ログ、メトリック、アプリケーショントレース、システム可用性を連結します。", "xpack.observability.featureCatalogueDescription1": "インフラストラクチャメトリックを監視します。", @@ -18583,6 +18593,7 @@ "xpack.observability.overview.uptime.up": "アップ", "xpack.observability.overview.ux.appLink": "アプリで表示", "xpack.observability.overview.ux.title": "ユーザーエクスペリエンス", + "xpack.observability.reportTypeCol.nodata": "利用可能なデータがありません", "xpack.observability.resources.documentation": "ドキュメント", "xpack.observability.resources.forum": "ディスカッションフォーラム", "xpack.observability.resources.title": "リソース", @@ -18596,6 +18607,7 @@ "xpack.observability.section.apps.uptime.description": "サイトとサービスの可用性をアクティブに監視するアラートを受信し、問題をより迅速に解決して、ユーザーエクスペリエンスを最適化します。", "xpack.observability.section.apps.uptime.title": "アップタイム", "xpack.observability.section.errorPanel": "データの取得時にエラーが発生しました。再試行してください", + "xpack.observability.seriesEditor.edit": "系列を編集", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均", "xpack.observability.ux.coreVitals.averageMessage": " {bad}未満", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fc8b27aa497cd..cf31ec8d8eceb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -18930,10 +18930,20 @@ "xpack.observability.expView.operationType.99thPercentile": "第 99 个百分位", "xpack.observability.expView.operationType.average": "平均值", "xpack.observability.expView.operationType.median": "中值", + "xpack.observability.expView.reportType.noDataType": "选择数据类型以开始构建序列。", + "xpack.observability.expView.seriesBuilder.breakdown": "分解", + "xpack.observability.expView.seriesBuilder.dataType": "数据类型", + "xpack.observability.expView.seriesBuilder.definition": "定义", + "xpack.observability.expView.seriesBuilder.filters": "筛选", + "xpack.observability.expView.seriesBuilder.report": "报告", "xpack.observability.expView.seriesBuilder.selectReportType": "选择报告类型以定义可视化。", + "xpack.observability.expView.seriesEditor.actions": "操作", + "xpack.observability.expView.seriesEditor.addFilter": "添加筛选", + "xpack.observability.expView.seriesEditor.breakdowns": "分解", "xpack.observability.expView.seriesEditor.clearFilter": "清除筛选", "xpack.observability.expView.seriesEditor.filters": "筛选", "xpack.observability.expView.seriesEditor.name": "名称", + "xpack.observability.expView.seriesEditor.removeSeries": "单击移除序列", "xpack.observability.expView.seriesEditor.time": "时间", "xpack.observability.featureCatalogueDescription": "通过专用 UI 整合您的日志、指标、应用程序跟踪和系统可用性。", "xpack.observability.featureCatalogueDescription1": "监测基础架构指标。", @@ -18999,6 +19009,7 @@ "xpack.observability.overview.uptime.up": "运行", "xpack.observability.overview.ux.appLink": "在应用中查看", "xpack.observability.overview.ux.title": "用户体验", + "xpack.observability.reportTypeCol.nodata": "没有可用数据", "xpack.observability.resources.documentation": "文档", "xpack.observability.resources.forum": "讨论论坛", "xpack.observability.resources.title": "资源", @@ -19012,6 +19023,7 @@ "xpack.observability.section.apps.uptime.description": "主动监测站点和服务的可用性。接收告警并更快地解决问题,从而优化用户体验。", "xpack.observability.section.apps.uptime.title": "运行时间", "xpack.observability.section.errorPanel": "尝试提取数据时发生错误。请重试", + "xpack.observability.seriesEditor.edit": "编辑序列", "xpack.observability.transactionRateLabel": "{value} tpm", "xpack.observability.ux.coreVitals.average": "平均值", "xpack.observability.ux.coreVitals.averageMessage": " 且小于 {bad}", diff --git a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx index aa981071b7ee2..1a53a2c9b64a0 100644 --- a/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx +++ b/x-pack/plugins/uptime/public/components/common/charts/ping_histogram.tsx @@ -22,7 +22,6 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import numeral from '@elastic/numeral'; import moment from 'moment'; -import { useSelector } from 'react-redux'; import { getChartDateLabel } from '../../../lib/helper'; import { ChartWrapper } from './chart_wrapper'; import { UptimeThemeContext } from '../../../contexts'; @@ -33,7 +32,6 @@ import { getDateRangeFromChartElement } from './utils'; import { STATUS_DOWN_LABEL, STATUS_UP_LABEL } from '../translations'; import { createExploratoryViewUrl } from '../../../../../observability/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; -import { monitorStatusSelector } from '../../../state/selectors'; export interface PingHistogramComponentProps { /** @@ -75,8 +73,6 @@ export const PingHistogramComponent: React.FC = ({ const monitorId = useMonitorId(); - const selectedMonitor = useSelector(monitorStatusSelector); - const { basePath } = useUptimeSettingsContext(); const [getUrlParams, updateUrlParams] = useUrlParams(); @@ -193,21 +189,12 @@ export const PingHistogramComponent: React.FC = ({ const pingHistogramExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-pings`, - dataType: 'synthetics', - selectedMetricField: 'summary.up', - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.name': - monitorId && selectedMonitor?.monitor?.name - ? [selectedMonitor.monitor.name] - : ['ALL_VALUES'], - }, - }, - ], + 'pings-over-time': { + dataType: 'synthetics', + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + ...(monitorId ? { filters: [{ field: 'monitor.id', values: [monitorId] }] } : {}), + }, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index c459fe46da975..9f00dd2e8f061 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -10,15 +10,13 @@ import { EuiHeaderLinks, EuiToolTip, EuiHeaderLink } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; -import { createExploratoryViewUrl } from '../../../../../observability/public'; +import { createExploratoryViewUrl, SeriesUrl } from '../../../../../observability/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useUptimeSettingsContext } from '../../../contexts/uptime_settings_context'; import { useGetUrlParams } from '../../../hooks'; import { ToggleAlertFlyoutButton } from '../../overview/alerts/alerts_containers'; import { SETTINGS_ROUTE } from '../../../../common/constants'; import { stringifyUrlParams } from '../../../lib/helper/stringify_url_params'; -import { monitorStatusSelector } from '../../../state/selectors'; const ADD_DATA_LABEL = i18n.translate('xpack.uptime.addDataButtonLabel', { defaultMessage: 'Add data', @@ -40,28 +38,13 @@ export function ActionMenuContent(): React.ReactElement { const { dateRangeStart, dateRangeEnd } = params; const history = useHistory(); - const selectedMonitor = useSelector(monitorStatusSelector); - - const monitorId = selectedMonitor?.monitor?.id; - const syntheticExploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - dataType: 'synthetics', - seriesType: 'area_stacked', - selectedMetricField: 'monitor.duration.us', - time: { from: dateRangeStart, to: dateRangeEnd }, - breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', - reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], - }, - name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', - }, - ], + 'synthetics-series': ({ + dataType: 'synthetics', + isNew: true, + time: { from: dateRangeStart, to: dateRangeEnd }, + } as unknown) as SeriesUrl, }, basePath ); diff --git a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx index 18aba948eaa37..1590e225f9ca8 100644 --- a/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/monitor_duration/monitor_duration_container.tsx @@ -55,19 +55,16 @@ export const MonitorDuration: React.FC = ({ monitorId }) => { const exploratoryViewLink = createExploratoryViewUrl( { - reportType: 'kpi-over-time', - allSeries: [ - { - name: `${monitorId}-response-duration`, - time: { from: dateRangeStart, to: dateRangeEnd }, - reportDefinitions: { - 'monitor.id': [monitorId] as string[], - }, - breakdown: 'observer.geo.name', - operationType: 'average', - dataType: 'synthetics', + [`monitor-duration`]: { + reportType: 'kpi-over-time', + time: { from: dateRangeStart, to: dateRangeEnd }, + reportDefinitions: { + 'monitor.id': [monitorId] as string[], }, - ], + breakdown: 'observer.geo.name', + operationType: 'average', + dataType: 'synthetics', + }, }, basePath ); diff --git a/x-pack/test/functional/apps/observability/exploratory_view.ts b/x-pack/test/functional/apps/observability/exploratory_view.ts deleted file mode 100644 index 8f27f20ce30e6..0000000000000 --- a/x-pack/test/functional/apps/observability/exploratory_view.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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 Path from 'path'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function ({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['observability', 'common', 'header']); - const esArchiver = getService('esArchiver'); - const find = getService('find'); - - const testSubjects = getService('testSubjects'); - - const rangeFrom = '2021-01-17T16%3A46%3A15.338Z'; - const rangeTo = '2021-01-19T17%3A01%3A32.309Z'; - - // Failing: See https://github.com/elastic/kibana/issues/106934 - describe.skip('ExploratoryView', () => { - before(async () => { - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - - await esArchiver.loadIfNeeded( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_test_data') - ); - - await PageObjects.common.navigateToApp('ux', { - search: `?rangeFrom=${rangeFrom}&rangeTo=${rangeTo}`, - }); - await PageObjects.header.waitUntilLoadingHasFinished(); - }); - - after(async () => { - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', '8.0.0') - ); - - await esArchiver.unload( - Path.join('x-pack/test/apm_api_integration/common/fixtures/es_archiver', 'rum_8.0.0') - ); - }); - - it('should able to open exploratory view from ux app', async () => { - await testSubjects.exists('uxAnalyzeBtn'); - await testSubjects.click('uxAnalyzeBtn'); - expect(await find.existsByCssSelector('.euiBasicTable')).to.eql(true); - }); - - it('renders lens visualization', async () => { - expect(await testSubjects.exists('lnsVisualizationContainer')).to.eql(true); - - expect( - await find.existsByCssSelector('div[data-title="Prefilled from exploratory view app"]') - ).to.eql(true); - - expect((await find.byCssSelector('dd')).getVisibleText()).to.eql(true); - }); - - it('can do a breakdown per series', async () => { - await testSubjects.click('seriesBreakdown'); - - expect(await find.existsByCssSelector('[id="user_agent.name"]')).to.eql(true); - - await find.clickByCssSelector('[id="user_agent.name"]'); - - await PageObjects.header.waitUntilLoadingHasFinished(); - - expect(await find.existsByCssSelector('[title="Chrome Mobile iOS"]')).to.eql(true); - expect(await find.existsByCssSelector('[title="Mobile Safari"]')).to.eql(true); - }); - }); -} diff --git a/x-pack/test/functional/apps/observability/index.ts b/x-pack/test/functional/apps/observability/index.ts index cce07b9ff7d86..b7f03b5f27bae 100644 --- a/x-pack/test/functional/apps/observability/index.ts +++ b/x-pack/test/functional/apps/observability/index.ts @@ -8,9 +8,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { - describe('ObservabilityApp', function () { + describe('Observability specs', function () { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./exploratory_view')); }); }