lazy load client stats in UI

pull/1161/merge
Justin Richer 2017-03-16 17:20:04 -04:00
parent 626b18d5ca
commit 256b79ae51
7 changed files with 182 additions and 65 deletions

View File

@ -0,0 +1,42 @@
/*******************************************************************************
* Copyright 2017 The MITRE Corporation
* and the MIT Internet Trust Consortium
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package org.mitre.openid.connect.model;
/**
* @author jricher
*
*/
public class ClientStat {
private Integer approvedSiteCount;
/**
* @return the count
*/
public Integer getApprovedSiteCount() {
return approvedSiteCount;
}
/**
* @param count the count to set
*/
public void setApprovedSiteCount(Integer count) {
this.approvedSiteCount = count;
}
}

View File

@ -21,6 +21,8 @@ package org.mitre.openid.connect.service;
import java.util.Map;
import org.mitre.openid.connect.model.ClientStat;
/**
* @author jricher
*
@ -42,15 +44,15 @@ public interface StatsService {
*
* @return a map of id of client object to number of approvals
*/
public Map<Long, Integer> getByClientId();
public Map<String, Integer> getByClientId();
/**
* Calculate the usage count for a single client
*
* @param id the id of the client to search on
* @param clientId the id of the client to search on
* @return
*/
public Integer getCountForClientId(Long id);
public ClientStat getCountForClientId(String clientId);
/**
* Trigger the stats to be recalculated upon next update.

View File

@ -299,12 +299,6 @@ var BreadCrumbView = Backbone.View.extend({
});
// Stats table
var StatsModel = Backbone.Model.extend({
url: "api/stats/byclientid"
});
// User Profile
var UserProfileView = Backbone.View.extend({
@ -430,8 +424,6 @@ var AppRouter = Backbone.Router.extend({
initialize:function () {
this.clientStats = new StatsModel();
this.breadCrumbView = new BreadCrumbView({
collection:new Backbone.Collection()
});

View File

@ -194,6 +194,10 @@ var RegistrationTokenModel = Backbone.Model.extend({
urlRoot: 'api/tokens/registration'
});
var ClientStatsModel = Backbone.Model.extend({
urlRoot: 'api/stats/byclientid'
});
var ClientCollection = Backbone.Collection.extend({
initialize: function() {
@ -216,6 +220,8 @@ var ClientCollection = Backbone.Collection.extend({
var ClientView = Backbone.View.extend({
tagName: 'tr',
isRendered: false,
initialize:function (options) {
this.options = options;
@ -236,6 +242,10 @@ var ClientView = Backbone.View.extend({
this.registrationTokenTemplate = _.template($('#tmpl-client-registration-token').html());
}
if (!this.countTemplate) {
this.countTemplate = _.template($('#tmpl-client-count').html());
}
this.model.bind('change', this.render, this);
},
@ -259,7 +269,7 @@ var ClientView = Backbone.View.extend({
}
var json = {client: this.model.toJSON(), count: this.options.count, whiteList: this.options.whiteList,
var json = {client: this.model.toJSON(), whiteList: this.options.whiteList,
displayCreationDate: displayCreationDate, hoverCreationDate: hoverCreationDate};
this.$el.html(this.template(json));
@ -273,10 +283,19 @@ var ClientView = Backbone.View.extend({
this.$('.allow-introspection').tooltip({title: $.t('client.client-table.allow-introspection-tooltip')});
this.updateMatched();
this.updateStats();
$(this.el).i18n();
this.isRendered = true;
return this;
},
updateStats:function(eventName) {
$('.count', this.el).html(this.countTemplate({count: this.options.clientStat.get('approvedSiteCount')}));
},
showRegistrationToken:function(e) {
e.preventDefault();
@ -412,6 +431,8 @@ var ClientListView = Backbone.View.extend({
tagName: 'span',
stats: {},
initialize:function (options) {
this.options = options;
this.filteredModel = this.model;
@ -420,7 +441,6 @@ var ClientListView = Backbone.View.extend({
load:function(callback) {
if (this.model.isFetched &&
this.options.whiteListList.isFetched &&
this.options.stats.isFetched &&
this.options.systemScopeList.isFetched) {
callback();
return;
@ -430,13 +450,11 @@ var ClientListView = Backbone.View.extend({
$('#loading').html(
'<span class="label" id="loading-clients">' + $.t("common.clients") + '</span> ' +
'<span class="label" id="loading-whitelist">' + $.t("whitelist.whitelist") + '</span> ' +
'<span class="label" id="loading-scopes">' + $.t("common.scopes") + '</span> ' +
'<span class="label" id="loading-stats">' + $.t("common.statistics") + '</span> '
'<span class="label" id="loading-scopes">' + $.t("common.scopes") + '</span> '
);
$.when(this.model.fetchIfNeeded({success:function(e) {$('#loading-clients').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.whiteListList.fetchIfNeeded({success:function(e) {$('#loading-whitelist').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.stats.fetchIfNeeded({success:function(e) {$('#loading-stats').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.systemScopeList.fetchIfNeeded({success:function(e) {$('#loading-scopes').addClass('label-success');}, error:app.errorHandlerView.handleError()}))
.done(function() {
$('#loadingbox').sheet('hide');
@ -471,27 +489,44 @@ var ClientListView = Backbone.View.extend({
renderInner:function(eventName) {
// render the rows
// set up the rows to render
// (note that this doesn't render until visibility is determined in togglePlaceholder)
_.each(this.filteredModel.models, function (client, index) {
var clientStat = this.getStat(client.get('clientId'));
var view = new ClientView({
model:client,
count:this.options.stats.get(client.get('id')),
model:client,
clientStat:clientStat,
systemScopeList: this.options.systemScopeList,
whiteList: this.options.whiteListList.getByClientId(client.get('clientId'))
});
view.parentView = this;
var element = view.render().el;
//var element = view.render().el;
var element = view.el;
$("#client-table",this.el).append(element);
if (Math.ceil((index + 1) / 10) != 1) {
$(element).hide();
}
this.addView(client.get('id'), view);
}, this);
this.togglePlaceholder();
},
views:{},
addView:function(index, view) {
this.views[index] = view;
},
getView:function(index) {
return this.views[index];
},
getStat:function(index) {
if (!this.stats[index]) {
this.stats[index] = new ClientStatsModel({id: index});
}
return this.stats[index];
},
togglePlaceholder:function() {
// set up pagination
var numPages = Math.ceil(this.filteredModel.length / 10);
@ -508,6 +543,7 @@ var ClientListView = Backbone.View.extend({
}
if (this.filteredModel.length > 0) {
this.changePage(undefined, 1);
$('#client-table', this.el).show();
$('#client-table-empty', this.el).hide();
$('#client-table-search-empty', this.el).hide();
@ -527,14 +563,54 @@ var ClientListView = Backbone.View.extend({
},
changePage:function(event, num) {
console.log('Page changed: ' + num);
$('.paginator', this.el).bootpag({page:num});
var _self = this;
_.each(this.filteredModel.models, function (client, index) {
var view = _self.getView(client.get('id'));
if (!view) {
console.log('Error: no view for client ' + client.get('id'));
return;
}
// only show/render clients on the current page
console.log(':: ' + index + ' ' + num + ' ' + Math.ceil((index + 1) / 10) != num);
if (Math.ceil((index + 1) / 10) != num) {
$(view.el).hide();
} else {
if (!view.isRendered) {
view.render();
var clientStat = view.options.clientStat;
// load and display the stats
$.when(clientStat.fetchIfNeeded({
success:function(e) {
},
error:app.errorHandlerView.handleError()}))
.done(function(e) {
view.updateStats();
});
}
$(view.el).show();
}
});
/*
$('#client-table tbody tr', this.el).each(function(index, element) {
if (Math.ceil((index + 1) / 10) != num) {
// hide the element
$(element).hide();
} else {
// show the element
$(element).show();
}
});
*/
},
refreshTable:function(e) {
@ -543,14 +619,12 @@ var ClientListView = Backbone.View.extend({
$('#loading').html(
'<span class="label" id="loading-clients">' + $.t("common.clients") + '</span> ' +
'<span class="label" id="loading-whitelist">' + $.t("whitelist.whitelist") + '</span> ' +
'<span class="label" id="loading-scopes">' + $.t("common.scopes") + '</span> ' +
'<span class="label" id="loading-stats">' + $.t("common.statistics") + '</span> '
'<span class="label" id="loading-scopes">' + $.t("common.scopes") + '</span> '
);
var _self = this;
$.when(this.model.fetch({success:function(e) {$('#loading-clients').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.whiteListList.fetch({success:function(e) {$('#loading-whitelist').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.stats.fetch({success:function(e) {$('#loading-stats').addClass('label-success');}, error:app.errorHandlerView.handleError()}),
this.options.systemScopeList.fetch({success:function(e) {$('#loading-scopes').addClass('label-success');}, error:app.errorHandlerView.handleError()}))
.done(function() {
$('#loadingbox').sheet('hide');
@ -1210,8 +1284,7 @@ ui.routes.push({path: "admin/clients", name: "listClients", callback:
this.updateSidebar('admin/clients');
var view = new ClientListView({model:this.clientList, stats: this.clientStats, systemScopeList: this.systemScopeList, whiteListList: this.whiteListList});
var view = new ClientListView({model:this.clientList, systemScopeList: this.systemScopeList, whiteListList: this.whiteListList});
view.load(function() {
$('#content').html(view.render().el);
view.delegateEvents();

View File

@ -17,14 +17,8 @@
<!-- client -->
<script type="text/html" id="tmpl-client-table-item">
<td>
<% if (count == 0) { %>
<span class="label label-important">0</span>
<% } else if (count != null) { %>
<span class="label label-info"><%- count %></span>
<% } else { %>
<span class="label label-warning">?</span>
<% } %>
<td class="count">
</td>
<td>
@ -900,3 +894,13 @@
</div>
</script>
<script type="text/html" id="tmpl-client-count">
<% if (count == 0) { %>
<span class="label label-important">0</span>
<% } else if (count != null) { %>
<span class="label label-info"><%- count %></span>
<% } else { %>
<span class="label label-warning">?</span>
<% } %>
</script>

View File

@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
import org.mitre.oauth2.model.ClientDetailsEntity;
import org.mitre.oauth2.service.ClientDetailsEntityService;
import org.mitre.openid.connect.model.ApprovedSite;
import org.mitre.openid.connect.model.ClientStat;
import org.mitre.openid.connect.service.ApprovedSiteService;
import org.mitre.openid.connect.service.StatsService;
import org.springframework.beans.factory.annotation.Autowired;
@ -65,12 +66,12 @@ public class DefaultStatsService implements StatsService {
}, 10, TimeUnit.MINUTES);
}
private Supplier<Map<Long, Integer>> byClientIdCache = createByClientIdCache();
private Supplier<Map<String, Integer>> byClientIdCache = createByClientIdCache();
private Supplier<Map<Long, Integer>> createByClientIdCache() {
return Suppliers.memoizeWithExpiration(new Supplier<Map<Long, Integer>>() {
private Supplier<Map<String, Integer>> createByClientIdCache() {
return Suppliers.memoizeWithExpiration(new Supplier<Map<String, Integer>>() {
@Override
public Map<Long, Integer> get() {
public Map<String, Integer> get() {
return computeByClientId();
}
@ -107,11 +108,11 @@ public class DefaultStatsService implements StatsService {
* @see org.mitre.openid.connect.service.StatsService#calculateByClientId()
*/
@Override
public Map<Long, Integer> getByClientId() {
public Map<String, Integer> getByClientId() {
return byClientIdCache.get();
}
private Map<Long, Integer> computeByClientId() {
private Map<String, Integer> computeByClientId() {
// get all approved sites
Collection<ApprovedSite> allSites = approvedSiteService.getAll();
@ -120,10 +121,10 @@ public class DefaultStatsService implements StatsService {
clientIds.add(approvedSite.getClientId());
}
Map<Long, Integer> counts = getEmptyClientCountMap();
Map<String, Integer> counts = getEmptyClientCountMap();
for (String clientId : clientIds) {
ClientDetailsEntity client = clientService.loadClientByClientId(clientId);
counts.put(client.getId(), clientIds.count(clientId));
counts.put(client.getClientId(), clientIds.count(clientId));
}
return counts;
@ -133,22 +134,24 @@ public class DefaultStatsService implements StatsService {
* @see org.mitre.openid.connect.service.StatsService#countForClientId(java.lang.String)
*/
@Override
public Integer getCountForClientId(Long id) {
Map<Long, Integer> counts = getByClientId();
return counts.get(id);
public ClientStat getCountForClientId(String id) {
Map<String, Integer> counts = getByClientId();
ClientStat stat = new ClientStat();
stat.setApprovedSiteCount(counts.get(id));
return stat;
}
/**
* Create a new map of all client ids set to zero
* @return
*/
private Map<Long, Integer> getEmptyClientCountMap() {
Map<Long, Integer> counts = new HashMap<>();
private Map<String, Integer> getEmptyClientCountMap() {
Map<String, Integer> counts = new HashMap<>();
Collection<ClientDetailsEntity> clients = clientService.getAllClients();
for (ClientDetailsEntity client : clients) {
counts.put(client.getId(), 0);
counts.put(client.getClientId(), 0);
}
return counts;

View File

@ -18,6 +18,7 @@ package org.mitre.openid.connect.web;
import java.util.Map;
import org.mitre.openid.connect.model.ClientStat;
import org.mitre.openid.connect.service.StatsService;
import org.mitre.openid.connect.view.JsonEntityView;
import org.slf4j.Logger;
@ -53,20 +54,20 @@ public class StatsAPI {
}
@PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping(value = "byclientid", produces = MediaType.APPLICATION_JSON_VALUE)
public String statsByClient(ModelMap m) {
Map<Long, Integer> e = statsService.getByClientId();
m.put(JsonEntityView.ENTITY, e);
return JsonEntityView.VIEWNAME;
}
// @PreAuthorize("hasRole('ROLE_USER')")
// @RequestMapping(value = "byclientid", produces = MediaType.APPLICATION_JSON_VALUE)
// public String statsByClient(ModelMap m) {
// Map<Long, Integer> e = statsService.getByClientId();
//
// m.put(JsonEntityView.ENTITY, e);
//
// return JsonEntityView.VIEWNAME;
// }
//
@PreAuthorize("hasRole('ROLE_USER')")
@RequestMapping(value = "byclientid/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public String statsByClientId(@PathVariable("id") Long id, ModelMap m) {
Integer e = statsService.getCountForClientId(id);
public String statsByClientId(@PathVariable("id") String clientId, ModelMap m) {
ClientStat e = statsService.getCountForClientId(clientId);
m.put(JsonEntityView.ENTITY, e);