[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:
parent
613e4b9b15
commit
8021fd887a
|
@ -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>;
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -305,6 +305,7 @@ export const SnapshotMetricTypeRT = rt.keyof({
|
|||
sqsMessagesSent: null,
|
||||
sqsMessagesEmpty: null,
|
||||
sqsOldestMessage: null,
|
||||
custom: null,
|
||||
});
|
||||
|
||||
export type SnapshotMetricType = rt.TypeOf<typeof SnapshotMetricTypeRT>;
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 '';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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];
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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} />;
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
);
|
|
@ -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';
|
|
@ -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}
|
||||
|
|
|
@ -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]);
|
||||
|
|
|
@ -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();
|
||||
};
|
||||
};
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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'
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in a new issue