[Monitoring/React] Render ES Nodes Listing with Base Controller (#18585) (#18594)

* [Monitoring/React] Render ES Nodes Listing with Base Controller

Refactors the ES Nodes Listing to use Base Controller and a React component instead of an Angular directive

* remove another obsolete file
This commit is contained in:
Tim Sullivan 2018-04-30 09:14:13 -07:00 committed by GitHub
parent 5b4e00b053
commit 9219ef2413
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 233 additions and 240 deletions

View file

@ -5,3 +5,4 @@
*/
export { ElasticsearchOverview } from './overview';
export { ElasticsearchNodes } from './nodes';

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { NodeStatusIcon } from './status_icon';

View file

@ -5,12 +5,12 @@
*/
import React from 'react';
import { StatusIcon } from 'plugins/monitoring/components/status_icon';
import { StatusIcon } from '../../status_icon';
export function NodeStatusIcon({ status }) {
const type = (status === 'Online') ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY;
export function NodeStatusIcon({ isOnline, status }) {
const type = isOnline ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY;
return (
<StatusIcon type={type} label={`Health: ${status}`} />
<StatusIcon type={type} label={`Status: ${status}`} />
);
}

View file

@ -0,0 +1,7 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { ElasticsearchNodes } from './nodes';

View file

@ -0,0 +1,171 @@
/*
* 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, { Fragment } from 'react';
import { get } from 'lodash';
import { SORT_ASCENDING } from '../../../../common/constants';
import { NodeStatusIcon } from '../node';
import { extractIp } from '../../../lib/extract_ip'; // TODO this is only used for elasticsearch nodes summary / node detail, so it should be moved to components/elasticsearch/nodes/lib
import { ClusterStatus } from '../cluster_status';
import { MonitoringTable } from '../../';
import { MetricCell, OfflineCell } from './cells';
import { EuiLink, EuiToolTip } from '@elastic/eui';
import { KuiTableRowCell, KuiTableRow } from '@kbn/ui-framework/components';
const filterFields = ['node.name', 'status', 'type', 'transport_address'];
const getColumns = showCgroupMetricsElasticsearch => {
const cols = [];
cols.push({ title: 'Name', sortKey: 'node.name', sortOrder: SORT_ASCENDING });
cols.push({ title: 'Status', sortKey: 'online' });
if (showCgroupMetricsElasticsearch) {
cols.push({ title: 'CPU Usage', sortKey: 'node_cgroup_quota.lastVal' });
cols.push({
title: 'CPU Throttling',
sortKey: 'node_cgroup_throttled.lastVal'
});
} else {
cols.push({ title: 'CPU Usage', sortKey: 'node_cpu_utilization.lastVal' });
cols.push({ title: 'Load Average', sortKey: 'node_load_average.lastVal' });
}
cols.push({ title: 'JVM Memory', sortKey: 'node_jvm_mem_percent.lastVal' });
cols.push({ title: 'Disk Free Space', sortKey: 'node_free_space.lastVal' });
cols.push({ title: 'Shards', sortKey: 'shardCount' });
return cols;
};
const nodeRowFactory = showCgroupMetricsElasticsearch => {
return class NodeRow extends React.Component {
constructor(props) {
super(props);
}
isOnline() {
return this.props.isOnline === true;
}
getCpuComponents() {
const isOnline = this.isOnline();
if (showCgroupMetricsElasticsearch) {
return [
<MetricCell
key="cpuCol1"
isOnline={isOnline}
metric={get(this.props, 'node_cgroup_quota')}
isPercent={true}
/>,
<MetricCell
key="cpuCol2"
isOnline={isOnline}
metric={get(this.props, 'node_cgroup_throttled')}
isPercent={false}
/>
];
}
return [
<MetricCell
key="cpuCol1"
isOnline={isOnline}
metric={get(this.props, 'node_cpu_utilization')}
isPercent={true}
/>,
<MetricCell
key="cpuCol2"
isOnline={isOnline}
metric={get(this.props, 'node_load_average')}
isPercent={false}
/>
];
}
getShardCount() {
if (this.isOnline()) {
return (
<KuiTableRowCell>
<div className="monitoringTableCell__number">
{get(this.props, 'shardCount')}
</div>
</KuiTableRowCell>
);
}
return <OfflineCell />;
}
render() {
const isOnline = this.isOnline();
const status = this.props.isOnline ? 'Online' : 'Offline';
return (
<KuiTableRow>
<KuiTableRowCell>
<div className="monitoringTableCell__name">
<EuiToolTip
position="bottom"
content={this.props.nodeTypeLabel}
>
<span className={`fa ${this.props.nodeTypeClass}`} />
</EuiToolTip>
&nbsp;
<EuiLink
href={`#/elasticsearch/nodes/${this.props.resolver}`}
data-test-subj={`nodeLink-${this.props.resolver}`}
>
{this.props.name}
</EuiLink>
</div>
<div className="monitoringTableCell__transportAddress">
{extractIp(this.props.transport_address)}
</div>
</KuiTableRowCell>
<KuiTableRowCell>
<div className="monitoringTableCell__status">
<NodeStatusIcon
isOnline={this.props.isOnline}
status={status}
/>{' '}
{status}
</div>
</KuiTableRowCell>
{this.getCpuComponents()}
<MetricCell
isOnline={isOnline}
metric={get(this.props, 'node_jvm_mem_percent')}
isPercent={true}
/>
<MetricCell
isOnline={isOnline}
metric={get(this.props, 'node_free_space')}
isPercent={false}
/>
{this.getShardCount()}
</KuiTableRow>
);
}
};
};
export function ElasticsearchNodes({ clusterStatus, nodes, showCgroupMetricsElasticsearch, ...props }) {
const columns = getColumns(showCgroupMetricsElasticsearch);
return (
<Fragment>
<ClusterStatus stats={clusterStatus} />
<MonitoringTable
className="elasticsearchNodesTable"
rows={nodes}
pageIndex={props.pageIndex}
filterText={props.filterText}
sortKey={props.sortKey}
sortOrder={props.sortOrder}
onNewState={props.onNewState}
placeholder="Filter Nodes..."
filterFields={filterFields}
columns={columns}
rowComponent={nodeRowFactory(showCgroupMetricsElasticsearch)}
/>
</Fragment>
);
}

View file

@ -5,9 +5,11 @@
*/
export { MonitoringTimeseriesContainer } from './chart';
export { MonitoringTable } from './table';
export { Tooltip } from './tooltip';
export { NoData } from './no_data';
export { License } from './license';
export { StatusIcon } from './status_icon';
export { SummaryStatus } from './summary_status';
export { PageLoading } from './page_loading';
export { ElasticsearchOverview } from './elasticsearch';
export { ElasticsearchOverview, ElasticsearchNodes } from './elasticsearch';

View file

@ -7,7 +7,7 @@
import React from 'react';
import { isEmpty, capitalize } from 'lodash';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { StatusIcon } from 'plugins/monitoring/components';
import { StatusIcon } from '../';
const wrapChild = ({ label, value, dataTestSubj }, index) => (
<EuiFlexItem
@ -26,7 +26,7 @@ const StatusIndicator = ({ status }) => {
return (
<div className="monitoring-summary-status__status-indicator">
Health: <StatusIcon type={status.toUpperCase()} label={status} />{' '}
Health: <StatusIcon type={status.toUpperCase()} label={`Status: ${status}`} />{' '}
{capitalize(status)}
</div>
);

View file

@ -14,7 +14,6 @@ import './cluster/listing';
import './elasticsearch/cluster_status';
import './elasticsearch/index_listing';
import './elasticsearch/index_summary';
import './elasticsearch/node_listing';
import './elasticsearch/node_summary';
import './elasticsearch/ml_job_listing';
import './elasticsearch/shard_allocation';

View file

@ -1,156 +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 { get } from 'lodash';
import React from 'react';
import { render } from 'react-dom';
import { uiModules } from 'ui/modules';
import {
KuiTableRowCell,
KuiTableRow
} from '@kbn/ui-framework/components';
import { MetricCell, OfflineCell } from 'plugins/monitoring/components/elasticsearch/node_listing/cells';
import { NodeStatusIcon } from 'plugins/monitoring/components/elasticsearch/node/status_icon';
import { Tooltip } from 'plugins/monitoring/components/tooltip';
import { MonitoringTable } from 'plugins/monitoring/components/table';
import { extractIp } from 'plugins/monitoring/lib/extract_ip';
import { SORT_ASCENDING } from '../../../../common/constants';
import {
EuiLink,
} from '@elastic/eui';
const filterFields = [ 'node.name', 'status', 'type', 'transport_address' ];
const getColumns = showCgroupMetricsElasticsearch => {
const cols = [];
cols.push({ title: 'Name', sortKey: 'node.name', sortOrder: SORT_ASCENDING });
cols.push({ title: 'Status', sortKey: 'online' });
if (showCgroupMetricsElasticsearch) {
cols.push({ title: 'CPU Usage', sortKey: 'node_cgroup_quota.lastVal' });
cols.push({ title: 'CPU Throttling', sortKey: 'node_cgroup_throttled.lastVal' });
} else {
cols.push({ title: 'CPU Usage', sortKey: 'node_cpu_utilization.lastVal' });
cols.push({ title: 'Load Average', sortKey: 'node_load_average.lastVal' });
}
cols.push({ title: 'JVM Memory', sortKey: 'node_jvm_mem_percent.lastVal' });
cols.push({ title: 'Disk Free Space', sortKey: 'node_free_space.lastVal' });
cols.push({ title: 'Shards', sortKey: 'shardCount' });
return cols;
};
const nodeRowFactory = (scope, kbnUrl, showCgroupMetricsElasticsearch) => {
return class NodeRow extends React.Component {
constructor(props) {
super(props);
this.goToNode = this.goToNode.bind(this);
}
goToNode() {
scope.$evalAsync(() => {
kbnUrl.changePath(`/elasticsearch/nodes/${this.props.resolver}`);
});
}
isOnline() {
return this.props.status === 'Online';
}
getCpuComponents() {
const isOnline = this.isOnline();
if (showCgroupMetricsElasticsearch) {
return [
<MetricCell key="cpuCol1" isOnline={isOnline} metric={get(this.props, 'node_cgroup_quota')} isPercent={true} />,
<MetricCell key="cpuCol2" isOnline={isOnline} metric={get(this.props, 'node_cgroup_throttled')} isPercent={false} />,
];
}
return [
<MetricCell key="cpuCol1" isOnline={isOnline} metric={get(this.props, 'node_cpu_utilization')} isPercent={true} />,
<MetricCell key="cpuCol2" isOnline={isOnline} metric={get(this.props, 'node_load_average')} isPercent={false} />,
];
}
getShardCount() {
if (this.isOnline()) {
return (
<KuiTableRowCell>
<div className="monitoringTableCell__number">
{get(this.props, 'shardCount')}
</div>
</KuiTableRowCell>
);
}
return <OfflineCell />;
}
render() {
const isOnline = this.isOnline();
return (
<KuiTableRow>
<KuiTableRowCell>
<div className="monitoringTableCell__name">
<Tooltip text={this.props.nodeTypeLabel} trigger="hover" placement="bottom">
<span className={`fa ${this.props.nodeTypeClass}`} />
</Tooltip>
&nbsp;
<EuiLink
onClick={this.goToNode}
data-test-subj={`nodeLink-${this.props.resolver}`}
>
{this.props.name}
</EuiLink>
</div>
<div className="monitoringTableCell__transportAddress">{extractIp(this.props.transport_address)}</div>
</KuiTableRowCell>
<KuiTableRowCell>
<div title={`Node status: ${this.props.status}`} className="monitoringTableCell__status">
<NodeStatusIcon status={this.props.status} />&nbsp;
{this.props.status}
</div>
</KuiTableRowCell>
{this.getCpuComponents()}
<MetricCell isOnline={isOnline} metric={get(this.props, 'node_jvm_mem_percent')} isPercent={true} />
<MetricCell isOnline={isOnline} metric={get(this.props, 'node_free_space')} isPercent={false} />
{this.getShardCount()}
</KuiTableRow>
);
}
};
};
// change the node to actually display the name
const uiModule = uiModules.get('monitoring/directives', []);
uiModule.directive('monitoringNodesListing', ($injector) => {
const kbnUrl = $injector.get('kbnUrl');
const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch');
const columns = getColumns(showCgroupMetricsElasticsearch);
return {
restrict: 'E',
scope: {
nodes: '=',
pageIndex: '=',
filterText: '=',
sortKey: '=',
sortOrder: '=',
onNewState: '=',
},
link(scope, $el) {
scope.$watch('nodes', (nodes = []) => {
const nodesTable = (
<MonitoringTable
className="nodesTable"
rows={nodes}
pageIndex={scope.pageIndex}
filterText={scope.filterText}
sortKey={scope.sortKey}
sortOrder={scope.sortOrder}
onNewState={scope.onNewState}
placeholder="Filter Nodes..."
filterFields={filterFields}
columns={columns}
rowComponent={nodeRowFactory(scope, kbnUrl, showCgroupMetricsElasticsearch)}
/>
);
render(nodesTable, $el[0]);
});
}
};
});

View file

@ -1,44 +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 { ajaxErrorHandlersProvider } from 'plugins/monitoring/lib/ajax_error_handler';
export function getPageData($injector) {
const $http = $injector.get('$http');
const globalState = $injector.get('globalState');
const url = `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes`;
const timefilter = $injector.get('timefilter');
const timeBounds = timefilter.getBounds();
const createRow = rowData => {
if (!rowData) {
return null;
}
return {
status: rowData.isOnline ? 'Online' : 'Offline',
...rowData
};
};
return $http.post(url, {
ccs: globalState.ccs,
timeRange: {
min: timeBounds.min.toISOString(),
max: timeBounds.max.toISOString()
}
})
.then(response => {
return {
clusterStatus: response.data.clusterStatus,
nodes: response.data.nodes.map(createRow)
};
})
.catch((err) => {
const Private = $injector.get('Private');
const ajaxErrorHandlers = Private(ajaxErrorHandlersProvider);
return ajaxErrorHandlers(err);
});
}

View file

@ -3,16 +3,5 @@
name="nodes"
data-test-subj="elasticsearchNodesListingPage"
>
<monitoring-cluster-status-elasticsearch status="esNodes.data.clusterStatus"></monitoring-cluster-status-elasticsearch>
<div class="page-row">
<h3 class="kuiScreenReaderOnly">Elasticsearch Nodes</h3>
<monitoring-nodes-listing
page-index="esNodes.pageIndex"
filter-text="esNodes.filterText"
sort-key="esNodes.sortKey"
sort-order="esNodes.sortOrder"
on-new-state="esNodes.onNewState"
nodes="esNodes.data.nodes"
></monitoring-nodes-listing>
</div>
<div id="elasticsearchNodesReact"></div>
</monitoring-main>

View file

@ -4,39 +4,56 @@
* you may not use this file except in compliance with the Elastic License.
*/
import React from 'react';
import { find } from 'lodash';
import uiRoutes from 'ui/routes';
import template from './index.html';
import { routeInitProvider } from 'plugins/monitoring/lib/route_init';
import { MonitoringViewBaseTableController } from '../../';
import { getPageData } from './get_page_data';
import template from './index.html';
import { ElasticsearchNodes } from '../../../components';
uiRoutes.when('/elasticsearch/nodes', {
template,
resolve: {
clusters: function (Private) {
clusters(Private) {
const routeInit = Private(routeInitProvider);
return routeInit();
},
pageData: getPageData
}
},
controllerAs: 'esNodes',
controller: class EsNodesList extends MonitoringViewBaseTableController {
controllerAs: 'elasticsearchNodes',
controller: class ElasticsearchNodesController extends MonitoringViewBaseTableController {
constructor($injector, $scope) {
const $route = $injector.get('$route');
const globalState = $injector.get('globalState');
const showCgroupMetricsElasticsearch = $injector.get('showCgroupMetricsElasticsearch');
$scope.cluster = find($route.current.locals.clusters, {
cluster_uuid: globalState.cluster_uuid
});
super({
title: 'Elasticsearch - Nodes',
storageKey: 'elasticsearch.nodes',
getPageData,
api: `../api/monitoring/v1/clusters/${globalState.cluster_uuid}/elasticsearch/nodes`,
reactNodeId: 'elasticsearchNodesReact',
defaultData: {},
$scope,
$injector
});
const $route = $injector.get('$route');
this.data = $route.current.locals.pageData;
const globalState = $injector.get('globalState');
$scope.cluster = find($route.current.locals.clusters, { cluster_uuid: globalState.cluster_uuid });
}
$scope.$watch(() => this.data, data => {
this.renderReact(data);
});
this.renderReact = ({ clusterStatus, nodes }) => {
super.renderReact(
<ElasticsearchNodes
clusterStatus={clusterStatus}
nodes={nodes}
showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch}
/>
);
};
}
}
});

View file

@ -48,7 +48,7 @@ export default function ({ getService, getPageObjects }) {
indicesCount: 'Indices: 20',
shardsCount: 'Shards: 38',
nodeType: 'Type: Master Node',
status: 'Health: Online',
status: 'Status: Online',
});
});
@ -64,7 +64,7 @@ export default function ({ getService, getPageObjects }) {
indicesCount: 'Indices: 4',
shardsCount: 'Shards: 4',
nodeType: 'Type: Node',
status: 'Health: Online',
status: 'Status: Online',
});
});
});
@ -99,7 +99,7 @@ export default function ({ getService, getPageObjects }) {
indicesCount: 'Indices: N/A',
shardsCount: 'Shards: N/A',
nodeType: 'Type: Offline Node',
status: 'Health: Offline',
status: 'Status: Offline',
});
});
});

View file

@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }) {
unassignedShards: '5',
documentCount: '25,927',
dataSize: '101.6 MB',
health: 'Health: yellow',
health: 'Status: yellow',
});
});

View file

@ -39,7 +39,7 @@ export default function ({ getService, getPageObjects }) {
unassignedShards: '5',
documentCount: '25,927',
dataSize: '101.6 MB',
health: 'yellow',
health: 'Status: yellow',
});
});
});

View file

@ -9,7 +9,7 @@ export function MonitoringElasticsearchNodesProvider({ getService/*, getPageObje
const retry = getService('retry');
const SUBJ_LISTING_PAGE = 'elasticsearchNodesListingPage';
const SUBJ_TABLE_BODY = 'nodesTableBody';
const SUBJ_TABLE_BODY = 'elasticsearchNodesTableBody';
const SUBJ_NODE_LINK_PREFIX = `${SUBJ_TABLE_BODY} nodeLink-`;
return new class ElasticsearchIndices {