Skip to content

Commit ee2957a

Browse files
[Metrics UI] Add support for multiple groupings to Metrics Explorer (and Alerts) (elastic#66503)
* [Metrics UI] Adding support for multiple groupings to Metrics Explorer * Adding keys to title parts * removing commented line Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
1 parent d625934 commit ee2957a

File tree

20 files changed

+256
-85
lines changed

20 files changed

+256
-85
lines changed

x-pack/plugins/infra/common/http_api/metrics_explorer.ts

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,12 @@ export const metricsExplorerRequestBodyRequiredFieldsRT = rt.type({
5252
metrics: rt.array(metricsExplorerMetricRT),
5353
});
5454

55+
const groupByRT = rt.union([rt.string, rt.null, rt.undefined]);
56+
export const afterKeyObjectRT = rt.record(rt.string, rt.union([rt.string, rt.null]));
57+
5558
export const metricsExplorerRequestBodyOptionalFieldsRT = rt.partial({
56-
groupBy: rt.union([rt.string, rt.null, rt.undefined]),
57-
afterKey: rt.union([rt.string, rt.null, rt.undefined]),
59+
groupBy: rt.union([groupByRT, rt.array(groupByRT)]),
60+
afterKey: rt.union([rt.string, rt.null, rt.undefined, afterKeyObjectRT]),
5861
limit: rt.union([rt.number, rt.null, rt.undefined]),
5962
filterQuery: rt.union([rt.string, rt.null, rt.undefined]),
6063
forceInterval: rt.boolean,
@@ -68,7 +71,7 @@ export const metricsExplorerRequestBodyRT = rt.intersection([
6871

6972
export const metricsExplorerPageInfoRT = rt.type({
7073
total: rt.number,
71-
afterKey: rt.union([rt.string, rt.null]),
74+
afterKey: rt.union([rt.string, rt.null, afterKeyObjectRT]),
7275
});
7376

7477
export const metricsExplorerColumnTypeRT = rt.keyof({
@@ -89,11 +92,16 @@ export const metricsExplorerRowRT = rt.intersection([
8992
rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])),
9093
]);
9194

92-
export const metricsExplorerSeriesRT = rt.type({
93-
id: rt.string,
94-
columns: rt.array(metricsExplorerColumnRT),
95-
rows: rt.array(metricsExplorerRowRT),
96-
});
95+
export const metricsExplorerSeriesRT = rt.intersection([
96+
rt.type({
97+
id: rt.string,
98+
columns: rt.array(metricsExplorerColumnRT),
99+
rows: rt.array(metricsExplorerRowRT),
100+
}),
101+
rt.partial({
102+
keys: rt.array(rt.string),
103+
}),
104+
]);
97105

98106
export const metricsExplorerResponseRT = rt.type({
99107
series: rt.array(metricsExplorerSeriesRT),

x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ export const Expressions: React.FC<Props> = props => {
138138
]);
139139

140140
const onGroupByChange = useCallback(
141-
(group: string | null) => {
141+
(group: string | null | string[]) => {
142142
setAlertParams('groupBy', group || '');
143143
},
144144
[setAlertParams]
@@ -206,7 +206,10 @@ export const Expressions: React.FC<Props> = props => {
206206
convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || ''
207207
);
208208
} else if (md && md.currentOptions?.groupBy && md.series) {
209-
const filter = `${md.currentOptions?.groupBy}: "${md.series.id}"`;
209+
const { groupBy } = md.currentOptions;
210+
const filter = Array.isArray(groupBy)
211+
? groupBy.map((field, index) => `${field}: "${md.series?.keys?.[index]}"`).join(' and ')
212+
: `${groupBy}: "${md.series.id}"`;
210213
setAlertParams('filterQueryText', filter);
211214
setAlertParams(
212215
'filterQuery',

x-pack/plugins/infra/public/alerting/metric_threshold/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export enum AGGREGATION_TYPES {
3535

3636
export interface MetricThresholdAlertParams {
3737
criteria?: MetricExpression[];
38-
groupBy?: string;
38+
groupBy?: string | string[];
3939
filterQuery?: string;
4040
sourceId?: string;
4141
}

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { getChartTheme } from './helpers/get_chart_theme';
3535
import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting';
3636
import { calculateDomain } from './helpers/calculate_domain';
3737
import { useKibana, useUiSetting } from '../../../../../../../../src/plugins/kibana_react/public';
38+
import { ChartTitle } from './chart_title';
3839

3940
interface Props {
4041
title?: string | null;
@@ -91,16 +92,17 @@ export const MetricsExplorerChart = ({
9192
chartOptions.yAxisMode === MetricsExplorerYAxisMode.fromZero
9293
? { ...dataDomain, min: 0 }
9394
: dataDomain;
95+
9496
return (
9597
<div style={{ padding: 24 }}>
9698
{options.groupBy ? (
9799
<EuiTitle size="xs">
98100
<EuiFlexGroup alignItems="center">
99-
<ChartTitle>
101+
<ChartTitleContainer>
100102
<EuiToolTip content={title} anchorClassName="metricsExplorerTitleAnchor">
101-
<span>{title}</span>
103+
<ChartTitle series={series} />
102104
</EuiToolTip>
103-
</ChartTitle>
105+
</ChartTitleContainer>
104106
<EuiFlexItem grow={false}>
105107
<MetricsExplorerChartContextMenu
106108
timeRange={timeRange}
@@ -169,7 +171,7 @@ export const MetricsExplorerChart = ({
169171
);
170172
};
171173

172-
const ChartTitle = euiStyled.div`
174+
const ChartTitleContainer = euiStyled.div`
173175
width: 100%;
174176
overflow: hidden;
175177
text-overflow: ellipsis;

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,16 @@ export interface Props {
3939

4040
const fieldToNodeType = (
4141
source: SourceConfiguration,
42-
field: string
42+
groupBy: string | string[]
4343
): InventoryItemType | undefined => {
44-
if (source.fields.host === field) {
44+
const fields = Array.isArray(groupBy) ? groupBy : [groupBy];
45+
if (fields.includes(source.fields.host)) {
4546
return 'host';
4647
}
47-
if (source.fields.pod === field) {
48+
if (fields.includes(source.fields.pod)) {
4849
return 'pod';
4950
}
50-
if (source.fields.container === field) {
51+
if (fields.includes(source.fields.container)) {
5152
return 'container';
5253
}
5354
};
@@ -88,10 +89,16 @@ export const MetricsExplorerChartContextMenu: React.FC<Props> = ({
8889
// onFilter needs check for Typescript even though it's
8990
// covered by supportFiltering variable
9091
if (supportFiltering && onFilter) {
91-
onFilter(`${options.groupBy}: "${series.id}"`);
92+
if (Array.isArray(options.groupBy)) {
93+
onFilter(
94+
options.groupBy.map((field, index) => `${field}: "${series.keys?.[index]}"`).join(' and ')
95+
);
96+
} else {
97+
onFilter(`${options.groupBy}: "${series.id}"`);
98+
}
9299
}
93100
setPopoverState(false);
94-
}, [supportFiltering, options.groupBy, series.id, onFilter]);
101+
}, [supportFiltering, onFilter, options, series.keys, series.id]);
95102

96103
// Only display the "Add Filter" option if it's supported
97104
const filterByItem = supportFiltering
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
import React, { Fragment } from 'react';
8+
import { EuiText, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
9+
import { MetricsExplorerSeries } from '../../../../../common/http_api';
10+
11+
interface Props {
12+
series: MetricsExplorerSeries;
13+
}
14+
15+
export const ChartTitle = ({ series }: Props) => {
16+
if (series.keys != null) {
17+
const { keys } = series;
18+
return (
19+
<EuiFlexGroup gutterSize="xs">
20+
{keys.map((name, i) => (
21+
<Fragment key={name}>
22+
<EuiFlexItem grow={false}>
23+
<EuiText size="m" color={keys.length - 1 > i ? 'subdued' : 'default'}>
24+
<strong>{name}</strong>
25+
</EuiText>
26+
</EuiFlexItem>
27+
{keys.length - 1 > i && (
28+
<EuiFlexItem grow={false}>
29+
<EuiText size="m" color="subdued">
30+
<span>/</span>
31+
</EuiText>
32+
</EuiFlexItem>
33+
)}
34+
</Fragment>
35+
))}
36+
</EuiFlexGroup>
37+
);
38+
}
39+
return <span>{series.id}</span>;
40+
};

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/charts.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@ import { NoData } from '../../../../components/empty_states/no_data';
1919
import { MetricsExplorerChart } from './chart';
2020
import { SourceQuery } from '../../../../graphql/types';
2121

22+
type stringOrNull = string | null;
23+
2224
interface Props {
2325
loading: boolean;
2426
options: MetricsExplorerOptions;
2527
chartOptions: MetricsExplorerChartOptions;
26-
onLoadMore: (afterKey: string | null) => void;
28+
onLoadMore: (afterKey: stringOrNull | Record<string, stringOrNull>) => void;
2729
onRefetch: () => void;
2830
onFilter: (filter: string) => void;
2931
onTimeChange: (start: string, end: string) => void;
@@ -73,6 +75,8 @@ export const MetricsExplorerCharts = ({
7375
);
7476
}
7577

78+
const and = i18n.translate('xpack.infra.metricsExplorer.andLabel', { defaultMessage: '" and "' });
79+
7680
return (
7781
<div style={{ width: '100%' }}>
7882
<EuiFlexGrid gutterSize="s" columns={data.series.length === 1 ? 1 : 3}>
@@ -104,7 +108,9 @@ export const MetricsExplorerCharts = ({
104108
values={{
105109
length: data.series.length,
106110
total: data.pageInfo.total,
107-
groupBy: options.groupBy,
111+
groupBy: Array.isArray(options.groupBy)
112+
? options.groupBy.join(and)
113+
: options.groupBy,
108114
}}
109115
/>
110116
</p>

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/group_by.tsx

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,25 @@ import { MetricsExplorerOptions } from '../hooks/use_metrics_explorer_options';
1313

1414
interface Props {
1515
options: MetricsExplorerOptions;
16-
onChange: (groupBy: string | null) => void;
16+
onChange: (groupBy: string | null | string[]) => void;
1717
fields: IFieldType[];
1818
}
1919

2020
export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) => {
2121
const handleChange = useCallback(
22-
selectedOptions => {
23-
const groupBy = (selectedOptions.length === 1 && selectedOptions[0].label) || null;
22+
(selectedOptions: Array<{ label: string }>) => {
23+
const groupBy = selectedOptions.map(option => option.label);
2424
onChange(groupBy);
2525
},
2626
[onChange]
2727
);
2828

29+
const selectedOptions = Array.isArray(options.groupBy)
30+
? options.groupBy.map(field => ({ label: field }))
31+
: options.groupBy
32+
? [{ label: options.groupBy }]
33+
: [];
34+
2935
return (
3036
<EuiComboBox
3137
placeholder={i18n.translate('xpack.infra.metricsExplorer.groupByLabel', {
@@ -35,8 +41,8 @@ export const MetricsExplorerGroupBy = ({ options, onChange, fields }: Props) =>
3541
defaultMessage: 'Graph per',
3642
})}
3743
fullWidth
38-
singleSelection={true}
39-
selectedOptions={(options.groupBy && [{ label: options.groupBy }]) || []}
44+
singleSelection={false}
45+
selectedOptions={selectedOptions}
4046
options={fields
4147
.filter(f => f.aggregatable && f.type === 'string')
4248
.map(f => ({ label: f.name }))}

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,21 @@ export const createFilterFromOptions = (
109109
}
110110
if (options.groupBy) {
111111
const id = series.id.replace('"', '\\"');
112-
filters.push(`${options.groupBy} : "${id}"`);
112+
const groupByFilters = Array.isArray(options.groupBy)
113+
? options.groupBy
114+
.map((field, index) => {
115+
if (!series.keys) {
116+
return null;
117+
}
118+
const value = series.keys[index];
119+
if (!value) {
120+
return null;
121+
}
122+
return `${field}: "${value.replace('"', '\\"')}"`;
123+
})
124+
.join(' and ')
125+
: `${options.groupBy} : "${id}"`;
126+
filters.push(groupByFilters);
113127
}
114128
return { language: 'kuery', query: filters.join(' and ') };
115129
};

x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ interface Props {
3737
defaultViewState: MetricExplorerViewState;
3838
onRefresh: () => void;
3939
onTimeChange: (start: string, end: string) => void;
40-
onGroupByChange: (groupBy: string | null) => void;
40+
onGroupByChange: (groupBy: string | null | string[]) => void;
4141
onFilterQuerySubmit: (query: string) => void;
4242
onMetricsChange: (metrics: MetricsExplorerMetric[]) => void;
4343
onAggregationChange: (aggregation: MetricsExplorerAggregation) => void;

0 commit comments

Comments
 (0)