[Metrics UI] Custom Metrics for Inventory View (#58072)

* [Metrics UI] Add custom metrics interface to Inventory View

* WIP

* Adding workflows for editing custom metrics

* Polishing visual design

* Removing extra text

* Fixing types and return values

* fixing i18n

* Adding aria labels for clearity

* Changing custom group by to match same width as custom metric

* updating integration test for custom metrics

* Fixing type def
This commit is contained in:
Chris Cowan 2020-02-26 09:36:43 -07:00 committed by GitHub
parent 613e4b9b15
commit 8021fd887a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 986 additions and 120 deletions

View file

@ -54,16 +54,41 @@ export const SnapshotGroupByRT = rt.array(
})
);
export const SnapshotMetricInputRT = rt.type({
export const SnapshotNamedMetricInputRT = rt.type({
type: SnapshotMetricTypeRT,
});
export const SNAPSHOT_CUSTOM_AGGREGATIONS = ['avg', 'max', 'min', 'rate'] as const;
export type SnapshotCustomAggregation = typeof SNAPSHOT_CUSTOM_AGGREGATIONS[number];
const snapshotCustomAggregationKeys = SNAPSHOT_CUSTOM_AGGREGATIONS.reduce<
Record<SnapshotCustomAggregation, null>
>((acc, agg) => ({ ...acc, [agg]: null }), {} as Record<SnapshotCustomAggregation, null>);
export const SnapshotCustomAggregationRT = rt.keyof(snapshotCustomAggregationKeys);
export const SnapshotCustomMetricInputRT = rt.intersection([
rt.type({
type: rt.literal('custom'),
field: rt.string,
aggregation: SnapshotCustomAggregationRT,
id: rt.string,
}),
rt.partial({
label: rt.string,
}),
]);
export const SnapshotMetricInputRT = rt.union([
SnapshotNamedMetricInputRT,
SnapshotCustomMetricInputRT,
]);
export const SnapshotRequestRT = rt.intersection([
rt.type({
timerange: InfraTimerangeInputRT,
metric: rt.type({
type: SnapshotMetricTypeRT,
}),
metric: SnapshotMetricInputRT,
groupBy: SnapshotGroupByRT,
nodeType: ItemTypeRT,
sourceId: rt.string,
@ -77,6 +102,7 @@ export const SnapshotRequestRT = rt.intersection([
export type SnapshotNodePath = rt.TypeOf<typeof SnapshotNodePathRT>;
export type SnapshotMetricInput = rt.TypeOf<typeof SnapshotMetricInputRT>;
export type SnapshotCustomMetricInput = rt.TypeOf<typeof SnapshotCustomMetricInputRT>;
export type InfraTimerangeInput = rt.TypeOf<typeof InfraTimerangeInputRT>;
export type SnapshotNodeMetric = rt.TypeOf<typeof SnapshotNodeMetricRT>;
export type SnapshotGroupBy = rt.TypeOf<typeof SnapshotGroupByRT>;

View file

@ -9,7 +9,7 @@ import { EuiFlexItem } from '@elastic/eui';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { ToolbarProps } from '../../../../public/components/inventory/toolbars/toolbar';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { WaffleMetricControls } from '../../../../public/components/waffle/waffle_metric_controls';
import { WaffleMetricControls } from '../../../../public/components/waffle/metric_control';
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
import { WaffleGroupByControls } from '../../../../public/components/waffle/waffle_group_by_controls';
import {
@ -25,7 +25,11 @@ interface Props extends ToolbarProps {
}
export const MetricsAndGroupByToolbarItems = (props: Props) => {
const metricOptions = useMemo(() => props.metricTypes.map(toMetricOpt), [props.metricTypes]);
const metricOptions = useMemo(
() =>
props.metricTypes.map(toMetricOpt).filter(v => v) as Array<{ text: string; value: string }>,
[props.metricTypes]
);
const groupByOptions = useMemo(() => props.groupByFields.map(toGroupByOpt), [
props.groupByFields,
@ -35,9 +39,12 @@ export const MetricsAndGroupByToolbarItems = (props: Props) => {
<>
<EuiFlexItem grow={false}>
<WaffleMetricControls
fields={props.createDerivedIndexPattern('metrics').fields}
options={metricOptions}
metric={props.metric}
onChange={props.changeMetric}
onChangeCustomMetrics={props.changeCustomMetrics}
customMetrics={props.customMetrics}
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>

View file

@ -305,6 +305,7 @@ export const SnapshotMetricTypeRT = rt.keyof({
sqsMessagesSent: null,
sqsMessagesEmpty: null,
sqsOldestMessage: null,
custom: null,
});
export type SnapshotMetricType = rt.TypeOf<typeof SnapshotMetricTypeRT>;

View file

@ -26,6 +26,18 @@ export const inventoryViewSavedObjectMappings: {
type: {
type: 'keyword',
},
field: {
type: 'keyword',
},
aggregation: {
type: 'keyword',
},
id: {
type: 'keyword',
},
label: {
type: 'keyword',
},
},
},
groupBy: {
@ -56,6 +68,26 @@ export const inventoryViewSavedObjectMappings: {
},
},
},
customMetrics: {
type: 'nested',
properties: {
type: {
type: 'keyword',
},
field: {
type: 'keyword',
},
aggregation: {
type: 'keyword',
},
id: {
type: 'keyword',
},
label: {
type: 'keyword',
},
},
},
boundsOverride: {
properties: {
max: {

View file

@ -7,7 +7,11 @@
import React, { FunctionComponent } from 'react';
import { Action } from 'typescript-fsa';
import { EuiFlexItem } from '@elastic/eui';
import { SnapshotMetricInput, SnapshotGroupBy } from '../../../../common/http_api/snapshot_api';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
} from '../../../../common/http_api/snapshot_api';
import { InventoryCloudAccount } from '../../../../common/http_api/inventory_meta_api';
import { findToolbar } from '../../../../common/inventory_models/toolbars';
import { ToolbarWrapper } from './toolbar_wrapper';
@ -35,6 +39,10 @@ export interface ToolbarProps {
region: ReturnType<typeof waffleOptionsSelectors.selectRegion>;
accounts: InventoryCloudAccount[];
regions: string[];
customMetrics: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
changeCustomMetrics: (
payload: SnapshotCustomMetricInput[]
) => Action<SnapshotCustomMetricInput[]>;
}
const wrapToolbarItems = (

View file

@ -37,6 +37,8 @@ export const ToolbarWrapper = (props: Props) => {
nodeType,
accountId,
region,
customMetrics,
changeCustomMetrics,
}) =>
props.children({
createDerivedIndexPattern,
@ -51,6 +53,8 @@ export const ToolbarWrapper = (props: Props) => {
nodeType,
region,
accountId,
customMetrics,
changeCustomMetrics,
})
}
</WithWaffleOptions>
@ -146,7 +150,7 @@ export const toGroupByOpt = (field: string) => ({
export const toMetricOpt = (
metric: SnapshotMetricType
): { text: string; value: SnapshotMetricType } => {
): { text: string; value: SnapshotMetricType } | undefined => {
switch (metric) {
case 'cpu':
return {

View file

@ -19,9 +19,10 @@ import { InfraLoadingPanel } from '../loading';
import { Map } from '../waffle/map';
import { ViewSwitcher } from '../waffle/view_switcher';
import { TableView } from './table';
import { SnapshotNode } from '../../../common/http_api/snapshot_api';
import { SnapshotNode, SnapshotCustomMetricInputRT } from '../../../common/http_api/snapshot_api';
import { convertIntervalToString } from '../../utils/convert_interval_to_string';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { createFormatterForMetric } from '../metrics_explorer/helpers/create_formatter_for_metric';
interface Props {
options: InfraWaffleMapOptions;
@ -211,6 +212,10 @@ export const NodesOverview = class extends React.Component<Props, {}> {
// TODO: Change this to a real implimentation using the tickFormatter from the prototype as an example.
private formatter = (val: string | number) => {
const { metric } = this.props.options;
if (SnapshotCustomMetricInputRT.is(metric)) {
const formatter = createFormatterForMetric(metric);
return formatter(val);
}
const metricFormatter = get(METRIC_FORMATTERS, metric.type, METRIC_FORMATTERS.count);
if (val == null) {
return '';

View file

@ -50,10 +50,10 @@ export const CustomFieldPanel = class extends React.PureComponent<Props, State>
helpText={i18n.translate('xpack.infra.waffle.customGroupByHelpText', {
defaultMessage: 'This is the field used for the terms aggregation',
})}
compressed
display="rowCompressed"
fullWidth
>
<EuiComboBox
compressed
placeholder={i18n.translate('xpack.infra.waffle.customGroupByDropdownPlacehoder', {
defaultMessage: 'Select one',
})}
@ -61,6 +61,7 @@ export const CustomFieldPanel = class extends React.PureComponent<Props, State>
selectedOptions={this.state.selectedOptions}
options={options}
onChange={this.handleFieldSelection}
fullWidth
isClearable={false}
/>
</EuiFormRow>

View file

@ -0,0 +1,247 @@
/*
* 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 React, { useState, useCallback } from 'react';
import uuid from 'uuid';
import {
EuiForm,
EuiButton,
EuiButtonEmpty,
EuiFormRow,
EuiFieldText,
EuiComboBox,
EuiSelect,
EuiFlexGroup,
EuiFlexItem,
EuiText,
EuiPopoverTitle,
} from '@elastic/eui';
import { IFieldType } from 'src/plugins/data/public';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import {
SnapshotCustomAggregation,
SnapshotCustomMetricInput,
SNAPSHOT_CUSTOM_AGGREGATIONS,
SnapshotCustomAggregationRT,
} from '../../../../common/http_api/snapshot_api';
import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components';
interface SelectedOption {
label: string;
}
const AGGREGATION_LABELS = {
['avg']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.avg', {
defaultMessage: 'Average',
}),
['max']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.max', {
defaultMessage: 'Max',
}),
['min']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.min', {
defaultMessage: 'Min',
}),
['rate']: i18n.translate('xpack.infra.waffle.customMetrics.aggregationLables.rate', {
defaultMessage: 'Rate',
}),
};
interface Props {
theme: EuiTheme;
metric?: SnapshotCustomMetricInput;
fields: IFieldType[];
customMetrics: SnapshotCustomMetricInput[];
onChange: (metric: SnapshotCustomMetricInput) => void;
onCancel: () => void;
}
export const CustomMetricForm = withTheme(
({ theme, onCancel, fields, onChange, metric }: Props) => {
const [label, setLabel] = useState<string | undefined>(metric ? metric.label : void 0);
const [aggregation, setAggregation] = useState<SnapshotCustomAggregation>(
metric ? metric.aggregation : 'avg'
);
const [field, setField] = useState<string | undefined>(metric ? metric.field : void 0);
const handleSubmit = useCallback(() => {
if (metric && aggregation && field) {
onChange({
...metric,
label,
aggregation,
field,
});
} else if (aggregation && field) {
const newMetric: SnapshotCustomMetricInput = {
type: 'custom',
id: uuid.v1(),
label,
aggregation,
field,
};
onChange(newMetric);
}
}, [metric, aggregation, field, onChange, label]);
const handleLabelChange = useCallback(
e => {
setLabel(e.target.value);
},
[setLabel]
);
const handleFieldChange = useCallback(
(selectedOptions: SelectedOption[]) => {
setField(selectedOptions[0].label);
},
[setField]
);
const handleAggregationChange = useCallback(
e => {
const value = e.target.value;
const aggValue: SnapshotCustomAggregation = SnapshotCustomAggregationRT.is(value)
? value
: 'avg';
setAggregation(aggValue);
},
[setAggregation]
);
const fieldOptions = fields
.filter(f => f.aggregatable && f.type === 'number' && !(field && field === f.name))
.map(f => ({ label: f.name }));
const aggregationOptions = SNAPSHOT_CUSTOM_AGGREGATIONS.map(k => ({
text: AGGREGATION_LABELS[k as SnapshotCustomAggregation],
value: k,
}));
const isSubmitDisabled = !field || !aggregation;
const title = metric
? i18n.translate('xpack.infra.waffle.customMetricPanelLabel.edit', {
defaultMessage: 'Edit custom metric',
})
: i18n.translate('xpack.infra.waffle.customMetricPanelLabel.add', {
defaultMessage: 'Add custom metric',
});
const titleAriaLabel = metric
? i18n.translate('xpack.infra.waffle.customMetricPanelLabel.editAriaLabel', {
defaultMessage: 'Back to custom metrics edit mode',
})
: i18n.translate('xpack.infra.waffle.customMetricPanelLabel.addAriaLabel', {
defaultMessage: 'Back to metric picker',
});
return (
<div style={{ width: 685 }}>
<EuiForm>
<EuiPopoverTitle>
<EuiButtonEmpty
iconType="arrowLeft"
onClick={onCancel}
color="text"
size="xs"
flush="left"
style={{ fontWeight: 700, textTransform: 'uppercase' }}
aria-label={titleAriaLabel}
>
{title}
</EuiButtonEmpty>
</EuiPopoverTitle>
<div
style={{
padding: theme.eui.paddingSizes.m,
borderBottom: `${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiBorderColor}`,
}}
>
<EuiFormRow
label={i18n.translate('xpack.infra.waffle.customMetrics.metricLabel', {
defaultMessage: 'Metric',
})}
display="rowCompressed"
fullWidth
>
<EuiFlexGroup alignItems="center" gutterSize="s">
<EuiFlexItem grow={false}>
<EuiSelect
onChange={handleAggregationChange}
value={aggregation}
options={aggregationOptions}
fullWidth
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiText color="subdued">
<span>of</span>
</EuiText>
</EuiFlexItem>
<EuiFlexItem>
<EuiComboBox
fullWidth
placeholder={i18n.translate(
'xpack.infra.waffle.customMetrics.fieldPlaceholder',
{
defaultMessage: 'Select a field',
}
)}
singleSelection={{ asPlainText: true }}
selectedOptions={field ? [{ label: field }] : []}
options={fieldOptions}
onChange={handleFieldChange}
isClearable={false}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFormRow>
<EuiFormRow
label={i18n.translate('xpack.infra.waffle.customMetrics.labelLabel', {
defaultMessage: 'Label (optional)',
})}
display="rowCompressed"
fullWidth
>
<EuiFieldText
name="label"
placeholder={i18n.translate('xpack.infra.waffle.customMetrics.labelPlaceholder', {
defaultMessage: 'Choose a name to appear in the "Metric" dropdown',
})}
value={label}
fullWidth
onChange={handleLabelChange}
/>
</EuiFormRow>
</div>
<div style={{ padding: theme.eui.paddingSizes.m, textAlign: 'right' }}>
<EuiButtonEmpty
onClick={onCancel}
size="s"
style={{ paddingRight: theme.eui.paddingSizes.xl }}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.cancelLabel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
<EuiButton
type="submit"
size="s"
fill
onClick={handleSubmit}
disabled={isSubmitDisabled}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.submitLabel"
defaultMessage="Save"
/>
</EuiButton>
</div>
</EuiForm>
</div>
);
}
);

View file

@ -0,0 +1,30 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api';
export const getCustomMetricLabel = (metric: SnapshotCustomMetricInput) => {
const METRIC_LABELS = {
avg: i18n.translate('xpack.infra.waffle.aggregationNames.avg', {
defaultMessage: 'Avg of {field}',
values: { field: metric.field },
}),
max: i18n.translate('xpack.infra.waffle.aggregationNames.max', {
defaultMessage: 'Max of {field}',
values: { field: metric.field },
}),
min: i18n.translate('xpack.infra.waffle.aggregationNames.min', {
defaultMessage: 'Min of {field}',
values: { field: metric.field },
}),
rate: i18n.translate('xpack.infra.waffle.aggregationNames.rate', {
defaultMessage: 'Rate of {field}',
values: { field: metric.field },
}),
};
return metric.label ? metric.label : METRIC_LABELS[metric.aggregation];
};

View file

@ -0,0 +1,199 @@
/*
* 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 { EuiFilterButton, EuiFilterGroup, EuiPopover } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React, { useState, useCallback } from 'react';
import { IFieldType } from 'src/plugins/data/public';
import {
SnapshotMetricInput,
SnapshotCustomMetricInput,
SnapshotCustomMetricInputRT,
} from '../../../../common/http_api/snapshot_api';
import { CustomMetricForm } from './custom_metric_form';
import { getCustomMetricLabel } from './get_custom_metric_label';
import { MetricsContextMenu } from './metrics_context_menu';
import { ModeSwitcher } from './mode_switcher';
import { MetricsEditMode } from './metrics_edit_mode';
import { CustomMetricMode } from './types';
import { SnapshotMetricType } from '../../../../common/inventory_models/types';
interface Props {
options: Array<{ text: string; value: string }>;
metric: SnapshotMetricInput;
fields: IFieldType[];
onChange: (metric: SnapshotMetricInput) => void;
onChangeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void;
customMetrics: SnapshotCustomMetricInput[];
}
export const WaffleMetricControls = ({
fields,
onChange,
onChangeCustomMetrics,
metric,
options,
customMetrics,
}: Props) => {
const [isPopoverOpen, setPopoverState] = useState<boolean>(false);
const [mode, setMode] = useState<CustomMetricMode>('pick');
const [editModeCustomMetrics, setEditModeCustomMetrics] = useState<SnapshotCustomMetricInput[]>(
[]
);
const [editCustomMetric, setEditCustomMetric] = useState<SnapshotCustomMetricInput | undefined>();
const handleClose = useCallback(() => {
setPopoverState(false);
}, [setPopoverState]);
const handleToggle = useCallback(() => {
setPopoverState(!isPopoverOpen);
}, [isPopoverOpen]);
const handleCustomMetric = useCallback(
(newMetric: SnapshotCustomMetricInput) => {
onChangeCustomMetrics([...customMetrics, newMetric]);
onChange(newMetric);
setMode('pick');
},
[customMetrics, onChange, onChangeCustomMetrics, setMode]
);
const setModeToEdit = useCallback(() => {
setMode('edit');
setEditModeCustomMetrics(customMetrics);
}, [customMetrics]);
const setModeToAdd = useCallback(() => {
setMode('addMetric');
}, [setMode]);
const setModeToPick = useCallback(() => {
setMode('pick');
setEditModeCustomMetrics([]);
}, [setMode]);
const handleDeleteCustomMetric = useCallback(
(m: SnapshotCustomMetricInput) => {
// If the metric we are deleting is the currently selected metric
// we need to change to the default.
if (SnapshotCustomMetricInputRT.is(metric) && m.id === metric.id) {
onChange({ type: options[0].value as SnapshotMetricType });
}
// Filter out the deleted metric from the editbale.
const newMetrics = editModeCustomMetrics.filter(v => v.id !== m.id);
setEditModeCustomMetrics(newMetrics);
},
[editModeCustomMetrics, metric, onChange, options]
);
const handleEditCustomMetric = useCallback(
(currentMetric: SnapshotCustomMetricInput) => {
const newMetrics = customMetrics.map(m => (m.id === currentMetric.id && currentMetric) || m);
onChangeCustomMetrics(newMetrics);
setModeToPick();
setEditCustomMetric(void 0);
setEditModeCustomMetrics([]);
},
[customMetrics, onChangeCustomMetrics, setModeToPick]
);
const handleSelectMetricToEdit = useCallback(
(currentMetric: SnapshotCustomMetricInput) => {
setEditCustomMetric(currentMetric);
setMode('editMetric');
},
[setMode, setEditCustomMetric]
);
const handleSaveEdit = useCallback(() => {
onChangeCustomMetrics(editModeCustomMetrics);
setMode('pick');
}, [editModeCustomMetrics, onChangeCustomMetrics]);
if (!options.length || !metric.type) {
throw Error(
i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', {
defaultMessage: 'Unable to select options or value for metric.',
})
);
}
const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type;
const currentLabel = SnapshotCustomMetricInputRT.is(metric)
? getCustomMetricLabel(metric)
: options.find(o => o.value === id)?.text;
if (!currentLabel) {
return null;
}
const button = (
<EuiFilterButton iconType="arrowDown" onClick={handleToggle}>
<FormattedMessage
id="xpack.infra.waffle.metricButtonLabel"
defaultMessage="Metric: {selectedMetric}"
values={{ selectedMetric: currentLabel }}
/>
</EuiFilterButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={isPopoverOpen}
id="metricsPanel"
button={button}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={handleClose}
>
{mode === 'pick' ? (
<MetricsContextMenu
onChange={onChange}
onClose={handleClose}
metric={metric}
customMetrics={customMetrics}
options={options}
/>
) : null}
{mode === 'addMetric' ? (
<CustomMetricForm
fields={fields}
customMetrics={customMetrics}
onChange={handleCustomMetric}
onCancel={setModeToPick}
/>
) : null}
{mode === 'editMetric' ? (
<CustomMetricForm
metric={editCustomMetric}
fields={fields}
customMetrics={customMetrics}
onChange={handleEditCustomMetric}
onCancel={setModeToEdit}
/>
) : null}
{mode === 'edit' ? (
<MetricsEditMode
customMetrics={editModeCustomMetrics}
options={options}
onEdit={handleSelectMetricToEdit}
onDelete={handleDeleteCustomMetric}
/>
) : null}
<ModeSwitcher
onEditCancel={setModeToPick}
onEdit={setModeToEdit}
onAdd={setModeToAdd}
mode={mode}
onSave={handleSaveEdit}
customMetrics={customMetrics}
/>
</EuiPopover>
</EuiFilterGroup>
);
};

View file

@ -0,0 +1,75 @@
/*
* 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 React, { useCallback } from 'react';
import { EuiContextMenuPanelDescriptor, EuiContextMenu } from '@elastic/eui';
import {
SnapshotMetricInput,
SnapshotCustomMetricInput,
SnapshotCustomMetricInputRT,
} from '../../../../common/http_api/snapshot_api';
import {
SnapshotMetricTypeRT,
SnapshotMetricType,
} from '../../../../common/inventory_models/types';
import { getCustomMetricLabel } from './get_custom_metric_label';
interface Props {
options: Array<{ text: string; value: string }>;
metric: SnapshotMetricInput;
onChange: (metric: SnapshotMetricInput) => void;
onClose: () => void;
customMetrics: SnapshotCustomMetricInput[];
}
export const MetricsContextMenu = ({
onClose,
onChange,
metric,
options,
customMetrics,
}: Props) => {
const id = SnapshotCustomMetricInputRT.is(metric) && metric.id ? metric.id : metric.type;
const handleClick = useCallback(
(val: string) => {
if (!SnapshotMetricTypeRT.is(val)) {
const selectedMetric = customMetrics.find(m => m.id === val);
if (selectedMetric) {
onChange(selectedMetric);
}
} else {
onChange({ type: val as SnapshotMetricType });
}
onClose();
},
[customMetrics, onChange, onClose]
);
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: [
...options.map(o => {
const icon = o.value === id ? 'check' : 'empty';
const panel = { name: o.text, onClick: () => handleClick(o.value), icon };
return panel;
}),
...customMetrics.map(m => {
const icon = m.id === id ? 'check' : 'empty';
const panel = {
name: getCustomMetricLabel(m),
onClick: () => handleClick(m.id),
icon,
};
return panel;
}),
],
},
];
return <EuiContextMenu initialPanelId={0} panels={panels} />;
};

View file

@ -0,0 +1,73 @@
/*
* 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 React from 'react';
import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api';
import { getCustomMetricLabel } from './get_custom_metric_label';
import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components';
interface Props {
theme: EuiTheme;
customMetrics: SnapshotCustomMetricInput[];
options: Array<{ text: string; value: string }>;
onEdit: (metric: SnapshotCustomMetricInput) => void;
onDelete: (metric: SnapshotCustomMetricInput) => void;
}
const ICON_WIDTH = 36;
export const MetricsEditMode = withTheme(
({ theme, customMetrics, options, onEdit, onDelete }: Props) => {
return (
<div style={{ width: 256 }}>
{options.map(option => (
<div key={option.value} style={{ padding: '14px 14px 13px 36px' }}>
<span style={{ color: theme.eui.euiButtonColorDisabled }}>{option.text}</span>
</div>
))}
{customMetrics.map(metric => (
<EuiFlexGroup
key={metric.id}
alignItems="center"
gutterSize="none"
style={{ padding: '10px 0px 9px' }}
>
<EuiFlexItem grow={false} style={{ width: ICON_WIDTH }}>
<EuiButtonIcon
iconType="pencil"
onClick={() => onEdit(metric)}
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.editMode.editButtonAriaLabel',
{
defaultMessage: 'Edit custom metric for {name}',
values: { name: getCustomMetricLabel(metric) },
}
)}
/>
</EuiFlexItem>
<EuiFlexItem grow={1} style={{ overflow: 'hidden' }}>
{getCustomMetricLabel(metric)}
</EuiFlexItem>
<EuiFlexItem grow={false} style={{ width: ICON_WIDTH, textAlign: 'right' }}>
<EuiButtonIcon
iconType="trash"
color="danger"
onClick={() => onDelete(metric)}
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.editMode.deleteAriaLabel',
{
defaultMessage: 'Delete custom metric for {name}',
values: { name: getCustomMetricLabel(metric) },
}
)}
/>
</EuiFlexItem>
</EuiFlexGroup>
))}
</div>
);
}
);

View file

@ -0,0 +1,113 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiButtonEmpty, EuiButton } from '@elastic/eui';
import React from 'react';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { CustomMetricMode } from './types';
import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api';
import { EuiTheme, withTheme } from '../../../../../../legacy/common/eui_styled_components';
interface Props {
theme: EuiTheme;
onEdit: () => void;
onAdd: () => void;
onSave: () => void;
onEditCancel: () => void;
mode: CustomMetricMode;
customMetrics: SnapshotCustomMetricInput[];
}
export const ModeSwitcher = withTheme(
({ onSave, onEditCancel, onEdit, onAdd, mode, customMetrics, theme }: Props) => {
if (['editMetric', 'addMetric'].includes(mode)) {
return null;
}
return (
<div
style={{
borderTop: `${theme.eui.euiBorderWidthThin} solid ${theme.eui.euiBorderColor}`,
padding: 12,
}}
>
<EuiFlexGroup justifyContent="spaceBetween">
{mode === 'edit' ? (
<>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
flush="left"
onClick={onEditCancel}
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.modeSwitcher.cancelAriaLabel',
{ defaultMessage: 'Cancel edit mode' }
)}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.modeSwitcher.cancel"
defaultMessage="Cancel"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton
onClick={onSave}
size="s"
fill
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.modeSwitcher.saveButtonAriaLabel',
{ defaultMessage: 'Save changes to custom metrics' }
)}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.modeSwitcher.saveButton"
defaultMessage="Save"
/>
</EuiButton>
</EuiFlexItem>
</>
) : (
<>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
size="s"
flush="left"
onClick={onEdit}
disabled={customMetrics.length === 0}
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.modeSwitcher.editAriaLabel',
{ defaultMessage: 'Edit custom metrics' }
)}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.modeSwitcher.edit"
defaultMessage="Edit"
/>
</EuiButtonEmpty>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButtonEmpty
onClick={onAdd}
size="s"
flush="right"
aria-label={i18n.translate(
'xpack.infra.waffle.customMetrics.modeSwitcher.addMetricAriaLabel',
{ defaultMessage: 'Add custom metric' }
)}
>
<FormattedMessage
id="xpack.infra.waffle.customMetrics.modeSwitcher.addMetric"
defaultMessage="Add metric"
/>
</EuiButtonEmpty>
</EuiFlexItem>
</>
)}
</EuiFlexGroup>
</div>
);
}
);

View file

@ -0,0 +1,7 @@
/*
* 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.
*/
export type CustomMetricMode = 'pick' | 'addMetric' | 'editMetric' | 'edit';

View file

@ -104,6 +104,7 @@ export const WaffleGroupByControls = class extends React.PureComponent<Props, St
title: i18n.translate('xpack.infra.waffle.customGroupByPanelTitle', {
defaultMessage: 'Group By Custom Field',
}),
width: 685,
content: (
<CustomFieldPanel
currentOptions={this.props.customOptions}

View file

@ -16,13 +16,18 @@ import React, { useCallback, useState, useMemo } from 'react';
import { FormattedMessage } from '@kbn/i18n/react';
import { findInventoryModel } from '../../../common/inventory_models';
import { InventoryItemType } from '../../../common/inventory_models/types';
import { SnapshotMetricInput, SnapshotGroupBy } from '../../../common/http_api/snapshot_api';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
} from '../../../common/http_api/snapshot_api';
interface WaffleInventorySwitcherProps {
nodeType: InventoryItemType;
changeNodeType: (nodeType: InventoryItemType) => void;
changeGroupBy: (groupBy: SnapshotGroupBy) => void;
changeMetric: (metric: SnapshotMetricInput) => void;
changeCustomMetrics: (metrics: SnapshotCustomMetricInput[]) => void;
changeAccount: (id: string) => void;
changeRegion: (name: string) => void;
}
@ -38,6 +43,7 @@ export const WaffleInventorySwitcher: React.FC<WaffleInventorySwitcherProps> = (
changeMetric,
changeAccount,
changeRegion,
changeCustomMetrics,
nodeType,
}) => {
const [isOpen, setIsOpen] = useState(false);
@ -48,6 +54,7 @@ export const WaffleInventorySwitcher: React.FC<WaffleInventorySwitcherProps> = (
closePopover();
changeNodeType(targetNodeType);
changeGroupBy([]);
changeCustomMetrics([]);
changeAccount('');
changeRegion('');
const inventoryModel = findInventoryModel(targetNodeType);
@ -55,7 +62,15 @@ export const WaffleInventorySwitcher: React.FC<WaffleInventorySwitcherProps> = (
type: inventoryModel.metrics.defaultSnapshot,
});
},
[closePopover, changeNodeType, changeGroupBy, changeMetric, changeAccount, changeRegion]
[
closePopover,
changeNodeType,
changeGroupBy,
changeCustomMetrics,
changeAccount,
changeRegion,
changeMetric,
]
);
const goToHost = useCallback(() => goToNodeType('host'), [goToNodeType]);
const goToK8 = useCallback(() => goToNodeType('pod'), [goToNodeType]);

View file

@ -1,97 +0,0 @@
/*
* 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 {
EuiContextMenu,
EuiContextMenuPanelDescriptor,
EuiFilterButton,
EuiFilterGroup,
EuiPopover,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import React from 'react';
import { SnapshotMetricInput } from '../../../common/http_api/snapshot_api';
import { SnapshotMetricType } from '../../../common/inventory_models/types';
interface Props {
options: Array<{ text: string; value: SnapshotMetricType }>;
metric: SnapshotMetricInput;
onChange: (metric: SnapshotMetricInput) => void;
}
const initialState = {
isPopoverOpen: false,
};
type State = Readonly<typeof initialState>;
export const WaffleMetricControls = class extends React.PureComponent<Props, State> {
public static displayName = 'WaffleMetricControls';
public readonly state: State = initialState;
public render() {
const { metric, options } = this.props;
const value = metric.type;
if (!options.length || !value) {
throw Error(
i18n.translate('xpack.infra.waffle.unableToSelectMetricErrorTitle', {
defaultMessage: 'Unable to select options or value for metric.',
})
);
}
const currentLabel = options.find(o => o.value === metric.type);
if (!currentLabel) {
return null;
}
const panels: EuiContextMenuPanelDescriptor[] = [
{
id: 0,
title: '',
items: options.map(o => {
const icon = o.value === metric.type ? 'check' : 'empty';
const panel = { name: o.text, onClick: this.handleClick(o.value), icon };
return panel;
}),
},
];
const button = (
<EuiFilterButton iconType="arrowDown" onClick={this.handleToggle}>
<FormattedMessage
id="xpack.infra.waffle.metricButtonLabel"
defaultMessage="Metric: {selectedMetric}"
values={{ selectedMetric: currentLabel.text }}
/>
</EuiFilterButton>
);
return (
<EuiFilterGroup>
<EuiPopover
isOpen={this.state.isPopoverOpen}
id="metricsPanel"
button={button}
anchorPosition="downLeft"
panelPaddingSize="none"
closePopover={this.handleClose}
>
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFilterGroup>
);
}
private handleClose = () => {
this.setState({ isPopoverOpen: false });
};
private handleToggle = () => {
this.setState(state => ({ isPopoverOpen: !state.isPopoverOpen }));
};
private handleClick = (value: SnapshotMetricType) => () => {
this.props.onChange({ type: value });
this.handleClose();
};
};

View file

@ -14,7 +14,11 @@ import { State, waffleOptionsActions, waffleOptionsSelectors } from '../../store
import { asChildFunctionRenderer } from '../../utils/typed_react';
import { bindPlainActionCreators } from '../../utils/typed_redux';
import { UrlStateContainer } from '../../utils/url_state';
import { SnapshotMetricInput, SnapshotGroupBy } from '../../../common/http_api/snapshot_api';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInputRT,
} from '../../../common/http_api/snapshot_api';
import {
SnapshotMetricTypeRT,
InventoryItemType,
@ -31,6 +35,7 @@ const selectOptionsUrlState = createSelector(
waffleOptionsSelectors.selectAutoBounds,
waffleOptionsSelectors.selectAccountId,
waffleOptionsSelectors.selectRegion,
waffleOptionsSelectors.selectCustomMetrics,
(
metric,
view,
@ -40,7 +45,8 @@ const selectOptionsUrlState = createSelector(
boundsOverride,
autoBounds,
accountId,
region
region,
customMetrics
) => ({
metric,
groupBy,
@ -51,6 +57,7 @@ const selectOptionsUrlState = createSelector(
autoBounds,
accountId,
region,
customMetrics,
})
);
@ -66,6 +73,7 @@ export const withWaffleOptions = connect(
accountId: waffleOptionsSelectors.selectAccountId(state),
region: waffleOptionsSelectors.selectRegion(state),
urlState: selectOptionsUrlState(state),
customMetrics: waffleOptionsSelectors.selectCustomMetrics(state),
}),
bindPlainActionCreators({
changeMetric: waffleOptionsActions.changeMetric,
@ -77,6 +85,7 @@ export const withWaffleOptions = connect(
changeAutoBounds: waffleOptionsActions.changeAutoBounds,
changeAccount: waffleOptionsActions.changeAccount,
changeRegion: waffleOptionsActions.changeRegion,
changeCustomMetrics: waffleOptionsActions.changeCustomMetrics,
})
);
@ -96,6 +105,7 @@ interface WaffleOptionsUrlState {
auto?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>;
accountId?: ReturnType<typeof waffleOptionsSelectors.selectAccountId>;
region?: ReturnType<typeof waffleOptionsSelectors.selectRegion>;
customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
}
export const WithWaffleOptionsUrlState = () => (
@ -111,6 +121,7 @@ export const WithWaffleOptionsUrlState = () => (
changeBoundsOverride,
changeAccount,
changeRegion,
changeCustomMetrics,
}) => (
<UrlStateContainer<WaffleOptionsUrlState>
urlState={urlState}
@ -144,6 +155,9 @@ export const WithWaffleOptionsUrlState = () => (
if (newUrlState && newUrlState.region) {
changeRegion(newUrlState.region);
}
if (newUrlState && newUrlState.customMetrics) {
changeCustomMetrics(newUrlState.customMetrics);
}
}}
onInitialize={initialUrlState => {
if (initialUrlState && initialUrlState.metric) {
@ -173,6 +187,9 @@ export const WithWaffleOptionsUrlState = () => (
if (initialUrlState && initialUrlState.region) {
changeRegion(initialUrlState.region);
}
if (initialUrlState && initialUrlState.customMetrics) {
changeCustomMetrics(initialUrlState.customMetrics);
}
}}
/>
)}
@ -191,6 +208,7 @@ const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined =>
auto: mapToAutoBoundsUrlState(value.autoBounds),
accountId: value.accountId,
region: value.region,
customMetrics: mapToCustomMetricsUrlState(value.customMetrics),
}
: undefined;
@ -232,6 +250,12 @@ const mapToCustomOptionsUrlState = (subject: any) => {
: undefined;
};
const mapToCustomMetricsUrlState = (subject: any) => {
return subject && Array.isArray(subject) && subject.every(s => SnapshotCustomMetricInputRT.is(s))
? subject
: [];
};
const mapToBoundsOverideUrlState = (subject: any) => {
return subject != null && isNumber(subject.max) && isNumber(subject.min) ? subject : undefined;
};

View file

@ -30,6 +30,7 @@ const selectViewState = createSelector(
waffleTimeSelectors.selectCurrentTime,
waffleTimeSelectors.selectIsAutoReloading,
waffleFilterSelectors.selectWaffleFilterQuery,
waffleOptionsSelectors.selectCustomMetrics,
(
metric,
view,
@ -40,7 +41,8 @@ const selectViewState = createSelector(
autoBounds,
time,
autoReload,
filterQuery
filterQuery,
customMetrics
) => ({
time,
autoReload,
@ -52,6 +54,7 @@ const selectViewState = createSelector(
boundsOverride,
autoBounds,
filterQuery,
customMetrics,
})
);
@ -90,6 +93,9 @@ export const withWaffleViewState = connect(
if (viewState.customOptions) {
dispatch(waffleOptionsActions.changeCustomOptions(viewState.customOptions));
}
if (viewState.customMetrics) {
dispatch(waffleOptionsActions.changeCustomMetrics(viewState.customMetrics));
}
if (viewState.boundsOverride) {
dispatch(waffleOptionsActions.changeBoundsOverride(viewState.boundsOverride));
}
@ -130,6 +136,7 @@ export interface WaffleViewState {
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
view?: ReturnType<typeof waffleOptionsSelectors.selectView>;
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
customMetrics?: ReturnType<typeof waffleOptionsSelectors.selectCustomMetrics>;
boundsOverride?: ReturnType<typeof waffleOptionsSelectors.selectBoundsOverride>;
autoBounds?: ReturnType<typeof waffleOptionsSelectors.selectAutoBounds>;
time?: ReturnType<typeof waffleTimeSelectors.selectCurrentTime>;

View file

@ -29,6 +29,7 @@ export const SnapshotToolbar = () => (
changeGroupBy,
changeAccount,
changeRegion,
changeCustomMetrics,
nodeType,
}) => (
<WaffleInventorySwitcher
@ -38,6 +39,7 @@ export const SnapshotToolbar = () => (
changeGroupBy={changeGroupBy}
changeAccount={changeAccount}
changeRegion={changeRegion}
changeCustomMetrics={changeCustomMetrics}
/>
)}
</WithWaffleOptions>

View file

@ -5,7 +5,11 @@
*/
import actionCreatorFactory from 'typescript-fsa';
import { SnapshotGroupBy, SnapshotMetricInput } from '../../../../common/http_api/snapshot_api';
import {
SnapshotGroupBy,
SnapshotMetricInput,
SnapshotCustomMetricInput,
} from '../../../../common/http_api/snapshot_api';
import { InventoryItemType } from '../../../../common/inventory_models/types';
import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib';
@ -20,3 +24,6 @@ export const changeBoundsOverride = actionCreator<InfraWaffleMapBounds>('CHANGE_
export const changeAutoBounds = actionCreator<boolean>('CHANGE_AUTO_BOUNDS');
export const changeAccount = actionCreator<string>('CHANGE_ACCOUNT');
export const changeRegion = actionCreator<string>('CHANGE_REGION');
export const changeCustomMetrics = actionCreator<SnapshotCustomMetricInput[]>(
'CHANGE_CUSTOM_METRICS'
);

View file

@ -6,7 +6,11 @@
import { combineReducers } from 'redux';
import { reducerWithInitialState } from 'typescript-fsa-reducers';
import { SnapshotMetricInput, SnapshotGroupBy } from '../../../../common/http_api/snapshot_api';
import {
SnapshotMetricInput,
SnapshotGroupBy,
SnapshotCustomMetricInput,
} from '../../../../common/http_api/snapshot_api';
import { InfraGroupByOptions, InfraWaffleMapBounds } from '../../../lib/lib';
import {
changeAutoBounds,
@ -18,6 +22,7 @@ import {
changeView,
changeAccount,
changeRegion,
changeCustomMetrics,
} from './actions';
import { InventoryItemType } from '../../../../common/inventory_models/types';
@ -31,6 +36,7 @@ export interface WaffleOptionsState {
autoBounds: boolean;
accountId: string;
region: string;
customMetrics: SnapshotCustomMetricInput[];
}
export const initialWaffleOptionsState: WaffleOptionsState = {
@ -43,6 +49,7 @@ export const initialWaffleOptionsState: WaffleOptionsState = {
autoBounds: true,
accountId: '',
region: '',
customMetrics: [],
};
const currentMetricReducer = reducerWithInitialState(initialWaffleOptionsState.metric).case(
@ -88,6 +95,10 @@ const currentRegionReducer = reducerWithInitialState(initialWaffleOptionsState.r
(current, target) => target
);
const currentCustomMetricsReducer = reducerWithInitialState(
initialWaffleOptionsState.customMetrics
).case(changeCustomMetrics, (current, target) => target);
export const waffleOptionsReducer = combineReducers<WaffleOptionsState>({
metric: currentMetricReducer,
groupBy: currentGroupByReducer,
@ -98,4 +109,5 @@ export const waffleOptionsReducer = combineReducers<WaffleOptionsState>({
autoBounds: currentAutoBoundsReducer,
accountId: currentAccountIdReducer,
region: currentRegionReducer,
customMetrics: currentCustomMetricsReducer,
});

View file

@ -15,3 +15,4 @@ export const selectBoundsOverride = (state: WaffleOptionsState) => state.boundsO
export const selectAutoBounds = (state: WaffleOptionsState) => state.autoBounds;
export const selectAccountId = (state: WaffleOptionsState) => state.accountId;
export const selectRegion = (state: WaffleOptionsState) => state.region;
export const selectCustomMetrics = (state: WaffleOptionsState) => state.customMetrics;

View file

@ -8,7 +8,16 @@ import { i18n } from '@kbn/i18n';
import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index';
import { InfraSnapshotRequestOptions } from './types';
import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds';
import { SnapshotModelRT, SnapshotModel } from '../../../common/inventory_models/types';
import {
SnapshotModelRT,
SnapshotModel,
InventoryItemType,
} from '../../../common/inventory_models/types';
import {
SnapshotMetricInput,
SnapshotCustomMetricInputRT,
} from '../../../common/http_api/snapshot_api';
import { networkTraffic } from '../../../common/inventory_models/shared/metrics/snapshot/network_traffic';
interface GroupBySource {
[id: string]: {
@ -45,9 +54,25 @@ export const getMetricsSources = (options: InfraSnapshotRequestOptions) => {
return [{ id: { terms: { field: fields.id } } }];
};
export const metricToAggregation = (nodeType: InventoryItemType, metric: SnapshotMetricInput) => {
const inventoryModel = findInventoryModel(nodeType);
if (SnapshotCustomMetricInputRT.is(metric)) {
if (metric.aggregation === 'rate') {
return networkTraffic(metric.type, metric.field);
}
return {
custom: {
[metric.aggregation]: {
field: metric.field,
},
},
};
}
return inventoryModel.metrics.snapshot?.[metric.type];
};
export const getMetricsAggregations = (options: InfraSnapshotRequestOptions): SnapshotModel => {
const inventoryModel = findInventoryModel(options.nodeType);
const aggregation = inventoryModel.metrics.snapshot?.[options.metric.type];
const aggregation = metricToAggregation(options.nodeType, options.metric);
if (!SnapshotModelRT.is(aggregation)) {
throw new Error(
i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', {

View file

@ -14,12 +14,15 @@ import {
InfraSnapshotGroupbyInput,
} from '../../../../plugins/infra/server/graphql/types';
import { FtrProviderContext } from '../../ftr_provider_context';
import { SnapshotNodeResponse } from '../../../../plugins/infra/common/http_api/snapshot_api';
import {
SnapshotNodeResponse,
SnapshotMetricInput,
} from '../../../../plugins/infra/common/http_api/snapshot_api';
import { DATES } from './constants';
interface SnapshotRequest {
filterQuery?: string | null;
metric: InfraSnapshotMetricInput;
metric: SnapshotMetricInput;
groupBy: InfraSnapshotGroupbyInput[];
nodeType: InfraNodeType;
sourceId: string;
@ -197,6 +200,44 @@ export default function({ getService }: FtrProviderContext) {
});
});
it('should work with custom metrics', async () => {
const data = await fetchSnapshot({
sourceId: 'default',
timerange: {
to: max,
from: min,
interval: '1m',
},
metric: {
type: 'custom',
field: 'system.cpu.user.pct',
aggregation: 'avg',
id: '1',
} as SnapshotMetricInput,
nodeType: 'host' as InfraNodeType,
groupBy: [],
});
const snapshot = data;
expect(snapshot).to.have.property('nodes');
if (snapshot) {
const { nodes } = snapshot;
expect(nodes.length).to.equal(1);
const firstNode = first(nodes);
expect(firstNode).to.have.property('path');
expect(firstNode.path.length).to.equal(1);
expect(first(firstNode.path)).to.have.property('value', 'demo-stack-mysql-01');
expect(first(firstNode.path)).to.have.property('label', 'demo-stack-mysql-01');
expect(firstNode).to.have.property('metric');
expect(firstNode.metric).to.eql({
name: 'custom',
value: 0.0041964285714285714,
max: 0.0041964285714285714,
avg: 0.0006994047619047619,
});
}
});
it('should basically work with 1 grouping', () => {
const resp = fetchSnapshot({
sourceId: 'default',