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..68dcd77e98990 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 @@ -45,6 +45,7 @@ import { TBT_LABEL, URL_LABEL, BACKEND_TIME_LABEL, + LABELS_FIELD, } from './labels'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -85,6 +86,8 @@ export const FieldLabels: Record = { 'performance.metric': METRIC_LABEL, 'Business.KPI': KPI_LABEL, 'http.request.method': REQUEST_METHOD, + LABEL_FIELDS_FILTER: LABELS_FIELD, + LABEL_FIELDS_BREAKDOWN: 'Labels field', }; export const DataViewLabels: Record = { @@ -113,3 +116,6 @@ export const TERMS_COLUMN = 'TERMS_COLUMN'; export const OPERATION_COLUMN = 'operation'; export const REPORT_METRIC_FIELD = 'REPORT_METRIC_FIELD'; + +export const LABEL_FIELDS_FILTER = 'LABEL_FIELDS_FILTER'; +export const LABEL_FIELDS_BREAKDOWN = 'LABEL_FIELDS_BREAKDOWN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index 5a3e773f7d8ee..cdaa89fc71389 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -169,6 +169,15 @@ export const TAGS_LABEL = i18n.translate('xpack.observability.expView.fieldLabel export const METRIC_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.metric', { defaultMessage: 'Metric', }); +export const LABELS_FIELD = i18n.translate('xpack.observability.expView.fieldLabels.labels', { + defaultMessage: 'Labels', +}); +export const LABELS_BREAKDOWN = i18n.translate( + 'xpack.observability.expView.fieldLabels.chooseField', + { + defaultMessage: 'Labels field', + } +); export const KPI_LABEL = i18n.translate('xpack.observability.expView.fieldLabels.kpi', { defaultMessage: 'KPI', }); 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..b66709d0e2286 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,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN } from '../constants'; +import { + FieldLabels, + LABEL_FIELDS_FILTER, + REPORT_METRIC_FIELD, + ReportTypes, + 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'; @@ -27,7 +33,7 @@ export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps) }, ], hasOperationType: false, - filterFields: Object.keys(MobileFields), + filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER], breakdownFields: Object.keys(MobileFields), baseFilters: [ ...buildPhraseFilter('agent.name', 'iOS/swift', indexPattern), 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..7a924a3bbb38c 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,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { + FieldLabels, + LABEL_FIELDS_FILTER, + RECORDS_FIELD, + REPORT_METRIC_FIELD, + ReportTypes, +} from '../constants'; import { buildPhrasesFilter } from '../utils'; import { METRIC_SYSTEM_CPU_USAGE, @@ -33,7 +39,7 @@ export function getMobileKPIDistributionConfig({ indexPattern }: ConfigProps): S }, ], hasOperationType: false, - filterFields: Object.keys(MobileFields), + filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER], breakdownFields: Object.keys(MobileFields), baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), 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..30efa2fd9ec5c 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 @@ -8,6 +8,7 @@ import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, + LABEL_FIELDS_FILTER, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD, @@ -45,7 +46,7 @@ export function getMobileKPIConfig({ indexPattern }: ConfigProps): SeriesConfig }, ], hasOperationType: true, - filterFields: Object.keys(MobileFields), + filterFields: [...Object.keys(MobileFields), LABEL_FIELDS_FILTER], breakdownFields: Object.keys(MobileFields), baseFilters: [ ...buildPhrasesFilter('agent.name', ['iOS/swift', 'open-telemetry/swift'], indexPattern), 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..e5113211e0a62 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 @@ -10,6 +10,7 @@ import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, FILTER_RECORDS, + LABEL_FIELDS_FILTER, REPORT_METRIC_FIELD, ReportTypes, USE_BREAK_DOWN_COLUMN, @@ -75,6 +76,7 @@ export function getCoreWebVitalsConfig({ indexPattern }: ConfigProps): SeriesCon field: USER_AGENT_NAME, nested: USER_AGENT_VERSION, }, + LABEL_FIELDS_FILTER, ], breakdownFields: [ SERVICE_NAME, 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..7796b381423bf 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 @@ -11,6 +11,7 @@ import { REPORT_METRIC_FIELD, RECORDS_PERCENTAGE_FIELD, ReportTypes, + LABEL_FIELDS_FILTER, } from '../constants'; import { buildPhraseFilter } from '../utils'; import { @@ -71,6 +72,7 @@ export function getRumDistributionConfig({ indexPattern }: ConfigProps): SeriesC field: USER_AGENT_NAME, nested: USER_AGENT_VERSION, }, + LABEL_FIELDS_FILTER, ], breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], definitionFields: [SERVICE_NAME, SERVICE_ENVIRONMENT], 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..de4f6b2198dbd 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 @@ -8,6 +8,8 @@ import { ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, + LABEL_FIELDS_BREAKDOWN, + LABEL_FIELDS_FILTER, OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD, @@ -72,8 +74,15 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon field: USER_AGENT_NAME, nested: USER_AGENT_VERSION, }, + LABEL_FIELDS_FILTER, + ], + breakdownFields: [ + USER_AGENT_NAME, + USER_AGENT_OS, + CLIENT_GEO_COUNTRY_NAME, + USER_AGENT_DEVICE, + LABEL_FIELDS_BREAKDOWN, ], - breakdownFields: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE], baseFilters: [ ...buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern), ...buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern), 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 fe00141e450cf..23eee140b68cf 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 @@ -22,6 +22,7 @@ import { IndexPatternState, useAppIndexPatternContext } from './use_app_index_pa import { ALL_VALUES_SELECTED } from '../../field_value_suggestions/field_value_combobox'; import { useTheme } from '../../../../hooks/use_theme'; import { EuiTheme } from '../../../../../../../../src/plugins/kibana_react/common'; +import { LABEL_FIELDS_BREAKDOWN } from '../configurations/constants'; export const getFiltersFromDefs = (reportDefinitions: SeriesUrl['reportDefinitions']) => { return Object.entries(reportDefinitions ?? {}) @@ -69,7 +70,7 @@ export function getLayerConfigs( seriesConfig, time: series.time, name: series.name, - breakdown: series.breakdown, + breakdown: series.breakdown === LABEL_FIELDS_BREAKDOWN ? undefined : series.breakdown, seriesType: series.seriesType, operationType: series.operationType, reportDefinitions: series.reportDefinitions ?? {}, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx similarity index 100% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.test.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx 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/breakdown/breakdowns.tsx similarity index 90% rename from x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/breakdowns.tsx rename to x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx index 6003ddbf0290f..cfd3d153a61c5 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/breakdown/breakdowns.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiSuperSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; +import { LABEL_FIELDS_BREAKDOWN, USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; import { SeriesConfig, SeriesUrl } from '../../types'; interface Props { @@ -62,9 +62,13 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { dropdownDisplay: label, })); - const valueOfSelected = + let valueOfSelected = selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN); + if (selectedBreakdown?.startsWith('labels.')) { + valueOfSelected = LABEL_FIELDS_BREAKDOWN; + } + return ( field.name.startsWith('labels.')); + + const { setSeries } = useSeriesStorage(); + + const { breakdown } = series; + + const hasLabelBreakdown = + breakdown === LABEL_FIELDS_BREAKDOWN || breakdown?.startsWith('labels.'); + + if (!hasLabelBreakdown) { + return null; + } + + const labelFieldOptions = labelFields?.map((field) => { + return { + label: field.name, + value: field.name, + }; + }); + + return ( + + labelField.label === breakdown)} + options={labelFieldOptions} + placeholder={CHOOSE_BREAKDOWN_FIELD} + onChange={(value) => { + setSeries(seriesId, { + ...series, + breakdown: value?.[0]?.label ?? LABEL_FIELDS_BREAKDOWN, + }); + }} + singleSelection={{ asPlainText: true }} + isInvalid={series.breakdown === LABEL_FIELDS_BREAKDOWN} + /> + + ); +} + +export const CHOOSE_BREAKDOWN_FIELD = i18n.translate( + 'xpack.observability.expView.seriesBuilder.labelField', + { + defaultMessage: 'Choose label field', + } +); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx index a88e2eadd10c9..0a5ac137a7870 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/filter_expanded.test.tsx @@ -26,7 +26,7 @@ describe('FilterExpanded', function () { series={mockSeries} label={'Browser Family'} field={USER_AGENT_NAME} - filters={[]} + baseFilters={[]} />, { initSeries } ); @@ -45,7 +45,7 @@ describe('FilterExpanded', function () { series={mockSeries} label={'Browser Family'} field={USER_AGENT_NAME} - filters={[]} + baseFilters={[]} />, { initSeries } ); @@ -69,7 +69,7 @@ describe('FilterExpanded', function () { series={mockSeries} label={'Browser Family'} field={USER_AGENT_NAME} - filters={[]} + baseFilters={[]} />, { initSeries } ); @@ -99,7 +99,7 @@ describe('FilterExpanded', function () { series={mockUxSeries} label={'Browser Family'} field={USER_AGENT_NAME} - filters={[]} + baseFilters={[]} />, { initSeries } ); 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 a65110b2c44ff..09b9f443389ce 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 @@ -5,37 +5,21 @@ * 2.0. */ -import React, { useState, Fragment } from 'react'; -import { - EuiFieldSearch, - EuiSpacer, - EuiFilterGroup, - EuiText, - EuiPopover, - EuiFilterButton, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { rgba } from 'polished'; -import { i18n } from '@kbn/i18n'; -import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; -import { map } from 'lodash'; -import { ExistsFilter, isExistsFilter } from '@kbn/es-query'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; -import { SeriesConfig, SeriesUrl, UrlFilter } from '../../types'; -import { FilterValueButton } from './filter_value_btn'; -import { useValuesList } from '../../../../../hooks/use_values_list'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; -import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; -import { PersistableFilter } from '../../../../../../../lens/common'; +import React, { useState } from 'react'; -interface Props { +import { EuiFilterButton, EuiPopover } from '@elastic/eui'; +import { SeriesConfig, SeriesUrl } from '../../types'; +import { useFilterValues } from '../use_filter_values'; +import { FilterValuesList } from '../components/filter_values_list'; + +export interface FilterProps { seriesId: number; series: SeriesUrl; label: string; field: string; isNegated?: boolean; nestedField?: string; - filters: SeriesConfig['baseFilters']; + baseFilters: SeriesConfig['baseFilters']; } export interface NestedFilterOpen { @@ -43,137 +27,30 @@ export interface NestedFilterOpen { negate: boolean; } -export function FilterExpanded({ - seriesId, - series, - field, - label, - nestedField, - isNegated, - filters: defaultFilters, -}: Props) { - const [value, setValue] = useState(''); - +export function FilterExpanded(props: FilterProps) { const [isOpen, setIsOpen] = useState(false); - const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); - - const queryFilters: ESFilter[] = []; - - const { indexPatterns } = useAppIndexPatternContext(series.dataType); - - defaultFilters?.forEach((qFilter: PersistableFilter | ExistsFilter) => { - if (qFilter.query) { - queryFilters.push(qFilter.query); - } - if (isExistsFilter(qFilter)) { - queryFilters.push({ exists: qFilter.query.exists } as QueryDslQueryContainer); - } - }); - const { values, loading } = useValuesList({ - query: value, - sourceField: field, - time: series.time, - keepHistory: true, - filters: queryFilters, - indexPatternTitle: indexPatterns[series.dataType]?.title, - }); + const [query, setQuery] = useState(''); - const filters = series?.filters ?? []; - - const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); - - const displayValues = map(values, 'label').filter((opt) => - opt.toLowerCase().includes(value.toLowerCase()) - ); + const { values, loading } = useFilterValues(props, query); return ( setIsOpen((prevState) => !prevState)} iconType="arrowDown"> - {label} + {props.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 && ( - - )} - - - - - ))} - - + ); } - -const ListWrapper = euiStyled.div` - height: 400px; - overflow-y: auto; - &::-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; - } -`; - -const Wrapper = styled.div` - width: 400px; -`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx index 3327ecf1fc9b6..803318aff9f32 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/selected_filters.tsx @@ -36,7 +36,7 @@ export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { return ( <> - + {filters.map(({ field, values, notValues }) => ( {(values ?? []).length > 0 && ( @@ -45,7 +45,7 @@ export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { seriesId={seriesId} series={series} field={field} - label={labels[field]} + label={labels[field] ?? field} value={values ?? []} removeFilter={() => { values?.forEach((val) => { @@ -63,7 +63,7 @@ export function SelectedFilters({ seriesId, series, seriesConfig }: Props) { series={series} seriesId={seriesId} field={field} - label={labels[field]} + label={labels[field] ?? field} value={notValues ?? []} negate={true} removeFilter={() => { 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 5b576d9da0172..fe02bdf305fb2 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 @@ -9,8 +9,9 @@ import React from 'react'; import { EuiFilterGroup, EuiSpacer } from '@elastic/eui'; import { FilterExpanded } from './filter_expanded'; import { SeriesConfig, SeriesUrl } from '../../types'; -import { FieldLabels } from '../../configurations/constants/constants'; +import { FieldLabels, LABEL_FIELDS_FILTER } from '../../configurations/constants/constants'; import { SelectedFilters } from './selected_filters'; +import { LabelsFieldFilter } from '../components/labels_filter'; interface Props { seriesId: number; @@ -21,7 +22,7 @@ interface Props { export interface Field { label: string; field: string; - nested?: string; + nestedField?: string; isNegated?: boolean; } @@ -33,7 +34,7 @@ export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { return { field: field.field, - nested: field.nested, + nestedField: field.nested, isNegated: field.isNegated, label: seriesConfig.labels?.[field.field] ?? FieldLabels[field.field], }; @@ -42,18 +43,25 @@ export function SeriesFilter({ series, seriesConfig, seriesId }: Props) { return ( <> - {options.map((opt) => ( - - ))} + {options.map((opt) => + opt.field === LABEL_FIELDS_FILTER ? ( + + ) : ( + + ) + )} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/filter_values_list.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/filter_values_list.tsx new file mode 100644 index 0000000000000..a6942418c609b --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/filter_values_list.tsx @@ -0,0 +1,143 @@ +/* + * 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 { EuiFieldSearch, EuiFilterGroup, EuiProgress, EuiSpacer, EuiText } from '@elastic/eui'; +import React, { Fragment, useState } from 'react'; +import { rgba } from 'polished'; +import styled from 'styled-components'; +import { map } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FilterValueButton } from '../columns/filter_value_btn'; +import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; +import { FilterProps, NestedFilterOpen } from '../columns/filter_expanded'; +import { UrlFilter } from '../../types'; +import { ListItem } from '../../../../../hooks/use_values_list'; + +interface Props extends FilterProps { + values: ListItem[]; + field: string; + query: string; + loading?: boolean; + setQuery: (q: string) => void; +} + +export function FilterValuesList({ + field, + values, + query, + setQuery, + label, + loading, + isNegated, + nestedField, + series, + seriesId, +}: Props) { + const [isNestedOpen, setIsNestedOpen] = useState({ value: '', negate: false }); + + const displayValues = map(values, 'label').filter((opt) => + opt.toLowerCase().includes(query.toLowerCase()) + ); + + const filters = series?.filters ?? []; + + const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd); + + const btnProps = { + field, + nestedField, + seriesId, + series, + isNestedOpen, + setIsNestedOpen, + }; + + return ( + + { + setQuery(evt.target.value); + }} + placeholder={getSearchLabel(label)} + /> + + + {loading && ( +
+ +
+ )} + {displayValues.length === 0 && !loading && ( + {NO_RESULT_FOUND} + )} + {displayValues.map((opt) => ( + + + {isNegated !== false && ( + + )} + + + + + ))} +
+
+ ); +} + +const NO_RESULT_FOUND = i18n.translate('xpack.observability.filters.expanded.noFilter', { + defaultMessage: 'No filters found.', +}); + +const getSearchLabel = (label: string) => + i18n.translate('xpack.observability.filters.expanded.search', { + defaultMessage: 'Search for {label}', + values: { label }, + }); + +const ListWrapper = euiStyled.div` + height: 370px; + overflow-y: auto; + &::-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; + } +`; + +const Wrapper = styled.div` + width: 400px; +`; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/labels_filter.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/labels_filter.tsx new file mode 100644 index 0000000000000..6abe2e8f2a7d9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/components/labels_filter.tsx @@ -0,0 +1,128 @@ +/* + * 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 { + EuiPopoverTitle, + EuiFilterButton, + EuiPopover, + EuiIcon, + EuiButtonEmpty, + EuiSelectableOption, +} from '@elastic/eui'; + +import { EuiSelectable } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { FilterProps } from '../columns/filter_expanded'; +import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { FilterValuesList } from './filter_values_list'; +import { useFilterValues } from '../use_filter_values'; + +export function LabelsFieldFilter(props: FilterProps) { + const { series } = props; + + const [query, setQuery] = useState(''); + + const { indexPattern } = useAppIndexPatternContext(series.dataType); + + const labelFields = indexPattern?.fields.filter((field) => field.name.startsWith('labels.')); + + const [isPopoverOpen, setPopover] = useState(false); + + const onButtonClick = () => { + setPopover(!isPopoverOpen); + }; + + const button = ( + + {LABELS_LABEL} + + ); + + const [selectedLabel, setSelectedLabel] = useState(''); + + const { values, loading } = useFilterValues({ ...props, field: selectedLabel }, query); + + const labelFieldOptions: EuiSelectableOption[] = (labelFields ?? []).map((field) => { + return { + label: field.name, + searchableLabel: field.name, + append: , + showIcons: false, + }; + }); + + labelFieldOptions.unshift({ + label: LABELS_FIELDS_LABEL, + isGroupLabel: true, + }); + + const closePopover = () => { + setPopover(false); + setSelectedLabel(''); + }; + + return ( + + {selectedLabel ? ( + <> + + setSelectedLabel('')} + > + {BACK_TO_LABEL} + + + + + ) : ( + { + const checked = optionsChange.find((option) => option.checked === 'on'); + setSelectedLabel(checked?.label ?? ''); + }} + listProps={{ + onFocusBadge: false, + }} + height={450} + > + {(list, search) => ( +
+ {search} + {list} +
+ )} +
+ )} +
+ ); +} + +const LABELS_LABEL = i18n.translate('xpack.observability.filters.expanded.labels.label', { + defaultMessage: 'Labels', +}); + +const LABELS_FIELDS_LABEL = i18n.translate('xpack.observability.filters.expanded.labels.fields', { + defaultMessage: 'Label fields', +}); + +const BACK_TO_LABEL = i18n.translate('xpack.observability.filters.expanded.labels.backTo', { + defaultMessage: 'Back to labels', +}); 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 index 9f4de1b6dd519..ac71f4ff5abe0 100644 --- 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 @@ -15,7 +15,8 @@ import { OperationTypeSelect } from './columns/operation_type_select'; import { parseCustomFieldName } from '../configurations/lens_attributes'; import { SeriesFilter } from './columns/series_filter'; import { DatePickerCol } from './columns/date_picker_col'; -import { Breakdowns } from './columns/breakdowns'; +import { Breakdowns } from './breakdown/breakdowns'; +import { LabelsBreakdown } from './breakdown/label_breakdown'; function getColumnType(seriesConfig: SeriesConfig, selectedMetricField?: string) { const { columnType } = parseCustomFieldName(seriesConfig, selectedMetricField); @@ -58,13 +59,18 @@ export function ExpandedSeriesRow(seriesProps: Props) { - - - + + + + + + + + {(hasOperationType || columnType === 'operation') && ( - + { + if (qFilter.query) { + queryFilters.push(qFilter.query); + } + if (isExistsFilter(qFilter)) { + queryFilters.push({ exists: qFilter.query.exists } as QueryDslQueryContainer); + } + }); + + return useValuesList({ + query, + sourceField: field, + time: series.time, + keepHistory: true, + filters: queryFilters, + indexPatternTitle: indexPatterns[series.dataType]?.title, + }); +} diff --git a/x-pack/plugins/observability/public/hooks/use_values_list.ts b/x-pack/plugins/observability/public/hooks/use_values_list.ts index 5aa7dd672cfda..73bbd97fe5d7a 100644 --- a/x-pack/plugins/observability/public/hooks/use_values_list.ts +++ b/x-pack/plugins/observability/public/hooks/use_values_list.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { capitalize, union } from 'lodash'; +import { capitalize, uniqBy } from 'lodash'; import { useEffect, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; import { ESFilter } from '../../../../../src/core/types/elasticsearch'; @@ -26,6 +26,10 @@ export interface ListItem { count: number; } +const uniqueValues = (values: ListItem[], prevValues: ListItem[]) => { + return uniqBy([...values, ...prevValues], 'label'); +}; + export const useValuesList = ({ sourceField, indexPatternTitle, @@ -113,29 +117,28 @@ export const useValuesList = ({ }, }, }), - [debouncedQuery, from, to, JSON.stringify(filters), indexPatternTitle] + [debouncedQuery, from, to, JSON.stringify(filters), indexPatternTitle, sourceField] ); useEffect(() => { + const valueBuckets = data?.aggregations?.values.buckets; const newValues = - data?.aggregations?.values.buckets.map( - ({ key: value, doc_count: count, count: aggsCount }) => { - if (aggsCount) { - return { - count: aggsCount.value, - label: String(value), - }; - } + valueBuckets?.map(({ key: value, doc_count: count, count: aggsCount }) => { + if (aggsCount) { return { - count, + count: aggsCount.value, label: String(value), }; } - ) ?? []; + return { + count, + label: String(value), + }; + }) ?? []; - if (keepHistory && query) { + if (keepHistory) { setValues((prevState) => { - return union(newValues, prevState); + return uniqueValues(newValues, prevState); }); } else { setValues(newValues);