Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# See https://github.com/apache/solr/blob/main/dev-docs/changelog.adoc
title: Show collection and shard health (GREEN/YELLOW/ORANGE/RED, from SOLR-15300) as a colored dot in the Admin UI's Collections screen.
type: added
authors:
- name: Eric Pugh
links:
- name: SOLR-15395
url: https://issues.apache.org/jira/browse/SOLR-15395
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,26 @@ public void testCollectionDetailDisplay() {
assertNoSevereConsoleErrors();
}

@Test
public void testHealthStatusDisplay() {
openPage("~collections/" + COLLECTION, By.id("collections"));
waitForPageContains("shard1");

Map<String, By> healthDots =
Map.of(
"collection header", By.cssSelector("#collection-data h2 .health-dot"),
"shard header", By.cssSelector("#shard-data h2 .health-dot"));
healthDots.forEach(
(label, locator) -> {
WebElement dot = waitFor(locator);
assertTrue(
label + " health dot should be GREEN: " + dot.getAttribute("class"),
dot.getAttribute("class").contains("health-GREEN"));
});

assertNoSevereConsoleErrors();
}

@Test
public void testCreateAndDeleteCollectionViaUi() throws Exception {
String name = "uicreated";
Expand Down
43 changes: 43 additions & 0 deletions solr/webapp/web/css/angular/collections.css
Original file line number Diff line number Diff line change
Expand Up @@ -395,3 +395,46 @@ button.disabled
/* styles when the element is disabled */
color: #c0c0c0;
}

.health-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin: 0 6px;
vertical-align: middle;
}

#content #collections #data #collection-data h2
{
display: flex;
align-items: center;
}

#content #collections #data #shard-data h2 .shard-name
{
display: inline-flex;
align-items: center;
}

#content #collections #data #collection-data h2 .health-dot,
#content #collections #data #shard-data h2 .shard-name .health-dot
{
flex: 0 0 auto;
}

.health-dot.health-GREEN {
background-color: #57A957;
}

.health-dot.health-YELLOW {
background-color: #d5dd00;
}

.health-dot.health-ORANGE {
background-color: #c48f00;
}

.health-dot.health-RED {
background-color: #C43C35;
}
137 changes: 99 additions & 38 deletions solr/webapp/web/js/angular/controllers/collections.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,80 @@ solrAdminApp.controller('CollectionsController',
});
});

CollectionsV2.listCollections({detailed: true}, function (error, data, response) {
// Just names for the sidebar/alias-picker - the detailed per-collection data
// (shards, replicas, health) for whichever one is selected is fetched separately
// below, narrowed to that one collection, instead of fetching detail for all of them.
CollectionsV2.listCollections({}, function (error, data, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
$scope.collections = [];
for (var name in data.collectionsDetail) {
for (var i in data.collections) {
var name = data.collections[i];
if (name.startsWith("._designer_")) {
continue;
}
var collection = data.collectionsDetail[name];
$scope.collections.push({name: name});
}
});
});

$scope.refreshSelectedCollection();

// Fetch aliases using getAliases to get properties
AliasesV2.getAliases(function (error, adata, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
// TODO: Population of aliases array duplicated in app.js
$scope.aliases = [];
for (var key in adata.aliases) {
var props = {};
if (key in adata.properties) {
props = adata.properties[key];
}
var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props};
$scope.aliases.push(alias);
if ($routeParams.collection == 'alias_' + key) {
$scope.collection = alias;
}
}
if ($routeParams.collection && $routeParams.collection.indexOf('alias_') === 0 && !$scope.collection) {
alert("No collection or alias called " + $routeParams.collection);
$location.path("/~collections");
}
});
});

ConfigSetsV2.listConfigSet(function(error, data, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
$scope.configs = [];
var items = data.configSets;
for (var i in items) {
$scope.configs.push({name: items[i]});
}
});
});

$timeout.cancel($scope.timeout);
$scope.timeout = $timeout($scope.refreshHealth, 10000);
};

// Fetches shard/replica detail for the single collection named by the route,
// narrowed server-side via the "collection" param instead of fetching every collection.
$scope.refreshSelectedCollection = function() {
var name = $routeParams.collection;
if (!name || name.indexOf('alias_') === 0) {
return;
}
CollectionsV2.listCollections({detailed: true, collection: name}, function(error, data, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
var collection = data.collectionsDetail && data.collectionsDetail[name];
if (!collection) {
alert("No collection or alias called " + name);
$location.path("/~collections");
return;
}
collection.name = name;
collection.type = 'collection';
var shards = collection.shards;
Expand All @@ -58,49 +123,44 @@ solrAdminApp.controller('CollectionsController',
}
collection.shards.push(shard);
}
$scope.collections.push(collection);
if ($routeParams.collection == name) {
$scope.collection = collection;
}
}
// Fetch aliases using getAliases to get properties
AliasesV2.getAliases(function (error, adata, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
// TODO: Population of aliases array duplicated in app.js
$scope.aliases = [];
for (var key in adata.aliases) {
var props = {};
if (key in adata.properties) {
props = adata.properties[key];
}
var alias = {name: key, collections: adata.aliases[key], type: 'alias', properties: props};
$scope.aliases.push(alias);
if ($routeParams.collection == 'alias_' + key) {
$scope.collection = alias;
}
}
// Decide what is selected in list
if ($routeParams.collection && !$scope.collection) {
alert("No collection or alias called " + $routeParams.collection);
$location.path("/~collections");
}
});
$scope.collection = collection;
});
});
});
ConfigSetsV2.listConfigSet(function(error, data, response) {
};

// Patches health onto the selected collection in place, so it never disturbs expanded/open UI state.
$scope.refreshHealth = function() {
var name = $scope.collection && $scope.collection.type === 'collection' && $scope.collection.name;
if (!name) {
$scope.timeout = $timeout($scope.refreshHealth, 10000);
return;
}
CollectionsV2.listCollections({detailed: true, collection: name}, function(error, data, response) {
$timeout(function() {
if (error) { ApiErrorHandler.handle(response); return; }
$scope.configs = [];
var items = data.configSets;
for (var i in items) {
$scope.configs.push({name: items[i]});
if (error) {
ApiErrorHandler.handle(response);
} else if ($scope.collection && $scope.collection.name === name) {
var freshCollection = data.collectionsDetail && data.collectionsDetail[name];
if (freshCollection) {
$scope.collection.health = freshCollection.health;
for (var shardName in freshCollection.shards) {
var existingShard = $scope.collection.shards.find(function(s) { return s.name === shardName; });
if (existingShard) {
existingShard.health = freshCollection.shards[shardName].health;
}
}
}
}
$scope.timeout = $timeout($scope.refreshHealth, 10000);
});
});
};

var onRouteChangeOff = $scope.$on('$routeChangeStart', function() {
$timeout.cancel($scope.timeout);
onRouteChangeOff();
});

$scope.hideAll = function() {
$scope.showRename = false;
$scope.showAdd = false;
Expand Down Expand Up @@ -242,6 +302,7 @@ solrAdminApp.controller('CollectionsController',
ApiErrorHandler.handle(response);
} else {
$scope.reloadSuccess = true;
$scope.refreshHealth();
}
});
});
Expand Down
4 changes: 2 additions & 2 deletions solr/webapp/web/partials/collections.html
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ <h2>Alias: {{collection.name}}</h2>

<div class="block" id="collection-data" ng-show="collection.type === 'collection'">

<h2>Collection: {{collection.name}}</h2>
<h2>Collection: {{collection.name}} <span class="health-dot" ng-if="collection.health" ng-class="'health-' + collection.health" title="Health: {{collection.health}}"></span></h2>

<div class="message-container">
<div class="message"></div>
Expand Down Expand Up @@ -253,7 +253,7 @@ <h2>Collection: {{collection.name}}</h2>
<div class="content shard" ng-repeat="shard in collection.shards">
<a ng-click="toggleShard(shard)">
<h2>
<span ng-class="{open:shard.show}"><b>Shard: {{shard.name}}</b></span>
<span class="shard-name" ng-class="{open:shard.show}"><b>Shard: {{shard.name}}</b> <span class="health-dot" ng-if="shard.health" ng-class="'health-' + shard.health" title="Health: {{shard.health}}"></span></span>
<div style="float:right" ng-show="!shard.range || shard.state == 'inactive'"><a ng-click="toggleRemoveShard(shard)"><span class="rem"></span></a></div>
</h2>
</a>
Expand Down
Loading