Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Infra UI] Custom Field Grouping for Waffle Map #28949

Merged
merged 9 commits into from
Jan 30, 2019
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