diff --git a/src/server/src/main/java/io/cassandrareaper/resources/view/NodesStatus.java b/src/server/src/main/java/io/cassandrareaper/resources/view/NodesStatus.java index f745af8fd..c42b21a76 100644 --- a/src/server/src/main/java/io/cassandrareaper/resources/view/NodesStatus.java +++ b/src/server/src/main/java/io/cassandrareaper/resources/view/NodesStatus.java @@ -43,6 +43,7 @@ public final class NodesStatus { private static final List ENDPOINT_SEVERITY_PATTERNS = Lists.newArrayList(); private static final List ENDPOINT_HOSTID_PATTERNS = Lists.newArrayList(); private static final List ENDPOINT_TOKENS_PATTERNS = Lists.newArrayList(); + private static final List ENDPOINT_TYPE_PATTERNS = Lists.newArrayList(); private static final Pattern ENDPOINT_NAME_PATTERN_IP4 = Pattern.compile("^([0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3})", Pattern.MULTILINE | Pattern.DOTALL); @@ -65,6 +66,7 @@ public final class NodesStatus { private static final Pattern ENDPOINT_SEVERITY_21_PATTERN = Pattern.compile("(SEVERITY)(:)([0-9.]+)"); private static final Pattern ENDPOINT_HOSTID_21_PATTERN = Pattern.compile("(HOST_ID)(:)([0-9a-z-]+)"); private static final Pattern ENDPOINT_LOAD_SCYLLA_44_PATTERN = Pattern.compile("(LOAD)(:)([0-9eE.\\+]+)"); + private static final Pattern ENDPOINT_TYPE_STARGATE_PATTERN = Pattern.compile("(X10):([0-9]*):(stargate)"); private static final String NOT_AVAILABLE = "Not available"; @@ -146,6 +148,7 @@ private GossipInfo parseEndpointStatesString( Optional tokens = parseEndpointState(ENDPOINT_TOKENS_PATTERNS, endpointString, 2, String.class); Optional load = parseEndpointState(ENDPOINT_LOAD_PATTERNS, endpointString, 3, Double.class); totalLoad += load.orElse(0.0); + Optional stargate = parseEndpointState(ENDPOINT_TYPE_PATTERNS, endpointString, 3, String.class); EndpointState endpointState = new EndpointState( endpoint.orElse(NOT_AVAILABLE), @@ -156,7 +159,8 @@ private GossipInfo parseEndpointStatesString( severity.orElse(0.0), releaseVersion.orElse(NOT_AVAILABLE), tokens.orElse(NOT_AVAILABLE), - load.orElse(0.0)); + load.orElse(0.0), + stargate.isPresent() ? NodeType.STARGATE : NodeType.CASSANDRA); if (!status.orElse(NOT_AVAILABLE).toLowerCase().contains("left") && !status.orElse(NOT_AVAILABLE).toLowerCase().contains("removed")) { @@ -207,6 +211,7 @@ private static void initPatterns() { ENDPOINT_SEVERITY_PATTERNS.addAll(Arrays.asList(ENDPOINT_SEVERITY_22_PATTERN, ENDPOINT_SEVERITY_21_PATTERN)); ENDPOINT_HOSTID_PATTERNS.addAll(Arrays.asList(ENDPOINT_HOSTID_22_PATTERN, ENDPOINT_HOSTID_21_PATTERN)); ENDPOINT_TOKENS_PATTERNS.add(ENDPOINT_TOKENS_22_PATTERN); + ENDPOINT_TYPE_PATTERNS.add(ENDPOINT_TYPE_STARGATE_PATTERN); } public static final class GossipInfo { @@ -236,6 +241,11 @@ public GossipInfo( } } + public enum NodeType { + CASSANDRA, + STARGATE; + } + public static final class EndpointState { @JsonProperty @@ -265,6 +275,9 @@ public static final class EndpointState { @JsonProperty public final Double load; + @JsonProperty + public final NodeType type; + public EndpointState( String endpoint, String hostId, @@ -274,7 +287,8 @@ public EndpointState( Double severity, String releaseVersion, String tokens, - Double load) { + Double load, + NodeType type) { this.endpoint = endpoint; this.hostId = hostId; @@ -285,6 +299,7 @@ public EndpointState( this.releaseVersion = releaseVersion; this.tokens = tokens; this.load = load; + this.type = type; } public String getDc() { @@ -322,7 +337,10 @@ public String toString() { + hostId + " / " + "Tokens : " - + tokens; + + tokens + + " / " + + "Type : " + + type; } } } diff --git a/src/ui/app/jsx/cluster-list.jsx b/src/ui/app/jsx/cluster-list.jsx index b9b585eeb..b9ef3bdf5 100644 --- a/src/ui/app/jsx/cluster-list.jsx +++ b/src/ui/app/jsx/cluster-list.jsx @@ -55,17 +55,15 @@ const Cluster = CreateReactClass({ method: 'GET', component: this, complete: function(data) { - console.log(this.component.props.name + " complete."); this.component.setState({clusterStatuses: setTimeout(this.component._refreshClusterStatus, 30000), clusterStatus: $.parseJSON(data.responseText)}); if(this.component.state.clusterStatus.nodes_status){ this.component.setState({nodes_status: this.component.state.clusterStatus.nodes_status}); } - console.log(this.component.props.name + " : Next attempt in 30s.") }, error: function(data) { - console.log(this.component.props.name + " failed."); + console.error(this.component.props.name + " failed."); this.component.setState({clusterStatuses: setTimeout(this.component._refreshClusterStatus, 30000)}); } diff --git a/src/ui/app/jsx/event-subscription-form.jsx b/src/ui/app/jsx/event-subscription-form.jsx index 95587ab83..143cda669 100644 --- a/src/ui/app/jsx/event-subscription-form.jsx +++ b/src/ui/app/jsx/event-subscription-form.jsx @@ -20,6 +20,7 @@ import CreateReactClass from 'create-react-class'; import PropTypes from 'prop-types'; import Select from 'react-select'; import {getUrlPrefix} from "jsx/mixin"; +import {getNodeOptions} from "../node-utils"; const subscriptionForm = CreateReactClass({ @@ -197,11 +198,7 @@ const subscriptionForm = CreateReactClass({ }, _getNodeOptions: function() { - this.setState({ - nodeOptions: this.state.clusterStatus.nodes_status.endpointStates[0].endpointNames.map( - obj => { return {value: obj, label: obj}; } - ) - }); + this.setState(getNodeOptions(this.state.clusterStatus.nodes_status.endpointStates, true)); }, _handleSelectOnChange: function(valueContext, actionContext) { diff --git a/src/ui/app/jsx/node-status.jsx b/src/ui/app/jsx/node-status.jsx index db7646117..e697c4127 100644 --- a/src/ui/app/jsx/node-status.jsx +++ b/src/ui/app/jsx/node-status.jsx @@ -185,6 +185,8 @@ const NodeStatus = CreateReactClass({ }, render: function() { + const isStargate = this.props.endpointStatus.type === "STARGATE"; + let displayNodeStyle = { display: "none" } @@ -213,8 +215,14 @@ const NodeStatus = CreateReactClass({ let buttonStyle = "btn btn-xs btn-success"; let largeButtonStyle = "btn btn-lg btn-static btn-success"; - - if(!this.props.endpointStatus.status.endsWith('UP')){ + + // If the node is a Stargate node we won't have real status for it + // so we'll display it slightly differently + if (isStargate) { + buttonStyle = "btn btn-xs btn-info"; + largeButtonStyle = "btn btn-lg btn-static btn-info"; + // If it's not a stargate node, then we can appropriately use its status + } else if(!this.props.endpointStatus.status.endsWith('UP')){ buttonStyle = "btn btn-xs btn-danger"; largeButtonStyle = "btn btn-lg btn-static btn-danger"; } @@ -234,9 +242,24 @@ const NodeStatus = CreateReactClass({ overflow: "auto", height: "200px" } + + const modalTitle = ( + + Endpoint {this.props.endpointStatus.endpoint} + {isStargate && +  (Stargate) + } + + ); const tooltip = ( - {this.props.endpointStatus.endpoint} ({humanFileSize(this.props.endpointStatus.load, 1024)}) + + {this.props.endpointStatus.endpoint} + {isStargate && +  (Stargate) + } +  ({humanFileSize(this.props.endpointStatus.load, 1024)}) + ); const tokenList = this.state.tokens.map(token =>
{token}
); @@ -271,7 +294,7 @@ const NodeStatus = CreateReactClass({ - Endpoint {this.props.endpointStatus.endpoint} + {modalTitle}
@@ -284,26 +307,39 @@ const NodeStatus = CreateReactClass({

{this.props.endpointStatus.dc} / {this.props.endpointStatus.rack}

-

Release version

-

{this.props.endpointStatus.releaseVersion}

-
-
-

Tokens

-

+

Node Type

+

{isStargate ? "Stargate" : "Cassandra"}

-

Status

-

-
-
-

Severity

-

{this.props.endpointStatus.severity}

-
-
-

Data size on disk

-

{humanFileSize(this.props.endpointStatus.load, 1024)}

+

Release version

+

{this.props.endpointStatus.releaseVersion}

+ {!isStargate && +
+

Tokens

+

+
+ } + {!isStargate && +
+

Status

+

+
+ } + {!isStargate && +
+

Severity

+

{this.props.endpointStatus.severity}

+
+ } + {!isStargate && +
+

Data size on disk

+

{humanFileSize(this.props.endpointStatus.load, 1024)}

+
+ } + {!isStargate &&
@@ -342,10 +378,10 @@ const NodeStatus = CreateReactClass({

} - +
-
 
+
 
{snapshots} @@ -354,10 +390,10 @@ const NodeStatus = CreateReactClass({ - + - + }
diff --git a/src/ui/app/jsx/repair-form.jsx b/src/ui/app/jsx/repair-form.jsx index 0d3449714..10d6b86cb 100644 --- a/src/ui/app/jsx/repair-form.jsx +++ b/src/ui/app/jsx/repair-form.jsx @@ -25,6 +25,7 @@ import Moment from 'moment'; import moment from "moment"; import Modal from 'react-bootstrap/lib/Modal'; import Button from 'react-bootstrap/lib/Button'; +import {getNodeOptions} from "../node-utils"; Moment.locale(navigator.language); @@ -165,11 +166,7 @@ const repairForm = CreateReactClass({ }, _getNodeOptions: function() { - this.setState({ - nodeOptions: this.state.clusterStatus.nodes_status.endpointStates[0].endpointNames.sort().map( - obj => { return {value: obj, label: obj}; } - ) - }); + this.setState(getNodeOptions(this.state.clusterStatus.nodes_status.endpointStates, true)); }, _getKeyspaceOptions: function() { diff --git a/src/ui/app/node-utils.js b/src/ui/app/node-utils.js new file mode 100644 index 000000000..8eced6530 --- /dev/null +++ b/src/ui/app/node-utils.js @@ -0,0 +1,73 @@ +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/** + * Utility method for digging through the endpointStates API response structure + * to identify the possible nodes + * + * @param endpointStates - Array + * @returns An array of node/endpoint objects + */ + +export const getNodesFromEndpointStates = function(endpointStates) { + const nodes = []; + if (!endpointStates || !endpointStates.length) { + return nodes; + } + + for(let endpointState of endpointStates) { + if (!endpointState || !endpointState.endpoints) { + continue; + } + for (let datacenterId in endpointState.endpoints) { + const datacenter = endpointState.endpoints[datacenterId]; + if (!datacenter) { + continue; + } + for (let rackId in datacenter) { + const rack = datacenter[rackId]; + if (!rack) { + continue; + } + for (let endpoint of rack) { + if (endpoint) { + nodes.push(endpoint); + } + } + } + } + } + return nodes; +} + +/** + * Utility function for generating a set of options for a select representing a drop down of nodes. + * If excludeStargateNodes is true stargate nodes will not be included in the set of options generated. + * + * @param endpointStates - Array + * @param excludeStargateNodes - Boolean + * @returns An object suitable for passing to Select component + */ +export const getNodeOptions = function(endpointStates, excludeStargateNodes) { + let nodes = getNodesFromEndpointStates(endpointStates); + if (!nodes) { + return { + nodeOptions: [] + } + } + const includedNodes = excludeStargateNodes ? nodes.filter(node => node.type !== "STARGATE") : nodes; + return { + nodeOptions: includedNodes.map(node => node.endpoint).sort().map( + obj => { return {value: obj, label: obj}; } + ) + }; +}