Skip to content

Commit dc54aca

Browse files
authored
[Infra UI] Custom Field Grouping for Waffle Map (#28949) (#29636)
* [Infra UI] Add Support for Grouping By Custom Field * fixiing typescript errors * Serializing custom options to url so they persist accross reloads * Fixing more errors * removing label; moving custom field to top of menu * fixing typescript error * Adding intl formatMessage to strings
1 parent 5323a57 commit dc54aca

File tree

14 files changed

+237
-37
lines changed

14 files changed

+237
-37
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui';
8+
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
9+
import React from 'react';
10+
import { InfraIndexField } from 'x-pack/plugins/infra/server/graphql/types';
11+
interface Props {
12+
onSubmit: (field: string) => void;
13+
fields: InfraIndexField[];
14+
intl: InjectedIntl;
15+
}
16+
17+
interface SelectedOption {
18+
label: string;
19+
}
20+
21+
const initialState = {
22+
selectedOptions: [] as SelectedOption[],
23+
};
24+
25+
type State = Readonly<typeof initialState>;
26+
27+
export const CustomFieldPanel = injectI18n(
28+
class extends React.PureComponent<Props, State> {
29+
public static displayName = 'CustomFieldPanel';
30+
public readonly state: State = initialState;
31+
public render() {
32+
const { fields, intl } = this.props;
33+
const options = fields
34+
.filter(f => f.aggregatable && f.type === 'string')
35+
.map(f => ({ label: f.name }));
36+
return (
37+
<div style={{ padding: 16 }}>
38+
<EuiForm>
39+
<EuiFormRow
40+
label={intl.formatMessage({
41+
id: 'xpack.infra.waffle.customGroupByFieldLabel',
42+
defaultMessage: 'Field',
43+
})}
44+
helpText={intl.formatMessage({
45+
id: 'xpack.infra.waffle.customGroupByHelpText',
46+
defaultMessage: 'This is the field used for the terms aggregation',
47+
})}
48+
compressed
49+
>
50+
<EuiComboBox
51+
placeholder={intl.formatMessage({
52+
id: 'xpack.infra.waffle.customGroupByDropdownPlacehoder',
53+
defaultMessage: 'Select one',
54+
})}
55+
singleSelection={{ asPlainText: true }}
56+
selectedOptions={this.state.selectedOptions}
57+
options={options}
58+
onChange={this.handleFieldSelection}
59+
isClearable={false}
60+
/>
61+
</EuiFormRow>
62+
<EuiButton type="submit" size="s" fill onClick={this.handleSubmit}>
63+
Add
64+
</EuiButton>
65+
</EuiForm>
66+
</div>
67+
);
68+
}
69+
70+
private handleSubmit = () => {
71+
this.props.onSubmit(this.state.selectedOptions[0].label);
72+
};
73+
74+
private handleFieldSelection = (selectedOptions: SelectedOption[]) => {
75+
this.setState({ selectedOptions });
76+
};
77+
}
78+
);

x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx

Lines changed: 53 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,28 @@ import {
88
EuiBadge,
99
EuiContextMenu,
1010
EuiContextMenuPanelDescriptor,
11+
EuiContextMenuPanelItemDescriptor,
1112
EuiFilterButton,
1213
EuiFilterGroup,
1314
EuiPopover,
1415
} from '@elastic/eui';
1516
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
1617
import React from 'react';
17-
import { InfraNodeType, InfraPathInput, InfraPathType } from '../../graphql/types';
18+
import { InfraIndexField, InfraNodeType, InfraPathInput, InfraPathType } from '../../graphql/types';
19+
import { InfraGroupByOptions } from '../../lib/lib';
20+
import { CustomFieldPanel } from './custom_field_panel';
1821

1922
interface Props {
2023
nodeType: InfraNodeType;
2124
groupBy: InfraPathInput[];
2225
onChange: (groupBy: InfraPathInput[]) => void;
26+
onChangeCustomOptions: (options: InfraGroupByOptions[]) => void;
27+
fields: InfraIndexField[];
2328
intl: InjectedIntl;
29+
customOptions: InfraGroupByOptions[];
2430
}
2531

26-
let OPTIONS: { [P in InfraNodeType]: Array<{ text: string; type: InfraPathType; field: string }> };
32+
let OPTIONS: { [P in InfraNodeType]: InfraGroupByOptions[] };
2733
const getOptions = (
2834
nodeType: InfraNodeType,
2935
intl: InjectedIntl
@@ -143,7 +149,7 @@ export const WaffleGroupByControls = injectI18n(
143149

144150
public render() {
145151
const { nodeType, groupBy, intl } = this.props;
146-
const options = getOptions(nodeType, intl);
152+
const options = getOptions(nodeType, intl).concat(this.props.customOptions);
147153

148154
if (!options.length) {
149155
throw Error(
@@ -165,11 +171,35 @@ export const WaffleGroupByControls = injectI18n(
165171
id: 'xpack.infra.waffle.selectTwoGroupingsTitle',
166172
defaultMessage: 'Select up to two groupings',
167173
}),
168-
items: options.map(o => {
169-
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
170-
const panel = { name: o.text, onClick: this.handleClick(o.field), icon };
171-
return panel;
174+
items: [
175+
{
176+
name: intl.formatMessage({
177+
id: 'xpack.infra.waffle.customGroupByOptionName',
178+
defaultMessage: 'Custom Field',
179+
}),
180+
icon: 'empty',
181+
panel: 'customPanel',
182+
},
183+
...options.map(o => {
184+
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
185+
const panel = {
186+
name: o.text,
187+
onClick: this.handleClick(o.field),
188+
icon,
189+
} as EuiContextMenuPanelItemDescriptor;
190+
return panel;
191+
}),
192+
],
193+
},
194+
{
195+
id: 'customPanel',
196+
title: intl.formatMessage({
197+
id: 'xpack.infra.waffle.customGroupByPanelTitle',
198+
defaultMessage: 'Group By Custom Field',
172199
}),
200+
content: (
201+
<CustomFieldPanel onSubmit={this.handleCustomField} fields={this.props.fields} />
202+
),
173203
},
174204
];
175205
const buttonBody =
@@ -228,6 +258,8 @@ export const WaffleGroupByControls = injectI18n(
228258
private handleRemove = (field: string) => () => {
229259
const { groupBy } = this.props;
230260
this.props.onChange(groupBy.filter(g => g.field !== field));
261+
const options = this.props.customOptions.filter(g => g.field !== field);
262+
this.props.onChangeCustomOptions(options);
231263
// We need to close the panel after we rmeove the pill icon otherwise
232264
// it will remain open because the click is still captured by the EuiFilterButton
233265
setTimeout(() => this.handleClose());
@@ -241,6 +273,20 @@ export const WaffleGroupByControls = injectI18n(
241273
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
242274
};
243275

276+
private handleCustomField = (field: string) => {
277+
const options = [
278+
...this.props.customOptions,
279+
{
280+
text: field,
281+
field,
282+
type: InfraPathType.custom,
283+
},
284+
];
285+
this.props.onChangeCustomOptions(options);
286+
const fn = this.handleClick(field);
287+
fn();
288+
};
289+
244290
private handleClick = (field: string) => () => {
245291
const { groupBy } = this.props;
246292
if (groupBy.some(g => g.field === field)) {

x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,15 @@ function findOrCreateGroupWithNodes(
4444
if (isWaffleMapGroupWithNodes(existingGroup)) {
4545
return existingGroup;
4646
}
47+
const lastPath = last(path);
4748
return {
4849
id,
4950
name:
5051
id === '__all__'
5152
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithNodes.allName', {
5253
defaultMessage: 'All',
5354
})
54-
: last(path).label,
55+
: (lastPath && lastPath.label) || 'No Group',
5556
count: 0,
5657
width: 0,
5758
squareSize: 0,
@@ -68,14 +69,15 @@ function findOrCreateGroupWithGroups(
6869
if (isWaffleMapGroupWithGroups(existingGroup)) {
6970
return existingGroup;
7071
}
72+
const lastPath = last(path);
7173
return {
7274
id,
7375
name:
7476
id === '__all__'
7577
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithGroups.allName', {
7678
defaultMessage: 'All',
7779
})
78-
: last(path).label,
80+
: (lastPath && lastPath.label) || 'No Group',
7981
count: 0,
8082
width: 0,
8183
squareSize: 0,
@@ -85,11 +87,14 @@ function findOrCreateGroupWithGroups(
8587

8688
function createWaffleMapNode(node: InfraNode): InfraWaffleMapNode {
8789
const nodePathItem = last(node.path);
90+
if (!nodePathItem) {
91+
throw new Error('There must be a minimum of one path');
92+
}
8893
return {
8994
pathId: node.path.map(p => p.value).join('/'),
9095
path: node.path,
9196
id: nodePathItem.value,
92-
name: nodePathItem.label,
97+
name: nodePathItem.label || nodePathItem.value,
9398
metric: node.metric,
9499
};
95100
}

x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
InfraNodeType,
1515
InfraPathType,
1616
} from '../../graphql/types';
17+
import { InfraGroupByOptions } from '../../lib/lib';
1718
import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store';
1819
import { asChildFunctionRenderer } from '../../utils/typed_react';
1920
import { bindPlainActionCreators } from '../../utils/typed_redux';
@@ -23,10 +24,12 @@ const selectOptionsUrlState = createSelector(
2324
waffleOptionsSelectors.selectMetric,
2425
waffleOptionsSelectors.selectGroupBy,
2526
waffleOptionsSelectors.selectNodeType,
26-
(metric, groupBy, nodeType) => ({
27+
waffleOptionsSelectors.selectCustomOptions,
28+
(metric, groupBy, nodeType, customOptions) => ({
2729
metric,
2830
groupBy,
2931
nodeType,
32+
customOptions,
3033
})
3134
);
3235

@@ -35,12 +38,14 @@ export const withWaffleOptions = connect(
3538
metric: waffleOptionsSelectors.selectMetric(state),
3639
groupBy: waffleOptionsSelectors.selectGroupBy(state),
3740
nodeType: waffleOptionsSelectors.selectNodeType(state),
41+
customOptions: waffleOptionsSelectors.selectCustomOptions(state),
3842
urlState: selectOptionsUrlState(state),
3943
}),
4044
bindPlainActionCreators({
4145
changeMetric: waffleOptionsActions.changeMetric,
4246
changeGroupBy: waffleOptionsActions.changeGroupBy,
4347
changeNodeType: waffleOptionsActions.changeNodeType,
48+
changeCustomOptions: waffleOptionsActions.changeCustomOptions,
4449
})
4550
);
4651

@@ -54,11 +59,12 @@ interface WaffleOptionsUrlState {
5459
metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
5560
groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
5661
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
62+
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
5763
}
5864

5965
export const WithWaffleOptionsUrlState = () => (
6066
<WithWaffleOptions>
61-
{({ changeMetric, urlState, changeGroupBy, changeNodeType }) => (
67+
{({ changeMetric, urlState, changeGroupBy, changeNodeType, changeCustomOptions }) => (
6268
<UrlStateContainer
6369
urlState={urlState}
6470
urlStateKey="waffleOptions"
@@ -73,6 +79,9 @@ export const WithWaffleOptionsUrlState = () => (
7379
if (newUrlState && newUrlState.nodeType) {
7480
changeNodeType(newUrlState.nodeType);
7581
}
82+
if (newUrlState && newUrlState.customOptions) {
83+
changeCustomOptions(newUrlState.customOptions);
84+
}
7685
}}
7786
onInitialize={initialUrlState => {
7887
if (initialUrlState && initialUrlState.metric) {
@@ -84,6 +93,9 @@ export const WithWaffleOptionsUrlState = () => (
8493
if (initialUrlState && initialUrlState.nodeType) {
8594
changeNodeType(initialUrlState.nodeType);
8695
}
96+
if (initialUrlState && initialUrlState.customOptions) {
97+
changeCustomOptions(initialUrlState.customOptions);
98+
}
8799
}}
88100
/>
89101
)}
@@ -96,6 +108,7 @@ const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined =>
96108
metric: mapToMetricUrlState(value.metric),
97109
groupBy: mapToGroupByUrlState(value.groupBy),
98110
nodeType: mapToNodeTypeUrlState(value.nodeType),
111+
customOptions: mapToCustomOptionsUrlState(value.customOptions),
99112
}
100113
: undefined;
101114

@@ -107,6 +120,15 @@ const isInfraPathInput = (subject: any): subject is InfraPathType => {
107120
return subject != null && subject.type != null && InfraPathType[subject.type] != null;
108121
};
109122

123+
const isInfraGroupByOption = (subject: any): subject is InfraGroupByOptions => {
124+
return (
125+
subject != null &&
126+
subject.text != null &&
127+
subject.field != null &&
128+
InfraPathType[subject.type] != null
129+
);
130+
};
131+
110132
const mapToMetricUrlState = (subject: any) => {
111133
return subject && isInfraMetricInput(subject) ? subject : undefined;
112134
};
@@ -118,3 +140,9 @@ const mapToGroupByUrlState = (subject: any) => {
118140
const mapToNodeTypeUrlState = (subject: any) => {
119141
return subject && InfraNodeType[subject] ? subject : undefined;
120142
};
143+
144+
const mapToCustomOptionsUrlState = (subject: any) => {
145+
return subject && Array.isArray(subject) && subject.every(isInfraGroupByOption)
146+
? subject
147+
: undefined;
148+
};

x-pack/plugins/infra/public/graphql/introspection.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"fields": [
1212
{
1313
"name": "source",
14-
"description": "Get an infrastructure data source by id.\n\nThe resolution order for the source configuration attributes is as follows\nwith the first defined value winning:\n\n1. The attributes of the saved object with the given 'id'.\n2. The attributes defined in the static Kibana configuration key\n 'xpack.infra.sources.default'.\n3. The hard-coded default values.\n\nAs a consequence, querying a source without a corresponding saved object\ndoesn't error out, but returns the configured or hardcoded defaults.",
14+
"description": "Get an infrastructure data source by id.\n\nThe resolution order for the source configuration attributes is as follows\nwith the first defined value winning:\n\n1. The attributes of the saved object with the given 'id'.\n2. The attributes defined in the static Kibana configuration key\n 'xpack.infra.sources.default'.\n3. The hard-coded default values.\n\nAs a consequence, querying a source that doesn't exist doesn't error out,\nbut returns the configured or hardcoded defaults.",
1515
"args": [
1616
{
1717
"name": "id",
@@ -1480,7 +1480,8 @@
14801480
"description": "",
14811481
"isDeprecated": false,
14821482
"deprecationReason": null
1483-
}
1483+
},
1484+
{ "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null }
14841485
],
14851486
"possibleTypes": null
14861487
},

x-pack/plugins/infra/public/graphql/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
// ====================================================
1010

1111
export interface Query {
12-
/** Get an infrastructure data source by id.The resolution order for the source configuration attributes is as followswith the first defined value winning:1. The attributes of the saved object with the given 'id'.2. The attributes defined in the static Kibana configuration key'xpack.infra.sources.default'.3. The hard-coded default values.As a consequence, querying a source without a corresponding saved objectdoesn't error out, but returns the configured or hardcoded defaults. */
12+
/** Get an infrastructure data source by id.The resolution order for the source configuration attributes is as followswith the first defined value winning:1. The attributes of the saved object with the given 'id'.2. The attributes defined in the static Kibana configuration key'xpack.infra.sources.default'.3. The hard-coded default values.As a consequence, querying a source that doesn't exist doesn't error out,but returns the configured or hardcoded defaults. */
1313
source: InfraSource;
1414
/** Get a list of all infrastructure data sources */
1515
allSources: InfraSource[];
@@ -456,6 +456,7 @@ export enum InfraPathType {
456456
hosts = 'hosts',
457457
pods = 'pods',
458458
containers = 'containers',
459+
custom = 'custom',
459460
}
460461

461462
export enum InfraMetricType {

x-pack/plugins/infra/public/lib/lib.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
InfraNodeMetric,
1616
InfraNodePath,
1717
InfraPathInput,
18+
InfraPathType,
1819
InfraTimerangeInput,
1920
SourceQuery,
2021
} from '../graphql/types';
@@ -204,3 +205,9 @@ export enum InfraWaffleMapDataFormat {
204205
bitsBinaryJEDEC = 'bitsBinaryJEDEC',
205206
abbreviatedNumber = 'abbreviatedNumber',
206207
}
208+
209+
export interface InfraGroupByOptions {
210+
text: string;
211+
type: InfraPathType;
212+
field: string;
213+
}

0 commit comments

Comments
 (0)