Merge pull request #202 from influxdata/feature/tr-remove-overview

Remove Overview page
pull/10616/head
Andrew Watkins 2016-10-06 09:36:34 -07:00 committed by GitHub
commit 2a44bc6807
16 changed files with 0 additions and 792 deletions

View File

@ -1,44 +0,0 @@
import NodeTable from 'src/overview/components/NodeTable';
import NodeTableRow from 'src/overview/components/NodeTableRow';
import React from 'react';
import TestUtils, {renderIntoDocument, scryRenderedComponentsWithType} from 'react-addons-test-utils';
import {findDOMNode} from 'react-dom';
describe('Overview.Components.NodeTable', () => {
it('renders a NodeTableRow for each node (meta + data)', () => {
const cluster = {
"data": [
{
"tcpAddr": "localhost:8088",
"httpAddr": "localhost:8086",
"id": 2,
"status": "joined"
},
{
"tcpAddr": "localhost:8188",
"httpAddr": "localhost:8186",
"id": 6,
"status": "joined"
}
],
"meta": [
{
"addr": "localhost:8091"
}
],
"id": "a cluster id"
};
const component = renderIntoDocument(
<NodeTable
clusterID='a cluster id'
cluster={cluster}
dataNodes={['localhost:8088', 'localhost:8188']}
refreshIntervalMs={2000}
/>
);
const nodeRows = scryRenderedComponentsWithType(component, NodeTableRow);
expect(nodeRows.length).to.equal(3);
});
});

View File

@ -1,109 +0,0 @@
import React from 'react';
import TestUtils, {renderIntoDocument, scryRenderedComponentsWithType} from 'react-addons-test-utils';
import {findDOMNode} from 'react-dom';
import {mount, shallow} from 'enzyme';
import sinon from 'sinon';
import * as api from 'src/shared/apis';
import {OverviewPage} from 'src/overview/containers/OverviewPage';
import ClusterStatsPanel from 'src/overview/components/ClusterStatsPanel';
import MiscStatsPanel from 'src/overview/components/MiscStatsPanel';
import NodeTable from 'src/overview/components/NodeTable';
const clusterID = '1000';
const showClustersResponse = {
data: [
{
tcpAddr: 'localhost:8088',
httpAddr: 'localhost:8086',
id: 1,
status: 'joined'
},
{
tcpAddr: 'localhost:8188',
httpAddr: 'localhost:8186',
id: 2,
status: 'joined'
}
],
meta: [
{
addr: 'localhost:8091'
}
]
};
function setup(customProps = {}) {
const defaultProps = {
dataNodes: ['localhost:8086'],
params: {clusterID},
addFlashMessage: function() {},
};
const props = Object.assign({}, defaultProps, customProps);
return mount(<OverviewPage {...props} />);
}
xdescribe('Overview.Containers.OverviewPage', function() {
it('renders a spinner initially', function() {
const wrapper = shallow(<OverviewPage dataNodes={['localhost:8086']} params={{clusterID}} addFlashMessage={() => {}} />);
expect(wrapper.contains(<div className="page-spinner" />)).to.be.true;
});
describe('after being mounted', function() {
let stub;
beforeEach(function() {
stub = sinon.stub(api, 'showCluster').returns(Promise.resolve({
data: showClustersResponse,
}));
});
afterEach(function() {
stub.restore();
});
it('fetches cluster information after being mounted', function() {
setup();
expect(api.showCluster.calledWith(clusterID)).to.be.true;
});
it('hides the spinner', function(done) {
const wrapper = setup();
setTimeout(() => {
expect(wrapper.contains(<div className="page-spinner" />)).to.be.false;
done();
}, 0);
});
it('fetches cluster information after being mounted', function() {
setup();
expect(api.showCluster.calledWith(clusterID)).to.be.true;
});
it('renders the correct panels', function(done) {
const wrapper = setup();
setTimeout(() => {
expect(wrapper.find(ClusterStatsPanel)).to.have.length(1);
expect(wrapper.find(MiscStatsPanel)).to.have.length(1);
expect(wrapper.find(NodeTable)).to.have.length(1);
done();
}, 0);
});
it('passes the correct props to NodeTable', function(done) {
const wrapper = setup();
setTimeout(() => {
const table = wrapper.find(NodeTable);
expect(table.props().cluster).to.eql(showClustersResponse);
expect(table.props().dataNodes).to.eql(['localhost:8086']);
expect(table.props().clusterID).to.equal(clusterID);
done();
}, 0);
});
});
});

View File

@ -6,7 +6,6 @@ import {Router, Route, browserHistory} from 'react-router';
import App from 'src/App';
import CheckDataNodes from 'src/CheckDataNodes';
import {HostsPage, HostPage} from 'src/hosts';
import OverviewPage from 'src/overview';
import QueriesPage from 'src/queries';
import TasksPage from 'src/tasks';
import RetentionPoliciesPage from 'src/retention_policies';
@ -122,7 +121,6 @@ const Root = React.createClass({
<Route path="/sources" component={SelectSourcePage} />
<Route path="/sources/:sourceID" component={App}>
<Route component={CheckDataNodes}>
<Route path="overview" component={OverviewPage} />
<Route path="queries" component={QueriesPage} />
<Route path="accounts" component={ClusterAccountsPage} />
<Route path="accounts/:accountID" component={ClusterAccountPage} />

View File

@ -1,64 +0,0 @@
import React, {PropTypes} from 'react';
import MiniGraph from 'shared/components/MiniGraph';
import AutoRefresh from 'shared/components/AutoRefresh';
const RefreshingMiniGraph = AutoRefresh(MiniGraph);
import {OVERVIEW_TIME_RANGE, OVERVIEW_INTERVAL} from '../constants';
const ClusterStatsPanel = React.createClass({
propTypes: {
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
clusterID: PropTypes.string.isRequired,
refreshIntervalMs: PropTypes.number.isRequired,
},
render() {
const {dataNodes, refreshIntervalMs} = this.props;
// For active writes/queries, we GROUP BY nodeID because we want to grab all of the
// values for each node and sum the results.
const queries = [
{
queryDescription: "Active Queries",
host: dataNodes,
database: '_internal',
text: `SELECT NON_NEGATIVE_DERIVATIVE(MAX(queryReq)) FROM httpd WHERE time > now() - ${OVERVIEW_TIME_RANGE} AND clusterID='${this.props.clusterID}' GROUP by nodeID, time(${OVERVIEW_INTERVAL})`,
options: {combineSeries: true},
},
{
queryDescription: "Avg Query Latency (ms)",
host: dataNodes,
database: '_internal',
text: `SELECT mean(queryDurationNs)/100000000000 FROM queryExecutor WHERE time > now() - ${OVERVIEW_TIME_RANGE} AND clusterID='${this.props.clusterID}' GROUP BY time(${OVERVIEW_INTERVAL})`,
},
{
queryDescription: "Active Writes",
host: dataNodes,
database: '_internal',
text: `SELECT NON_NEGATIVE_DERIVATIVE(MAX(req)) FROM "write" WHERE time > now() - ${OVERVIEW_TIME_RANGE} AND clusterID='${this.props.clusterID}' GROUP BY nodeID, time(${OVERVIEW_INTERVAL})`,
options: {combineSeries: true},
},
];
return (
<div className="col-lg-6">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Cluster Speed</h2>
</div>
<div className="panel-body" style={{height: '206px'}}>
{
queries.map(({options, host, database, text, queryDescription}, i) => {
return (
<RefreshingMiniGraph clusterID={this.props.clusterID} options={options} key={i} queries={[{host, database, text}]} autoRefresh={refreshIntervalMs} queryDescription={queryDescription}>
<p key={i} className="cluster-stat-empty"><span className="icon cubo"></span> Waiting for stats...</p>
</RefreshingMiniGraph>
);
})
}
</div>
</div>
</div>
);
},
});
export default ClusterStatsPanel;

View File

@ -1,81 +0,0 @@
import React, {PropTypes} from 'react';
import {formatBytes} from 'utils/formatting';
import {diskBytesFromShard} from 'shared/parsing/diskBytes';
import {clusterDiskUsage} from 'shared/apis/stats';
import MiniGraph from 'shared/components/MiniGraph';
import AutoRefresh from 'shared/components/AutoRefresh';
const RefreshingMiniGraph = AutoRefresh(MiniGraph);
import {OVERVIEW_TIME_RANGE, OVERVIEW_INTERVAL} from '../constants';
const MiscStatsPanel = React.createClass({
propTypes: {
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
clusterID: PropTypes.string.isRequired,
refreshIntervalMs: PropTypes.number.isRequired,
},
getInitialState() {
return {
diskUsed: 0,
influxVersion: null,
};
},
componentDidMount() {
clusterDiskUsage(this.props.dataNodes, this.props.clusterID).then((res) => {
this.setState({
diskUsed: diskBytesFromShard(res.data).bytes,
influxVersion: res.headers['x-influxdb-version'],
});
});
},
render() {
const {influxVersion, diskUsed} = this.state;
const {dataNodes, refreshIntervalMs} = this.props;
const queries = [
{
queryDescription: 'Bytes Allocated',
host: dataNodes,
database: '_internal',
text: `select max(Alloc) AS bytes_allocated from runtime where time > now() - ${OVERVIEW_TIME_RANGE} AND clusterID='${this.props.clusterID}' group by time(${OVERVIEW_INTERVAL})`,
},
{
queryDescription: 'Heap Bytes',
host: dataNodes,
database: '_internal',
text: `select max(HeapInUse) AS heap_bytes from runtime where time > now() - ${OVERVIEW_TIME_RANGE} AND clusterID='${this.props.clusterID}' group by time(${OVERVIEW_INTERVAL})`,
},
];
return (
<div className="col-lg-6">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Misc Stats</h2>
</div>
<div className="panel-body" style={{height: '206px'}}>
<div className="cluster-stat-2x">
<div className="influx-version">
<span className="cluster-stat-2x--label">Version:</span><span className="cluster-stat-2x--number">{influxVersion}</span>
</div>
<div className="disk-util">
<span className="cluster-stat-2x--label">Disk Use:</span><span className="cluster-stat-2x--number">{formatBytes(diskUsed)}</span>
</div>
</div>
<div className="top-stuff">
{
queries.map(({host, database, text, queryDescription}, i) => {
return (
<RefreshingMiniGraph key={i} clusterID={this.props.clusterID} queries={[{host, database, text}]} autoRefresh={refreshIntervalMs} queryDescription={queryDescription}>
<p className="cluster-stat-empty"><span className="icon cubo"></span> Waiting for stats...</p>
</RefreshingMiniGraph>
);
})
}
</div>
</div>
</div>
</div>
);
},
});
export default MiscStatsPanel;

View File

@ -1,80 +0,0 @@
import React, {PropTypes} from 'react';
import _ from 'lodash';
import NodeTableRow from './NodeTableRow';
const NodeTable = React.createClass({
propTypes: {
refreshIntervalMs: PropTypes.number.isRequired,
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
cluster: PropTypes.shape({
data: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
meta: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
}),
clusterID: PropTypes.string.isRequired,
},
render() {
const {cluster, clusterID, dataNodes, refreshIntervalMs} = this.props;
const data = _.map(cluster.data, (node) => {
// The nodeID tag is for data nodes is currently the TCP address, but is fickle and
// possibly subject to change.
return {addr: node.httpAddr, type: 'data', nodeID: node.tcpAddr};
});
const meta = _.map(cluster.meta, (node) => {
return {addr: node.addr, type: 'meta'};
});
return (
<div>
<div className="col-lg-8">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Data</h2>
</div>
<div className="panel-body">
<div className="table-responsive">
<table className="table v-center">
<thead>
<tr>
<th>Node</th>
<th>Disk Used</th>
<th>Active Queries</th>
<th>Active Writes</th>
</tr>
</thead>
<tbody>
{data.map((n, i) => <NodeTableRow key={i} node={n} clusterID={clusterID} dataNodes={dataNodes} refreshIntervalMs={refreshIntervalMs}/>)}
</tbody>
</table>
</div>
</div>
</div>
</div>
<div className="col-lg-4">
<div className="panel panel-minimal">
<div className="panel-heading">
<h2 className="panel-title">Meta</h2>
</div>
<div className="panel-body">
<div className="">
<table className="table v-center js-node-table">
<thead>
<tr>
<th>Node</th>
</tr>
</thead>
<tbody>
{meta.map((n, i) => <NodeTableRow key={i} node={n} clusterID={clusterID} dataNodes={dataNodes} refreshIntervalMs={refreshIntervalMs}/>)}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
);
},
});
export default NodeTable;

View File

@ -1,76 +0,0 @@
import React, {PropTypes} from 'react';
import {formatBytes} from 'utils/formatting';
import MiniGraph from 'shared/components/MiniGraph';
import AutoRefresh from 'shared/components/AutoRefresh';
import {nodeDiskUsage} from 'shared/apis/stats';
import {diskBytesFromShard} from 'shared/parsing/diskBytes';
const RefreshingMiniGraph = AutoRefresh(MiniGraph);
import {OVERVIEW_TIME_RANGE, OVERVIEW_INTERVAL} from '../constants';
const {string, shape, number} = PropTypes;
const NodeTableRow = React.createClass({
propTypes: {
refreshIntervalMs: number.isRequired,
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
clusterID: string.isRequired,
node: shape({
addr: string.isRequired,
type: string.isRequired,
}),
},
getInitialState() {
return {
diskUsed: 0,
};
},
componentDidMount() {
if (this.props.node.type !== 'data') {
return;
}
const {node, dataNodes, clusterID} = this.props;
nodeDiskUsage(dataNodes, clusterID, node.nodeID).then((resp) => {
this.setState({
diskUsed: diskBytesFromShard(resp.data).bytes,
});
});
},
render() {
const {node, dataNodes, clusterID, refreshIntervalMs} = this.props;
const diskUsed = formatBytes(this.state.diskUsed);
const activeQueries = `SELECT NON_NEGATIVE_DERIVATIVE(max(queryReq)) FROM httpd WHERE time > now() - ${OVERVIEW_TIME_RANGE} AND nodeID='${node.nodeID}' AND clusterID='${clusterID}' GROUP by time(${OVERVIEW_INTERVAL})`;
const activeWrites = `SELECT NON_NEGATIVE_DERIVATIVE(max(req)) FROM "write" WHERE time > now() - ${OVERVIEW_TIME_RANGE} AND nodeID='${node.nodeID}' AND clusterID='${clusterID}' GROUP BY time(${OVERVIEW_INTERVAL})`;
if (node.type !== 'data') {
return (
<tr className="node">
<td>{node.addr}</td>
</tr>
);
}
return (
<tr className="node">
<td>{node.addr}</td>
<td>{diskUsed}</td>
<td>
<RefreshingMiniGraph clusterID={clusterID} queries={[{host: dataNodes, database: '_internal', text: activeQueries}]} autoRefresh={refreshIntervalMs}>
<p className="cluster-stat-empty"><span className="icon cubo"></span> Waiting for stats...</p>
</RefreshingMiniGraph>
</td>
<td>
<RefreshingMiniGraph clusterID={clusterID} queries={[{host: dataNodes, database: '_internal', text: activeWrites}]} autoRefresh={refreshIntervalMs}>
<p className="cluster-stat-empty"><span className="icon cubo"></span> Waiting for stats..</p>
</RefreshingMiniGraph>
</td>
</tr>
);
},
});
export default NodeTableRow;

View File

@ -1,2 +0,0 @@
export const OVERVIEW_TIME_RANGE = '1h';
export const OVERVIEW_INTERVAL = '30s';

View File

@ -1,77 +0,0 @@
import React, {PropTypes} from 'react';
import NodeTable from '../components/NodeTable';
import MiscStatsPanel from '../components/MiscStatsPanel';
import ClusterStatsPanel from '../components/ClusterStatsPanel';
import FlashMessages from 'shared/components/FlashMessages';
import {showCluster} from 'shared/apis';
export const OverviewPage = React.createClass({
propTypes: {
params: PropTypes.shape({
clusterID: PropTypes.string.isRequired,
}).isRequired,
dataNodes: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
addFlashMessage: PropTypes.func.isRequired, // Injected by the `FlashMessages` wrapper
},
getInitialState() {
return {
isFetching: true,
// Raw output of plutonium's `/show-cluster` - includes both meta
// and data node information.
cluster: {},
};
},
componentDidMount() {
const {clusterID} = this.props.params;
showCluster(clusterID).then((resp) => {
this.setState({cluster: resp.data});
}).catch(() => {
this.props.addFlashMessage({
text: 'Something went wrong! Try refreshing your browser and email support@influxdata.com if the problem persists.',
type: 'error',
});
}).then(() => {
this.setState({isFetching: false});
});
},
render() {
if (this.state.isFetching) {
return <div className="page-spinner" />;
}
const {cluster} = this.state;
const {dataNodes, params: {clusterID}} = this.props;
const clusterPanelRefreshMs = 10000;
const nodeTableRefreshMs = 10000;
const miscPanelRefreshMs = 30000;
return (
<div className="overview">
<div className="enterprise-header">
<div className="enterprise-header__container">
<div className="enterprise-header__left">
<h1>
Cluster Overiew
</h1>
</div>
</div>
</div>
<div className="container-fluid">
<div className="row">
<ClusterStatsPanel clusterID={clusterID} refreshIntervalMs={clusterPanelRefreshMs} dataNodes={dataNodes} />
<MiscStatsPanel clusterID={clusterID} refreshIntervalMs={miscPanelRefreshMs} dataNodes={dataNodes} />
</div>{/* /row */}
<div className="row">
<NodeTable dataNodes={dataNodes} cluster={cluster} clusterID={clusterID} refreshIntervalMs={nodeTableRefreshMs}/>
</div>
</div>{/* /container */}
</div>
);
},
});
export default FlashMessages(OverviewPage);

View File

@ -1,2 +0,0 @@
import OverviewPage from './containers/OverviewPage';
export default OverviewPage;

View File

@ -1,68 +0,0 @@
import u from 'updeep';
export default function hosts(state = {}, action) {
switch (action.type) {
case 'ADD_HOST': {
const {url, params} = action.payload;
const update = {
[url]: {
nickname: params.nickname,
host: params.host,
port: params.port,
ssl: params.ssl,
username: params.username,
password: params.password,
},
};
return u(update, state);
}
case 'LOAD_HOST_DIAGNOSTICS': {
const allSeries = action.response.results[0].series;
const networkSeries = allSeries.find((s) => s.name === 'network');
const hostnameIndex = networkSeries.columns.indexOf('hostname');
const hostname = networkSeries.values[0][hostnameIndex];
const systemSeries = allSeries.find((s) => s.name === 'system');
const uptimeIndex = systemSeries.columns.indexOf('uptime');
const uptime = systemSeries.values[0][uptimeIndex];
const buildSeries = allSeries.find((s) => s.name === 'build');
const versionIndex = buildSeries.columns.indexOf('Version');
const version = buildSeries.values[0][versionIndex];
const update = {
[action.url]: {
diagnostics: {
hostname,
uptime,
version,
},
},
};
return u(update, state);
}
case 'DELETE_HOST': {
const stateCopy = Object.assign({}, state);
delete stateCopy[action.payload.url];
return stateCopy;
}
case 'LOAD_SERVERS_IN_CLUSTER': {
const {host, dataNodes, metaNodes} = action.payload;
const update = {
[host]: {
dataNodes,
metaNodes,
},
};
return u(update, state);
}
default:
return state;
}
}

View File

@ -1,10 +0,0 @@
import {combineReducers} from 'redux';
import time from './time';
import hosts from './hosts';
const rootReducer = combineReducers({
hosts,
time,
});
export default rootReducer;

View File

@ -1,26 +0,0 @@
import u from 'updeep';
export default function time(state = {}, action) {
switch (action.type) {
case 'SET_TIME_BOUNDS': {
const update = {
bounds: action.payload.bounds,
groupByInterval: action.payload.groupByInterval || state.groupByInterval,
};
return u(update, state);
}
case 'SET_AUTO_REFRESH': {
const update = {
autoRefresh: action.payload.milliseconds,
};
return u(update, state);
}
case 'SET_GROUP_BY': {
return u({groupByInterval: action.payload.groupByInterval}, state);
}
default:
return state;
}
}

View File

@ -1,18 +0,0 @@
import {createStore, applyMiddleware} from 'redux';
import thunkMiddleware from 'redux-thunk';
import rootReducer from '../reducers';
import makeAppStorage from 'shared/middleware/appStorage';
import makeQueryExecuter from 'shared/middleware/queryExecuter';
export default function configureStore(effectiveWindow, initialState) {
return createStore(
rootReducer,
initialState,
applyMiddleware(
thunkMiddleware,
makeAppStorage(effectiveWindow.localStorage),
makeQueryExecuter()
)
);
}

View File

@ -27,7 +27,6 @@ const SideNav = React.createClass({
</NavBlock>
<NavBlock matcher="overview" icon="crown" link={`${sourcePrefix}/overview`}>
<NavHeader link={`${sourcePrefix}/overview`} title="Sources" />
<NavListItem matcher="overview" link={`${sourcePrefix}/overview`}>Overview</NavListItem>
<NavListItem matcher="sources$" link={`/sources`}>Manage Sources</NavListItem>
<NavListItem matcher="queries" link={`${sourcePrefix}/queries`}>Queries</NavListItem>
<NavListItem matcher="tasks" link={`${sourcePrefix}/tasks`}>Tasks</NavListItem>

View File

@ -1,132 +0,0 @@
.cluster-stat-tiles {
display: flex;
justify-content: space-between;
}
.cluster-stat-tile {
line-height: 1.75;
width: 49%;
}
$cluster-stat-height: 44px;
$cluster-stat-padding: 2em;
@keyframes cluster-spinner {
0% {transform: rotate(0deg);}
100% {transform: rotate(360deg);}
}
.cluster-stat {
display: flex !important;
justify-content: space-between;
padding: $cluster-stat-padding 0;
align-items: center;
height: $cluster-stat-height;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
div {
div {
min-width: 150px;
max-width: 300px;
}
}
&-empty {
width: 100%;
background-color: $g19-ghost;
height: $cluster-stat-height;
margin: 0;
line-height: $cluster-stat-height;
text-align: center;
border-bottom: 2px solid $g18-cloud;
border-top: 2px solid $g20-white;
border-radius: 4px;
font-style: italic;
.icon {
animation: cluster-spinner 3.8s infinite linear;
display: inline-block;
vertical-align: middle;
position: relative;
top: -1px;
}
}
&--label {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding-left: 1.5em;
}
&-2x {
display: flex;
justify-content: space-between;
padding: $cluster-stat-padding 0;
align-items: center;
height: $cluster-stat-height;
margin: 0;
&--label,
&--number {
vertical-align: middle;
display: inline-block;
line-height: 1em;
}
&--number {
// font-size: 18px;
font-weight: 600;
margin-left: 15px;
}
}
}
.influx-version,
.disk-util {
border: 2px solid $g18-cloud;
border-radius: 4px;
padding: 4px 10px;
}
.quarter-table-width {
width: 25%;
}
.js-node-table {
margin-bottom: 0;
tbody tr td {
height: $cluster-stat-height + 16px;
}
.cluster-stat {
padding: 0;
}
}
.btn.rebalance {
display: flex;
justify-content: space-between;
align-items: center;
}
div {
#rebalance {
margin-left: 5px;
font-size: 20px;
animation-name: spin;
animation-duration: 4000ms;
animation-iteration-count: infinite;
animation-timing-function: linear;
}
}
@keyframes spin {
from {
transform:rotate(0deg);
}
to {
transform:rotate(360deg);
}
}
.tasks__popover {
-webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2);
box-shadow: 0 5px 10px rgba(0,0,0,.2);
position: absolute;
width: auto;
background: #FFFFFF;
right: 10px;
top: 53px;
z-index: 1;
border-radius: 5px;
}