diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/chart_templates/chart_templates.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/chart_templates/chart_templates.tsx index 20c41b20c6b97..2bad254c33d7b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/chart_templates/chart_templates.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/chart_templates/chart_templates.tsx @@ -26,6 +26,8 @@ export function ChartTemplates() { let flyout; + const style = { minWidth: 200 }; + if (isFlyoutVisible) { flyout = ( setIsFlyoutVisible(false)} aria-labelledby="flyoutTitle"> @@ -37,8 +39,8 @@ export function ChartTemplates() { - - + + } title={`Page load distribution`} @@ -46,7 +48,7 @@ export function ChartTemplates() { href="/app/observability/exploratory-view/page-load-dist" /> - + } title={`Page views`} @@ -54,7 +56,7 @@ export function ChartTemplates() { href="/app/observability/exploratory-view/page-views" /> - + } title={`Monitor duration`} @@ -62,6 +64,14 @@ export function ChartTemplates() { href="/app/observability/exploratory-view/uptime-duration" /> + + } + title={`APM Service latency`} + description="Uptime monitor duration, slice and dice by location etc" + href="/app/observability/exploratory-view/service-latency" + /> + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts index f1dce5421345b..85dd3cc6019ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants.ts @@ -13,6 +13,7 @@ export const FieldLabels: Record = { 'client.geo.country_name': 'Location', 'user_agent.device.name': 'Device', 'observer.geo.name': 'Observer location', + 'service.name': 'Service', }; export const DataViewLabels: Record = { @@ -20,4 +21,5 @@ export const DataViewLabels: Record = { 'page-views': 'Page views', 'uptime-duration': 'Uptime monitor duration', 'uptime-pings': 'Uptime pings', + 'service-latency': 'APM Service latency', }; 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 bbc4ae9739a93..b1b3cce83593c 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 @@ -9,15 +9,18 @@ import { DataViewType } from '../types'; import { getPageLoadDistLensConfig } from './page_load_dist_config'; import { getPageViewLensConfig } from './page_view_config'; import { getMonitorDurationConfig } from './monitor_duration_config'; +import { getServiceLatencyLensConfig } from './service_latency_config'; export const getDefaultConfigs = ({ dataViewType }: { dataViewType: DataViewType }) => { switch (dataViewType) { case 'page-load-dist': - return getPageLoadDistLensConfig(); + return getPageLoadDistLensConfig({}); case 'page-views': return getPageViewLensConfig(); case 'uptime-duration': return getMonitorDurationConfig(); + case 'service-latency': + return getServiceLatencyLensConfig(); default: return getPageViewLensConfig(); } 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 78cc6f903a590..3e649fcbcb76f 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 @@ -171,8 +171,8 @@ export class LensAttributes { } parseFilters() { - const defaultFilters = this.dataViewConfig.filters ?? {}; - const parsedFilters = this.dataViewConfig.filters ? [defaultFilters] : []; + const defaultFilters = this.dataViewConfig.filters ?? []; + const parsedFilters = this.dataViewConfig.filters ? [...defaultFilters] : []; this.filters.forEach(({ field, values = [], notValues = [] }) => { values?.forEach((value) => { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/page_load_dist_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/page_load_dist_config.ts index 4c97bb0471ccf..2a7dcbc83bd1e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/page_load_dist_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/page_load_dist_config.ts @@ -7,10 +7,14 @@ import { DataSeries } from '../types'; -export function getPageLoadDistLensConfig(): DataSeries { +interface Props { + seriesId: string; + serviceName?: string; +} + +export function getPageLoadDistLensConfig({ seriesId, serviceName }: Props): DataSeries { return { - name: 'elastic.co', - id: 'elastic-co', + id: seriesId ?? 'unique-key', dataViewType: 'page-load-dist', defaultSeriesType: 'line', indexPattern: 'apm_static_index_pattern_id', @@ -33,8 +37,9 @@ export function getPageLoadDistLensConfig(): DataSeries { 'client.geo.country_name', 'user_agent.device.name', ], - filters: { - query: { match_phrase: { 'transaction.type': 'page-load' } }, - }, + filters: [ + { query: { match_phrase: { 'transaction.type': 'page-load' } } }, + ...(serviceName ? [{ query: { match_phrase: { 'service.type': serviceName } } }] : []), + ], }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts new file mode 100644 index 0000000000000..9c15dda1c41f0 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/service_latency_config.ts @@ -0,0 +1,42 @@ +/* + * 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 { DataSeries } from '../types'; + +export function getServiceLatencyLensConfig(): DataSeries { + return { + name: 'elastic.co', + id: 'elastic-co', + dataViewType: 'service-latency', + defaultSeriesType: 'line', + indexPattern: 'apm_static_index_pattern_id', + seriesTypes: ['line', 'bar'], + xAxisColumn: { + sourceField: '@timestamp', + }, + yAxisColumn: { + operationType: 'avg', + sourceField: 'transaction.duration.us', + label: 'Latency', + }, + defaultFilters: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + breakdowns: [ + 'user_agent.name', + 'user_agent.os.name', + 'client.geo.country_name', + 'user_agent.device.name', + ], + filters: { + query: { match_phrase: { 'transaction.type': 'request' } }, + }, + }; +} 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 f7ca5877c9ca5..e70a9838b72fd 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 @@ -22,7 +22,6 @@ import { IndexPattern } from '../../../../../../../src/plugins/data/common'; import { ExploratoryViewHeader } from './header'; import { SeriesEditor } from './series_editor/series_editor'; import { useUrlStorage } from './hooks/use_url_strorage'; -import { SeriesUrl } from './types'; import { useLensAttributes } from './hooks/use_lens_attributes'; import styled from 'styled-components'; @@ -31,31 +30,24 @@ export interface Props { defaultIndexPattern?: IndexPattern | null; } -export const ExploratoryView = ({ seriesId, defaultIndexPattern }: Props) => { +export const ExploratoryView = ({ defaultIndexPattern }: Props) => { const { services: { lens }, } = useKibana(); const LensComponent = lens.EmbeddableComponent; - const storage = useUrlStorage(); + const { firstSeries: seriesId } = useUrlStorage(); - const series = storage.get('elastic-co'); + const { series } = useUrlStorage(seriesId); const lensAttributes = useLensAttributes({ - seriesId: 'elastic-co', + seriesId, }); return ( - - - -

Exploratory view

-
-
-
{defaultIndexPattern ? ( @@ -70,7 +62,7 @@ export const ExploratoryView = ({ seriesId, defaultIndexPattern }: Props) => { to: 'now', } } - attributes={lensAttributes} + attributes={seriesId ? lensAttributes : undefined} /> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header.tsx index 834bda562e023..eef54c9654cfe 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header.tsx @@ -29,12 +29,12 @@ export function ExploratoryViewHeader({ lensAttributes }: Props) { return ( - +

{DataViewLabels[dataViewType]}

- + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_types.tsx index 580869d4989d0..9656dba2c67d4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/chart_types.tsx @@ -13,16 +13,15 @@ import { i18n } from '@kbn/i18n'; import { LensIconChartBar } from '../assets/chart_bar'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { LensIconChartBarStacked } from '../assets/chart_bar_stacked'; -import { LensIconChartBarPercentage } from '../assets/chart_bar_percentage'; +// import { LensIconChartBarPercentage } from '../assets/chart_bar_percentage'; import { LensIconChartBarHorizontalStacked } from '../assets/chart_bar_horizontal_stacked'; -import { LensIconChartBarHorizontalPercentage } from '../assets/chart_bar_horizontal_percentage'; +// import { LensIconChartBarHorizontalPercentage } from '../assets/chart_bar_horizontal_percentage'; import { LensIconChartArea } from '../assets/chart_area'; -import { LensIconChartAreaPercentage } from '../assets/chart_area_percentage'; +// import { LensIconChartAreaPercentage } from '../assets/chart_area_percentage'; import { LensIconChartLine } from '../assets/chart_line'; import { LensIconChartAreaStacked } from '../assets/chart_area_stacked'; import styled from 'styled-components'; import { useUrlStorage } from '../hooks/use_url_strorage'; -import { SeriesUrl } from '../types'; const ButtonGroup = styled(EuiButtonGroup)` &&& { @@ -33,9 +32,7 @@ const ButtonGroup = styled(EuiButtonGroup)` `; export const ChartTypes = () => { - const storage = useUrlStorage(); - - const series = storage.get('elastic-co'); + const { series, setSeries, allSeries } = useUrlStorage(); return ( { }))} idSelected={series?.seriesType ?? 'line'} onChange={(seriesType: string) => { - storage.set('elastic-co', { ...series, seriesType }); + Object.keys(allSeries).forEach((seriesKey) => { + const series = allSeries[seriesKey]; + + setSeries(seriesKey, { ...series, seriesType }); + }); }} /> ); @@ -96,14 +97,14 @@ export const visualizationTypes: VisualizationType[] = [ }), groupLabel: groupLabelForBar, }, - { - id: 'bar_percentage_stacked', - icon: LensIconChartBarPercentage, - label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarLabel', { - defaultMessage: 'Bar vertical percentage', - }), - groupLabel: groupLabelForBar, - }, + // { + // id: 'bar_percentage_stacked', + // icon: LensIconChartBarPercentage, + // label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarLabel', { + // defaultMessage: 'Bar vertical percentage', + // }), + // groupLabel: groupLabelForBar, + // }, { id: 'bar_horizontal_stacked', icon: LensIconChartBarHorizontalStacked, @@ -115,20 +116,20 @@ export const visualizationTypes: VisualizationType[] = [ }), groupLabel: groupLabelForBar, }, - { - id: 'bar_horizontal_percentage_stacked', - icon: LensIconChartBarHorizontalPercentage, - label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel', { - defaultMessage: 'H. Percentage bar', - }), - fullLabel: i18n.translate( - 'xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel', - { - defaultMessage: 'Bar horizontal percentage', - } - ), - groupLabel: groupLabelForBar, - }, + // { + // id: 'bar_horizontal_percentage_stacked', + // icon: LensIconChartBarHorizontalPercentage, + // label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageBarHorizontalLabel', { + // defaultMessage: 'H. Percentage bar', + // }), + // fullLabel: i18n.translate( + // 'xpack.lens.xyVisualization.stackedPercentageBarHorizontalFullLabel', + // { + // defaultMessage: 'Bar horizontal percentage', + // } + // ), + // groupLabel: groupLabelForBar, + // }, { id: 'area', icon: LensIconChartArea, @@ -145,14 +146,14 @@ export const visualizationTypes: VisualizationType[] = [ }), groupLabel: groupLabelForLineAndArea, }, - { - id: 'area_percentage_stacked', - icon: LensIconChartAreaPercentage, - label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageAreaLabel', { - defaultMessage: 'Area percentage', - }), - groupLabel: groupLabelForLineAndArea, - }, + // { + // id: 'area_percentage_stacked', + // icon: LensIconChartAreaPercentage, + // label: i18n.translate('xpack.lens.xyVisualization.stackedPercentageAreaLabel', { + // defaultMessage: 'Area percentage', + // }), + // groupLabel: groupLabelForLineAndArea, + // }, { id: 'line', icon: LensIconChartLine, 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 be0568f2cdc22..e781cc2d03cdf 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 @@ -27,9 +27,7 @@ export const useLensAttributes = ({ const { indexPattern: defaultIndexPattern } = useIndexPatternContext(dataViewConfig.indexPattern); - const storage = useUrlStorage(); - - const series = storage.get(seriesId); + const { series } = useUrlStorage(seriesId); const { filters = [] } = series ?? {}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx index 177d90d906ccf..9b4ad7c4e1a70 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_url_strorage.tsx @@ -7,6 +7,7 @@ import React, { createContext, useContext, Context } from 'react'; import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { SeriesUrl } from '../types'; export const UrlStorageContext = createContext(null); @@ -18,5 +19,31 @@ export const UrlStorageContextProvider: React.FC = ({ children, s return {children}; }; -export const useUrlStorage = () => - useContext((UrlStorageContext as unknown) as Context); +type AllSeries = Record; + +export const useUrlStorage = (seriesId?: string) => { + const allSeriesKey = 'sr'; + const storage = useContext((UrlStorageContext as unknown) as Context); + let series: SeriesUrl = {} as SeriesUrl; + const allSeries = storage.get(allSeriesKey) ?? {}; + + if (seriesId) { + series = allSeries?.[seriesId] ?? ({} as SeriesUrl); + } + + const setSeries = (seriesId: string, newValue: SeriesUrl) => { + allSeries[seriesId] = newValue; + storage.set(allSeriesKey, allSeries); + }; + + const removeSeries = (seriesId: string) => { + delete allSeries[seriesId]; + storage.set(allSeriesKey, allSeries); + }; + + const allSeriesIds = Object.keys(allSeries); + + const firstSeries = allSeriesIds?.[0]; + + return { storage, setSeries, removeSeries, series, firstSeries, allSeries, allSeriesIds }; +}; 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..e112905a7f4f9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Dispatch, SetStateAction } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { AppDataType } from '../../types'; + +interface Props { + selectedDataType: AppDataType | null; + onChange: Dispatch>; +} + +const dataTypes: { id: AppDataType; label: string }[] = [ + { id: 'synthetics', label: 'Synthetic Monitoring' }, + { id: 'rum', label: 'User Experience(RUM)' }, + { id: 'logs', label: 'Logs' }, + { id: 'metrics', label: 'Logs' }, + { id: 'apm', label: 'APM' }, +]; +export const DataTypesCol = ({ selectedDataType, onChange }: Props) => { + return ( + + {dataTypes.map(({ id: dt, label }) => ( + + onChange(dt === selectedDataType ? null : dt)} + > + {label} + + + ))} + + ); +}; 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..eced44f7475a9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.tsx @@ -0,0 +1,14 @@ +/* + * 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'; + +interface Props {} + +export const ReportBreakdowns = (props: Props) => { + return
; +}; 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..eb265df8b1f0c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -0,0 +1,42 @@ +/* + * 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, { Dispatch, SetStateAction } from 'react'; +import { FieldValueSelection } from '../../../field_value_selection'; +import { useIndexPatternContext } from '../../../../../hooks/use_default_index_pattern'; +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; + +interface Props { + selectedServiceName: string | null; + reportType: string; + onChange: Dispatch>; +} + +export const ReportDefinitionCol = ({ selectedServiceName, onChange }: Props) => { + const { indexPattern } = useIndexPatternContext(); + + return ( +
+ {selectedServiceName && ( + + + + Web App: {selectedServiceName} + + + + )} + + onChange(val)} + /> +
+ ); +}; 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..35e6fa68a0e03 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.tsx @@ -0,0 +1,13 @@ +/* + * 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'; + +interface Props {} +export const ReportFilters = (props: Props) => { + return
; +}; 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..05441cc93927e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -0,0 +1,38 @@ +/* + * 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, { Dispatch, SetStateAction } from 'react'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; + +interface Props { + selectedReportType: string | null; + reportTypes: string[]; + onChange: Dispatch>; +} + +export const ReportTypesCol = ({ selectedReportType, reportTypes, onChange }: Props) => { + return reportTypes?.length > 0 ? ( + + {reportTypes.map((reportType) => ( + + onChange(reportType === selectedReportType ? null : reportType)} + > + {reportType} + + + ))} + + ) : ( + Select a data type to start building a series. + ); +}; 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..57a9ce714a2fb --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/series_builder.tsx @@ -0,0 +1,144 @@ +/* + * 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, EuiBasicTable, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import styled from 'styled-components'; +import { AppDataType, 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 { useUrlStorage } from '../hooks/use_url_strorage'; + +export const ReportTypes = { + synthetics: ['Monitor duration', 'Pings histogram'], + rum: ['Performance distribution', 'Page views', 'KPI over time'], + apm: ['Latency', 'Throughput'], + logs: [], + metrics: [], +}; + +export const SeriesBuilder = () => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + + const [dataType, setDataType] = useState(null); + const [reportType, setReportType] = useState(null); + const [serviceName, setServiceName] = useState(null); + + const columns = [ + { + name: 'DataType', + field: 'dataType', + width: '20%', + render: (val: string) => , + }, + { + name: 'Report', + field: 'defaultFilters', + width: '30%', + render: (val: string) => ( + + ), + }, + { + name: 'Definition', + field: 'defaultFilters', + width: '30%', + render: (val: string) => + reportType ? ( + + ) : null, + }, + { + name: 'Filters', + field: 'filters', + width: '25%', + render: (val: string[]) => (reportType ? : null), + }, + { + name: 'Breakdowns', + width: '25%', + field: 'id', + render: (val: string[]) => (reportType ? : null), + }, + ]; + + const { setSeries, allSeriesIds } = useUrlStorage(); + + const addSeries = () => { + const newSeriesId = `${serviceName!}-pd`; + + const newSeries = { reportType: 'pd', serviceName } as SeriesUrl; + setSeries(newSeriesId, newSeries); + + // reset state + setDataType(null); + setReportType(null); + setServiceName(null); + setIsFlyoutVisible(false); + }; + + const items = [{ dataTypes: ['APM'] }]; + + let flyout; + + if (isFlyoutVisible) { + flyout = ( + + + + + + + Add + + + + + Cancel + + + + + ); + } + + return ( +
+ setIsFlyoutVisible((prevState) => !prevState)} + disabled={allSeriesIds.length > 0} + > + {'Add series'} + + + {flyout} +
+ ); +}; + +const BottomFlyout = styled.div` + height: 300px; +`; 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 index dc6dc803d22ec..d81823f51205a 100644 --- 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 @@ -10,7 +10,6 @@ import React, { useEffect } from 'react'; import { useHasData } from '../../../../hooks/use_has_data'; import { UI_SETTINGS, useKibanaUISettings } from '../../../../hooks/use_kibana_ui_settings'; import { useUrlStorage } from '../hooks/use_url_strorage'; -import { SeriesUrl } from '../types'; export interface TimePickerTime { from: string; @@ -38,18 +37,16 @@ export function SeriesDatePicker({ seriesId }: Props) { label: display, })); - const storage = useUrlStorage(); - - const series = storage.get(seriesId); + const { series, setSeries } = useUrlStorage(seriesId); function onTimeChange({ start, end }: { start: string; end: string }) { onRefreshTimeRange(); - storage.set(seriesId, { ...series, time: { from: start, to: end } }); + setSeries(seriesId, { ...series, time: { from: start, to: end } }); } useEffect(() => { if (!series || !series.time) { - storage.set(seriesId, { ...series, time: { from: 'now-15m', to: 'now' } }); + setSeries(seriesId, { ...series, time: { from: 'now-15m', to: 'now' } }); } }, []); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx index bb9febf47f6f7..0b8f6f2fef128 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx @@ -25,11 +25,10 @@ export const Breakdowns = ({ seriesId, breakdowns = [] }: Props) => { setSelectedBreakdown((prevState) => (prevState === optionId ? undefined : optionId)); }; - const storage = useUrlStorage(); - const series = storage.get(seriesId) ?? {}; + const { setSeries, series } = useUrlStorage(seriesId); useEffect(() => { - storage.set(seriesId, { ...series, breakdown: selectedBreakdown }); + setSeries(seriesId, { ...series, breakdown: selectedBreakdown }); }, [selectedBreakdown]); return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx index e1315dc785be9..336e771f09fe2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.tsx @@ -19,15 +19,16 @@ import { useFetcher } from '../../../../..'; import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; import { useIndexPatternContext } from '../../../../../hooks/use_default_index_pattern'; import { useUrlStorage } from '../../hooks/use_url_strorage'; -import { SeriesUrl, UrlFilter } from '../../types'; +import { UrlFilter } from '../../types'; interface Props { + seriesId: string; label: string; field: string; goBack: () => void; } -export const FilterExpanded = ({ field, label, goBack }: Props) => { +export const FilterExpanded = ({ seriesId, field, label, goBack }: Props) => { const { indexPattern } = useIndexPatternContext(); const [value, setValue] = useState(''); @@ -36,7 +37,7 @@ export const FilterExpanded = ({ field, label, goBack }: Props) => { services: { data }, } = useKibana(); - const storage = useUrlStorage(); + const { series, setSeries } = useUrlStorage(seriesId); const { data: values, status } = useFetcher(() => { return data.autocomplete.getValueSuggestions({ @@ -47,8 +48,6 @@ export const FilterExpanded = ({ field, label, goBack }: Props) => { }); }, [field]); - const series = storage.get('elastic-co'); - const filters = series?.filters ?? []; let currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); @@ -61,7 +60,7 @@ export const FilterExpanded = ({ field, label, goBack }: Props) => { } else { currFilter.values = [id]; } - storage.set('elastic-co', { ...series, filters: [currFilter] }); + setSeries(seriesId, { ...series, filters: [currFilter] }); return; } @@ -82,9 +81,9 @@ export const FilterExpanded = ({ field, label, goBack }: Props) => { currFilter.values = values.length > 0 ? values : undefined; if (notValues.length > 0 || values.length > 0) { - storage.set('elastic-co', { ...series, filters: [currFilter] }); + setSeries(seriesId, { ...series, filters: [currFilter] }); } else { - storage.set('elastic-co', { ...series, filters: undefined }); + setSeries(seriesId, { ...series, filters: undefined }); } } }; 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 index 677804405b8d6..c55c0388357bb 100644 --- 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 @@ -7,8 +7,18 @@ import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; +import { DataSeries } from '../../types'; +import { useUrlStorage } from '../../hooks/use_url_strorage'; -interface Props {} -export const RemoveSeries = (props: Props) => { - return ; +interface Props { + series: DataSeries; +} + +export const RemoveSeries = ({ series }: Props) => { + const { removeSeries } = useUrlStorage(); + + const onClick = () => { + removeSeries(series.id); + }; + return ; }; 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 index dfd4b6969be5c..0ea8ab4686e4d 100644 --- 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 @@ -20,6 +20,7 @@ import { FieldLabels } from '../../configurations/constants'; import { SelectedFilters } from '../selected_filters'; interface Props { + seriesId: string; defaultFilters: DataSeries['defaultFilters']; } @@ -28,7 +29,7 @@ export interface Field { field: string; } -export const SeriesFilter = ({ defaultFilters = [] }: Props) => { +export const SeriesFilter = ({ seriesId, defaultFilters = [] }: Props) => { const [isPopoverVisible, setIsPopoverVisible] = useState(false); const [selectedField, setSelectedField] = useState(null); @@ -95,7 +96,7 @@ export const SeriesFilter = ({ defaultFilters = [] }: Props) => {
- +
); 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 index a1d4981d620b9..83f54a3913194 100644 --- 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 @@ -7,33 +7,65 @@ import React, { Fragment } from 'react'; import { useUrlStorage } from '../hooks/use_url_strorage'; -import { SeriesUrl } from '../types'; import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { FieldLabels } from '../configurations/constants'; -interface Props {} -export const SelectedFilters = (props: Props) => { - const storage = useUrlStorage(); +interface Props { + seriesId: string; +} +export const SelectedFilters = ({ seriesId }: Props) => { + const { series, setSeries } = useUrlStorage(seriesId); - const { filters = [] } = storage.get('elastic-co') ?? {}; + const filters = series?.filters ?? []; const style = { maxWidth: 250 }; + const removeFilter = (field: string, value: string, notVal: boolean) => { + const filtersN = filters.map((filter) => { + if (filter.field === field) { + if (notVal) { + const notValuesN = filter.notValues?.filter((val) => val !== value); + return { ...filter, notValues: notValuesN }; + } else { + const valuesN = filter.values?.filter((val) => val !== value); + return { ...filter, values: valuesN }; + } + } + + return filter; + }); + setSeries(seriesId, { ...series, filters: filtersN }); + }; + return ( {filters.map(({ field, values, notValues }) => ( {(values ?? []).map((val) => ( - + removeFilter(field, val, false)} + > {FieldLabels[field]}: {val} ))} {(notValues ?? []).map((val) => ( - - {FieldLabels[field]}: {val} + removeFilter(field, val, true)} + > + Not {FieldLabels[field]}: {val} ))} 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 dff1921763782..de34f3f86ffdd 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 @@ -7,18 +7,21 @@ import React from 'react'; import { useParams } from 'react-router-dom'; -import { EuiBasicTable, EuiButton, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; +import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui'; import { SeriesFilter } from './columns/series_filter'; import { ActionsCol } from './columns/actions_col'; import { Breakdowns } from './columns/breakdowns'; import { DataSeries, DataViewType } from '../types'; -import { getDefaultConfigs } from '../configurations/default_configs'; +import { SeriesBuilder } from '../series_builder/series_builder'; +import { useUrlStorage } from '../hooks/use_url_strorage'; +import { getPageLoadDistLensConfig } from '../configurations/page_load_dist_config'; +import { RemoveSeries } from './columns/remove_series'; export const SeriesEditor = () => { const columns = [ { name: 'Name', - field: 'name', + field: 'id', width: '20%', render: (val: string) => ( @@ -30,7 +33,9 @@ export const SeriesEditor = () => { name: 'Filter', field: 'defaultFilters', width: '30%', - render: (defaultFilters: string[]) => , + render: (defaultFilters: string[], series: DataSeries) => ( + + ), }, { name: 'Breakdowns', @@ -46,11 +51,29 @@ export const SeriesEditor = () => { field: 'id', render: (val: string, item: DataSeries) => , }, + { + name: 'Remove', + width: '5%', + field: 'id', + render: (val: string, item: DataSeries) => , + }, ]; const { dataViewType } = useParams<{ dataViewType: DataViewType }>(); - const items: DataSeries[] = [getDefaultConfigs({ dataViewType })]; + const { allSeries } = useUrlStorage(); + + console.log(allSeries); + + const allSeriesKeys = Object.keys(allSeries); + + const items: DataSeries[] = allSeriesKeys.map((seriesKey) => { + const series = allSeries[seriesKey]; + return getPageLoadDistLensConfig({ + seriesId: seriesKey, + serviceName: series.serviceName, + }); + }); return ( <> @@ -62,9 +85,7 @@ export const SeriesEditor = () => { rowProps={() => ({ height: 100 })} /> - - Add series - + ); }; 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 ed6cf901115c5..2d9464dc8600b 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 @@ -12,12 +12,16 @@ import { LastValueIndexPatternColumn, } from '../../../../../lens/public'; -export type DataViewType = 'page-load-dist' | 'page-views' | 'uptime-duration' | 'uptime-pings'; +export type DataViewType = + | 'page-load-dist' + | 'page-views' + | 'uptime-duration' + | 'uptime-pings' + | 'service-latency'; export interface DataSeries { dataViewType: DataViewType; indexPattern: string; - name: string; id: string; xAxisColumn: Partial | Partial; yAxisColumn: @@ -32,12 +36,13 @@ export interface DataSeries { } export interface SeriesUrl { + id: string; time: { to: string; from: string; }; - breakdown: string; - filters: UrlFilter[]; + breakdown?: string; + filters?: UrlFilter[]; seriesType: string; } @@ -46,3 +51,5 @@ export interface UrlFilter { values?: string[]; notValues?: string[]; } + +export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm'; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_selection.tsx b/x-pack/plugins/observability/public/components/shared/field_value_selection.tsx new file mode 100644 index 0000000000000..bab924e875095 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/field_value_selection.tsx @@ -0,0 +1,118 @@ +/* + * 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, { FormEvent, Fragment, useEffect, useState } from 'react'; +import { + EuiButton, + EuiPopover, + EuiPopoverFooter, + EuiPopoverTitle, + EuiSelectable, +} from '@elastic/eui'; +import { useValuesList } from '../../hooks/use_values_list'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common'; +import { FieldLabels } from './exploratory_view/configurations/constants'; + +interface Option { + id: string; + label: string; + checked?: 'on'; +} + +interface Props { + value?: string; + indexPattern: IIndexPattern; + sourceField: string; + onChange: (val: string) => void; +} + +export const FieldValueSelection = ({ + sourceField, + indexPattern, + value, + onChange: onSelectionChange, +}: Props) => { + const [query, setQuery] = useState(''); + const { values, loading } = useValuesList({ indexPattern, query, sourceField }); + + const [options, setOptions] = useState([]); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + + useEffect(() => { + setOptions( + (values ?? []).map((val) => ({ id: val, label: val, ...(value ? { checked: 'on' } : {}) })) + ); + }, [values]); + + const onButtonClick = () => { + setIsPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const onChange = (options: Option[]) => { + setOptions(options); + }; + + const onValueChange = (evt: FormEvent) => { + setQuery(evt.target.value); + }; + + const button = ( + + Web Application + + ); + + return ( + + + + {(list, search) => ( +
+ {search} + {list} + + opt?.checked === 'on')} + onClick={() => { + const selected = options.find((opt) => opt?.checked === 'on')!; + onSelectionChange(selected.label); + setIsPopoverOpen(false); + }} + > + Apply + + +
+ )} +
+
+
+ ); +}; diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts new file mode 100644 index 0000000000000..f795f121250f2 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -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 { IIndexPattern } from '../../../../../src/plugins/data/common'; +import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { ObservabilityClientPluginsStart } from '../plugin'; +import { useFetcher } from './use_fetcher'; + +interface Props { + sourceField: string; + query?: string; + indexPattern: IIndexPattern; +} + +export const useValuesList = ({ sourceField, indexPattern, query }: Props) => { + const { + services: { data }, + } = useKibana(); + + const { data: values, status } = useFetcher(() => { + return data.autocomplete.getValueSuggestions({ + indexPattern, + query: query || '', + useTimeRange: false, + field: indexPattern.fields.find(({ name }) => name === sourceField)!, + }); + }, [sourceField, query]); + + return { values, loading: status === 'loading' || status === 'pending' }; +};