SOLR-4388 Add a Collections UI for SolrCloud

git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1707245 13f79535-47bb-0310-9956-ffa450edef68
This commit is contained in:
Upayavira 2015-10-07 11:06:09 +00:00
parent 20a8df7a5d
commit c8ed0a0df8
7 changed files with 967 additions and 1 deletions

View File

@ -0,0 +1,353 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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.
*/
#content #collections
{
position: relative;
}
#content #collections #ui-block
{
background-color: #fff;
height: 200px;
position: absolute;
left: -5px;
top: 35px;
width: 500px;
}
#content #collections #frame
{
float: right;
width: 86%;
}
#content #collections #navigation
{
padding-top: 50px;
width: 12%;
}
#content #collections #navigation a
{
padding-left: 5px;
}
#content #collections #frame .actions
{
margin-bottom: 20px;
min-height: 30px;
}
#content #collections .actions div.action
{
width: 320px;
}
#content #collections .actions div.action .cloud
{
}
#content #collections .actions form .directory-note
{
background-image: url( ../../img/ico/information-white.png );
background-position: 22% 1px;
color: #c0c0c0;
}
#content #collections .actions form .error
{
background-image: url( ../../img/ico/cross-button.png );
background-position: 22% 1px;
color: #c00;
font-weight: bold;
}
#content #collections .actions form p
{
padding-bottom: 8px;
}
#content #collections .actions form label
{
float: left;
padding-top: 3px;
padding-bottom: 3px;
text-align: right;
width: 25%;
}
#content #collections .actions form input,
#content #collections .actions form select,
#content #collections .actions form .chosen-container
#content #collections .actions form .buttons,
#content #collections .actions form .note span
{
float: right;
width: 71%;
}
#content #collections .actions form .note span
{
padding-left: 3px;
padding-right: 3px;
}
#content #collections .actions form .buttons
{
padding-top: 10px;
}
#content #collections .actions form button.submit
{
margin-right: 20px;
}
#content #collections .actions form button.submit span
{
background-image: url( ../../img/ico/tick.png );
}
#content #collections .actions form button.reset span
{
background-image: url( ../../img/ico/cross.png );
}
#content #collections .actions #add
{
left: 0;
position: absolute;
}
#content #collections .actions #add span
{
background-image: url( ../../img/ico/plus-button.png );
}
#content #collections .actions #delete
{
margin-right: 20px;
}
#content #collections .actions #delete span
{
background-image: url( ../../img/ico/cross.png );
}
#content #collections .actions #reload span
{
background-image: url( ../../img/ico/arrow-circle.png );
}
#content #collections .actions #rename span
{
background-image: url( ../../img/ico/ui-text-field-select.png );
}
#content #collections .actions #create-alias span
{
background-image: url( ../../img/ico/arrow-switch.png );
}
#content #collections .actions #delete-alias span
{
background-image: url( ../../img/ico/cross-button.png );
}
#content #collections .actions #optimize span
{
background-image: url( ../../img/ico/hammer-screwdriver.png );
}
#content #collections .actions div.action
{
background-color: #fff;
border: 1px solid #f0f0f0;
box-shadow: 5px 5px 10px #c0c0c0;
-moz-box-shadow: 5px 5px 10px #c0c0c0;
-webkit-box-shadow: 5px 5px 10px #c0c0c0;
position: absolute;
left: 50px;
top: 40px;
padding: 10px;
}
#content #collections .actions #add-replica span
{
background-image: url( ../../img/ico/plus-button.png );
}
#content #collections div.action.add-replica {
border: 1px solid #f0f0f0;
width: 400px;
margin-right: 0px;
padding: 10px;
float: right;
}
#content #collections div.action.add-replica p {
padding-bottom: 8px;
}
#content #collections div.action.add-replica .buttons {
float: right;
}
#content #collections div.action.add-replica .buttons .submit span {
background-image: url( ../../img/ico/tick.png );
background-position: 0% 50%;
}
#content #collections div.action.add-replica .buttons .reset span {
background-image: url( ../../img/ico/cross.png );
background-position: 0% 50%;
}
#content #collections #data #collection-data h2 { background-image: url( ../../img/ico/box.png ); }
#content #collections #data #shard-data h2 { background-image: url( ../../img/ico/sitemap.png ); }
#content #collections #data #shard-data .replica h2 { background-image: url( ../../img/ico/node-slave.png ); }
#content #collections #data #index-data
{
margin-top: 10px;
}
#content #collections #data li
{
padding-bottom: 3px;
padding-top: 3px;
}
#content #collections #data li.odd
{
background-color: #f8f8f8;
}
#content #collections #data li dt
{
float: left;
width: 50%;
}
#content #collections #data li dd
{
float: right;
width: 50%;
}
#content #collections #data li dd.ico
{
background-image: url( ../../img/ico/slash.png );
height: 20px;
}
#content #collections #data li dd.ico.ico-1
{
background-image: url( ../../img/ico/tick.png );
}
#content #collections #data li dd.ico span
{
}
#content #collections #add_advanced {
background-image: url( ../../img/ico/chevron-small-expand.png );
background-position: 100% 50%;
cursor: pointer;
padding-right: 21px;
}
#content #collections #add_advanced .open {
background-image: url( ../../img/ico/chevron-small.png );
}
#content #collections .shard {
margin-left: 40px;
}
#content #collections .replica {
margin-left: 40px;
}
#content #collections .shard h2 span.openReplica {
background-image: url( ../../img/ico/chevron-small-expand.png );
background-position: 100% 50%;
cursor: pointer;
padding-right: 21px;
}
#content #collections .shard h2 span.openReplica .open {
background-image: url( ../../img/ico/chevron-small.png );
}
#content #collections .replica h2 span {
background-image: url( ../../img/ico/chevron-small-expand.png );
background-position: 100% 50%;
cursor: pointer;
padding-right: 21px;
}
#content #collections .replica h2 span.rem {
background-image: url( ../../img/ico/cross.png );
background-position: 100% 50%;
cursor: pointer;
padding-right: 21px;
right:10px;
}
#content #collections .replica h2 span .open {
background-image: url( ../../img/ico/chevron-small.png );
}
#content #collections #add-replica {
float: right;
}
#content #collections .add select {
width: 100%;
}
#content #collections .chosen-container ul {
width: 100%;
padding: 5px;
}
#content #collections .delete-replica span
{
background-image: url( ../../img/ico/cross.png );
}
#content #collections .delete-replica button.submit span
{
background-image: url( ../../img/ico/tick.png );
}
#content #collections #node-name .chosen-container
{
width: 100% !important;
}
#content #collections #collection-data {
float: left;
width: 35%;
}
#content #collections #shard-data {
float: left;
width: 65%;
}

View File

@ -255,6 +255,7 @@ limitations under the License.
#menu #threads.global p a { background-image: url( ../../img/ico/ui-accordion.png ); }
#menu #collections.global p a { background-image: url( ../../img/ico/documents-stack.png ); }
#menu #cores.global p a { background-image: url( ../../img/ico/databases.png ); }
#menu #cloud.global p a { background-image: url( ../../img/ico/network-cloud.png ); }

View File

@ -27,6 +27,7 @@ limitations under the License.
<link rel="stylesheet" type="text/css" href="css/angular/analysis.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/cloud.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/cores.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/collections.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/dashboard.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/dataimport.css?_=${version}">
<link rel="stylesheet" type="text/css" href="css/angular/files.css?_=${version}">
@ -60,6 +61,7 @@ limitations under the License.
<script src="js/angular/controllers/index.js"></script>
<script src="js/angular/controllers/logging.js"></script>
<script src="js/angular/controllers/cloud.js"></script>
<script src="js/angular/controllers/collections.js"></script>
<script src="js/angular/controllers/cores.js"></script>
<script src="js/angular/controllers/threads.js"></script>
<script src="js/angular/controllers/java-properties.js"></script>
@ -151,7 +153,8 @@ limitations under the License.
</ul>
</li>
<li id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
<li ng-show="isCloudEnabled" id="collections" class="global" ng-class="{active:page=='collections'}"><p><a href="#/~collections">Collections</a></p></li>
<li ng-hide="isCloudEnabled" id="cores" class="global" ng-class="{active:page=='cores'}"><p><a href="#/~cores">Core Admin</a></p></li>
<li id="java-properties" class="global" ng-class="{active:page=='java-props'}"><p><a href="#/~java-properties">Java Properties</a></li>

View File

@ -51,6 +51,14 @@ solrAdminApp.config([
templateUrl: 'partials/cores.html',
controller: 'CoreAdminController'
}).
when('/~collections', {
templateUrl: 'partials/collections.html',
controller: 'CollectionsController'
}).
when('/~collections/:collection', {
templateUrl: 'partials/collections.html',
controller: 'CollectionsController'
}).
when('/~threads', {
templateUrl: 'partials/threads.html',
controller: 'ThreadsController'

View File

@ -0,0 +1,256 @@
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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.
*/
// @todo test optimize (delete stuff, watch button appear, test button/form)
solrAdminApp.controller('CollectionsController',
function($scope, $routeParams, $location, $timeout, Collections, Zookeeper, Constants){
$scope.resetMenu("collections", Constants.IS_ROOT_PAGE);
$scope.refresh = function() {
Collections.status(function (data) {
$scope.collections = [];
for (var name in data.cluster.collections) {
var collection = data.cluster.collections[name];
collection.name = name;
var shards = collection.shards;
collection.shards = [];
for (var shardName in shards) {
var shard = shards[shardName];
shard.name = shardName;
shard.collection = collection.name;
var replicas = shard.replicas;
shard.replicas = [];
for (var replicaName in replicas) {
var replica = replicas[replicaName];
replica.name = replicaName;
replica.collection = collection.name;
replica.shard = shard.name;
shard.replicas.push(replica);
}
collection.shards.push(shard);
}
$scope.collections.push(collection);
if ($routeParams.collection == name) {
$scope.collection = collection;
}
}
if ($routeParams.collection && !$scope.collection) {
alert("No collection called " + $routeParams.collection)
$location.path("/~collections");
}
$scope.liveNodes = data.cluster.liveNodes;
});
Zookeeper.configs(function(data) {
$scope.configs = [];
var items = data.tree[0].children;
for (var i in items) {
$scope.configs.push({name: items[i].data.title});
}
});
};
$scope.hideAll = function() {
$scope.showRename = false;
$scope.showAdd = false;
$scope.showDelete = false;
$scope.showSwap = false;
$scope.showCreateAlias = false;
$scope.showDeleteAlias = false;
};
$scope.showAddCollection = function() {
$scope.hideAll();
$scope.showAdd = true;
$scope.newCollection = {
name: "new_collection",
routerName: "compositeId",
numShards: 1,
configName: "",
replicationFactor: 1,
maxShardsPerNode: 1
};
};
$scope.toggleCreateAlias = function() {
$scope.hideAll();
$scope.showCreateAlias = true;
}
$scope.toggleDeleteAlias = function() {
$scope.hideAll();
$scope.showDeleteAlias = true;
Zookeeper.aliases({}, function(data){
if (Object.keys(data.aliases).length == 0) {
delete $scope.aliases;
} else {
$scope.aliases = data.aliases;
}
});
}
$scope.cancelCreateAlias = $scope.cancelDeleteAlias = function() {
$scope.hideAll();
}
$scope.createAlias = function() {
var collections = $scope.aliasCollections.join(",");
Collections.createAlias({name: $scope.aliasToCreate, collections: collections}, function(data) {
$scope.hideAll();
});
}
$scope.deleteAlias = function() {
Collections.deleteAlias({name: $scope.aliasToDelete}, function(data) {
$scope.hideAll();
});
};
$scope.addCollection = function() {
if (!$scope.newCollection.name) {
$scope.addMessage = "Please provide a core name";
} else if (false) { //@todo detect whether core exists
$scope.AddMessage = "A core with that name already exists";
} else {
var coll = $scope.newCollection;
var params = {
name: coll.name,
"router.name": coll.routerName,
numShards: coll.numShards,
"collection.configName": coll.configName,
replicationFactor: coll.replicationFactor,
maxShardsPerNode: coll.maxShardsPerNode
};
if (coll.shards) params.shards = coll.shards;
if (coll.routerField) params.routerField = coll.routerField;
if (coll.routerName) params.routerName = coll.routerName;
Collections.add(params, function(data) {
$scope.cancelAddCollection();
$scope.resetMenu("collections", Constants.IS_ROOT_PAGE);
$location.path("/~collections/" + $scope.newCollection.name);
});
}
};
$scope.cancelAddCollection = function() {
delete $scope.addMessage;
$scope.showAdd = false;
};
$scope.showDeleteCollection = function() {
$scope.hideAll();
if ($scope.collection) {
$scope.showDelete = true;
} else {
alert("No collection selected.");
}
};
$scope.deleteCollection = function() {
if ($scope.collection.name == $scope.collectionDeleteConfirm) {
Collections.delete({name: $scope.collection.name}, function (data) {
$location.path("/~collections");
});
} else {
$scope.deleteMessage = "Collection names do not match.";
}
};
$scope.reloadCollection = function() {
if (!$scope.collection) {
alert("No collection selected.");
return;
}
Collections.reload({name: $scope.collection.name},
function(successData) {
$scope.reloadSuccess = true;
$timeout(function() {$scope.reloadSuccess=false}, 1000);
},
function(failureData) {
$scope.reloadFailure = true;
$timeout(function() {$scope.reloadFailure=false}, 1000);
$location.path("/~collections");
});
};
$scope.toggleAddReplica = function(shard) {
$scope.hideAll();
shard.showAdd = !shard.showAdd;
delete $scope.addReplicaMessage;
Zookeeper.liveNodes({}, function(data) {
$scope.nodes = [];
var children = data.tree[0].children;
for (var child in children) {
$scope.nodes.push(children[child].data.title);
}
});
};
$scope.toggleRemoveReplica = function(replica) {
$scope.hideAll();
replica.showRemove = !replica.showRemove;
};
$scope.deleteReplica = function(replica) {
alert("DELETE");
Collections.deleteReplica({collection: replica.collection, shard:replica.shard, replica:replica.name}, function(data) {
replica.deleted = true;
$timeout(function() {
$scope.refresh();
}, 2000);
});
}
$scope.addReplica = function(shard) {
var params = {
collection: shard.collection,
shard: shard.name,
}
if (shard.replicaNodeName && shard.replicaNodeName != "") {
params.node = shard.replicaNodeName;
}
Collections.addReplica(params, function(data) {
shard.replicaAdded = true;
$timeout(function () {
shard.replicaAdded = false;
shard.showAdd = false;
$$scope.refresh();
}, 2000);
});
};
$scope.toggleShard = function(shard) {
shard.show = !shard.show;
}
$scope.toggleReplica = function(replica) {
replica.show = !replica.show;
}
$scope.refresh();
}
);
var flatten = function(data) {
var list = [];
for (var name in data) {
var entry = data[name];
entry.name = name;
list.push(entry);
}
return list;
}

View File

@ -32,6 +32,8 @@ solrAdminServices.factory('System',
"rename": {params:{action: "RENAME"}},
"createAlias": {params:{action: "CREATEALIAS"}},
"deleteAlias": {params:{action: "DELETEALIAS"}},
"deleteReplica": {params:{action: "DELETEREPLICA"}},
"addReplica": {params:{action: "ADDREPLICA"}},
"reload": {method: "GET", params:{action:"RELOAD", core: "@core"}},
"optimize": {params:{}}
});

View File

@ -0,0 +1,343 @@
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You 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.
-->
<div id="collections" class="clearfix empty">
<div id="ui-block" style="display:none">&nbsp;</div><!-- @todo what is this for? -->
<div id="frame">
<div id="actions" class="actions clearfix">
<button id="add" class="action" ng-click="showAddCollection()"><span>Add Collection</span></button>
<button id="delete" class="warn requires-core" ng-click="showDeleteCollection()" ng-show="collection"><span>Delete</span></button>
<button id="reload" class="requires-core" ng-click="reloadCollection()" ng-show="collection"
ng-class="{success: reloadSuccess, warn: reloadFailure}"><span>Reload</span></button>
<button id="create-alias" class="action requires-core" ng-click="toggleCreateAlias()"><span>Create Alias</span></button>
<button id="delete-alias" class="action requires-core" ng-click="toggleDeleteAlias()"><span>Delete Alias</span></button>
<div class="action add" data-rel="add" ng-show="showAdd" style="left:0px">
<form>
<p class="clearfix"><label for="add_name">name:</label>
<input type="text" name="name" id="add_name" ng-model="newCollection.name"></p>
<p class="clearfix"><label for="add_config">config set:</label>&nbsp;
<select chosen ng-options="config.name as config.name for config in configs" name="config_name" id="add_config" ng-model="newCollection.configName">
</select>
</p>
<p class="clearfix"><label for="add_numShards">numShards:</label>
<input type="text" name="numShards" id="add_numShards" ng-model="newCollection.numShards"></p>
<p class="clearfix"><label for="add_replicationFactor">replicationFactor:</label>
<input type="text" name="replicationFactor" id="add_replicationFactor" ng-model="newCollection.replicationFactor"></p>
<p class="clearfix"><a ng-click="showAdvanced=!showAdvanced">
<span id="add_advanced" ng-class="{open: showAdvanced}">Show advanced</span></a></p>
<div ng-show="showAdvanced">
<p>Advanced options: </p>
<p class="clearfix"><label for="add_router_name">router:</label>
<select name="routerName" id="add_router_name" ng-model="newCollection.routerName">
<option value="compositeId">Composite ID</option>
<option value="implicit">Implicit</option>
</select>
</p>
<p class="clearfix"><label for="add_maxShardsPerNode">maxShardsPerNode:</label>
<input type="text" name="replicationFactor" id="add_maxShardsPerNode" ng-model="newCollection.maxShardsPerNode"></p>
<p class="clearfix"><label for="add_shards">shards:</label>
<input type="text" name="shards" id="add_shards" ng-model="newCollection.shards"></p>
<p class="clearfix"><label for="add_routerField">routerField:</label>
<input type="text" name="routerField" id="add_routerField" ng-model="newCollection.routerField"></p>
</div>
<p class="clearfix note error" ng-show="addMessage">
<span>{{addMessage}}</span>
</p>
<p class="clearfix buttons">
<button type="submit" class="submit" ng-click="addCollection()"><span>Add Collection</span></button>
<button type="reset" class="reset" ng-click="cancelAddCollection()"><span>Cancel</span></button>
</p>
</form>
</div>
<div class="action delete" ng-show="showDelete">
<form>
<p>Please type collection name to confirm deletion:</p>
<p class="clearfix"><label for="collectiondeleteConfirm">Collection</label>
<input type="text" ng-model="collectionDeleteConfirm" id="collectionDeleteConfirm"></p>
<p class="clearfix note error" ng-show="deleteMessage">
<span>{{deleteMessage}}</span>
</p>
<p class="clearfix buttons">
<button class="submit" ng-click="deleteCollection()"><span>Delete</span></button>
<button type="reset" class="reset" ng-click="showDelete=false"><span>Cancel</span></button>
</p>
</form>
</div>
<div class="action create-alias" ng-show="showCreateAlias">
<form>
<input type="hidden" name="core" data-core="current">
<p class="clearfix"><label for="alias">Alias Name:</label>
<input type="text" name="alias" ng-model="aliasToCreate" id="alias"></p>
<p class="clearfix"><label for="aliasCollections">Collections:</label>
<select multiple id="aliasCollections" ng-model="aliasCollections" ng-options="collection.name for collection in collections" class="other">
</select></p>
<p class="clearfix note error" ng-show="renameMessage">
<span>{{renameMessage}}</span>
</p>
<p class="clearfix buttons">
<button class="submit" ng-click="createAlias()"><span>Create Alias</span></button>
<button type="reset" class="reset" ng-click="cancelCreateAlias()"><span>Cancel</span></button>
</p>
</form>
</div>
<div class="action delete-alias" ng-show="showDeleteAlias">
<form>
<span ng-show="aliases">
<p class="clearfix"><label for="deleteAlias">Alias:</label>
<select id="deleteAlias" ng-model="aliasToDelete" ng-options="alias as alias for (alias, collections) in aliases" class="other">
</select></p>
<p class="clearfix note error" ng-show="swapMessage">
<span>{{swapMessage}}</span>
</p>
<p class="clearfix buttons">
<button type="submit" class="submit" ng-click="deleteAlias()"><span>Delete Alias</span></button>
<button type="reset" class="reset" ng-click="cancelDeleteAlias()"><span>Cancel</span></button>
</p>
</span>
<span ng-hide="aliases">
<p>No aliases to delete.</p>
<p class="clearfix buttons">
<button type="reset" class="reset" ng-click="cancelDeleteAlias()"><span>Cancel</span></button>
</p>
</span>
</form>
</div>
</div>
<div class="requires-core" ng-hide="collection">
<h2>Please select a collection</h2>
</div>
<div id="data" class="requires-core clearfix" ng-show="collection">
<div class="block" id="collection-data">
<h2>Collection: {{collection.name}}</h2>
<div class="message-container">
<div class="message"></div>
</div>
<div class="content">
<ul>
<li>
<dl class="clearfix">
<dt><span>Shard count:</span></dt>
<dd>{{collection.shards.length}}</dd>
</dl>
</li>
<li><dl class="clearfix">
<dt><span>configName:</span></dt>
<dd>{{collection.configName}}</dd>
</dl></li>
<li><dl class="clearfix">
<dt><span>replicationFactor:</span></dt>
<dd>{{collection.replicationFactor}}</dd>
</dl></li>
<li><dl class="clearfix">
<dt><span>maxShardsPerNode:</span></dt>
<dd>{{collection.maxShardsPerNode}}</dd>
</dl></li>
<li><dl class="clearfix">
<dt><span>router:</span></dt>
<dd>{{collection.router.name}}</dd>
</dl></li>
<li><dl class="clearfix">
<dt><span>autoAddReplicas:</span></dt>
<dd>{{collection.autoAddReplicas}}</dd>
</dl></li>
</ul>
</div>
</div>
<div class="block" id="shard-data">
<div class="content shard" ng-repeat="shard in collection.shards">
<a ng-click="toggleShard(shard)"><h2><span ng-class="{open:shard.show}">Shard: {{shard.name}}</span></h2></a>
<ul ng-show="shard.show">
<li>
<ul>
<li>
<dl class="clearfix">
<dt><span>state:</span></dt>
<dd>{{shard.state}}</dd>
</dl>
</li>
<li>
<dl class="clearfix">
<dt><span>range:</span></dt>
<dd>{{shard.range}}</dd>
</dl>
<br/>
</li>
<li>
<ul class="replica" ng-repeat="replica in shard.replicas">
<li>
<h2>
<a ng-click="toggleReplica(replica)">
<span class="openReplica" ng-class="{open:replica.show}">Replica: {{replica.name}}</span>
</a>
<div style="float:right"><a ng-click="toggleRemoveReplica(replica)"><span class="rem"></span></a></div>
</h2>
</li>
<li>
<ul ng-show="replica.showRemove">
<li>
<form class="delete-replica">
<p class="clearfix"><em>Are you sure you want to delete this replica?</em></p>
<p class="clearfix buttons">
<button class="submit" ng-class="{success: replica.deleted}" ng-click="deleteReplica(replica)"><span>Delete Replica</span></button>
<button type="reset" class="reset" ng-click="toggleRemoveReplica(replica)"><span>Cancel</span></button>
</p>
</form>
</li>
</ul>
<ul ng-show="replica.show">
<li>
<dl class="clearfix">
<dt><span>core:</span></dt>
<dd>{{replica.core}}</dd>
</dl>
</li>
<li>
<dl class="clearfix">
<dt><span>base URL:</span></dt>
<dd><a ng-href="{{replica.base_url}}">{{replica.base_url}}</a></dd>
</dl>
</li>
<li>
<dl class="clearfix">
<dt><span>node name:</span></dt>
<dd>{{replica.node_name}}</dd>
</dl>
</li>
<li>
<dl class="clearfix">
<dt><span>state:</span></dt>
<dd>{{replica.state}}</dd>
</dl>
</li>
<li>
<dl class="clearfix">
<dt><span>leader:</span></dt>
<dd class="ico" ng-class="replica.leader ?'ico-1' : 'ico-0'"><span></span></dd>
</dl>
</li>
</ul>
</li>
</ul>
</li>
<li ng-hide="shard.showAdd">
<span class="actions replica">
<button class="action" id="add-replica" ng-click="toggleAddReplica(shard)"><span>add replica</span></button>
</span>
</li>
</ul>
<div class="action add-replica" ng-show="shard.showAdd">
<form>
<p id="node-name" class="clearfix"><label for="node-name">Node:</label>
<select chosen ng-model="shard.replicaNodeName" ng-options="node for node in nodes" class="other">
<option value="">No specified node</option>
</select>
node: {{shard.replicaNodeName}}
</p>
<p class="clearfix note error" ng-show="createReplicaMessage">
<span>{{createReplicaMessage}}</span>
</p>
<p class="clearfix buttons">
<button class="submit delete" ng-class="{success: shard.replicaAdded}" ng-click="addReplica(shard)"><span>Create Replica</span></button>
<button type="reset" class="reset" ng-click="toggleAddReplica(shard)"><span>Cancel</span></button>
</p>
<p clas="clearfix">&nbsp;</p>
</form>
</div>
</li>
</ul>
</div>
</div>
</div>
</div>
<div id="navigation" class="requires-core clearfix">
<ul>
<li ng-repeat="c in collections" ng-class="{current: collection.name == c.name}"><a href="#~collections/{{c.name}}">{{c.name}}</a></li>
</ul>
</div>
</div>