Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { EuiButton, EuiComboBox, EuiForm, EuiFormRow } from '@elastic/eui';
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraIndexField } from 'x-pack/plugins/infra/server/graphql/types';
interface Props {
onSubmit: (field: string) => void;
fields: InfraIndexField[];
intl: InjectedIntl;
}

interface SelectedOption {
label: string;
}

const initialState = {
selectedOptions: [] as SelectedOption[],
};

type State = Readonly<typeof initialState>;

export const CustomFieldPanel = injectI18n(
class extends React.PureComponent<Props, State> {
public static displayName = 'CustomFieldPanel';
public readonly state: State = initialState;
public render() {
const { fields, intl } = this.props;
const options = fields
.filter(f => f.aggregatable && f.type === 'string')
.map(f => ({ label: f.name }));
return (
<div style={{ padding: 16 }}>
<EuiForm>
<EuiFormRow
label={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByFieldLabel',
defaultMessage: 'Field',
})}
helpText={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByHelpText',
defaultMessage: 'This is the field used for the terms aggregation',
})}
compressed
>
<EuiComboBox
placeholder={intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByDropdownPlacehoder',
defaultMessage: 'Select one',
})}
singleSelection={{ asPlainText: true }}
selectedOptions={this.state.selectedOptions}
options={options}
onChange={this.handleFieldSelection}
isClearable={false}
/>
</EuiFormRow>
<EuiButton type="submit" size="s" fill onClick={this.handleSubmit}>
Add
</EuiButton>
</EuiForm>
</div>
);
}

private handleSubmit = () => {
this.props.onSubmit(this.state.selectedOptions[0].label);
};

private handleFieldSelection = (selectedOptions: SelectedOption[]) => {
this.setState({ selectedOptions });
};
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,28 @@ import {
EuiBadge,
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiContextMenuPanelItemDescriptor,
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { FormattedMessage, InjectedIntl, injectI18n } from '@kbn/i18n/react';
import React from 'react';
import { InfraNodeType, InfraPathInput, InfraPathType } from '../../graphql/types';
import { InfraIndexField, InfraNodeType, InfraPathInput, InfraPathType } from '../../graphql/types';
import { InfraGroupByOptions } from '../../lib/lib';
import { CustomFieldPanel } from './custom_field_panel';

interface Props {
nodeType: InfraNodeType;
groupBy: InfraPathInput[];
onChange: (groupBy: InfraPathInput[]) => void;
onChangeCustomOptions: (options: InfraGroupByOptions[]) => void;
fields: InfraIndexField[];
intl: InjectedIntl;
customOptions: InfraGroupByOptions[];
}

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

public render() {
const { nodeType, groupBy, intl } = this.props;
const options = getOptions(nodeType, intl);
const options = getOptions(nodeType, intl).concat(this.props.customOptions);

if (!options.length) {
throw Error(
Expand All @@ -165,11 +171,35 @@ export const WaffleGroupByControls = injectI18n(
id: 'xpack.infra.waffle.selectTwoGroupingsTitle',
defaultMessage: 'Select up to two groupings',
}),
items: options.map(o => {
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.field), icon };
return panel;
items: [
{
name: intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByOptionName',
defaultMessage: 'Custom Field',
}),
icon: 'empty',
panel: 'customPanel',
},
...options.map(o => {
const icon = groupBy.some(g => g.field === o.field) ? 'check' : 'empty';
const panel = {
name: o.text,
onClick: this.handleClick(o.field),
icon,
} as EuiContextMenuPanelItemDescriptor;
return panel;
}),
],
},
{
id: 'customPanel',
title: intl.formatMessage({
id: 'xpack.infra.waffle.customGroupByPanelTitle',
defaultMessage: 'Group By Custom Field',
}),
content: (
<CustomFieldPanel onSubmit={this.handleCustomField} fields={this.props.fields} />
),
},
];
const buttonBody =
Expand Down Expand Up @@ -228,6 +258,8 @@ export const WaffleGroupByControls = injectI18n(
private handleRemove = (field: string) => () => {
const { groupBy } = this.props;
this.props.onChange(groupBy.filter(g => g.field !== field));
const options = this.props.customOptions.filter(g => g.field !== field);
this.props.onChangeCustomOptions(options);
// We need to close the panel after we rmeove the pill icon otherwise
// it will remain open because the click is still captured by the EuiFilterButton
setTimeout(() => this.handleClose());
Expand All @@ -241,6 +273,20 @@ export const WaffleGroupByControls = injectI18n(
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};

private handleCustomField = (field: string) => {
const options = [
...this.props.customOptions,
{
text: field,
field,
type: InfraPathType.custom,
},
];
this.props.onChangeCustomOptions(options);
const fn = this.handleClick(field);
fn();
};

private handleClick = (field: string) => () => {
const { groupBy } = this.props;
if (groupBy.some(g => g.field === field)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,15 @@ function findOrCreateGroupWithNodes(
if (isWaffleMapGroupWithNodes(existingGroup)) {
return existingGroup;
}
const lastPath = last(path);
return {
id,
name:
id === '__all__'
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithNodes.allName', {
defaultMessage: 'All',
})
: last(path).label,
: (lastPath && lastPath.label) || 'No Group',
count: 0,
width: 0,
squareSize: 0,
Expand All @@ -68,14 +69,15 @@ function findOrCreateGroupWithGroups(
if (isWaffleMapGroupWithGroups(existingGroup)) {
return existingGroup;
}
const lastPath = last(path);
return {
id,
name:
id === '__all__'
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithGroups.allName', {
defaultMessage: 'All',
})
: last(path).label,
: (lastPath && lastPath.label) || 'No Group',
count: 0,
width: 0,
squareSize: 0,
Expand All @@ -85,11 +87,14 @@ function findOrCreateGroupWithGroups(

function createWaffleMapNode(node: InfraNode): InfraWaffleMapNode {
const nodePathItem = last(node.path);
if (!nodePathItem) {
throw new Error('There must be a minimum of one path');
}
return {
pathId: node.path.map(p => p.value).join('/'),
path: node.path,
id: nodePathItem.value,
name: nodePathItem.label,
name: nodePathItem.label || nodePathItem.value,
metric: node.metric,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
InfraNodeType,
InfraPathType,
} from '../../graphql/types';
import { InfraGroupByOptions } from '../../lib/lib';
import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store';
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
Expand All @@ -23,10 +24,12 @@ const selectOptionsUrlState = createSelector(
waffleOptionsSelectors.selectMetric,
waffleOptionsSelectors.selectGroupBy,
waffleOptionsSelectors.selectNodeType,
(metric, groupBy, nodeType) => ({
waffleOptionsSelectors.selectCustomOptions,
(metric, groupBy, nodeType, customOptions) => ({
metric,
groupBy,
nodeType,
customOptions,
})
);

Expand All @@ -35,12 +38,14 @@ export const withWaffleOptions = connect(
metric: waffleOptionsSelectors.selectMetric(state),
groupBy: waffleOptionsSelectors.selectGroupBy(state),
nodeType: waffleOptionsSelectors.selectNodeType(state),
customOptions: waffleOptionsSelectors.selectCustomOptions(state),
urlState: selectOptionsUrlState(state),
}),
bindPlainActionCreators({
changeMetric: waffleOptionsActions.changeMetric,
changeGroupBy: waffleOptionsActions.changeGroupBy,
changeNodeType: waffleOptionsActions.changeNodeType,
changeCustomOptions: waffleOptionsActions.changeCustomOptions,
})
);

Expand All @@ -54,11 +59,12 @@ interface WaffleOptionsUrlState {
metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
}

export const WithWaffleOptionsUrlState = () => (
<WithWaffleOptions>
{({ changeMetric, urlState, changeGroupBy, changeNodeType }) => (
{({ changeMetric, urlState, changeGroupBy, changeNodeType, changeCustomOptions }) => (
<UrlStateContainer
urlState={urlState}
urlStateKey="waffleOptions"
Expand All @@ -73,6 +79,9 @@ export const WithWaffleOptionsUrlState = () => (
if (newUrlState && newUrlState.nodeType) {
changeNodeType(newUrlState.nodeType);
}
if (newUrlState && newUrlState.customOptions) {
changeCustomOptions(newUrlState.customOptions);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.metric) {
Expand All @@ -84,6 +93,9 @@ export const WithWaffleOptionsUrlState = () => (
if (initialUrlState && initialUrlState.nodeType) {
changeNodeType(initialUrlState.nodeType);
}
if (initialUrlState && initialUrlState.customOptions) {
changeCustomOptions(initialUrlState.customOptions);
}
}}
/>
)}
Expand All @@ -96,6 +108,7 @@ const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined =>
metric: mapToMetricUrlState(value.metric),
groupBy: mapToGroupByUrlState(value.groupBy),
nodeType: mapToNodeTypeUrlState(value.nodeType),
customOptions: mapToCustomOptionsUrlState(value.customOptions),
}
: undefined;

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

const isInfraGroupByOption = (subject: any): subject is InfraGroupByOptions => {
return (
subject != null &&
subject.text != null &&
subject.field != null &&
InfraPathType[subject.type] != null
);
};

const mapToMetricUrlState = (subject: any) => {
return subject && isInfraMetricInput(subject) ? subject : undefined;
};
Expand All @@ -118,3 +140,9 @@ const mapToGroupByUrlState = (subject: any) => {
const mapToNodeTypeUrlState = (subject: any) => {
return subject && InfraNodeType[subject] ? subject : undefined;
};

const mapToCustomOptionsUrlState = (subject: any) => {
return subject && Array.isArray(subject) && subject.every(isInfraGroupByOption)
? subject
: undefined;
};
5 changes: 3 additions & 2 deletions x-pack/plugins/infra/public/graphql/introspection.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"fields": [
{
"name": "source",
"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.",
"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.",
"args": [
{
"name": "id",
Expand Down Expand Up @@ -1480,7 +1480,8 @@
"description": "",
"isDeprecated": false,
"deprecationReason": null
}
},
{ "name": "custom", "description": "", "isDeprecated": false, "deprecationReason": null }
],
"possibleTypes": null
},
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/infra/public/graphql/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
// ====================================================

export interface Query {
/** 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. */
/** 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. */
source: InfraSource;
/** Get a list of all infrastructure data sources */
allSources: InfraSource[];
Expand Down Expand Up @@ -456,6 +456,7 @@ export enum InfraPathType {
hosts = 'hosts',
pods = 'pods',
containers = 'containers',
custom = 'custom',
}

export enum InfraMetricType {
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/infra/public/lib/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
InfraNodeMetric,
InfraNodePath,
InfraPathInput,
InfraPathType,
InfraTimerangeInput,
SourceQuery,
} from '../graphql/types';
Expand Down Expand Up @@ -204,3 +205,9 @@ export enum InfraWaffleMapDataFormat {
bitsBinaryJEDEC = 'bitsBinaryJEDEC',
abbreviatedNumber = 'abbreviatedNumber',
}

export interface InfraGroupByOptions {
text: string;
type: InfraPathType;
field: string;
}
Loading