diff --git a/package.json b/package.json index 186a1c1942b69..d9b0b8fbe051a 100644 --- a/package.json +++ b/package.json @@ -411,4 +411,4 @@ "node": "10.14.1", "yarn": "^1.10.1" } -} +} \ No newline at end of file diff --git a/x-pack/plugins/infra/public/components/waffle/index.tsx b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx similarity index 56% rename from x-pack/plugins/infra/public/components/waffle/index.tsx rename to x-pack/plugins/infra/public/components/nodes_overview/index.tsx index d38301f6eb1f8..6663b2354a40a 100644 --- a/x-pack/plugins/infra/public/components/waffle/index.tsx +++ b/x-pack/plugins/infra/public/components/nodes_overview/index.tsx @@ -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 { 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 ( ); - } else if (!loading && map && map.length === 0) { + } else if (!loading && nodes && nodes.length === 0) { return ( - {({ measureRef, content: { width = 0, height = 0 } }) => { - const groupsWithLayout = applyWaffleMapLayout(map, width, height); - return ( - measureRef(el)} - data-test-subj="waffleMap" - > - - {groupsWithLayout.map(this.renderGroup(bounds, timeRange))} - - - - ); - }} - + + + + + {view === 'table' ? ( + + + + ) : ( + + + + )} + ); } + 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 ( - - ); - } - if (isWaffleMapGroupWithNodes(group)) { - return ( - - ); - } - }; } ); -const WaffleMapOuterContiner = styled.div` - flex: 1 0 0%; - display: flex; - justify-content: center; - flex-direction: column; - overflow-x: hidden; - overflow-y: auto; +const CenteredEmptyPrompt = styled(EuiEmptyPrompt)` + align-self: center; `; -const WaffleMapInnerContainer = styled.div` - display: flex; - flex-direction: row; - flex-wrap: wrap; - justify-content: center; - align-content: flex-start; - padding: 10px; +const MainContainer = styled.div` + position: relative; + flex: 1 1 auto; `; -const CenteredEmptyPrompt = styled(EuiEmptyPrompt)` - align-self: center; +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; `; diff --git a/x-pack/plugins/infra/public/components/nodes_overview/table.tsx b/x-pack/plugins/infra/public/components/nodes_overview/table.tsx new file mode 100644 index 0000000000000..ac188d5e0d169 --- /dev/null +++ b/x-pack/plugins/infra/public/components/nodes_overview/table.tsx @@ -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; + +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 { + 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 }) => ( + + + {value} + + + ), + }, + ...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 ( + + {value} + + ); + }, + })), + { + field: 'value', + name: intl.formatMessage({ + id: 'xpack.infra.tableView.columnName.last1m', + defaultMessage: 'Last 1m', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'avg', + name: intl.formatMessage({ + id: 'xpack.infra.tableView.columnName.avg', + defaultMessage: 'Avg', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + { + field: 'max', + name: intl.formatMessage({ + id: 'xpack.infra.tableView.columnName.max', + defaultMessage: 'Max', + }), + sortable: true, + truncateText: true, + dataType: 'number', + render: (value: number) => {formatter(value)}, + }, + ]; + 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 ( + + ); + } + + 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), + })); + }; + } +); diff --git a/x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts b/x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts new file mode 100644 index 0000000000000..58f47b4ff46cf --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/lib/field_to_display_name.ts @@ -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; +}; diff --git a/x-pack/plugins/infra/public/components/waffle/map.tsx b/x-pack/plugins/infra/public/components/waffle/map.tsx new file mode 100644 index 0000000000000..0bd4c5a8e388b --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/map.tsx @@ -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 = ({ + nodes, + options, + timeRange, + onFilter, + formatter, + bounds, + nodeType, +}) => { + const map = nodesToWaffleMap(nodes); + return ( + + {({ measureRef, content: { width = 0, height = 0 } }) => { + const groupsWithLayout = applyWaffleMapLayout(map, width, height); + return ( + measureRef(el)} + data-test-subj="waffleMap" + > + + {groupsWithLayout.map(group => { + if (isWaffleMapGroupWithGroups(group)) { + return ( + + ); + } + if (isWaffleMapGroupWithNodes(group)) { + return ( + + ); + } + })} + + + + ); + }} + + ); +}; + +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; +`; diff --git a/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx b/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx new file mode 100644 index 0000000000000..83952ff8f5c1f --- /dev/null +++ b/x-pack/plugins/infra/public/components/waffle/view_switcher.tsx @@ -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 ( + + ); +}); diff --git a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx b/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx index 6832460aa4f7a..7b58bed4d69b2 100644 --- a/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx +++ b/x-pack/plugins/infra/public/components/waffle/waffle_group_by_controls.tsx @@ -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), }; } diff --git a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts index 59a015d6d5ca3..7544f4347ae77 100644 --- a/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts +++ b/x-pack/plugins/infra/public/containers/waffle/nodes_to_wafflemap.ts @@ -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('/'), diff --git a/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts b/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts index 51b67086a4bfa..8226cf9ad9b32 100644 --- a/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts +++ b/x-pack/plugins/infra/public/containers/waffle/waffle_nodes.gql_query.ts @@ -25,6 +25,8 @@ export const waffleNodesQuery = gql` metric { name value + avg + max } } } diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx index 2f8a18ca45ab7..b1fdd3a457a54 100644 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_nodes.tsx @@ -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, }) diff --git a/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx b/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx index 508d356666941..b71cb00ed2dd4 100644 --- a/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx +++ b/x-pack/plugins/infra/public/containers/waffle/with_waffle_options.tsx @@ -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; groupBy?: ReturnType; nodeType?: ReturnType; + view?: ReturnType; customOptions?: ReturnType; } export const WithWaffleOptionsUrlState = () => ( - {({ changeMetric, urlState, changeGroupBy, changeNodeType, changeCustomOptions }) => ( + {({ + changeMetric, + urlState, + changeGroupBy, + changeNodeType, + changeView, + changeCustomOptions, + }) => ( ( 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 diff --git a/x-pack/plugins/infra/public/graphql/introspection.json b/x-pack/plugins/infra/public/graphql/introspection.json index 5fad4b5cdc2b6..ffa92f18249fb 100644 --- a/x-pack/plugins/infra/public/graphql/introspection.json +++ b/x-pack/plugins/infra/public/graphql/introspection.json @@ -1661,6 +1661,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, diff --git a/x-pack/plugins/infra/public/graphql/types.ts b/x-pack/plugins/infra/public/graphql/types.ts index ce622a3283304..314885b4c3b71 100644 --- a/x-pack/plugins/infra/public/graphql/types.ts +++ b/x-pack/plugins/infra/public/graphql/types.ts @@ -197,6 +197,10 @@ export interface InfraNodeMetric { name: InfraMetricType; value: number; + + avg: number; + + max: number; } export interface InfraMetricData { @@ -659,6 +663,10 @@ export namespace WaffleNodesQuery { name: InfraMetricType; value: number; + + avg: number; + + max: number; }; } diff --git a/x-pack/plugins/infra/public/pages/home/page_content.tsx b/x-pack/plugins/infra/public/pages/home/page_content.tsx index 2b6990a6dbe54..c409be860c062 100644 --- a/x-pack/plugins/infra/public/pages/home/page_content.tsx +++ b/x-pack/plugins/infra/public/pages/home/page_content.tsx @@ -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 = () => ( {({ currentTimeRange, isAutoReloading }) => ( - {({ metric, groupBy, nodeType }) => ( + {({ metric, groupBy, nodeType, view, changeView }) => ( ( timerange={currentTimeRange} > {({ nodes, loading, refetch }) => ( - 0 && isAutoReloading ? false : loading} nodeType={nodeType} options={{ ...wafflemap, metric, fields: configuredFields, groupBy }} reload={refetch} onDrilldown={applyFilterQuery} timeRange={currentTimeRange} + view={view} + onViewChange={changeView} /> )} diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts b/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts index 79a47736e3ef4..5ef315e4b36d8 100644 --- a/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts +++ b/x-pack/plugins/infra/public/store/local/waffle_options/actions.ts @@ -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('CHANGE_METRIC'); export const changeGroupBy = actionCreator('CHANGE_GROUP_BY'); export const changeCustomOptions = actionCreator('CHANGE_CUSTOM_OPTIONS'); export const changeNodeType = actionCreator('CHANGE_NODE_TYPE'); +export const changeView = actionCreator('CHANGE_VIEW'); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts b/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts index e6404f6b3213b..d778eee60afb7 100644 --- a/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts +++ b/x-pack/plugins/infra/public/store/local/waffle_options/reducer.ts @@ -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({ metric: currentMetricReducer, groupBy: currentGroupByReducer, nodeType: currentNodeTypeReducer, + view: currentViewReducer, customOptions: currentCustomOptionsReducer, }); diff --git a/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts b/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts index d5c4f4826e8b4..d5ee4c2071539 100644 --- a/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts +++ b/x-pack/plugins/infra/public/store/local/waffle_options/selector.ts @@ -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; diff --git a/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts b/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts index ba47ccbc2be22..b1b9b57392054 100644 --- a/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts +++ b/x-pack/plugins/infra/server/graphql/nodes/schema.gql.ts @@ -10,6 +10,8 @@ export const nodesSchema: any = gql` type InfraNodeMetric { name: InfraMetricType! value: Float! + avg: Float! + max: Float! } type InfraNodePath { diff --git a/x-pack/plugins/infra/server/graphql/types.ts b/x-pack/plugins/infra/server/graphql/types.ts index 10193b2998ee6..34c74e50877b4 100644 --- a/x-pack/plugins/infra/server/graphql/types.ts +++ b/x-pack/plugins/infra/server/graphql/types.ts @@ -225,6 +225,10 @@ export interface InfraNodeMetric { name: InfraMetricType; value: number; + + avg: number; + + max: number; } export interface InfraMetricData { @@ -1204,6 +1208,10 @@ export namespace InfraNodeMetricResolvers { name?: NameResolver; value?: ValueResolver; + + avg?: AvgResolver; + + max?: MaxResolver; } export type NameResolver< @@ -1216,6 +1224,16 @@ export namespace InfraNodeMetricResolvers { Parent = InfraNodeMetric, Context = InfraContext > = Resolver; + export type AvgResolver = Resolver< + R, + Parent, + Context + >; + export type MaxResolver = Resolver< + R, + Parent, + Context + >; } export namespace InfraMetricDataResolvers { diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts index 5a5bdabdfdedc..7c02b2d61b3ec 100644 --- a/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/lib/create_node_item.ts @@ -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), }; } diff --git a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts index 31ef88ed3229c..df63cf4234645 100644 --- a/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts +++ b/x-pack/plugins/infra/server/lib/adapters/nodes/processors/last/date_histogram_processor.ts @@ -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, }, }, diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index 4082f16a5d91c..01bddbfc4264d 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -1,3 +1,3 @@ { - "extends": "../../tsconfig.json" -} + "extends": "../../tsconfig.json", +} \ No newline at end of file diff --git a/x-pack/plugins/infra/types/eui.d.ts b/x-pack/plugins/infra/types/eui.d.ts index c22337f16feda..78a9f81e5e6fd 100644 --- a/x-pack/plugins/infra/types/eui.d.ts +++ b/x-pack/plugins/infra/types/eui.d.ts @@ -170,4 +170,34 @@ declare module '@elastic/eui' { }; export const EuiDatePickerRange: React.SFC; + + 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; + + 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; } diff --git a/x-pack/test/api_integration/apis/infra/waffle.ts b/x-pack/test/api_integration/apis/infra/waffle.ts index 742e980230430..9efdbfdde119a 100644 --- a/x-pack/test/api_integration/apis/infra/waffle.ts +++ b/x-pack/test/api_integration/apis/infra/waffle.ts @@ -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', }); }