Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add multiple n_val selection.

* Add the ability to select all available n_vals in cluster.
* Add n_val context information.
* Add pure CSS tooltips.
* Provide description and link to the documentation.
  • Loading branch information...
commit 4db5a27a726539161930ef539fe390011af11bc7 1 parent 311926d
@cmeiklejohn cmeiklejohn authored
View
2  include/riak_control.hrl
@@ -34,6 +34,8 @@
-type plan() :: [] | legacy | ring_not_ready | unavailable.
-type transfer() :: riak_core_ring:pending_change().
-type transfers() :: [transfer()].
+-type single_n_val() :: pos_integer().
+-type n_vals() :: [pos_integer()].
-type stage_error() :: nodedown
| already_leaving
View
5 priv/admin/css/cluster.styl
@@ -63,9 +63,12 @@ CSS to be applied ONLY on the cluster page
.open .actions-container
css-transition(height .4s ease, margin-bottom .4s ease)
- height : 375px
+ height : 325px
margin-bottom : 25px
+ .open .node.taller .actions-container
+ height : 375px
+
.gui-light
vertical-align : middle
padding : 9px 5px 9px 14px
View
273 priv/admin/css/compiled/style.css
@@ -229,7 +229,7 @@ th {
position: fixed;
top: 0;
left: 0;
- z-index: 10;
+ z-index: 500;
}
#navbar {
width: auto;
@@ -441,23 +441,6 @@ footer .side-line {
display: block;
background: transparent;
}
-#tooltips {
- font-family: 'noticia', georgia, serif;
- width: 100%;
- height: auto;
- position: fixed;
- bottom: 0;
- left: 0;
- border-top: 2px solid #777;
-}
-#display-tips {
- background: rgba(0,0,0,0.85);
- line-height: 1.5;
- font-style: italic;
- font-size: 15px;
- color: #aaa;
- padding: 25px 25px 25px;
-}
.gui-dropdown-wrapper {
width: auto;
height: 30px;
@@ -793,6 +776,120 @@ input.gui-point-button-right:active {
.gui-light.orange {
background-position: -4px -73px;
}
+div.tooltip {
+ width: 25px;
+ height: 25px;
+ position: relative;
+ color: #b2564e;
+ text-decoration: none;
+ display: block;
+ background: url("/admin/ui/images/tooltip-mark.png") center center no-repeat transparent;
+ text-align: center;
+}
+div.tooltip.right {
+ margin-left: 5px;
+}
+div.tooltip a {
+ color: #f99d33;
+}
+div.tooltip a:hover {
+ text-decoration: underline;
+}
+div.tooltip a.docs:before {
+ content: 'Read the docs »';
+}
+div.tooltip span {
+ display: none;
+}
+div.tooltip > span {
+ width: 190px;
+ height: auto;
+ -webkit-border-radius: 3px;
+ -moz-border-radius: 3px;
+ -ms-border-radius: 3px;
+ -o-border-radius: 3px;
+ border-radius: 3px;
+ position: absolute;
+ left: -402%;
+ top: 30px;
+ -webkit-transition: opacity 0.8s ease;
+ -moz-transition: opacity 0.8s ease;
+ -ms-transition: opacity 0.8s ease;
+ -webkit-transition: opacity 0.8s ease;
+ -moz-transition: opacity 0.8s ease;
+ -o-transition: opacity 0.8s ease;
+ transition: opacity 0.8s ease;
+ font-family: 'titillium', helvetica, arial, sans-serif;
+ padding: 20px;
+ background: rgba(85,85,85,0.95);
+ color: #fff;
+ text-align: left;
+ text-shadow: none;
+ z-index: 200;
+ font-size: 15px;
+ line-height: 1.3;
+}
+div.tooltip > span .tooltip-heading {
+ font-family: 'titillium', helvetica, arial, sans-serif;
+ font-weight: bold;
+ display: block;
+ margin-bottom: 15px;
+}
+div.tooltip > span:before {
+ width: 0;
+ height: 0;
+ position: absolute;
+ left: 45.5%;
+ top: -6px;
+ border-left: 8px solid transparent;
+ border-right: 8px solid transparent;
+ border-bottom: 6px solid rgba(85,85,85,0.95);
+ z-index: 205;
+ content: " ";
+}
+div.tooltip:hover span {
+ display: block;
+}
+div.tooltip:hover > span {
+ -webkit-animation: fadeOpacity 0.5s;
+ -moz-animation: fadeOpacity 0.5s;
+ -ms-animation: fadeOpacity 0.5s;
+ -o-animation: fadeOpacity 0.5s;
+ animation: fadeOpacity 0.5s;
+}
+@-webkit-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@-moz-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@-o-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
.has-cut,
.cut {
background: url("/admin/ui/images/cut-bg.png") left bottom repeat-x transparent;
@@ -922,6 +1019,61 @@ input.gui-point-button-right:active {
padding-bottom: 5px;
}
}
+@-moz-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@-webkit-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@-o-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@-ms-keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
+@keyframes fadeOpacity {
+ 0% {
+ opacity: 0;
+ filter: alpha(opacity=0);
+ }
+
+ 100% {
+ opacity: 1;
+ filter: alpha(opacity=100);
+ }
+}
#snapshot-page #healthy-cluster,
#snapshot-page #unhealthy-cluster {
padding-top: 25px;
@@ -1076,9 +1228,12 @@ input.gui-point-button-right:active {
-moz-transition: height 0.4s ease;
-o-transition: height 0.4s ease;
transition: height 0.4s ease;
- height: 375px;
+ height: 325px;
margin-bottom: 25px;
}
+#cluster-page .open .node.taller .actions-container {
+ height: 375px;
+}
#cluster-page .gui-light {
vertical-align: middle;
padding: 9px 5px 9px 14px;
@@ -1954,6 +2109,12 @@ input.gui-point-button-right:active {
width: 15%;
}
}
+#ring-page select {
+ float: right;
+ min-width: 100px;
+ outline: none;
+ margin-top: 3px;
+}
#ring-page h4 {
margin-bottom: 20px;
}
@@ -1961,6 +2122,21 @@ input.gui-point-button-right:active {
font-family: 'titillium', helvetica, arial, sans-serif;
font-weight: bold;
}
+#ring-page .right {
+ float: right;
+}
+#ring-page .gui-dropdown-bg {
+ margin-right: 3px;
+}
+#ring-page .gui-dropdown {
+ margin-top: -8px;
+ margin-left: -6px;
+ position: absolute;
+ z-index: 200;
+}
+#ring-page .gui-dropdown-cap {
+ z-index: 100;
+}
#ring-page #current-ring {
max-width: 850px;
margin: 0 auto 45px auto;
@@ -1973,6 +2149,22 @@ input.gui-point-button-right:active {
#ring-page .clear {
clear: both;
}
+#ring-page .chart-wrapper {
+ position: relative;
+ overflow: visible;
+}
+#ring-page .nval-dropdown-container {
+ position: absolute;
+ right: 5px;
+ top: 0;
+}
+#ring-page .nval-dropdown-container .gui-dropdown-wrapper {
+ width: 120px;
+ float: right;
+}
+#ring-page .nval-dropdown-container h4 {
+ margin-bottom: 5px;
+}
#ring-page .chart-spacer {
float: left;
width: 30px;
@@ -2032,6 +2224,12 @@ input.gui-point-button-right:active {
text-align: left;
display: none;
}
+#ring-page .details a {
+ color: #f99d33;
+}
+#ring-page .details a:hover {
+ text-decoration: underline;
+}
#ring-page .details h4 {
font-style: normal;
}
@@ -2276,7 +2474,44 @@ input.gui-point-button-right:active {
-o-animation: pulseGreen 1s infinite;
animation: pulseGreen 1s infinite;
}
+@media screen and (max-width: 1050px) {
+ #ring-page .nval-dropdown-container div.tooltip > span {
+ position: absolute;
+ left: -750%;
+ top: 30px;
+ }
+ #ring-page .nval-dropdown-container div.tooltip > span:before {
+ position: absolute;
+ left: 83%;
+ top: -6px;
+ }
+}
@media screen and (max-width: 950px) {
+ #ring-page .nval-dropdown-container {
+ position: static;
+ }
+ #ring-page .nval-dropdown-container h4 {
+ margin-top: 15px;
+ }
+ #ring-page .nval-dropdown-container select {
+ float: none;
+ margin-top: 0;
+ }
+ #ring-page .nval-dropdown-container div.tooltip {
+ float: none;
+ display: inline-block;
+ vertical-align: middle;
+ }
+ #ring-page .nval-dropdown-container div.tooltip > span {
+ position: absolute;
+ left: -18%;
+ top: 30px;
+ }
+ #ring-page .nval-dropdown-container div.tooltip > span:before {
+ position: absolute;
+ left: 4%;
+ top: -6px;
+ }
#ring-page .chart-wrapper {
float: left;
border-right: 1px solid rgba(255,255,255,0.18);
View
115 priv/admin/css/general.styl
@@ -70,7 +70,7 @@ th
position : fixed
top : 0
left : 0
- z-index : 10
+ z-index : 500
#navbar
dimensions(auto, 49px)
@@ -206,22 +206,6 @@ footer
display : block
background : transparent
-#tooltips
- copy-font()
- dimensions(100%, auto)
- position : fixed
- bottom : 0
- left : 0
- border-top : 2px solid #777
-
-#display-tips
- background : coverup
- line-height : 1.5
- font-style : italic
- font-size : 15px
- color : #aaa
- padding : 25px 25px 25px
-
/* Ende IDs & Classes */
/* CODE FOR DROPDOWNS */
@@ -493,6 +477,103 @@ input.gui-point-button:active, input.gui-rect-button:active, input.gui-point-but
/* END CODE FOR GENERAL GUI ELEMENTS */
+/* CODE FOR PURE CSS TOOLTIPS */
+
+div.tooltip
+ dimensions(25px, 25px)
+ position : relative
+ color : #b2564e
+ text-decoration : none
+ display : block
+ background : url('/admin/ui/images/tooltip-mark.png') center center no-repeat transparent
+ text-align : center
+
+ &.right
+ margin-left : 5px
+
+ a
+ color : riakorange
+
+ a:hover
+ text-decoration : underline
+
+ a.docs:before
+ content : 'Read the docs »'
+
+div.tooltip span
+ display : none
+
+div.tooltip > span
+ dimensions(190px)
+ corners(3px)
+ absoluteLeft(-402%, 30px)
+ css-transition(opacity .8s ease)
+ headline-font()
+ padding : 20px
+ background : rgba(85, 85, 85, .95)
+ color : #ffffff
+ text-align : left
+ text-shadow : none
+ z-index : 200
+ font-size : 15px
+ line-height : 1.3
+
+ .tooltip-heading
+ headline-bold()
+ display : block
+ margin-bottom : 15px
+
+
+div.tooltip > span:before
+ dimensions(0, 0)
+ absoluteLeft(45.5%, -6px)
+ border-left : 8px solid transparent
+ border-right : 8px solid transparent
+ border-bottom : 6px solid rgba(85, 85, 85, .95)
+ z-index : 205
+ content : " "
+
+div.tooltip:hover span
+ display : block
+
+div.tooltip:hover > span
+ vendor('animation', fadeOpacity .5s)
+
+@-webkit-keyframes fadeOpacity {
+ 0% {
+ opaque(0)
+ }
+ 100% {
+ opaque(1)
+ }
+}
+@-moz-keyframes fadeOpacity {
+ 0% {
+ opaque(0)
+ }
+ 100% {
+ opaque(1)
+ }
+}
+@-o-keyframes fadeOpacity {
+ 0% {
+ opaque(0)
+ }
+ 100% {
+ opaque(1)
+ }
+}
+@keyframes fadeOpacity {
+ 0% {
+ opaque(0)
+ }
+ 100% {
+ opaque(1)
+ }
+}
+
+/* END CODE FOR TOOLTIPS */
+
.has-cut
background : url('/admin/ui/images/cut-bg.png') left bottom repeat-x transparent
View
75 priv/admin/css/ring.styl
@@ -7,12 +7,33 @@ CSS to be applied ONLY on the ring page
#ring-page
+ select
+ float : right
+ min-width : 100px
+ outline : none
+ margin-top : 3px
+
h4
margin-bottom : 20px
strong
headline-bold()
+ .right
+ float : right
+
+ .gui-dropdown-bg
+ margin-right : 3px
+
+ .gui-dropdown
+ margin-top : -8px
+ margin-left : -6px
+ position : absolute
+ z-index : 200
+
+ .gui-dropdown-cap
+ z-index : 100
+
#current-ring
max-width : 850px
margin : 0 auto 45px auto
@@ -25,6 +46,22 @@ CSS to be applied ONLY on the ring page
.clear
clear : both
+ .chart-wrapper
+ position : relative
+ overflow : visible
+
+ .nval-dropdown-container
+ position : absolute
+ right : 5px
+ top : 0
+
+ .gui-dropdown-wrapper
+ width : 120px
+ float : right
+
+ h4
+ margin-bottom : 5px
+
.chart-spacer
float : left
width : 30px
@@ -72,6 +109,12 @@ CSS to be applied ONLY on the ring page
text-align : left
display : none
+ a
+ color : riakorange
+
+ &:hover
+ text-decoration : underline
+
h4
font-style : normal
@@ -217,7 +260,39 @@ CSS to be applied ONLY on the ring page
&.pulse
vendor('animation', pulseGreen 1s infinite)
+ @media screen and (max-width: 1050px)
+ .nval-dropdown-container
+
+ div.tooltip
+
+ & > span
+ absoluteLeft(-750%, 30px)
+
+ &:before
+ absoluteLeft(83%, -6px)
+
@media screen and (max-width: 950px)
+ .nval-dropdown-container
+ position : static
+
+ h4
+ margin-top : 15px
+
+ select
+ float : none
+ margin-top : 0
+
+ div.tooltip
+ float : none
+ display : inline-block
+ vertical-align : middle
+
+ & > span
+ absoluteLeft(-18%, 30px)
+
+ &:before
+ absoluteLeft(4%, -6px)
+
.chart-wrapper
float : left
border-right : 1px solid lightWhite
View
BIN  priv/admin/images/tooltip-mark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
View
17 priv/admin/js/app.js
@@ -24,19 +24,18 @@ minispade.register('app', function() {
*/
DS.Model.reopen({
reload: function() {
- var store = this.get('store');
- store.get('adapter').find(store,
- this.constructor, this.get('id'));
- }
+ var store = this.get('store');
+ store.get('adapter').find(store, this.constructor, this.get('id'));
+ }
});
DS.RecordArray.reopen({
reload: function() {
- Ember.assert("Can only reload base RecordArrays",
- this.constructor === DS.RecordArray);
- var store = this.get('store');
- store.get('adapter').findAll(store, this.get('type'));
- }
+ Ember.assert("Can only reload base RecordArrays",
+ this.constructor === DS.RecordArray);
+ var store = this.get('store');
+ store.get('adapter').findAll(store, this.get('type'));
+ }
});
/**
View
40 priv/admin/js/cluster.js
@@ -68,10 +68,21 @@ minispade.register('cluster', function() {
* @returns {void}
*/
refresh: function(newCluster, existingCluster, nodeFactory) {
+
+ /*
+ * For every object in newCluster...
+ */
newCluster.forEach(function(node) {
+
+ /*
+ * Use a unique property to locate the corresponding
+ * object in existingCluster.
+ */
var exists = existingCluster.findProperty('name', node.name);
- // If it doesn't exist yet, add it. If it does, update it.
+ /*
+ * If it doesn't exist yet, add it. If it does, update it.
+ */
if(exists !== undefined) {
exists.setProperties(node);
} else {
@@ -79,14 +90,33 @@ minispade.register('cluster', function() {
}
});
- // Iterate the cluster removing nodes that shouldn't
- // be there.
+ /*
+ * We've already updated corresponding objects and added
+ * new ones. Now we need to remove ones that don't exist in
+ * the new cluster.
+ */
+
var changesOccurred = false;
var replacementCluster = [];
+ /*
+ * For every object in the existingCluster...
+ */
existingCluster.forEach(function(node, i) {
+
+ /*
+ * Use a unique property to locate the corresponding object
+ * in newCluster.
+ */
var exists = newCluster.findProperty('name', node.name);
+ /*
+ * If it doesn't exist in the newCluster, destroy it.
+ * If this happens even one time, we'll mark changesOccurred as true.
+ *
+ * Otherwise, this node is a good node and we can add it to
+ * the replacementCluster.
+ */
if(exists === undefined) {
node.destroy();
changesOccurred = true;
@@ -95,6 +125,10 @@ minispade.register('cluster', function() {
}
});
+ /*
+ * If we ended up having to remove any nodes,
+ * replace the cluster.
+ */
if(changesOccurred) {
existingCluster.set('[]', replacementCluster.get('[]'));
}
View
74 priv/admin/js/core.js
@@ -56,78 +56,4 @@ minispade.register('core', function() {
claimant: DS.attr("boolean")
});
- /**
- * @class
- *
- * Partition represents one of the partitions in the
- * consistent hashing ring owned by the cluster.
- */
- RiakControl.Partition = DS.Model.extend(
- /** @scope RiakControl.Partition.prototype */ {
-
- /**
- * Use the index into the ring as the primary key and
- * unique identifier for this particular partition.
- */
- primaryKey: 'index',
-
- /* Partition index. */
- index: DS.attr("string"),
-
- /* Cluster n_val. */
- n_val: DS.attr("number"),
-
- /* Number of reachable/available primaries. */
- available: DS.attr("number"),
-
- /**
- * Return status of whether the node is incompatible or not.
- *
- * @returns {Boolean}
- */
- incompatible: function() {
- return this.get('status') === 'incompatible';
- }.property('status'),
-
- /**
- * Consider an available node one which is compatible, and
- * reachable.
- *
- * @returns {Boolean}
- */
- available: function() {
- return !this.get('incompatible') && this.get('reachable');
- }.property('status', 'reachable'),
-
- /* Cluster quorum value. */
- quorum: DS.attr("number"),
-
- /* Whether all primaries are on distinct nodes. */
- distinct: DS.attr("boolean"),
-
- /* The list of unavailable primaries. */
- unavailable_nodes: DS.attr("array"),
-
- /* Whether unavailable nodes are present. */
- unavailable: function() {
- return this.get('unavailable_nodes').length > 0;
- }.property('unavailable_nodes'),
-
- /* The list of available primaries. */
- available_nodes: DS.attr("array"),
-
- /* The list of primaries. */
- all_nodes: DS.attr("array"),
-
- /* Whether or not all primaries are down or not. */
- allPrimariesDown: function() {
- return this.get('available') === 0;
- }.property('available'),
-
- /* Whether or not a quorum of primaries are down. */
- quorumUnavailable: function() {
- return this.get('available') < this.get('quorum');
- }.property('quorum', 'available')
- });
-
});
View
4 priv/admin/js/generated/templates.js
@@ -2,8 +2,8 @@ Ember.TEMPLATES['application'] = Ember.Handlebars.compile('<div id="header">
Ember.TEMPLATES['snapshot'] = Ember.Handlebars.compile('<div id="snapshot-page"> <section id="title-container"> <div class="side-line"></div> <div class="title-box"> <span class="vert-border-left"></span> <h1 id="snapshot-headline" class="gui-headline-bold page-title">Current Snapshot</h1> <span class="vert-border-right"></span> </div> <div class="side-line"></div> <div class="clear"></div> </section> <div class="relative health-info"> {{#if healthyCluster}} <div id="healthy-cluster"> <img id="health-indicator" src="/admin/ui/images/healthy-cluster.png" alt="" /> <section> <h2 class="gui-headline-bold has-cut">Your cluster is healthy.</h2> <h3 class="">You currently have...</h3> <ul class="gui-text bulleted"> <li><span class="emphasize monospace">0</span> Unreachable nodes</li> <li><span class="emphasize monospace">0</span> Incompatible nodes</li> <li><span class="emphasize monospace">0</span> Nodes marked as down</li> <li><span class="emphasize monospace">0</span> Nodes experiencing low memory</li> <li>Nothing to worry about because Riak is your friend</li> </ul> </section> </div> {{else}} <div id="unhealthy-cluster"> <img id="health-indicator" src="/admin/ui/images/unhealthy-cluster.png" alt="" /> <section> <h2 class="gui-headline-bold has-cut">Your cluster has problems.</h2> {{#if areUnreachableNodes}} <!-- Unreachable Nodes List --> <h3 id="unreachable-nodes-title" class="">The following nodes are currently unreachable:</h3> <ul id="unreachable-nodes-list" class="gui-text bulleted monospace"> {{#each unreachableNodes}} <li><a class="go-to-cluster" {{action showCluster href=true}}>{{name}}</a></li> {{/each}} </ul> {{/if}} {{#if areIncompatibleNodes}} <!-- Incompatible Nodes List --> <h3 id="incompatible-nodes-title" class="">Some information about the following nodes may be temporarily unavailable:</h3> <ul id="incompatible-nodes-list" class="gui-text bulleted monospace"> {{#each incompatibleNodes}} <li><a class="go-to-cluster" {{action showCluster href=true}}>{{name}}</a></li> {{/each}} </ul> <h4>This may be triggered by using control in the middle of a rolling upgrade or during startup of the node.</h4> {{/if}} {{#if areDownNodes}} <!-- Down Nodes List --> <h3 id="down-nodes-title" class="">The following nodes are currently marked down:</h3> <ul id="down-nodes-list" class="gui-text bulleted monospace"> {{#each downNodes}} <li><a class="go-to-cluster" {{action showCluster href=true}}>{{name}}</a></li> {{/each}} </ul> {{/if}} {{#if areLowMemNodes}} <!-- Low-Mem Nodes List --> <h3 id="low_mem-nodes-title" class="">The following nodes are currently experiencing low memory:</h3> <ul id="low_mem-nodes-list" class="gui-text bulleted monospace"> {{#each lowMemNodes}} <li><a class="go-to-cluster" {{action showCluster href=true}}>{{name}}</a></li> {{/each}} </ul> {{/if}} </section> </div> {{/if}} </div> </div>');
Ember.TEMPLATES['cluster'] = Ember.Handlebars.compile('<div id="cluster-page"> <section id="title-container"> <div class="side-line"></div> <div class="title-box"> <span class="vert-border-left"></span> <h1 id="cluster-headline" class="gui-headline-bold page-title">Cluster Management</h1> <span class="vert-border-right"></span> </div> <div class="side-line"></div> <div class="clear"></div> </section> <div id="add-node"> {{#if standalone}} <h2 class="gui-headline">Join Node</h2> <span class="gui-text-flat italic">Type the name of a node in an existing cluster to join this node to.</span> {{else}} <h2 class="gui-headline">Add Node</h2> <span class="gui-text-flat italic">Type the name of a node to add to this cluster.</span> {{/if}} <table class="add-node-table"> <tr class="no-highlight"> <td id="add-node-box"> {{view RiakControl.JoinNodeView}} </td> <td class="button-column"> <a class="gui-point-button gui-text-bold right" {{action joinNode target="controller"}}> {{#if standalone}} <span class="gui-button-msg">JOIN NODE</span> {{else}} <span class="gui-button-msg">ADD NODE</span> {{/if}} </a> </td> </tr> </table> {{#if errorMessage}} <div class="error-message"> <a class="close-error gui-text" {{action hideError target="controller"}}></a> <a class="error-text offline gui-text-flat">{{errorMessage}}</a> </div> {{/if}} </div><!-- #add-node --> <div id="current-area"> <h2 class="gui-headline"> Current Cluster <span id="total-number" class="gui-text-flat italic"></span><br/> </h2> <section id="node-list"> {{#if controller.isLoading}} <div class="spinner-box"> <img id="cluster-spinner" class="spinner" src="/admin/ui/images/spinner.gif"> <h4 class="gui-headline-bold">Loading...</h4> </div> {{else}} <ul class="list-header"> <li class="item1"><h4 class="gui-headline-bold">Actions</h4></li> <li class="item2"><h4 class="gui-headline-bold">Name &amp; Status</h4></li> <li class="item3"><h4 class="gui-headline-bold">Partitions</h4></li> <li class="item4"><h4 class="gui-headline-bold">RAM Usage</h4></li> </ul> <div class="clear"></div> {{collection RiakControl.CurrentClusterView contentBinding="activeCurrentCluster"}} {{/if}} </section> </div> <div id="area-separator"> <div class="vert-line"></div> </div> <div id="planned-area"> <h2 class="gui-headline"> Staged Changes <span class="gui-text-flat italic">(Your new cluster after convergence.)</span> </h2> {{#if controller.displayPlan}} <section id="planned-list" class=""> <ul class="list-header"> <li class="item1"><h4 class="gui-headline-bold">Name &amp; Status</h4></li> <li class="item2"><h4 class="gui-headline-bold">Partitions</h4></li> <li class="item3"><h4 class="gui-headline-bold">Action</h4></li> <li class="item4"><h4 class="gui-headline-bold">Replacement</h4></li> </ul> <div class="clear"></div> {{collection RiakControl.StagedClusterView contentBinding="activeStagedCluster"}} </section> <div class="accept-plan"> <div class="gui-checkbox-wrapper"> <label for="confirmed-check">This plan is correct.</label> <input class="gui-checkbox" type="checkbox" name="confirmed" id="confirmed-check" value="accept"/> </div> <a id="commit-button" class="gui-point-button-right gui-text-bold right" {{action commitPlan target="controller"}}> <span class="gui-button-msg">COMMIT</span> </a> </div> <div class="clear-plan-box"> <span class="gui-text-flat serif"> Changed your mind? Click this button to remove all staged changes. </span> <a class="gui-rect-button gui-text-bold" {{action clearPlan target="controller"}}> <span class="gui-button-msg">CLEAR PLAN</span> </a> </div> {{else}} <section id="planned-list"> <div class="spinner-box"> {{#if controller.ringNotReady}} <h4 class="gui-headline-bold"> Please wait, the ring is converging. </h4> {{else}} {{#if controller.legacyRing}} <h4 class="gui-headline-bold">You are currently running a legacy version of Riak that does not support staged changes.</h4> {{else}} {{#if controller.emptyPlan}} <h4 class="gui-headline-bold">Currently no staged changes to display.</h4> {{else}} {{#if controller.isLoading}} <img id="cluster-spinner" class="spinner" src="/admin/ui/images/spinner.gif"> <h4 class="gui-headline-bold">Loading...</h4> {{/if}} {{/if}} {{/if}} {{/if}} </div> </section> {{/if}} </div> <div class="clear"></div> </div>');
Ember.TEMPLATES['nodes'] = Ember.Handlebars.compile('<div id="nodes-page"> <section id="title-container"> <div class="side-line"></div> <div class="title-box"> <span class="vert-border-left"></span> <h1 id="node-headline" class="gui-headline-bold page-title">Node Management</h1> <span class="vert-border-right"></span> </div> <div class="side-line"></div> <div class="clear"></div> </section> {{#if errorMessage}} <div class="error-message"> <a class="close-error gui-text" {{action hideError target="controller"}}></a> <a class="error-text offline gui-text-flat">{{errorMessage}}</a> </div> {{/if}} <div id="current-area"> <h2 class="gui-headline"> Current Cluster<br/> </h2> <span id="total-number" class="gui-text-flat italic"> Click the radio button for each node you would like to stop or mark as down, then click "APPLY" to apply your changes. If the radio button is grayed out, the action is not available due to the current status of the node. </span><br/> <section id="node-list"> {{#if controller.isLoading}} <div class="spinner-box"> <img id="cluster-spinner" class="spinner" src="/admin/ui/images/spinner.gif"> <h4 class="gui-headline-bold">Loading...</h4> </div> {{else}} <ul class="list-header"> <li class="item1"><h4 class="gui-headline-bold">Stop</h4></li> <li class="item2"><h4 class="gui-headline-bold">Down</h4></li> <li class="item3"><h4 class="gui-headline-bold">Name &amp; Status</h4></li> <li class="item4"><h4 class="gui-headline-bold">Partitions</h4></li> <li class="item5"><h4 class="gui-headline-bold">RAM Usage</h4></li> </ul> <div class="clear"></div> {{collection RiakControl.CurrentNodesView contentBinding="content"}} {{/if}} </section> <section class="buttons"> <a class="gui-point-button-right gui-text-bold right" {{action applyChanges target="controller"}}> <span class="gui-button-msg">APPLY</span> </a> <a class="gui-rect-button gui-text-bold right" {{action clearChecked target="controller"}}> <span class="gui-button-msg">CLEAR</span> </a> <div class="clear"></div> </section> </div></div>');
-Ember.TEMPLATES['ring'] = Ember.Handlebars.compile('<div id="ring-page"> <section id="title-container"> <div class="side-line"></div> <div class="title-box"> <span class="vert-border-left"></span> <h1 id="ring-headline" class="gui-headline-bold page-title">Ring Status</h1> <span class="vert-border-right"></span> </div> <div class="side-line"></div> <div class="clear"></div> </section> <div id="current-ring"> <div class="chart-wrapper"> <h2 class="gui-headline">Availability</h2> {{view RiakControl.DegeneratePreflistChart}} <div class="chart-spacer"></div> {{view RiakControl.QuorumUnavailableChart}} <div class="chart-spacer"></div> {{view RiakControl.AllUnavailableChart}} <div class="clear"></div> </div> <div class="partition-wrapper"> <h2 class="gui-headline partition-section">Partitions</h2> <section id="partition-container"> <h4 class="gui-headline-bold partition-header">Partitions</h4> {{collection RiakControl.PartitionsView contentBinding="content"}} <div class="clear"></div> </section> <section id="legend"> <div id="contextual-info"> <h4 class="gui-headline-bold">Partition Information</h4> <div class="contextual-container"> {{#if partitionSelected}} <div class="info-section"> <h4 class="gui-headline-bold">Index</h4> <span class="monospace"> {{selectedPartition.index}} </span> </div> {{#if selectedPartition.unavailable}} <div class="info-section"> <h4 class="gui-headline-bold">Unavailable Primaries</h4> <ul> {{#each selectedPartition.unavailable_nodes}} <li>{{this}}</li> {{/each}} </ul> </div> {{/if}} <div class="info-section"> {{#if selectedPartition.all_nodes.length}} <h4 class="gui-headline-bold">All Primaries</h4> <ul> {{#each selectedPartition.all_nodes}} <li>{{this}}</li> {{/each}} </ul> {{else}} <h4 class="gui-headline-bold">No Primaries To Display</h4> {{/if}} </div> {{else}} <span class="gui-text italic hover-message"> Click on a partition square to view its more information. </span> {{/if}} </div> </div> <h4 class="gui-headline-bold">Legend</h4> <div class="partition red"></div> <span class="legend-label gui-text">Only fallbacks available.</span><br/> <div class="partition orange"></div> <span class="legend-label gui-text">Majority of primary replicas down.</span><br/> <div class="partition blue"></div> <span class="legend-label gui-text">Replicas do not live on unique nodes.</span><br/> <div class="partition green"></div> <span class="legend-label gui-text">Available.</span><br/> </section> <div class="clear"></div> </div> </div> <div class="clear"></div></div>');
-Ember.TEMPLATES['current_cluster_item'] = Ember.Handlebars.compile('{{#with view}} <div class="node"> <div class="item1 toggle-container"> {{#view RiakControl.CurrentClusterToggleView}} <div class="actions-toggle gui-field"> <a class="slider"></a> </div> {{/view}} </div> <div class="item2 name-box gui-text"> <div {{bindAttr class="indicatorLights"}}> </div><div class="gui-text field-container inline-block"> <div class="name gui-field">{{name}}</div> </div> </div> <div class="item3 gui-text ring-pct-box"> <div {{bindAttr class="coloredArrows"}}></div> <div class="left gui-text pct-box"> <span class="ring-pct">{{ringPctReadable}}%</span> </div> <div class="clear"></div> </div> <div class="item4 gui-text memory-box"> {{#if available}} <div class="membar-bg"> <div class="mem-colors"> <div class="erlang-mem mem-color" {{bindAttr style="memErlangStyle"}} {{bindAttr name="memErlangCeil"}}></div> <div class="non-erlang-mem mem-color" {{bindAttr style="memNonErlangStyle"}} {{bindAttr name="memNonErlang"}}></div> <div class="unknown-mem" {{bindAttr style="memFreeStyle"}} {{bindAttr name="memFreeReadable"}}></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory">{{memUsedReadable}}%</span> {{else}} <div class="membar-bg"> <div class="mem-colors"> <div class="unknown-mem" style="width: 100%"></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory"></span> {{/if}} </div> <div class="clear"></div> <!-- Actions container --> <div class="actions-container"> <div class="actions-pointer"></div> <div class="actions-box"> <h4 class="gui-headline-bold"> Use these actions to prepare this node to leave the cluster. </h4> {{#if me}} <span class="warning gui-text-flat italic"> Warning: This node is hosting Riak Control. If it leaves the cluster, Riak Control will be shut down. </span> {{/if}} <div class="replacement-controls"> <div class="gui-radio-wrapper default"> <input class="gui-radio" type="radio" value="leave" {{bindAttr name="name" id="normalLeaveRadio"}} checked="checked"/> <label class="serif" {{bindAttr for="normalLeaveRadio"}}>Allow this node to leave normally.</label> </div> <div class="gui-radio-wrapper"> <input class="gui-radio" type="radio" value="remove" {{bindAttr name="name" id="forceLeaveRadio"}} /> <label class="serif" {{bindAttr for="forceLeaveRadio"}}>Force this node to leave.</label> </div> <div {{bindAttr class="replaceRadioClasses"}}> <input class="gui-radio" type="radio" value="replace" {{bindAttr name="name" id="replaceRadio"}} /> <label class="serif" {{bindAttr for="replaceRadio"}}>Choose a new node to replace this one.</label> </div> <div class="extra-actions"> <div class="right-angle-arrow"></div> {{#if controller.joiningNodesExist}} <div class="gui-dropdown-wrapper replacement-node-dropdown"> <div class="gui-dropdown-bg gui-text">Select Replacement Node</div> <div class="gui-dropdown-cap left"></div> {{view RiakControl.ClusterItemSelectView prompt="Select Replacement Node" classNames="gui-dropdown" contentBinding="controller.joiningNodes" optionLabelPath="content.name"}} </div> <div class="gui-checkbox-wrapper"> <input class="gui-checkbox" type="checkbox" {{bindAttr name="name" id="forceReplaceCheck"}} value="true" /> <label class="serif" {{bindAttr for="forceReplaceCheck"}}>Force this replacement?</label> </div> <div class="clear"></div> <div class="disabler"></div> {{else}} <div class="no-joining-nodes gui-text-flat serif italic"> No new nodes are currently staged to join. </div> <div class="disabler show slide-up"></div> {{/if}} </div> <div class="clear"></div> </div> <div class="clear"></div> <span class="gui-text-flat serif italic stage-instructions">Click "STAGE" when you are ready to stage this action.</span> <a class="stage-button gui-point-button-right gui-text-bold right" {{action stageChange target="view"}}> <span class="gui-button-msg">STAGE</span> </a> <div class="clear"></div> </div> <div class="clear"></div> </div><!-- .actions-box --> <div class="clear"></div> </div><!-- .node -->{{/with}}');
+Ember.TEMPLATES['ring'] = Ember.Handlebars.compile('<div id="ring-page"> <section id="title-container"> <div class="side-line"></div> <div class="title-box"> <span class="vert-border-left"></span> <h1 id="ring-headline" class="gui-headline-bold page-title">Ring Status</h1> <span class="vert-border-right"></span> </div> <div class="side-line"></div> <div class="clear"></div> </section> <div id="current-ring"> <div class="chart-wrapper"> <h2 class="gui-headline">Availability</h2> <div class="nval-dropdown-container"> <h4 class="gui-headline-bold">See visualizations using a different n_val.</h4> <div class="tooltip right"> <span> <span class="tooltip-heading"> n_val specifies the number of copies of each object to be stored in the cluster. <a class="docs" href="http://docs.basho.com/riak/latest/theory/concepts/Buckets/"></a> </span> <span class="tooltip-body"> Right now, you\'re seeing what your ring looks like for all objects stored with an n_val of 3. If you have objects stored with other n_vals, select an n_val from the dropdown to see what your ring looks like for data stored with the selected n_val. </span> </span> </div> {{view RiakControl.NValSelectView contentBinding="content.n_vals" valueBinding="content.selected" dataBinding="content" selectionBinding="content.selected" }} </div> {{view RiakControl.DegeneratePreflistChart}} <div class="chart-spacer"></div> {{view RiakControl.QuorumUnavailableChart}} <div class="chart-spacer"></div> {{view RiakControl.AllUnavailableChart}} <div class="clear"></div> </div> <div class="partition-wrapper"> <h2 class="gui-headline partition-section">Partitions</h2> <section id="partition-container"> <h4 class="gui-headline-bold partition-header">Partitions</h4> {{collection RiakControl.PartitionsView contentBinding="content"}} <div class="clear"></div> </section> <section id="legend"> <div id="contextual-info"> <h4 class="gui-headline-bold">Partition Information</h4> <div class="contextual-container"> {{#if partitionSelected}} <div class="info-section"> <h4 class="gui-headline-bold">Index</h4> <span class="monospace"> {{selectedPartition.index}} </span> </div> {{#if selectedPartition.unavailable}} <div class="info-section"> <h4 class="gui-headline-bold">Unavailable Primaries</h4> <ul> {{#each selectedPartition.unavailable_nodes}} <li>{{this}}</li> {{/each}} </ul> </div> {{/if}} <div class="info-section"> {{#if selectedPartition.all_nodes.length}} <h4 class="gui-headline-bold">All Primaries</h4> <ul> {{#each selectedPartition.all_nodes}} <li>{{this}}</li> {{/each}} </ul> {{else}} <h4 class="gui-headline-bold">No Primaries To Display</h4> {{/if}} </div> {{else}} <span class="gui-text italic hover-message"> Click on a partition square to view its more information. </span> {{/if}} </div> </div> <h4 class="gui-headline-bold">Legend</h4> <div class="partition red"></div> <span class="legend-label gui-text">Only fallbacks available.</span><br/> <div class="partition orange"></div> <span class="legend-label gui-text">Majority of primary replicas down.</span><br/> <div class="partition blue"></div> <span class="legend-label gui-text">Replicas do not live on unique nodes.</span><br/> <div class="partition green"></div> <span class="legend-label gui-text">Available.</span><br/> </section> <div class="clear"></div> </div> </div> <div class="clear"></div></div>');
+Ember.TEMPLATES['current_cluster_item'] = Ember.Handlebars.compile('{{#with view}} <div {{bindAttr class="hostNodeClasses"}}> <div class="item1 toggle-container"> {{#view RiakControl.CurrentClusterToggleView}} <div class="actions-toggle gui-field"> <a class="slider"></a> </div> {{/view}} </div> <div class="item2 name-box gui-text"> <div {{bindAttr class="indicatorLights"}}> </div><div class="gui-text field-container inline-block"> <div class="name gui-field">{{name}}</div> </div> </div> <div class="item3 gui-text ring-pct-box"> <div {{bindAttr class="coloredArrows"}}></div> <div class="left gui-text pct-box"> <span class="ring-pct">{{ringPctReadable}}%</span> </div> <div class="clear"></div> </div> <div class="item4 gui-text memory-box"> {{#if available}} <div class="membar-bg"> <div class="mem-colors"> <div class="erlang-mem mem-color" {{bindAttr style="memErlangStyle"}} {{bindAttr name="memErlangCeil"}}></div> <div class="non-erlang-mem mem-color" {{bindAttr style="memNonErlangStyle"}} {{bindAttr name="memNonErlang"}}></div> <div class="unknown-mem" {{bindAttr style="memFreeStyle"}} {{bindAttr name="memFreeReadable"}}></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory">{{memUsedReadable}}%</span> {{else}} <div class="membar-bg"> <div class="mem-colors"> <div class="unknown-mem" style="width: 100%"></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory"></span> {{/if}} </div> <div class="clear"></div> <!-- Actions container --> <div class="actions-container"> <div class="actions-pointer"></div> <div class="actions-box"> <h4 class="gui-headline-bold"> Use these actions to prepare this node to leave the cluster. </h4> {{#if me}} <span class="warning gui-text-flat italic"> Warning: This node is hosting Riak Control. If it leaves the cluster, Riak Control will be shut down. </span> {{/if}} <div class="replacement-controls"> <div class="gui-radio-wrapper default"> <input class="gui-radio" type="radio" value="leave" {{bindAttr name="name" id="normalLeaveRadio"}} checked="checked"/> <label class="serif" {{bindAttr for="normalLeaveRadio"}}>Allow this node to leave normally.</label> </div> <div class="gui-radio-wrapper"> <input class="gui-radio" type="radio" value="remove" {{bindAttr name="name" id="forceLeaveRadio"}} /> <label class="serif" {{bindAttr for="forceLeaveRadio"}}>Force this node to leave.</label> </div> <div {{bindAttr class="replaceRadioClasses"}}> <input class="gui-radio" type="radio" value="replace" {{bindAttr name="name" id="replaceRadio"}} /> <label class="serif" {{bindAttr for="replaceRadio"}}>Choose a new node to replace this one.</label> </div> <div class="extra-actions"> <div class="right-angle-arrow"></div> {{#if controller.joiningNodesExist}} <div class="gui-dropdown-wrapper replacement-node-dropdown"> <div class="gui-dropdown-bg gui-text">Select Replacement Node</div> <div class="gui-dropdown-cap left"></div> {{view RiakControl.ClusterItemSelectView prompt="Select Replacement Node" classNames="gui-dropdown" contentBinding="controller.joiningNodes" optionLabelPath="content.name"}} </div> <div class="gui-checkbox-wrapper"> <input class="gui-checkbox" type="checkbox" {{bindAttr name="name" id="forceReplaceCheck"}} value="true" /> <label class="serif" {{bindAttr for="forceReplaceCheck"}}>Force this replacement?</label> </div> <div class="clear"></div> <div class="disabler"></div> {{else}} <div class="no-joining-nodes gui-text-flat serif italic"> No new nodes are currently staged to join. </div> <div class="disabler show slide-up"></div> {{/if}} </div> <div class="clear"></div> </div> <div class="clear"></div> <span class="gui-text-flat serif italic stage-instructions">Click "STAGE" when you are ready to stage this action.</span> <a class="stage-button gui-point-button-right gui-text-bold right" {{action stageChange target="view"}}> <span class="gui-button-msg">STAGE</span> </a> <div class="clear"></div> </div> <div class="clear"></div> </div><!-- .actions-box --> <div class="clear"></div> </div><!-- .node -->{{/with}}');
Ember.TEMPLATES['current_nodes_item'] = Ember.Handlebars.compile('{{#with view}} <div class="node"> <div class="item1"> <div {{bindAttr class="stopRadioClasses"}}> <input class="gui-radio" type="radio" value="stop" {{bindAttr name="name" id="stopRadio"}} /> <div {{bindAttr class="stopDisablerClasses"}}></div> </div> </div> <div class="item2"> <div {{bindAttr class="downRadioClasses"}}> <input class="gui-radio" type="radio" value="down" {{bindAttr name="name" id="downRadio"}} /> <div {{bindAttr class="downDisablerClasses"}}></div> </div> </div> <div class="item3 name-box gui-text"> <div {{bindAttr class="indicatorLights"}}> </div><div class="gui-text field-container inline-block"> <div class="name gui-field">{{name}}</div> </div> </div> <div class="item4 gui-text ring-pct-box"> <div {{bindAttr class="coloredArrows"}}></div> <div class="left gui-text pct-box"> <span class="ring-pct">{{ringPctReadable}}%</span> </div> <div class="clear"></div> </div> <div class="item5 gui-text memory-box"> {{#if available}} <div class="membar-bg"> <div class="mem-colors"> <div class="erlang-mem mem-color" {{bindAttr style="memErlangStyle"}} {{bindAttr name="memErlangCeil"}}></div> <div class="non-erlang-mem mem-color" {{bindAttr style="memNonErlangStyle"}} {{bindAttr name="memNonErlang"}}></div> <div class="unknown-mem" {{bindAttr style="memFreeStyle"}} {{bindAttr name="memFreeReadable"}}></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory">{{memUsedReadable}}%</span> {{else}} <div class="membar-bg"> <div class="mem-colors"> <div class="unknown-mem" style="width: 100%"></div> </div> <div class="membar-fg"></div> </div> <span class="used-memory"></span> {{/if}} </div> <div class="clear"></div> </div><!-- .node -->{{/with}}');
Ember.TEMPLATES['staged_cluster_item'] = Ember.Handlebars.compile('{{#with view}} <div class="node"> <div class="item1 name-box gui-text"> <div {{bindAttr class="indicatorLights"}}> </div><div class="gui-text field-container inline-block"> <div class="name gui-field">{{name}}</div> </div> </div> <div class="item2 gui-text ring-pct-box"> <div class="left gui-text pct-box"> <span class="ring-pct">{{ringPctReadable}}%</span> </div> <div class="clear"></div> </div> {{#if isAction}} <div class="item3 action-taken gui-text"> <span class="action-name">{{node_action}}</span> </div> {{/if}} {{#if isReplaced}} <div class="item4 replacing-box"> <div class="gui-text field-container inline-block"> <div class="name gui-field">{{replacement}}</div> </div> </div> {{/if}} <div class="clear"></div> </div>{{/with}}');
Ember.TEMPLATES['partition'] = Ember.Handlebars.compile('{{#with view}} <div {{bindAttr class="color"}}></div>{{/with}}');
View
200 priv/admin/js/ring.js
@@ -1,21 +1,200 @@
minispade.register('ring', function() {
+ RiakControl.PartitionNValList = Ember.ArrayProxy.extend({});
+
+ RiakControl.SelectedPartitionNValList = Ember.ArrayProxy.extend({
+ selectionWatcher: function() {
+ var partitions = this.get('partitions').
+ findProperty('n_val', parseInt(this.get('selected'))).
+ partitions;
+ this.setProperties({ content: partitions });
+ }.observes('selected'),
+
+ n_vals: function() {
+
+ return this.get('partitions').map(function(x) {
+ return x.n_val;
+ });
+ }.property('partitions.@each')
+ });
+
+ /**
+ * Creates the n_val dropdown menu.
+ */
+ RiakControl.NValSelectView = Ember.Select.extend({});
+
+ /**
+ * @class
+ *
+ * Partition represents one of the partitions in the
+ * consistent hashing ring owned by the cluster.
+ */
+ RiakControl.Partition = Ember.Object.extend(
+ /** @scope RiakControl.Partition.prototype */ {
+
+ /* Whether unavailable nodes are present. */
+ unavailable: function() {
+ return this.get('unavailable_nodes').length > 0;
+ }.property('unavailable_nodes'),
+
+ /* Whether or not all primaries are down or not. */
+ allPrimariesDown: function() {
+ return this.get('available') === 0;
+ }.property('available'),
+
+ /* Whether or not a quorum of primaries are down. */
+ quorumUnavailable: function() {
+ return this.get('available') < this.get('quorum');
+ }.property('quorum', 'available')
+ });
+
/**
* @class
*
* Controls filtering, pagination and loading/reloading of the
* partition list for the cluster.
*/
- RiakControl.RingController = Ember.ObjectController.extend(
- /** @scope RiakControl.RingController.prototype */ {
+ RiakControl.RingController = Ember.ObjectController.extend({
/**
- * Reloads the record array associated with this controller.
+ * Refresh the list of partitions, using partitions returned as JSON,
+ * and partitions already modeled in Ember.
*
* @returns {void}
*/
- reload: function() {
- this.get('content').reload();
+ refresh: function(newPartitions,
+ existingPartitions,
+ partitionFactory) {
+
+ /*
+ * For every object in newPartitions...
+ */
+ newPartitions.forEach(function(partition) {
+
+ /*
+ * Use a unique property to locate the corresponding
+ * object in existingPartitions.
+ */
+ var exists = existingPartitions.findProperty('index',
+ partition.index);
+
+ /*
+ * If it doesn't exist yet, add it. If it does, update it.
+ */
+ if(exists !== undefined) {
+ exists.setProperties(partition);
+ } else {
+ existingPartitions.pushObject(
+ partitionFactory.create(partition));
+ }
+ });
+
+ /*
+ * We've already updated corresponding objects and added
+ * new ones. Now we need to remove ones that don't exist in
+ * the new cluster.
+ */
+
+ var changesOccurred = false;
+ var replacementPartitions = [];
+
+ /*
+ * For every object in the existingPartitions...
+ */
+ existingPartitions.forEach(function(partition, i) {
+
+ /*
+ * Use a unique property to locate the corresponding object
+ * in newPartitions.
+ */
+ var exists = newPartitions.findProperty('index',
+ partition.index);
+
+ /*
+ * If it doesn't exist in the newPartitions, destroy it.
+ *
+ * If this happens even one time, we'll mark changesOccurred as
+ * true.
+ *
+ * Otherwise, this partition is a good partition and we can add
+ * it to the replacementPartitions.
+ */
+ if(exists === undefined) {
+ partition.destroy();
+ changesOccurred = true;
+ } else {
+ replacementPartitions.pushObject(partition);
+ }
+ });
+
+ /*
+ * If we ended up having to remove any partitions,
+ * replace the cluster.
+ */
+ if(changesOccurred) {
+ existingPartitions.set('[]', replacementPartitions.get('[]'));
+ }
+ },
+
+ /**
+ * Load data from the server.
+ */
+ load: function () {
+ var that = this;
+ $.ajax({
+ type: 'GET',
+ url: '/admin/partitions',
+ dataType: 'json',
+
+ success: function (data) {
+ var curSelected = that.get('content.selected'),
+ curPartitions = that.get('content.partitions'),
+ toRemove = [],
+ i;
+
+ /*
+ * Remove any old partition lists that no longer exist
+ * within data.partitions.
+ */
+ curPartitions.forEach(function(hash) {
+ if (!data.partitions.findProperty('n_val', hash.n_val)) {
+ hash.partitions.forEach(function (partition) {
+ partition.destroy();
+ });
+ toRemove.push(i)
+ }
+ });
+
+ toRemove.forEach(function(pIndex) {
+ curPartitions.removeAt(pIndex);
+ });
+
+ /*
+ * Update each partition list.
+ */
+ data.partitions.forEach(function (hash) {
+ var corresponder = curPartitions.findProperty('n_val', hash.n_val);
+ if (!corresponder) {
+ corresponder = curPartitions.pushObject({n_val: hash.n_val, partitions: []});
+ }
+ that.refresh(hash.partitions, corresponder.partitions, RiakControl.Partition);
+ });
+
+ /*
+ * Manually select a dropdown item on the first ajax call.
+ */
+ if(that.get('content.selected') === undefined) {
+ that.set('content.selected', curSelected || data.default_n_val);
+ }
+ }
+ });
+ },
+
+ /**
+ * Call the load function.
+ */
+ reload: function () {
+ this.load();
},
/**
@@ -26,7 +205,7 @@ minispade.register('ring', function() {
*/
startInterval: function() {
this._intervalId = setInterval(
- $.proxy(this.reload, this), RiakControl.refreshInterval);
+ $.proxy(this.reload, this), RiakControl.refreshInterval);
},
/**
@@ -95,7 +274,8 @@ minispade.register('ring', function() {
* @returns {array}
*/
allUnavailable: function() {
- return this.get('content').filterProperty('allPrimariesDown', true);
+ return this.get('content').
+ filterProperty('allPrimariesDown', true);
}.property('content.@each.allPrimariesDown'),
/**
@@ -122,7 +302,8 @@ minispade.register('ring', function() {
* @returns {array}
*/
quorumUnavailable: function() {
- return this.get('content').filterProperty('quorumUnavailable', true);
+ return this.get('content').
+ filterProperty('quorumUnavailable', true);
}.property('content.@each.quorumUnavailable'),
/**
@@ -142,9 +323,9 @@ minispade.register('ring', function() {
quorumUnavailableExist: function() {
return this.get('quorumUnavailableCount') > 0;
}.property('quorumUnavailableCount')
-
});
+
/**
* @class
*
@@ -480,7 +661,6 @@ minispade.register('ring', function() {
templateName: 'partition',
indexBinding: 'content.index',
- n_valBinding: 'content.n_val',
quorumBinding: 'content.quorum',
availableBinding: 'content.available',
distinctBinding: 'content.distinct',
View
9 priv/admin/js/router.js
@@ -99,7 +99,14 @@ minispade.register('router', function() {
connectOutlets: function(router) {
router.get('applicationController').
- connectOutlet('ring', RiakControl.Partition.find());
+ connectOutlet('ring',
+ RiakControl.SelectedPartitionNValList.create({
+ content: [],
+ selected: undefined,
+ partitions: RiakControl.PartitionNValList.create({
+ content: []
+ })
+ }));
$.riakControl.markNavActive('nav-ring');
},
View
10 priv/admin/js/shared.js
@@ -125,6 +125,16 @@ minispade.register('shared', function () {
RiakControl.NodeProperties = Ember.Mixin.create({
/**
+ * In the current cluster area, the host node has extra content in its
+ * actions box so it needs to be a little taller than the others.
+ *
+ * @returns {String}
+ */
+ hostNodeClasses: function () {
+ return this.get('me') ? 'node taller' : 'node';
+ }.property('me'),
+
+ /**
* Color the lights appropriately based on the node status.
*
* @returns {string}
View
2  priv/admin/js/templates/current_cluster_item.hbs
@@ -1,5 +1,5 @@
{{#with view}}
- <div class="node">
+ <div {{bindAttr class="hostNodeClasses"}}>
<div class="item1 toggle-container">
{{#view RiakControl.CurrentClusterToggleView}}
<div class="actions-toggle gui-field">
View
7 priv/admin/js/templates/partition_filter.hbs
@@ -1,7 +0,0 @@
-<div id="ring-filter" class="right">
- <div class="gui-dropdown-wrapper">
- <div class="gui-dropdown-bg gui-text">Filter by...</div>
- <div class="gui-dropdown-cap left"></div>
- {{view RiakControl.PartitionFilterSelectView id="filter" classNames="gui-dropdown" contentBinding="filters" optionLabelPath="content.name" optionValuePath="content.value" prompt="All" selectionBinding="controller.selectedPartitionFilter"}}
- </div>
-</div>
View
26 priv/admin/js/templates/ring.hbs
@@ -16,6 +16,32 @@
<div class="chart-wrapper">
<h2 class="gui-headline">Availability</h2>
+ <div class="nval-dropdown-container">
+ <h4 class="gui-headline-bold">See visualizations using a different n_val.</h4>
+ <div class="tooltip right">
+ <span>
+ <span class="tooltip-heading">
+ n_val specifies the number of copies of each object to be stored
+ in the cluster.
+ <a class="docs" href="http://docs.basho.com/riak/latest/theory/concepts/Buckets/"></a>
+ </span>
+ <span class="tooltip-body">
+ Right now, you\'re seeing what your ring looks like
+ for all objects stored with an n_val of 3. If you have
+ objects stored with other n_vals, select an n_val
+ from the dropdown to see what your ring looks like
+ for data stored with the selected n_val.
+ </span>
+ </span>
+ </div>
+ {{view RiakControl.NValSelectView
+ contentBinding="content.n_vals"
+ valueBinding="content.selected"
+ dataBinding="content"
+ selectionBinding="content.selected"
+ }}
+ </div>
+
{{view RiakControl.DegeneratePreflistChart}}
<div class="chart-spacer"></div>
View
3  src/riak_control_ring.erl
@@ -97,8 +97,7 @@ fold_preflist_proplist(Preflist,
UAll = lists:usort(All),
UDown = lists:usort(Down),
- Status = [[{n_val, NVal},
- {quorum, Quorum},
+ Status = [[{quorum, Quorum},
{distinct, length(UAll) =:= length(All)},
{index, pretty_index(Index)},
{available, length(Up)},
View
52 src/riak_control_session.erl
@@ -40,6 +40,8 @@
get_partitions/0,
get_status/0,
get_plan/0,
+ get_n_vals/0,
+ get_default_n_val/0,
clear_plan/0,
stage_change/3,
commit_plan/0,
@@ -56,13 +58,15 @@
%% exported for RPC calls.
-export([get_my_info/0]).
--record(state, {vsn :: version(),
- services :: services(),
- ring :: ring(),
- partitions :: partitions(),
- nodes :: members(),
- update_tick :: boolean(),
- transfers :: transfers()}).
+-record(state, {vsn :: version(),
+ services :: services(),
+ ring :: ring(),
+ partitions :: partitions(),
+ nodes :: members(),
+ update_tick :: boolean(),
+ transfers :: transfers(),
+ n_vals :: n_vals(),
+ default_n_val :: pos_integer()}).
-type normalized_action() :: leave
| remove
@@ -116,6 +120,16 @@ get_services() ->
get_partitions() ->
gen_server:call(?MODULE, get_partitions, infinity).
+%% @doc Return list of available n_vals.
+-spec get_n_vals() -> {ok, version(), n_vals()}.
+get_n_vals() ->
+ gen_server:call(?MODULE, get_n_vals, infinity).
+
+%% @doc Return list of available n_vals.
+-spec get_default_n_val() -> {ok, version(), pos_integer()}.
+get_default_n_val() ->
+ gen_server:call(?MODULE, get_default_n_val, infinity).
+
%% @doc Get the staged cluster plan.
-spec get_plan() -> {ok, list(), list()} | {error, atom()}.
get_plan() ->
@@ -200,7 +214,11 @@ handle_call(get_nodes, _From, State=#state{vsn=V,nodes=N}) ->
handle_call(get_services, _From, State=#state{vsn=V,services=S}) ->
{reply, {ok, V, S}, State};
handle_call(get_partitions, _From, State=#state{vsn=V,partitions=P}) ->
- {reply, {ok, V, P}, State}.
+ {reply, {ok, V, P}, State};
+handle_call(get_n_vals, _From, State=#state{vsn=V,n_vals=N}) ->
+ {reply, {ok, V, N}, State};
+handle_call(get_default_n_val, _From, State=#state{vsn=V,default_n_val=N}) ->
+ {reply, {ok, V, N}, State}.
%% @doc
%%
@@ -265,12 +283,21 @@ update_services(State=#state{services=S}, Services) ->
NewState = update_partitions(NodeState),
rev_state(NewState).
+%% @doc Update list of all available nvals.
+-spec update_n_vals(#state{}) -> #state{}.
+update_n_vals(State=#state{ring=Ring}) ->
+ DefaultNVal = riak_control_ring:n_val(),
+ Unique = lists:usort([DefaultNVal |
+ [NVal || {_, NVal} <- riak_core_bucket:bucket_nval_map(Ring)]]),
+ State#state{n_vals=Unique, default_n_val=DefaultNVal}.
+
%% @doc Update ring state and partitions.
-spec update_ring(#state{}, ring()) -> #state{}.
update_ring(State, Ring) ->
erlang:send_after(?UPDATE_TICK_TIMEOUT, self(), clear_update_tick),
NodeState = update_nodes(State#state{update_tick=true, ring=Ring}),
- FinalState = update_partitions(NodeState),
+ NValsAdded = update_n_vals(NodeState),
+ FinalState = update_partitions(NValsAdded),
rev_state(FinalState).
%% @doc Update ring.
@@ -282,10 +309,9 @@ update_nodes(State=#state{ring=Ring}) ->
%% @doc Update partitions.
-spec update_partitions(#state{}) -> #state{}.
-update_partitions(State=#state{ring=Ring, nodes=Nodes}) ->
- Unavailable = [Name ||
- ?MEMBER_INFO{node=Name, reachable=false} <- Nodes],
- Partitions = riak_control_ring:status(Ring, Unavailable),
+update_partitions(State=#state{ring=Ring, nodes=Nodes, n_vals=NVals}) ->
+ Unavailable = [Name || ?MEMBER_INFO{node=Name, reachable=false} <- Nodes],
+ Partitions = [[{n_val, NVal}, {partitions, riak_control_ring:status(Ring, NVal, Unavailable)}] || NVal <- NVals],
State#state{partitions=Partitions}.
%% @doc Ping and retrieve vnode workers.
View
10 src/riak_control_wm_partitions.erl
@@ -40,7 +40,7 @@
-define(VNODE_TYPES, [riak_kv,riak_pipe,riak_search]).
--record(context, {partitions}).
+-record(context, {partitions, default_n_val}).
-type context() :: #context{}.
%% @doc Route handling.
@@ -52,7 +52,8 @@ routes() ->
-spec init(list()) -> {ok, context()}.
init([]) ->
{ok, _, Partitions} = riak_control_session:get_partitions(),
- {ok, #context{partitions=Partitions}}.
+ {ok, _, DefNVal} = riak_control_session:get_default_n_val(),
+ {ok, #context{partitions=Partitions, default_n_val=DefNVal}}.
%% @doc Validate origin.
-spec forbidden(wrq:reqdata(), context()) ->
@@ -81,6 +82,7 @@ content_types_provided(ReqData, Context) ->
%% @doc Return a list of partitions.
-spec to_json(wrq:reqdata(),context()) ->
{iolist(), wrq:reqdata(), context()}.
-to_json(ReqData, Context=#context{partitions=Partitions}) ->
- Encoded = mochijson2:encode({struct,[{partitions, Partitions}]}),
+to_json(ReqData, Context=#context{partitions=Partitions, default_n_val=DefNVal}) ->
+ Encoded = mochijson2:encode({struct,[{partitions, Partitions},
+ {default_n_val, DefNVal}]}),
{Encoded, ReqData, Context}.
Please sign in to comment.
Something went wrong with that request. Please try again.