mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[Infra UI] Table View for Home Page (#29192)
* Addding initial table implimentation * Moving waffle map to seperate component; adding contextual menu to nodes; adding filter to groups; adding pagination; adding sorting * Fixing EUI types for EuiInMemoryTable to work for EVERYONE * Adding server plugin for tslint for VIM; Fixing tests * Adding the view switcher * removing dependency * updating yarn.lock * Change padding to use EUI rules * Rename waffle/index to nodes_overview; move table to nodes_overview * Adding missed files in last commit * Adding textOnly to the columns that need special truncation because they are buttons * Fixed an error in the merge * Fixing merge issues
This commit is contained in:
parent
402b8cfa8a
commit
fada5d9e15
24 changed files with 624 additions and 235 deletions
|
@ -411,4 +411,4 @@
|
|||
"node": "10.14.1",
|
||||
"yarn": "^1.10.1"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,34 +10,29 @@ import React from 'react';
|
|||
import styled from 'styled-components';
|
||||
|
||||
import {
|
||||
isWaffleMapGroupWithGroups,
|
||||
isWaffleMapGroupWithNodes,
|
||||
} from '../../containers/waffle/type_guards';
|
||||
import { InfraMetricType, InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
|
||||
import {
|
||||
InfraFormatterType,
|
||||
InfraWaffleData,
|
||||
InfraWaffleMapBounds,
|
||||
InfraWaffleMapGroup,
|
||||
InfraWaffleMapOptions,
|
||||
} from '../../lib/lib';
|
||||
InfraMetricType,
|
||||
InfraNode,
|
||||
InfraNodeType,
|
||||
InfraTimerangeInput,
|
||||
} from '../../graphql/types';
|
||||
import { InfraFormatterType, InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib';
|
||||
import { KueryFilterQuery } from '../../store/local/waffle_filter';
|
||||
import { createFormatter } from '../../utils/formatters';
|
||||
import { AutoSizer } from '../auto_sizer';
|
||||
import { InfraLoadingPanel } from '../loading';
|
||||
import { GroupOfGroups } from './group_of_groups';
|
||||
import { GroupOfNodes } from './group_of_nodes';
|
||||
import { Legend } from './legend';
|
||||
import { applyWaffleMapLayout } from './lib/apply_wafflemap_layout';
|
||||
import { Map } from '../waffle/map';
|
||||
import { ViewSwitcher } from '../waffle/view_switcher';
|
||||
import { TableView } from './table';
|
||||
|
||||
interface Props {
|
||||
options: InfraWaffleMapOptions;
|
||||
nodeType: InfraNodeType;
|
||||
map: InfraWaffleData;
|
||||
nodes: InfraNode[];
|
||||
loading: boolean;
|
||||
reload: () => void;
|
||||
onDrilldown: (filter: KueryFilterQuery) => void;
|
||||
timeRange: InfraTimerangeInput;
|
||||
onViewChange: (view: string) => void;
|
||||
view: string;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
|
@ -71,24 +66,8 @@ const METRIC_FORMATTERS: MetricFormatters = {
|
|||
},
|
||||
};
|
||||
|
||||
const extractValuesFromMap = (groups: InfraWaffleMapGroup[], values: number[] = []): number[] => {
|
||||
return groups.reduce((acc: number[], group: InfraWaffleMapGroup) => {
|
||||
if (isWaffleMapGroupWithGroups(group)) {
|
||||
return acc.concat(extractValuesFromMap(group.groups, values));
|
||||
}
|
||||
if (isWaffleMapGroupWithNodes(group)) {
|
||||
return acc.concat(
|
||||
group.nodes.map(node => {
|
||||
return node.metric.value || 0;
|
||||
})
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, values);
|
||||
};
|
||||
|
||||
const calculateBoundsFromMap = (map: InfraWaffleData): InfraWaffleMapBounds => {
|
||||
const values = extractValuesFromMap(map);
|
||||
const calculateBoundsFromNodes = (nodes: InfraNode[]): InfraWaffleMapBounds => {
|
||||
const values = nodes.map(node => node.metric.value);
|
||||
// if there is only one value then we need to set the bottom range to zero
|
||||
if (values.length === 1) {
|
||||
values.unshift(0);
|
||||
|
@ -96,11 +75,11 @@ const calculateBoundsFromMap = (map: InfraWaffleData): InfraWaffleMapBounds => {
|
|||
return { min: min(values) || 0, max: max(values) || 0 };
|
||||
};
|
||||
|
||||
export const Waffle = injectI18n(
|
||||
export const NodesOverview = injectI18n(
|
||||
class extends React.Component<Props, {}> {
|
||||
public static displayName = 'Waffle';
|
||||
public render() {
|
||||
const { loading, map, reload, timeRange, intl } = this.props;
|
||||
const { loading, nodes, nodeType, reload, intl, view, options, timeRange } = this.props;
|
||||
if (loading) {
|
||||
return (
|
||||
<InfraLoadingPanel
|
||||
|
@ -112,7 +91,7 @@ export const Waffle = injectI18n(
|
|||
})}
|
||||
/>
|
||||
);
|
||||
} else if (!loading && map && map.length === 0) {
|
||||
} else if (!loading && nodes && nodes.length === 0) {
|
||||
return (
|
||||
<CenteredEmptyPrompt
|
||||
title={
|
||||
|
@ -157,31 +136,42 @@ export const Waffle = injectI18n(
|
|||
metric.type,
|
||||
METRIC_FORMATTERS[InfraMetricType.count]
|
||||
);
|
||||
const bounds = (metricFormatter && metricFormatter.bounds) || calculateBoundsFromMap(map);
|
||||
const bounds = (metricFormatter && metricFormatter.bounds) || calculateBoundsFromNodes(nodes);
|
||||
return (
|
||||
<AutoSizer content>
|
||||
{({ measureRef, content: { width = 0, height = 0 } }) => {
|
||||
const groupsWithLayout = applyWaffleMapLayout(map, width, height);
|
||||
return (
|
||||
<WaffleMapOuterContiner
|
||||
innerRef={(el: any) => measureRef(el)}
|
||||
data-test-subj="waffleMap"
|
||||
>
|
||||
<WaffleMapInnerContainer>
|
||||
{groupsWithLayout.map(this.renderGroup(bounds, timeRange))}
|
||||
</WaffleMapInnerContainer>
|
||||
<Legend
|
||||
formatter={this.formatter}
|
||||
bounds={bounds}
|
||||
legend={this.props.options.legend}
|
||||
/>
|
||||
</WaffleMapOuterContiner>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
<MainContainer>
|
||||
<ViewSwitcherContainer>
|
||||
<ViewSwitcher view={view} onChange={this.handleViewChange} />
|
||||
</ViewSwitcherContainer>
|
||||
{view === 'table' ? (
|
||||
<TableContainer>
|
||||
<TableView
|
||||
nodeType={nodeType}
|
||||
nodes={nodes}
|
||||
options={options}
|
||||
formatter={this.formatter}
|
||||
timeRange={timeRange}
|
||||
onFilter={this.handleDrilldown}
|
||||
/>
|
||||
</TableContainer>
|
||||
) : (
|
||||
<MapContainer>
|
||||
<Map
|
||||
nodeType={nodeType}
|
||||
nodes={nodes}
|
||||
options={options}
|
||||
formatter={this.formatter}
|
||||
timeRange={timeRange}
|
||||
onFilter={this.handleDrilldown}
|
||||
bounds={bounds}
|
||||
/>
|
||||
</MapContainer>
|
||||
)}
|
||||
</MainContainer>
|
||||
);
|
||||
}
|
||||
|
||||
private handleViewChange = (view: string) => this.props.onViewChange(view);
|
||||
|
||||
// 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;
|
||||
|
@ -204,61 +194,31 @@ export const Waffle = injectI18n(
|
|||
});
|
||||
return;
|
||||
};
|
||||
|
||||
private renderGroup = (bounds: InfraWaffleMapBounds, timeRange: InfraTimerangeInput) => (
|
||||
group: InfraWaffleMapGroup
|
||||
) => {
|
||||
if (isWaffleMapGroupWithGroups(group)) {
|
||||
return (
|
||||
<GroupOfGroups
|
||||
onDrilldown={this.handleDrilldown}
|
||||
key={group.id}
|
||||
options={this.props.options}
|
||||
group={group}
|
||||
formatter={this.formatter}
|
||||
bounds={bounds}
|
||||
nodeType={this.props.nodeType}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isWaffleMapGroupWithNodes(group)) {
|
||||
return (
|
||||
<GroupOfNodes
|
||||
key={group.id}
|
||||
options={this.props.options}
|
||||
group={group}
|
||||
onDrilldown={this.handleDrilldown}
|
||||
formatter={this.formatter}
|
||||
isChild={false}
|
||||
bounds={bounds}
|
||||
nodeType={this.props.nodeType}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const WaffleMapOuterContiner = styled.div`
|
||||
flex: 1 0 0%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const WaffleMapInnerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-content: flex-start;
|
||||
padding: 10px;
|
||||
`;
|
||||
|
||||
const CenteredEmptyPrompt = styled(EuiEmptyPrompt)`
|
||||
align-self: center;
|
||||
`;
|
||||
|
||||
const MainContainer = styled.div`
|
||||
position: relative;
|
||||
flex: 1 1 auto;
|
||||
`;
|
||||
|
||||
const TableContainer = styled.div`
|
||||
padding: ${props => props.theme.eui.paddingSizes.l};
|
||||
`;
|
||||
|
||||
const ViewSwitcherContainer = styled.div`
|
||||
padding: ${props => props.theme.eui.paddingSizes.l};
|
||||
`;
|
||||
|
||||
const MapContainer = styled.div`
|
||||
position: absolute;
|
||||
display: flex;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
`;
|
167
x-pack/plugins/infra/public/components/nodes_overview/table.tsx
Normal file
167
x-pack/plugins/infra/public/components/nodes_overview/table.tsx
Normal file
|
@ -0,0 +1,167 @@
|
|||
/*
|
||||
* 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 { EuiButtonEmpty, EuiInMemoryTable, EuiToolTip } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import { last } from 'lodash';
|
||||
import React from 'react';
|
||||
import { InfraNodeType } from '../../../server/lib/adapters/nodes';
|
||||
import { createWaffleMapNode } from '../../containers/waffle/nodes_to_wafflemap';
|
||||
import { InfraNode, InfraNodePath, InfraTimerangeInput } from '../../graphql/types';
|
||||
import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../lib/lib';
|
||||
import { fieldToName } from '../waffle/lib/field_to_display_name';
|
||||
import { NodeContextMenu } from '../waffle/node_context_menu';
|
||||
|
||||
interface Props {
|
||||
nodes: InfraNode[];
|
||||
nodeType: InfraNodeType;
|
||||
options: InfraWaffleMapOptions;
|
||||
formatter: (subject: string | number) => string;
|
||||
timeRange: InfraTimerangeInput;
|
||||
intl: InjectedIntl;
|
||||
onFilter: (filter: string) => void;
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
isPopoverOpen: [] as string[],
|
||||
};
|
||||
|
||||
type State = Readonly<typeof initialState>;
|
||||
|
||||
const getGroupPaths = (path: InfraNodePath[]) => {
|
||||
switch (path.length) {
|
||||
case 3:
|
||||
return path.slice(0, 2);
|
||||
case 2:
|
||||
return path.slice(0, 1);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
export const TableView = injectI18n(
|
||||
class extends React.PureComponent<Props, State> {
|
||||
public readonly state: State = initialState;
|
||||
public render() {
|
||||
const { nodes, options, formatter, intl, timeRange, nodeType } = this.props;
|
||||
const columns = [
|
||||
{
|
||||
field: 'name',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.infra.tableView.columnName.name',
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
textOnly: true,
|
||||
render: (value: string, item: { node: InfraWaffleMapNode }) => (
|
||||
<NodeContextMenu
|
||||
node={item.node}
|
||||
nodeType={nodeType}
|
||||
closePopover={this.closePopoverFor(item.node.pathId)}
|
||||
timeRange={timeRange}
|
||||
isPopoverOpen={this.state.isPopoverOpen.includes(item.node.pathId)}
|
||||
options={options}
|
||||
>
|
||||
<EuiButtonEmpty onClick={this.openPopoverFor(item.node.pathId)}>
|
||||
{value}
|
||||
</EuiButtonEmpty>
|
||||
</NodeContextMenu>
|
||||
),
|
||||
},
|
||||
...options.groupBy.map((grouping, index) => ({
|
||||
field: `group_${index}`,
|
||||
name: fieldToName((grouping && grouping.field) || '', intl),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
textOnly: true,
|
||||
render: (value: string) => {
|
||||
const handleClick = () => this.props.onFilter(`${grouping.field}:"${value}"`);
|
||||
return (
|
||||
<EuiToolTip content="Set Filter">
|
||||
<EuiButtonEmpty onClick={handleClick}>{value}</EuiButtonEmpty>
|
||||
</EuiToolTip>
|
||||
);
|
||||
},
|
||||
})),
|
||||
{
|
||||
field: 'value',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.infra.tableView.columnName.last1m',
|
||||
defaultMessage: 'Last 1m',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => <span>{formatter(value)}</span>,
|
||||
},
|
||||
{
|
||||
field: 'avg',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.infra.tableView.columnName.avg',
|
||||
defaultMessage: 'Avg',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => <span>{formatter(value)}</span>,
|
||||
},
|
||||
{
|
||||
field: 'max',
|
||||
name: intl.formatMessage({
|
||||
id: 'xpack.infra.tableView.columnName.max',
|
||||
defaultMessage: 'Max',
|
||||
}),
|
||||
sortable: true,
|
||||
truncateText: true,
|
||||
dataType: 'number',
|
||||
render: (value: number) => <span>{formatter(value)}</span>,
|
||||
},
|
||||
];
|
||||
const items = nodes.map(node => {
|
||||
const name = last(node.path);
|
||||
return {
|
||||
name: (name && name.value) || 'unknown',
|
||||
...getGroupPaths(node.path).reduce(
|
||||
(acc, path, index) => ({
|
||||
...acc,
|
||||
[`group_${index}`]: path.label,
|
||||
}),
|
||||
{}
|
||||
),
|
||||
value: node.metric.value,
|
||||
avg: node.metric.avg,
|
||||
max: node.metric.max,
|
||||
node: createWaffleMapNode(node),
|
||||
};
|
||||
});
|
||||
const initialSorting = {
|
||||
sort: {
|
||||
field: 'value',
|
||||
direction: 'desc',
|
||||
},
|
||||
};
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
pagination={true}
|
||||
sorting={initialSorting}
|
||||
items={items}
|
||||
columns={columns}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
private openPopoverFor = (id: string) => () => {
|
||||
this.setState(prevState => ({ isPopoverOpen: [...prevState.isPopoverOpen, id] }));
|
||||
};
|
||||
|
||||
private closePopoverFor = (id: string) => () => {
|
||||
this.setState(prevState => ({
|
||||
isPopoverOpen: prevState.isPopoverOpen.filter(subject => subject !== id),
|
||||
}));
|
||||
};
|
||||
}
|
||||
);
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 { InjectedIntl } from '@kbn/i18n/react';
|
||||
|
||||
interface Lookup {
|
||||
[id: string]: string;
|
||||
}
|
||||
|
||||
export const fieldToName = (field: string, intl: InjectedIntl) => {
|
||||
const LOOKUP: Lookup = {
|
||||
'kubernetes.namespace': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.kubernetesNamespace',
|
||||
defaultMessage: 'Namespace',
|
||||
}),
|
||||
'kubernetes.node.name': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.kubernetesNodeName',
|
||||
defaultMessage: 'Node',
|
||||
}),
|
||||
'host.name': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.hostName',
|
||||
defaultMessage: 'Host',
|
||||
}),
|
||||
'meta.cloud.availability_zone': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.availabilityZone',
|
||||
defaultMessage: 'Availability Zone',
|
||||
}),
|
||||
'meta.cloud.machine_type': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.machineType',
|
||||
defaultMessage: 'Machine Type',
|
||||
}),
|
||||
'meta.cloud.project_id': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.projectID',
|
||||
defaultMessage: 'Project ID',
|
||||
}),
|
||||
'meta.cloud.provider': intl.formatMessage({
|
||||
id: 'xpack.infra.groupByDisplayNames.provider',
|
||||
defaultMessage: 'Cloud Provider',
|
||||
}),
|
||||
};
|
||||
return LOOKUP[field] || field;
|
||||
};
|
107
x-pack/plugins/infra/public/components/waffle/map.tsx
Normal file
107
x-pack/plugins/infra/public/components/waffle/map.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* 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 styled from 'styled-components';
|
||||
import { nodesToWaffleMap } from '../../containers/waffle/nodes_to_wafflemap';
|
||||
import {
|
||||
isWaffleMapGroupWithGroups,
|
||||
isWaffleMapGroupWithNodes,
|
||||
} from '../../containers/waffle/type_guards';
|
||||
import { InfraNode, InfraNodeType, InfraTimerangeInput } from '../../graphql/types';
|
||||
import { InfraWaffleMapBounds, InfraWaffleMapOptions } from '../../lib/lib';
|
||||
import { AutoSizer } from '../auto_sizer';
|
||||
import { GroupOfGroups } from './group_of_groups';
|
||||
import { GroupOfNodes } from './group_of_nodes';
|
||||
import { Legend } from './legend';
|
||||
import { applyWaffleMapLayout } from './lib/apply_wafflemap_layout';
|
||||
|
||||
interface Props {
|
||||
nodes: InfraNode[];
|
||||
nodeType: InfraNodeType;
|
||||
options: InfraWaffleMapOptions;
|
||||
formatter: (subject: string | number) => string;
|
||||
timeRange: InfraTimerangeInput;
|
||||
onFilter: (filter: string) => void;
|
||||
bounds: InfraWaffleMapBounds;
|
||||
}
|
||||
|
||||
export const Map: React.SFC<Props> = ({
|
||||
nodes,
|
||||
options,
|
||||
timeRange,
|
||||
onFilter,
|
||||
formatter,
|
||||
bounds,
|
||||
nodeType,
|
||||
}) => {
|
||||
const map = nodesToWaffleMap(nodes);
|
||||
return (
|
||||
<AutoSizer content>
|
||||
{({ measureRef, content: { width = 0, height = 0 } }) => {
|
||||
const groupsWithLayout = applyWaffleMapLayout(map, width, height);
|
||||
return (
|
||||
<WaffleMapOuterContainer
|
||||
innerRef={(el: any) => measureRef(el)}
|
||||
data-test-subj="waffleMap"
|
||||
>
|
||||
<WaffleMapInnerContainer>
|
||||
{groupsWithLayout.map(group => {
|
||||
if (isWaffleMapGroupWithGroups(group)) {
|
||||
return (
|
||||
<GroupOfGroups
|
||||
onDrilldown={onFilter}
|
||||
key={group.id}
|
||||
options={options}
|
||||
group={group}
|
||||
formatter={formatter}
|
||||
bounds={bounds}
|
||||
nodeType={nodeType}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (isWaffleMapGroupWithNodes(group)) {
|
||||
return (
|
||||
<GroupOfNodes
|
||||
key={group.id}
|
||||
options={options}
|
||||
group={group}
|
||||
onDrilldown={onFilter}
|
||||
formatter={formatter}
|
||||
isChild={false}
|
||||
bounds={bounds}
|
||||
nodeType={nodeType}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</WaffleMapInnerContainer>
|
||||
<Legend formatter={formatter} bounds={bounds} legend={options.legend} />
|
||||
</WaffleMapOuterContainer>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
);
|
||||
};
|
||||
|
||||
const WaffleMapOuterContainer = styled.div`
|
||||
flex: 1 0 0%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
`;
|
||||
|
||||
const WaffleMapInnerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
align-content: flex-start;
|
||||
padding: 10px;
|
||||
`;
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { EuiButtonGroup } from '@elastic/eui';
|
||||
import { InjectedIntl, injectI18n } from '@kbn/i18n/react';
|
||||
import React from 'react';
|
||||
|
||||
interface Props {
|
||||
view: string;
|
||||
onChange: (view: string) => void;
|
||||
intl: InjectedIntl;
|
||||
}
|
||||
|
||||
export const ViewSwitcher = injectI18n(({ view, onChange, intl }: Props) => {
|
||||
const buttons = [
|
||||
{
|
||||
id: 'map',
|
||||
label: intl.formatMessage({
|
||||
id: 'xpack.infra.viewSwitcher.mapViewLabel',
|
||||
defaultMessage: 'Map View',
|
||||
}),
|
||||
iconType: 'apps',
|
||||
},
|
||||
{
|
||||
id: 'table',
|
||||
label: intl.formatMessage({
|
||||
id: 'xpack.infra.viewSwitcher.tableViewLabel',
|
||||
defaultMessage: 'Table View',
|
||||
}),
|
||||
iconType: 'editorUnorderedList',
|
||||
},
|
||||
];
|
||||
return (
|
||||
<EuiButtonGroup
|
||||
legend={intl.formatMessage({
|
||||
id: 'xpack.infra.viewSwitcher.lenged',
|
||||
defaultMessage: 'Switch between table and map view',
|
||||
})}
|
||||
options={buttons}
|
||||
color="primary"
|
||||
idSelected={view}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
});
|
|
@ -18,6 +18,7 @@ import React from 'react';
|
|||
import { InfraIndexField, InfraNodeType, InfraPathInput, InfraPathType } from '../../graphql/types';
|
||||
import { InfraGroupByOptions } from '../../lib/lib';
|
||||
import { CustomFieldPanel } from './custom_field_panel';
|
||||
import { fieldToName } from './lib/field_to_display_name';
|
||||
|
||||
interface Props {
|
||||
nodeType: InfraNodeType;
|
||||
|
@ -29,107 +30,34 @@ interface Props {
|
|||
customOptions: InfraGroupByOptions[];
|
||||
}
|
||||
|
||||
const createFieldToOptionMapper = (intl: InjectedIntl) => (field: string) => ({
|
||||
text: fieldToName(field, intl),
|
||||
type: InfraPathType.terms,
|
||||
field,
|
||||
});
|
||||
|
||||
let OPTIONS: { [P in InfraNodeType]: InfraGroupByOptions[] };
|
||||
const getOptions = (
|
||||
nodeType: InfraNodeType,
|
||||
intl: InjectedIntl
|
||||
): Array<{ text: string; type: InfraPathType; field: string }> => {
|
||||
if (!OPTIONS) {
|
||||
const mapFieldToOption = createFieldToOptionMapper(intl);
|
||||
OPTIONS = {
|
||||
[InfraNodeType.pod]: [
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.podGroupByOptions.namespaceLabel',
|
||||
defaultMessage: 'Namespace',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'kubernetes.namespace',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.podGroupByOptions.nodeLabel',
|
||||
defaultMessage: 'Node',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'kubernetes.node.name',
|
||||
},
|
||||
],
|
||||
[InfraNodeType.pod]: ['kubernetes.namespace', 'kubernetes.node.name'].map(mapFieldToOption),
|
||||
[InfraNodeType.container]: [
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.containerGroupByOptions.hostLabel',
|
||||
defaultMessage: 'Host',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'host.name',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.containerGroupByOptions.availabilityZoneLabel',
|
||||
defaultMessage: 'Availability Zone',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.availability_zone',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.containerGroupByOptions.machineTypeLabel',
|
||||
defaultMessage: 'Machine Type',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.machine_type',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.containerGroupByOptions.projectIDLabel',
|
||||
defaultMessage: 'Project ID',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.project_id',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.containerGroupByOptions.providerLabel',
|
||||
defaultMessage: 'Provider',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.provider',
|
||||
},
|
||||
],
|
||||
'host.name',
|
||||
'meta.cloud.availability_zone',
|
||||
'meta.cloud.machine_type',
|
||||
'meta.cloud.project_id',
|
||||
'meta.cloud.provider',
|
||||
].map(mapFieldToOption),
|
||||
[InfraNodeType.host]: [
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.hostGroupByOptions.availabilityZoneLabel',
|
||||
defaultMessage: 'Availability Zone',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.availability_zone',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.hostGroupByOptions.machineTypeLabel',
|
||||
defaultMessage: 'Machine Type',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.machine_type',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.hostGroupByOptions.projectIDLabel',
|
||||
defaultMessage: 'Project ID',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.project_id',
|
||||
},
|
||||
{
|
||||
text: intl.formatMessage({
|
||||
id: 'xpack.infra.waffle.hostGroupByOptions.cloudProviderLabel',
|
||||
defaultMessage: 'Cloud Provider',
|
||||
}),
|
||||
type: InfraPathType.terms,
|
||||
field: 'meta.cloud.provider',
|
||||
},
|
||||
],
|
||||
'meta.cloud.availability_zone',
|
||||
'meta.cloud.machine_type',
|
||||
'meta.cloud.project_id',
|
||||
'meta.cloud.provider',
|
||||
].map(mapFieldToOption),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
} from '../../lib/lib';
|
||||
import { isWaffleMapGroupWithGroups, isWaffleMapGroupWithNodes } from './type_guards';
|
||||
|
||||
function createId(path: InfraNodePath[]) {
|
||||
export function createId(path: InfraNodePath[]) {
|
||||
return path.map(p => p.value).join('/');
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ function findOrCreateGroupWithNodes(
|
|||
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithNodes.allName', {
|
||||
defaultMessage: 'All',
|
||||
})
|
||||
: (lastPath && lastPath.label) || 'No Group',
|
||||
: (lastPath && lastPath.label) || 'Unknown Group',
|
||||
count: 0,
|
||||
width: 0,
|
||||
squareSize: 0,
|
||||
|
@ -77,7 +77,7 @@ function findOrCreateGroupWithGroups(
|
|||
? i18n.translate('xpack.infra.nodesToWaffleMap.groupsWithGroups.allName', {
|
||||
defaultMessage: 'All',
|
||||
})
|
||||
: (lastPath && lastPath.label) || 'No Group',
|
||||
: (lastPath && lastPath.label) || 'Unknown Group',
|
||||
count: 0,
|
||||
width: 0,
|
||||
squareSize: 0,
|
||||
|
@ -85,10 +85,10 @@ function findOrCreateGroupWithGroups(
|
|||
};
|
||||
}
|
||||
|
||||
function createWaffleMapNode(node: InfraNode): InfraWaffleMapNode {
|
||||
export function createWaffleMapNode(node: InfraNode): InfraWaffleMapNode {
|
||||
const nodePathItem = last(node.path);
|
||||
if (!nodePathItem) {
|
||||
throw new Error('There must be a minimum of one path');
|
||||
throw new Error('There must be at least one node path item');
|
||||
}
|
||||
return {
|
||||
pathId: node.path.map(p => p.value).join('/'),
|
||||
|
|
|
@ -25,6 +25,8 @@ export const waffleNodesQuery = gql`
|
|||
metric {
|
||||
name
|
||||
value
|
||||
avg
|
||||
max
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,18 +9,17 @@ import { Query } from 'react-apollo';
|
|||
|
||||
import {
|
||||
InfraMetricInput,
|
||||
InfraNode,
|
||||
InfraNodeType,
|
||||
InfraPathInput,
|
||||
InfraPathType,
|
||||
InfraTimerangeInput,
|
||||
WaffleNodesQuery,
|
||||
} from '../../graphql/types';
|
||||
import { InfraWaffleMapGroup } from '../../lib/lib';
|
||||
import { nodesToWaffleMap } from './nodes_to_wafflemap';
|
||||
import { waffleNodesQuery } from './waffle_nodes.gql_query';
|
||||
|
||||
interface WithWaffleNodesArgs {
|
||||
nodes: InfraWaffleMapGroup[];
|
||||
nodes: InfraNode[];
|
||||
loading: boolean;
|
||||
refetch: () => void;
|
||||
}
|
||||
|
@ -67,7 +66,7 @@ export const WithWaffleNodes = ({
|
|||
loading,
|
||||
nodes:
|
||||
data && data.source && data.source.map && data.source.map.nodes
|
||||
? nodesToWaffleMap(data.source.map.nodes)
|
||||
? data.source.map.nodes
|
||||
: [],
|
||||
refetch,
|
||||
})
|
||||
|
|
|
@ -22,13 +22,15 @@ import { UrlStateContainer } from '../../utils/url_state';
|
|||
|
||||
const selectOptionsUrlState = createSelector(
|
||||
waffleOptionsSelectors.selectMetric,
|
||||
waffleOptionsSelectors.selectView,
|
||||
waffleOptionsSelectors.selectGroupBy,
|
||||
waffleOptionsSelectors.selectNodeType,
|
||||
waffleOptionsSelectors.selectCustomOptions,
|
||||
(metric, groupBy, nodeType, customOptions) => ({
|
||||
(metric, view, groupBy, nodeType, customOptions) => ({
|
||||
metric,
|
||||
groupBy,
|
||||
nodeType,
|
||||
view,
|
||||
customOptions,
|
||||
})
|
||||
);
|
||||
|
@ -38,6 +40,7 @@ export const withWaffleOptions = connect(
|
|||
metric: waffleOptionsSelectors.selectMetric(state),
|
||||
groupBy: waffleOptionsSelectors.selectGroupBy(state),
|
||||
nodeType: waffleOptionsSelectors.selectNodeType(state),
|
||||
view: waffleOptionsSelectors.selectView(state),
|
||||
customOptions: waffleOptionsSelectors.selectCustomOptions(state),
|
||||
urlState: selectOptionsUrlState(state),
|
||||
}),
|
||||
|
@ -45,6 +48,7 @@ export const withWaffleOptions = connect(
|
|||
changeMetric: waffleOptionsActions.changeMetric,
|
||||
changeGroupBy: waffleOptionsActions.changeGroupBy,
|
||||
changeNodeType: waffleOptionsActions.changeNodeType,
|
||||
changeView: waffleOptionsActions.changeView,
|
||||
changeCustomOptions: waffleOptionsActions.changeCustomOptions,
|
||||
})
|
||||
);
|
||||
|
@ -59,12 +63,20 @@ interface WaffleOptionsUrlState {
|
|||
metric?: ReturnType<typeof waffleOptionsSelectors.selectMetric>;
|
||||
groupBy?: ReturnType<typeof waffleOptionsSelectors.selectGroupBy>;
|
||||
nodeType?: ReturnType<typeof waffleOptionsSelectors.selectNodeType>;
|
||||
view?: ReturnType<typeof waffleOptionsSelectors.selectView>;
|
||||
customOptions?: ReturnType<typeof waffleOptionsSelectors.selectCustomOptions>;
|
||||
}
|
||||
|
||||
export const WithWaffleOptionsUrlState = () => (
|
||||
<WithWaffleOptions>
|
||||
{({ changeMetric, urlState, changeGroupBy, changeNodeType, changeCustomOptions }) => (
|
||||
{({
|
||||
changeMetric,
|
||||
urlState,
|
||||
changeGroupBy,
|
||||
changeNodeType,
|
||||
changeView,
|
||||
changeCustomOptions,
|
||||
}) => (
|
||||
<UrlStateContainer
|
||||
urlState={urlState}
|
||||
urlStateKey="waffleOptions"
|
||||
|
@ -79,6 +91,9 @@ export const WithWaffleOptionsUrlState = () => (
|
|||
if (newUrlState && newUrlState.nodeType) {
|
||||
changeNodeType(newUrlState.nodeType);
|
||||
}
|
||||
if (newUrlState && newUrlState.view) {
|
||||
changeView(newUrlState.view);
|
||||
}
|
||||
if (newUrlState && newUrlState.customOptions) {
|
||||
changeCustomOptions(newUrlState.customOptions);
|
||||
}
|
||||
|
@ -93,6 +108,9 @@ export const WithWaffleOptionsUrlState = () => (
|
|||
if (initialUrlState && initialUrlState.nodeType) {
|
||||
changeNodeType(initialUrlState.nodeType);
|
||||
}
|
||||
if (initialUrlState && initialUrlState.view) {
|
||||
changeView(initialUrlState.view);
|
||||
}
|
||||
if (initialUrlState && initialUrlState.customOptions) {
|
||||
changeCustomOptions(initialUrlState.customOptions);
|
||||
}
|
||||
|
@ -108,6 +126,7 @@ const mapToUrlState = (value: any): WaffleOptionsUrlState | undefined =>
|
|||
metric: mapToMetricUrlState(value.metric),
|
||||
groupBy: mapToGroupByUrlState(value.groupBy),
|
||||
nodeType: mapToNodeTypeUrlState(value.nodeType),
|
||||
view: mapToViewUrlState(value.view),
|
||||
customOptions: mapToCustomOptionsUrlState(value.customOptions),
|
||||
}
|
||||
: undefined;
|
||||
|
@ -141,6 +160,10 @@ const mapToNodeTypeUrlState = (subject: any) => {
|
|||
return subject && InfraNodeType[subject] ? subject : undefined;
|
||||
};
|
||||
|
||||
const mapToViewUrlState = (subject: any) => {
|
||||
return subject && ['map', 'table'].includes(subject) ? subject : undefined;
|
||||
};
|
||||
|
||||
const mapToCustomOptionsUrlState = (subject: any) => {
|
||||
return subject && Array.isArray(subject) && subject.every(isInfraGroupByOption)
|
||||
? subject
|
||||
|
|
|
@ -1774,6 +1774,30 @@
|
|||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "avg",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "max",
|
||||
"description": "",
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": { "kind": "SCALAR", "name": "Float", "ofType": null }
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
|
|
|
@ -215,6 +215,10 @@ export interface InfraNodeMetric {
|
|||
name: InfraMetricType;
|
||||
|
||||
value: number;
|
||||
|
||||
avg: number;
|
||||
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface InfraMetricData {
|
||||
|
@ -719,6 +723,10 @@ export namespace WaffleNodesQuery {
|
|||
name: InfraMetricType;
|
||||
|
||||
value: number;
|
||||
|
||||
avg: number;
|
||||
|
||||
max: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,8 @@
|
|||
|
||||
import React from 'react';
|
||||
|
||||
import { NodesOverview } from '../../components/nodes_overview';
|
||||
import { PageContent } from '../../components/page';
|
||||
import { Waffle } from '../../components/waffle';
|
||||
|
||||
import { WithWaffleFilter } from '../../containers/waffle/with_waffle_filters';
|
||||
import { WithWaffleNodes } from '../../containers/waffle/with_waffle_nodes';
|
||||
|
@ -27,7 +27,7 @@ export const HomePageContent: React.SFC = () => (
|
|||
<WithWaffleTime>
|
||||
{({ currentTimeRange, isAutoReloading }) => (
|
||||
<WithWaffleOptions>
|
||||
{({ metric, groupBy, nodeType }) => (
|
||||
{({ metric, groupBy, nodeType, view, changeView }) => (
|
||||
<WithWaffleNodes
|
||||
filterQuery={filterQueryAsJson}
|
||||
metric={metric}
|
||||
|
@ -37,14 +37,16 @@ export const HomePageContent: React.SFC = () => (
|
|||
timerange={currentTimeRange}
|
||||
>
|
||||
{({ nodes, loading, refetch }) => (
|
||||
<Waffle
|
||||
map={nodes}
|
||||
<NodesOverview
|
||||
nodes={nodes}
|
||||
loading={nodes.length > 0 && isAutoReloading ? false : loading}
|
||||
nodeType={nodeType}
|
||||
options={{ ...wafflemap, metric, fields: configuredFields, groupBy }}
|
||||
reload={refetch}
|
||||
onDrilldown={applyFilterQuery}
|
||||
timeRange={currentTimeRange}
|
||||
view={view}
|
||||
onViewChange={changeView}
|
||||
/>
|
||||
)}
|
||||
</WithWaffleNodes>
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import actionCreatorFactory from 'typescript-fsa';
|
||||
|
||||
import { InfraMetricInput, InfraNodeType, InfraPathInput } from '../../../graphql/types';
|
||||
import { InfraGroupByOptions } from '../../../lib/lib';
|
||||
|
||||
|
@ -15,3 +14,4 @@ export const changeMetric = actionCreator<InfraMetricInput>('CHANGE_METRIC');
|
|||
export const changeGroupBy = actionCreator<InfraPathInput[]>('CHANGE_GROUP_BY');
|
||||
export const changeCustomOptions = actionCreator<InfraGroupByOptions[]>('CHANGE_CUSTOM_OPTIONS');
|
||||
export const changeNodeType = actionCreator<InfraNodeType>('CHANGE_NODE_TYPE');
|
||||
export const changeView = actionCreator<string>('CHANGE_VIEW');
|
||||
|
|
|
@ -14,12 +14,19 @@ import {
|
|||
InfraPathInput,
|
||||
} from '../../../graphql/types';
|
||||
import { InfraGroupByOptions } from '../../../lib/lib';
|
||||
import { changeCustomOptions, changeGroupBy, changeMetric, changeNodeType } from './actions';
|
||||
import {
|
||||
changeCustomOptions,
|
||||
changeGroupBy,
|
||||
changeMetric,
|
||||
changeNodeType,
|
||||
changeView,
|
||||
} from './actions';
|
||||
|
||||
export interface WaffleOptionsState {
|
||||
metric: InfraMetricInput;
|
||||
groupBy: InfraPathInput[];
|
||||
nodeType: InfraNodeType;
|
||||
view: string;
|
||||
customOptions: InfraGroupByOptions[];
|
||||
}
|
||||
|
||||
|
@ -27,6 +34,7 @@ export const initialWaffleOptionsState: WaffleOptionsState = {
|
|||
metric: { type: InfraMetricType.cpu },
|
||||
groupBy: [],
|
||||
nodeType: InfraNodeType.host,
|
||||
view: 'map',
|
||||
customOptions: [],
|
||||
};
|
||||
|
||||
|
@ -49,9 +57,15 @@ const currentNodeTypeReducer = reducerWithInitialState(initialWaffleOptionsState
|
|||
(current, target) => target
|
||||
);
|
||||
|
||||
const currentViewReducer = reducerWithInitialState(initialWaffleOptionsState.view).case(
|
||||
changeView,
|
||||
(current, target) => target
|
||||
);
|
||||
|
||||
export const waffleOptionsReducer = combineReducers<WaffleOptionsState>({
|
||||
metric: currentMetricReducer,
|
||||
groupBy: currentGroupByReducer,
|
||||
nodeType: currentNodeTypeReducer,
|
||||
view: currentViewReducer,
|
||||
customOptions: currentCustomOptionsReducer,
|
||||
});
|
||||
|
|
|
@ -10,3 +10,4 @@ export const selectMetric = (state: WaffleOptionsState) => state.metric;
|
|||
export const selectGroupBy = (state: WaffleOptionsState) => state.groupBy;
|
||||
export const selectCustomOptions = (state: WaffleOptionsState) => state.customOptions;
|
||||
export const selectNodeType = (state: WaffleOptionsState) => state.nodeType;
|
||||
export const selectView = (state: WaffleOptionsState) => state.view;
|
||||
|
|
|
@ -10,6 +10,8 @@ export const nodesSchema: any = gql`
|
|||
type InfraNodeMetric {
|
||||
name: InfraMetricType!
|
||||
value: Float!
|
||||
avg: Float!
|
||||
max: Float!
|
||||
}
|
||||
|
||||
type InfraNodePath {
|
||||
|
|
|
@ -243,6 +243,10 @@ export interface InfraNodeMetric {
|
|||
name: InfraMetricType;
|
||||
|
||||
value: number;
|
||||
|
||||
avg: number;
|
||||
|
||||
max: number;
|
||||
}
|
||||
|
||||
export interface InfraMetricData {
|
||||
|
@ -1283,6 +1287,10 @@ export namespace InfraNodeMetricResolvers {
|
|||
name?: NameResolver<InfraMetricType, TypeParent, Context>;
|
||||
|
||||
value?: ValueResolver<number, TypeParent, Context>;
|
||||
|
||||
avg?: AvgResolver<number, TypeParent, Context>;
|
||||
|
||||
max?: MaxResolver<number, TypeParent, Context>;
|
||||
}
|
||||
|
||||
export type NameResolver<
|
||||
|
@ -1295,6 +1303,16 @@ export namespace InfraNodeMetricResolvers {
|
|||
Parent = InfraNodeMetric,
|
||||
Context = InfraContext
|
||||
> = Resolver<R, Parent, Context>;
|
||||
export type AvgResolver<R = number, Parent = InfraNodeMetric, Context = InfraContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
export type MaxResolver<R = number, Parent = InfraNodeMetric, Context = InfraContext> = Resolver<
|
||||
R,
|
||||
Parent,
|
||||
Context
|
||||
>;
|
||||
}
|
||||
|
||||
export namespace InfraMetricDataResolvers {
|
||||
|
|
|
@ -4,11 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { get, last } from 'lodash';
|
||||
import { isNumber } from 'lodash';
|
||||
import { get, isNumber, last, max, sum } from 'lodash';
|
||||
import moment from 'moment';
|
||||
|
||||
import { InfraNode, InfraNodeMetric } from '../../../../graphql/types';
|
||||
import { InfraMetricType, InfraNode, InfraNodeMetric } from '../../../../graphql/types';
|
||||
import { InfraBucket, InfraNodeRequestOptions } from '../adapter_types';
|
||||
import { NAME_FIELDS } from '../constants';
|
||||
import { getBucketSizeInSeconds } from './get_bucket_size_in_seconds';
|
||||
|
@ -34,6 +33,21 @@ const findLastFullBucket = (
|
|||
}, last(buckets));
|
||||
};
|
||||
|
||||
const getMetricValueFromBucket = (type: InfraMetricType) => (bucket: InfraBucket) => {
|
||||
const metric = bucket[type];
|
||||
return (metric && (metric.normalized_value || metric.value)) || 0;
|
||||
};
|
||||
|
||||
function calculateMax(bucket: InfraBucket, type: InfraMetricType) {
|
||||
const { buckets } = bucket.timeseries;
|
||||
return max(buckets.map(getMetricValueFromBucket(type))) || 0;
|
||||
}
|
||||
|
||||
function calculateAvg(bucket: InfraBucket, type: InfraMetricType) {
|
||||
const { buckets } = bucket.timeseries;
|
||||
return sum(buckets.map(getMetricValueFromBucket(type))) / buckets.length || 0;
|
||||
}
|
||||
|
||||
function createNodeMetrics(
|
||||
options: InfraNodeRequestOptions,
|
||||
node: InfraBucket,
|
||||
|
@ -45,11 +59,11 @@ function createNodeMetrics(
|
|||
if (!lastBucket) {
|
||||
throw new Error('Date histogram returned an empty set of buckets.');
|
||||
}
|
||||
const metricObj = lastBucket[metric.type];
|
||||
const value = (metricObj && (metricObj.normalized_value || metricObj.value)) || 0;
|
||||
return {
|
||||
name: metric.type,
|
||||
value,
|
||||
value: getMetricValueFromBucket(metric.type)(lastBucket),
|
||||
max: calculateMax(bucket, metric.type),
|
||||
avg: calculateAvg(bucket, metric.type),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import { cloneDeep, set } from 'lodash';
|
||||
import moment from 'moment';
|
||||
import { InfraESSearchBody, InfraProcesorRequestOptions } from '../../adapter_types';
|
||||
import { createBasePath } from '../../lib/create_base_path';
|
||||
import { getBucketSizeInSeconds } from '../../lib/get_bucket_size_in_seconds';
|
||||
|
@ -24,10 +23,6 @@ export const dateHistogramProcessor = (options: InfraProcesorRequestOptions) =>
|
|||
const result = cloneDeep(doc);
|
||||
const { timerange, sourceConfiguration, groupBy } = options.nodeOptions;
|
||||
const bucketSizeInSeconds = getBucketSizeInSeconds(timerange.interval);
|
||||
const boundsMin = moment
|
||||
.utc(timerange.from)
|
||||
.subtract(5 * bucketSizeInSeconds, 's')
|
||||
.valueOf();
|
||||
const path = createBasePath(groupBy).concat('timeseries');
|
||||
const bucketOffset = calculateOffsetInSeconds(timerange.from, bucketSizeInSeconds);
|
||||
const offset = `${Math.floor(bucketOffset)}s`;
|
||||
|
@ -38,7 +33,7 @@ export const dateHistogramProcessor = (options: InfraProcesorRequestOptions) =>
|
|||
min_doc_count: 0,
|
||||
offset,
|
||||
extended_bounds: {
|
||||
min: boundsMin,
|
||||
min: timerange.from,
|
||||
max: timerange.to,
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"extends": "../../tsconfig.json"
|
||||
}
|
||||
"extends": "../../tsconfig.json",
|
||||
}
|
30
x-pack/plugins/infra/types/eui.d.ts
vendored
30
x-pack/plugins/infra/types/eui.d.ts
vendored
|
@ -170,4 +170,34 @@ declare module '@elastic/eui' {
|
|||
};
|
||||
|
||||
export const EuiDatePickerRange: React.SFC<EuiDatePickerRangeProps>;
|
||||
|
||||
type EuiInMemoryTableProps = CommonProps & {
|
||||
items?: any;
|
||||
columns?: any;
|
||||
sorting?: any;
|
||||
search?: any;
|
||||
selection?: any;
|
||||
pagination?: any;
|
||||
itemId?: any;
|
||||
isSelectable?: any;
|
||||
loading?: any;
|
||||
hasActions?: any;
|
||||
message?: any;
|
||||
};
|
||||
export const EuiInMemoryTable: React.SFC<EuiInMemoryTableProps>;
|
||||
|
||||
type EuiButtonGroupProps = CommonProps & {
|
||||
buttonSize?: any;
|
||||
color?: any;
|
||||
idToSelectedMap?: any;
|
||||
options?: any;
|
||||
type?: any;
|
||||
onChange?: any;
|
||||
isIconOnly?: any;
|
||||
isDisabled?: any;
|
||||
isFullWidth?: any;
|
||||
legend?: any;
|
||||
idSelected?: any;
|
||||
};
|
||||
export const EuiButtonGroup: React.SFC<EuiButtonGroupProps>;
|
||||
}
|
||||
|
|
|
@ -48,6 +48,8 @@ const waffleTests: KbnTestProvider = ({ getService }) => {
|
|||
expect(firstNode.metric).to.eql({
|
||||
name: 'cpu',
|
||||
value: 0.011,
|
||||
avg: 0.012215686274509805,
|
||||
max: 0.020999999999999998,
|
||||
__typename: 'InfraNodeMetric',
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue