Split tests page into list and detail page
This commit splits the tests page into a list page and its detail page for improving the performance of showing the page. Previously, we rendered numerous tests list in one tests page. So it causes the performance issue. Splitting the page reduces the number of showing test lists. This is not perfect solution but it's better than before at least. Change-Id: If1a97740d09359ae17d0f0b7df8d36256ac99f47 Closes-Bug: #1548354
This commit is contained in:
parent
972e247b32
commit
8a0ab1d425
|
@ -0,0 +1,82 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
var controllersModule = require('./_index');
|
||||||
|
var _ = require('underscore');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @ngInject
|
||||||
|
*/
|
||||||
|
function TestsDetailController($scope, healthService, testService, key, $location) {
|
||||||
|
|
||||||
|
// ViewModel
|
||||||
|
var vm = this;
|
||||||
|
vm.searchTest = '';
|
||||||
|
vm.key = decodeURIComponent(key);
|
||||||
|
|
||||||
|
vm.processData = function(data) {
|
||||||
|
vm.chartData = {};
|
||||||
|
|
||||||
|
var testsByHierarchy = _.groupBy(data.tests, function(test) {
|
||||||
|
var testId = testService.removeIdNoise(test.test_id);
|
||||||
|
var keyMatcher = /^(\w*)\./g;
|
||||||
|
var matches = keyMatcher.exec(testId);
|
||||||
|
|
||||||
|
if (matches) {
|
||||||
|
return matches[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Others';
|
||||||
|
});
|
||||||
|
|
||||||
|
var getTestFailureAvg = function(test) {
|
||||||
|
return test.failure / test.run_count;
|
||||||
|
};
|
||||||
|
|
||||||
|
_.each(testsByHierarchy, function(tests, hierarchy, list) {
|
||||||
|
if (!vm.chartData[hierarchy]) {
|
||||||
|
vm.chartData[hierarchy] = [{
|
||||||
|
key: hierarchy,
|
||||||
|
values: [],
|
||||||
|
tests: []
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedTests = _.sortBy(tests, function(test) {
|
||||||
|
return getTestFailureAvg(test) * -1;
|
||||||
|
});
|
||||||
|
|
||||||
|
var topFailures = _.first(orderedTests, 10);
|
||||||
|
|
||||||
|
topFailures.forEach(function(test) {
|
||||||
|
var failureAverage = getTestFailureAvg(test);
|
||||||
|
if (!isNaN(failureAverage) && parseFloat(failureAverage) > 0.01) {
|
||||||
|
var chartData = {
|
||||||
|
label: test.test_id,
|
||||||
|
value: failureAverage
|
||||||
|
};
|
||||||
|
vm.chartData[hierarchy][0].values.push(chartData);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
orderedTests.forEach(function(test) {
|
||||||
|
test.failureAverage = getTestFailureAvg(test);
|
||||||
|
vm.chartData[hierarchy][0].tests.push(test);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.loadData = function() {
|
||||||
|
healthService.getTests().then(function(response) {
|
||||||
|
vm.processData(response.data);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.searchTest = $location.search().searchTest || '';
|
||||||
|
|
||||||
|
vm.loadData();
|
||||||
|
|
||||||
|
vm.onSearchChange = function() {
|
||||||
|
$location.search('searchTest', $scope.testsDetail.searchTest);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
controllersModule.controller('TestsDetailController', TestsDetailController);
|
|
@ -27,40 +27,15 @@ function TestsController($scope, healthService, testService, $location) {
|
||||||
return 'Others';
|
return 'Others';
|
||||||
});
|
});
|
||||||
|
|
||||||
var getTestFailureAvg = function(test) {
|
var sortedKeys = _.sortBy(_.keys(testsByHierarchy));
|
||||||
return test.failure / test.run_count;
|
_.each(sortedKeys, function(key) {
|
||||||
};
|
if (!vm.chartData[key]) {
|
||||||
|
vm.chartData[key] = [{
|
||||||
_.each(testsByHierarchy, function(tests, hierarchy, list) {
|
key: key,
|
||||||
if (!vm.chartData[hierarchy]) {
|
|
||||||
vm.chartData[hierarchy] = [{
|
|
||||||
key: hierarchy,
|
|
||||||
values: [],
|
values: [],
|
||||||
tests: []
|
tests: []
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|
||||||
var orderedTests = _.sortBy(tests, function(test) {
|
|
||||||
return getTestFailureAvg(test) * -1;
|
|
||||||
});
|
|
||||||
|
|
||||||
var topFailures = _.first(orderedTests, 10);
|
|
||||||
|
|
||||||
topFailures.forEach(function(test) {
|
|
||||||
var failureAverage = getTestFailureAvg(test);
|
|
||||||
if (!isNaN(failureAverage) && parseFloat(failureAverage) > 0.01) {
|
|
||||||
var chartData = {
|
|
||||||
label: test.test_id,
|
|
||||||
value: failureAverage
|
|
||||||
};
|
|
||||||
vm.chartData[hierarchy][0].values.push(chartData);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
orderedTests.forEach(function(test) {
|
|
||||||
test.failureAverage = getTestFailureAvg(test);
|
|
||||||
vm.chartData[hierarchy][0].tests.push(test);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -31,6 +31,17 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) {
|
||||||
templateUrl: 'tests.html',
|
templateUrl: 'tests.html',
|
||||||
title: 'Tests'
|
title: 'Tests'
|
||||||
})
|
})
|
||||||
|
.state('testsDetail', {
|
||||||
|
url: '/tests/:key',
|
||||||
|
controller: 'TestsDetailController as testsDetail',
|
||||||
|
templateUrl: 'tests-detail.html',
|
||||||
|
title: 'Tests Detail',
|
||||||
|
resolve: /*@ngInject*/ {
|
||||||
|
'key': function($stateParams) {
|
||||||
|
return $stateParams.key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.state('job', {
|
.state('job', {
|
||||||
url: '/job/:jobName',
|
url: '/job/:jobName',
|
||||||
controller: 'JobController as job',
|
controller: 'JobController as job',
|
||||||
|
|
|
@ -100,6 +100,7 @@ function HealthService($http, config) {
|
||||||
service.getTests = function() {
|
service.getTests = function() {
|
||||||
return config.get().then(function(config) {
|
return config.get().then(function(config) {
|
||||||
return $http.jsonp(config.apiRoot + '/tests', {
|
return $http.jsonp(config.apiRoot + '/tests', {
|
||||||
|
cache: true,
|
||||||
params: { callback: 'JSON_CALLBACK' }
|
params: { callback: 'JSON_CALLBACK' }
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
<header class="bs-header">
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="page-header">Tests Detail</h1>
|
||||||
|
<crumb-menu></crumb-menu>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<loading-indicator></loading-indicator>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<accordion close-others="false">
|
||||||
|
<div class="panel panel-body panel-default">
|
||||||
|
<accordion-group heading="Details for {{ testsDetail.key }}" is-open="true">
|
||||||
|
<div>
|
||||||
|
<form>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="input-group">
|
||||||
|
<div class="input-group-addon"><i class="fa fa-search"></i></div>
|
||||||
|
<input type="text" class="form-control"
|
||||||
|
placeholder="Search for test"
|
||||||
|
ng-model="testsDetail.searchTest"
|
||||||
|
ng-model-options="{debounce: 250}"
|
||||||
|
ng-change="testsDetail.onSearchChange()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<table table-sort data="testsDetail.chartData[testsDetail.key][0]['tests']" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th sort-field="test_id" class="text-left">
|
||||||
|
Test ID
|
||||||
|
</th>
|
||||||
|
<th sort-field="success" class="text-right" style="min-width:80px">
|
||||||
|
Passed
|
||||||
|
</th>
|
||||||
|
<th sort-field="failure" class="text-right" style="min-width:80px">
|
||||||
|
Failed
|
||||||
|
</th>
|
||||||
|
<th sort-default='reversed' sort-field="failureAverage" class="text-right" style="min-width:95px">
|
||||||
|
Failure %
|
||||||
|
</th>
|
||||||
|
<th sort-field="run_time" class="text-right" style="min-width:80px">
|
||||||
|
Avg. Runtime (secs.)
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr table-ref="table" ng-repeat="test in table.dataSorted | filter:testsDetail.searchTest">
|
||||||
|
<td class="text-left">
|
||||||
|
<a ui-sref="test({ testId: test.test_id })"> {{test.test_id | limitTo: 110}}</a>
|
||||||
|
</td>
|
||||||
|
<td class="text-right">{{ test.success | number }}</td>
|
||||||
|
<td class="text-right">{{ test.failure | number }}</td>
|
||||||
|
<td class="text-right">{{ test.failureAverage * 100 | number: 2 }}%</td>
|
||||||
|
<td class="text-right">{{ test.run_time | number: 2 }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</accordion-group>
|
||||||
|
</div>
|
||||||
|
</accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
|
@ -13,58 +13,18 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-12">
|
<div class="col-lg-12">
|
||||||
<accordion close-others="false">
|
<div class="panel panel-default">
|
||||||
<div class="panel panel-body panel-default" ng-repeat="(key, value) in tests.chartData">
|
<div class="panel-heading">
|
||||||
<accordion-group heading="Details for {{ key }}" is-open="true">
|
<h3 class="panel-title">Details list</h3>
|
||||||
<div>
|
</div>
|
||||||
<form>
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
<div class="list-group">
|
||||||
<div class="input-group">
|
<a ng-repeat="(key, value) in tests.chartData"
|
||||||
<div class="input-group-addon"><i class="fa fa-search"></i></div>
|
ui-sref="testsDetail({ key: key })"
|
||||||
<input type="text" class="form-control"
|
class="list-group-item">{{ key }}</a>
|
||||||
placeholder="Search for test"
|
|
||||||
ng-model="tests.searchTest"
|
|
||||||
ng-model-options="{debounce: 250}"
|
|
||||||
ng-change="tests.onSearchChange()">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
<table table-sort data="tests.chartData[key][0]['tests']" class="table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th sort-field="test_id" class="text-left">
|
|
||||||
Test ID
|
|
||||||
</th>
|
|
||||||
<th sort-field="success" class="text-right" style="min-width:80px">
|
|
||||||
Passed
|
|
||||||
</th>
|
|
||||||
<th sort-field="failure" class="text-right" style="min-width:80px">
|
|
||||||
Failed
|
|
||||||
</th>
|
|
||||||
<th sort-default='reversed' sort-field="failureAverage" class="text-right" style="min-width:95px">
|
|
||||||
Failure %
|
|
||||||
</th>
|
|
||||||
<th sort-field="run_time" class="text-right" style="min-width:80px">
|
|
||||||
Avg. Runtime (secs.)
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr table-ref="table" ng-repeat="test in table.dataSorted | filter:tests.searchTest">
|
|
||||||
<td class="text-left">
|
|
||||||
<a ui-sref="test({ testId: test.test_id })"> {{test.test_id | limitTo: 110}}</a>
|
|
||||||
</td>
|
|
||||||
<td class="text-right">{{ test.success | number }}</td>
|
|
||||||
<td class="text-right">{{ test.failure | number }}</td>
|
|
||||||
<td class="text-right">{{ test.failureAverage * 100 | number: 2 }}%</td>
|
|
||||||
<td class="text-right">{{ test.run_time | number: 2 }}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</accordion-group>
|
|
||||||
</div>
|
|
||||||
</accordion>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,107 @@
|
||||||
|
describe('TestsDetailController', function() {
|
||||||
|
beforeEach(function() {
|
||||||
|
module('app');
|
||||||
|
module('app.controllers');
|
||||||
|
});
|
||||||
|
|
||||||
|
var $scope, $httpBackend, $controller, healthService;
|
||||||
|
var API_ROOT = 'http://8.8.4.4:8080';
|
||||||
|
var DEFAULT_START_DATE = new Date();
|
||||||
|
|
||||||
|
beforeEach(inject(function($rootScope, _$httpBackend_, _$controller_, _healthService_) {
|
||||||
|
$httpBackend = _$httpBackend_;
|
||||||
|
|
||||||
|
mockConfigService();
|
||||||
|
mockHealthService();
|
||||||
|
|
||||||
|
$scope = $rootScope.$new();
|
||||||
|
$controller = _$controller_;
|
||||||
|
healthService = _healthService_;
|
||||||
|
}));
|
||||||
|
|
||||||
|
function mockHealthService() {
|
||||||
|
var expectedResponse = {
|
||||||
|
tests: [
|
||||||
|
{
|
||||||
|
failure: 5592,
|
||||||
|
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
||||||
|
run_count: 55920,
|
||||||
|
run_time: 0.608151,
|
||||||
|
success: 55920,
|
||||||
|
test_id: 'tempest.api.identity.admin.v2.test_users.one'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failure: 0,
|
||||||
|
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
||||||
|
run_count: 4939,
|
||||||
|
run_time: 5.97596,
|
||||||
|
success: 4939,
|
||||||
|
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
failure: 1,
|
||||||
|
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
||||||
|
run_count: 32292,
|
||||||
|
run_time: 1.18864,
|
||||||
|
success: 32291,
|
||||||
|
test_id: 'tempest.api.network.test_routers.three'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
var endpoint = API_ROOT + '/tests?callback=JSON_CALLBACK';
|
||||||
|
$httpBackend.expectJSONP(endpoint)
|
||||||
|
.respond(200, expectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockConfigService() {
|
||||||
|
var expectedResponse = { apiRoot: API_ROOT };
|
||||||
|
var endpoint = 'config.json';
|
||||||
|
$httpBackend.expectGET(endpoint).respond(200, expectedResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should process chart data correctly', function() {
|
||||||
|
var testsDetailController = $controller('TestsDetailController', {
|
||||||
|
healthService: healthService,
|
||||||
|
$scope: $scope,
|
||||||
|
key: 'tempest'
|
||||||
|
});
|
||||||
|
$httpBackend.flush();
|
||||||
|
|
||||||
|
var expectedChartData = {
|
||||||
|
'tempest': [{
|
||||||
|
key: 'tempest',
|
||||||
|
values: [{
|
||||||
|
label: 'tempest.api.identity.admin.v2.test_users.one',
|
||||||
|
value: 0.1
|
||||||
|
}],
|
||||||
|
tests: [{
|
||||||
|
failure: 5592,
|
||||||
|
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
||||||
|
run_count: 55920,
|
||||||
|
run_time: 0.608151,
|
||||||
|
success: 55920,
|
||||||
|
test_id: 'tempest.api.identity.admin.v2.test_users.one',
|
||||||
|
failureAverage: 0.1
|
||||||
|
}, {
|
||||||
|
failure: 1,
|
||||||
|
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
||||||
|
run_count: 32292,
|
||||||
|
run_time: 1.18864,
|
||||||
|
success: 32291,
|
||||||
|
test_id: 'tempest.api.network.test_routers.three',
|
||||||
|
failureAverage: 0.0000309674222717701
|
||||||
|
}, {
|
||||||
|
failure: 0,
|
||||||
|
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
||||||
|
run_count: 4939,
|
||||||
|
run_time: 5.97596,
|
||||||
|
success: 4939,
|
||||||
|
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two',
|
||||||
|
failureAverage: 0
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
expect(testsDetailController.chartData).toEqual(expectedChartData);
|
||||||
|
});
|
||||||
|
});
|
|
@ -70,35 +70,8 @@ describe('TestsController', function() {
|
||||||
var expectedChartData = {
|
var expectedChartData = {
|
||||||
'tempest': [{
|
'tempest': [{
|
||||||
key: 'tempest',
|
key: 'tempest',
|
||||||
values: [{
|
values: [],
|
||||||
label: 'tempest.api.identity.admin.v2.test_users.one',
|
tests: []
|
||||||
value: 0.1
|
|
||||||
}],
|
|
||||||
tests: [{
|
|
||||||
failure: 5592,
|
|
||||||
id: '00187173-ab23-4181-9a15-e291a0d8e2d1',
|
|
||||||
run_count: 55920,
|
|
||||||
run_time: 0.608151,
|
|
||||||
success: 55920,
|
|
||||||
test_id: 'tempest.api.identity.admin.v2.test_users.one',
|
|
||||||
failureAverage: 0.1
|
|
||||||
}, {
|
|
||||||
failure: 1,
|
|
||||||
id: '002a15e0-f6d1-472a-bd66-bb13ac4d77aa',
|
|
||||||
run_count: 32292,
|
|
||||||
run_time: 1.18864,
|
|
||||||
success: 32291,
|
|
||||||
test_id: 'tempest.api.network.test_routers.three',
|
|
||||||
failureAverage: 0.0000309674222717701
|
|
||||||
}, {
|
|
||||||
failure: 0,
|
|
||||||
id: '001c6860-c966-4c0b-9928-ecccd162bed0',
|
|
||||||
run_count: 4939,
|
|
||||||
run_time: 5.97596,
|
|
||||||
success: 4939,
|
|
||||||
test_id: 'tempest.api.volume.admin.test_snapshots_actions.two',
|
|
||||||
failureAverage: 0
|
|
||||||
}]
|
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
expect(testsController.chartData).toEqual(expectedChartData);
|
expect(testsController.chartData).toEqual(expectedChartData);
|
||||||
|
|
Loading…
Reference in New Issue