diff --git a/app/js/controllers/home.js b/app/js/controllers/home.js index d709c00..ccbc98e 100644 --- a/app/js/controllers/home.js +++ b/app/js/controllers/home.js @@ -9,10 +9,14 @@ function HomeCtrl($scope, $state, datasetService) { // ViewModel var vm = this; - vm.focus = $state.params.datasetId; + vm.focus = $state.params.artifactName; - datasetService.list().then(function(response) { - vm.tempest = response.data.tempest; + datasetService.groups().then(function(groups) { + vm.groups = groups; + + if (!vm.focus) { + vm.focus = groups[0]; + } }); // update the page url as the focus id changes, but don't reload @@ -20,7 +24,7 @@ function HomeCtrl($scope, $state, datasetService) { return vm.focus; }, function(value, old) { if (value !== old) { - $state.go('home', { datasetId: value }, { notify: false }); + $state.go('home', { artifactName: value }, { notify: false }); } }); diff --git a/app/js/controllers/test-details.js b/app/js/controllers/test-details.js index dc5736a..c67336b 100644 --- a/app/js/controllers/test-details.js +++ b/app/js/controllers/test-details.js @@ -2,10 +2,6 @@ var controllersModule = require('./_index'); -/** - * @ngInject - */ -var TestDetailsCtrl = /** * Responsible for making three calls to the dataset service. First, the * dataset corresponding to the given int id is loaded, then the raw and details @@ -13,78 +9,84 @@ var TestDetailsCtrl = * of the details JSON is kept in `originalDetails` so that information is not * lost when parsing. Progress of the dataset service calls is recorded and * displayed in a progress bar on `test-details.html`. -*/ -function($scope, $location, $stateParams, $log, datasetService, progressService) { + * @ngInject + */ +function TestDetailsCtrl( + $scope, $location, $stateParams, $log, $q, + datasetService, progressService) { var vm = this; - vm.datasetId = $stateParams.datasetId; - var testName = $stateParams.test; - vm.testName = testName; + vm.artifactName = $stateParams.artifactName; + vm.testName = $stateParams.test; progressService.start({ parent: 'div[role="main"] .panel-body' }); // load dataset, raw json, and details json - datasetService.get($stateParams.datasetId) - .then(function(response) { - vm.dataset = response; - vm.stats = response.stats; - return datasetService.raw(response); - }) - .then(function(raw) { - var item = null; - for (var t in raw.data) { - if (raw.data[t].name === testName) { - item = raw.data[t]; - } - } - vm.item = item; - progressService.inc(); - return datasetService.details(vm.dataset); - }) - .then(function(deets) { - vm.details = deets; - vm.originalDetails = angular.copy(deets.data[testName]); - vm.itemDetails = deets.data[testName]; - progressService.done(); - }) - .catch(function(error) { - $log.error(error); - progressService.done(); - }); + var statsArtifact = datasetService.artifact(vm.artifactName, 'subunit-stats'); + var subunitArtifact = datasetService.artifact(vm.artifactName, 'subunit'); + var detailsArtifact = datasetService.artifact(vm.artifactName, 'subunit-details'); + + var statsPromise = statsArtifact.then(function(response) { + vm.stats = response.data; + }); + + var subunitPromise = subunitArtifact.then(function(response) { + var item = null; + for (var t in response.data) { + if (response.data[t].name === vm.testName) { + item = response.data[t]; + } + } + vm.item = item; + progressService.inc(); + }); + + var detailsPromise = detailsArtifact.then(function(details) { + vm.details = details; + vm.originalDetails = angular.copy(details.data[vm.testName]); + vm.itemDetails = details.data[vm.testName]; + }).catch(function(ex) { + // ignore errors, details won't exist for deployer + }); + + $q.all([statsPromise, subunitPromise, detailsPromise]).catch(function(ex) { + $log.error(ex); + }).finally(function() { + progressService.done(); + }); - vm.parsePythonLogging = /** * This function changes the `itemDetails.pythonlogging` variable to only * show lines with the log levels specified by the four boolean parameters. - * EX: If the `showINFO` parameter is set to true, `itemDetails.pythonlogging` + * EX: If the `info` parameter is set to true, `itemDetails.pythonlogging` * will display lines that contain the text `INFO`. - * @param {boolean} showINFO - * @param {boolean} showDEBUG - * @param {boolean} showWARNING - * @param {boolean} showERROR + * @param {boolean} info + * @param {boolean} debug + * @param {boolean} warning + * @param {boolean} error */ - function(showINFO, showDEBUG, showWARNING, showERROR) { + vm.parsePythonLogging = function(info, debug, warning, error) { if (vm.originalDetails && vm.originalDetails.pythonlogging) { var log = vm.originalDetails.pythonlogging; var ret = []; var lines = log.split('\n'); for (var i in lines) { var line = lines[i]; - if (showINFO && line.includes("INFO")) { + if (info && line.includes("INFO")) { ret.push(line); } - if (showDEBUG && line.includes("DEBUG")) { + if (debug && line.includes("DEBUG")) { ret.push(line); } - if (showWARNING && line.includes("WARNING")) { + if (warning && line.includes("WARNING")) { ret.push(line); } - if (showERROR && line.includes("ERROR")) { + if (error && line.includes("ERROR")) { ret.push(line); } } vm.itemDetails.pythonlogging = ret.join('\n'); } }; +} -}; controllersModule.controller('TestDetailsController', TestDetailsCtrl); diff --git a/app/js/controllers/timeline.js b/app/js/controllers/timeline.js index 7f0c7c8..04f0c6b 100644 --- a/app/js/controllers/timeline.js +++ b/app/js/controllers/timeline.js @@ -9,12 +9,7 @@ function TimelineCtrl($scope, $location, $stateParams, datasetService) { // ViewModel var vm = this; - - datasetService.get($stateParams.datasetId).then(function(dataset) { - vm.dataset = dataset; - }, function(reason) { - vm.error = "Unable to load dataset: " + reason; - }); + vm.artifactName = $stateParams.artifactName; vm.hoveredItem = null; vm.selectedItem = null; diff --git a/app/js/directives/tempest-summary.js b/app/js/directives/tempest-summary.js index 4eac7db..ceddf41 100644 --- a/app/js/directives/tempest-summary.js +++ b/app/js/directives/tempest-summary.js @@ -7,27 +7,27 @@ var directivesModule = require('./_index.js'); */ function tempestSummary() { - /** - * @ngInject - */ - var controller = /** * Responsible for getting the basic run summary stats via the dataset service. * Also calculates the duration of the run - `timeDiff` - by subtracting the * run's start and end timestamps. + * @ngInject */ - function($scope, $attrs, datasetService) { - $scope.$watch('dataset', function(dataset) { - var stats = dataset.stats; - $scope.stats = stats; - $scope.timeDiff = (new Date(stats.end) - new Date(stats.start)) / 1000; + var controller = function($scope, $attrs, datasetService) { + $scope.$watch('artifactName', function(artifactName) { + datasetService.artifact(artifactName, 'subunit-stats').then(function(response) { + var stats = response.data; + $scope.stats = stats; + $scope.timeDiff = (new Date(stats.end) - new Date(stats.start)) / 1000; + }); }); }; return { restrict: 'EA', scope: { - 'dataset': '=' + 'index': '=', + 'artifactName': '=' }, controller: controller, templateUrl: 'directives/tempest-summary.html' diff --git a/app/js/directives/timeline-details.js b/app/js/directives/timeline-details.js index ddd71d5..37f2e4a 100644 --- a/app/js/directives/timeline-details.js +++ b/app/js/directives/timeline-details.js @@ -16,7 +16,7 @@ function timelineDetails() { return { restrict: 'EA', scope: { - 'dataset': '=', + 'artifactName': '=', 'item': '=' }, controller: controller, diff --git a/app/js/directives/timeline-dstat.js b/app/js/directives/timeline-dstat.js index 747a28b..9ab3907 100644 --- a/app/js/directives/timeline-dstat.js +++ b/app/js/directives/timeline-dstat.js @@ -7,7 +7,7 @@ var parseDstat = require('../util/dstat-parse'); var d3 = require('d3'); var getDstatLanes = function(data, mins, maxes) { - if (!data) { + if (!data || !data.length) { return []; } @@ -245,6 +245,10 @@ function timelineDstat($document, $window) { var bottom = y(laneIndex) + laneHeight; for (var pathIndex = 0; pathIndex < laneDef.length; pathIndex++) { + if (!region.data.length) { + continue; + } + var pathDef = laneDef[pathIndex]; var line = pathDef.type === 'line'; diff --git a/app/js/directives/timeline.js b/app/js/directives/timeline.js index 458cd41..596fdcf 100644 --- a/app/js/directives/timeline.js +++ b/app/js/directives/timeline.js @@ -378,6 +378,9 @@ function timeline($window, $log, datasetService, progressService) { var accessor = function(d) { return d.system_time; }; var minIndex = arrayUtil.binaryMinIndex(min, raw.entries, accessor); var maxIndex = arrayUtil.binaryMaxIndex(max, raw.entries, accessor); + if (minIndex < 0) { + minIndex = 0; + } self.dstat = { entries: raw.entries.slice(minIndex, maxIndex), @@ -388,8 +391,8 @@ function timeline($window, $log, datasetService, progressService) { $scope.$broadcast('dstatLoaded', self.dstat); }; - $scope.$watch('dataset', function(dataset) { - if (!dataset) { + $scope.$watch('artifactName', function(artifactName) { + if (!artifactName) { return; } @@ -398,11 +401,11 @@ function timeline($window, $log, datasetService, progressService) { // load dataset details (raw log entries and dstat) sequentially // we need to determine the initial date from the subunit data to parse // dstat - datasetService.raw(dataset).then(function(response) { + datasetService.artifact(artifactName, 'subunit').then(function(response) { progressService.set(0.33); initData(response.data); - return datasetService.dstat(dataset); + return datasetService.artifact('dstat'); }).then(function(response) { progressService.set(0.66); var firstDate = new Date(self.dataRaw[0].timestamps[0]); @@ -462,7 +465,7 @@ function timeline($window, $log, datasetService, progressService) { transclude: true, templateUrl: 'directives/timeline.html', scope: { - 'dataset': '=', + 'artifactName': '=', 'hoveredItem': '=', 'selectedItem': '=', 'preselect': '=' diff --git a/app/js/on_config.js b/app/js/on_config.js index 2f9624a..28405a1 100644 --- a/app/js/on_config.js +++ b/app/js/on_config.js @@ -6,15 +6,15 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) { $stateProvider.state('home', { - url: '/{datasetId:int}', - params: { datasetId: 0 }, + url: '/{artifactName}', + params: { artifactName: null }, controller: 'HomeController as home', templateUrl: 'home.html', title: 'Home' }); $stateProvider.state('timeline', { - url: '/{datasetId:int}/timeline?test', + url: '/{artifactName}/timeline?test', controller: 'TimelineController as timeline', templateUrl: 'timeline.html', reloadOnSearch: false, @@ -22,7 +22,7 @@ function OnConfig($stateProvider, $locationProvider, $urlRouterProvider) { }); $stateProvider.state('testDetails', { - url: '/{datasetId:int}/test-details/{test}', + url: '/{artifactName}/test-details/{test}', controller: 'TestDetailsController', controllerAs: 'testDetails', templateUrl: 'test-details.html', diff --git a/app/js/services/dataset.js b/app/js/services/dataset.js index 53ef0ec..cad7bca 100644 --- a/app/js/services/dataset.js +++ b/app/js/services/dataset.js @@ -5,73 +5,258 @@ var servicesModule = require('./_index.js'); /** * @ngInject */ -function DatasetService($q, $http) { +function DatasetService($q, $http, $window) { var service = {}; - service.list = function() { + var config = null; + var datasets = null; + var artifacts = new Map(); + var deployer = false; + + /** + * Return a promise to fetch the dataset associated with the current URL path. + * This is only valid when in deployer mode. + * @return {Promise} an $http promise for the current deployer dataset + */ + var fetchDeployerDataset = function() { + // get uuid from first segment of url, but remove any defined config root + var path = $window.location.pathname; + if (config.root && path.startsWith(config.root)) { + path = path.replace(config.root, ''); + } + + // remove leading '/' (if any) + if (path.startsWith('/')) { + path = path.substr(1, path.length - 1); + } + + // trim to first segment if necessary + if (path.includes('/')) { + path = path.substring(0, path.indexOf('/')); + } + return $http({ cache: true, - url: 'data/config.json', - method: 'GET' + url: config.apiRoot + '/task', + method: 'POST', + data: { q: path } }); }; - service.get = function(id) { - return $q(function(resolve, reject) { - service.list().then(function(response) { - for (var i in response.data.tempest) { - var entry = response.data.tempest[i]; - if (entry.id === id) { - resolve(entry); - return; - } - } + /** + * Adds the given list of artifacts to the global artifact map, based on their + * `artifact_name` fields. + * @param {object[]} artifacts a list of artifacts + */ + var initArtifacts = function(list) { + list.forEach(function(artifact) { + if (artifacts.has(artifact.artifact_name)) { + artifacts.get(artifact.artifact_name).push(artifact); + } else { + artifacts.set(artifact.artifact_name, [artifact]); + } + }); + }; - reject("Dataset not found with ID: " + id); + service.config = function() { + return $q(function(resolve, reject) { + if (config) { + resolve({ config: config, datasets: datasets, artifacts: artifacts }); + return; + } + + $http({ + cache: true, + url: 'data/config.json', + method: 'GET' + }).then(function(response) { + config = response.data; + + if (config.deployer === true) { + deployer = true; + + fetchDeployerDataset().then(function(apiResponse) { + datasets = [ apiResponse.data ]; + initArtifacts(apiResponse.data.artifacts); + resolve({ + config: config, + datasets: datasets, + artifacts: artifacts + }); + }, function(reason) { + reject(reason); + }); + } else { + datasets = config.datasets; + + // merge all datasets into a 1-level grouping for now + config.datasets.forEach(function(dataset) { + initArtifacts(dataset.artifacts); + }); + + resolve({ + config: config, + datasets: datasets, + artifacts: artifacts + }); + } }, function(reason) { reject(reason); }); }); }; - service.raw = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.raw, - method: 'GET' - }); - }; - - service.details = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.details, - method: 'GET' - }); - }; - - service.tree = function(dataset) { - return $http({ - cache: true, - url: "data/" + dataset.tree, - method: 'GET' - }); - }; - - service.dstat = function(dataset) { + /** + * Lists all datasets. + * @return {Promise} a Promise for the global list of datasets + */ + service.list = function() { return $q(function(resolve, reject) { - if (!dataset.dstat) { - reject({ status: -1, statusText: 'Dstat not available for dataset.' }); - return; + /* eslint-disable angular/di */ + service.config().then(function(config) { + resolve(config.datasets); + }, reject); + /* eslint-enable angular/di */ + }); + }; + + /** + * Lists all artifact groups that contain at least one artifact. If `primary` + * is true (default), only groups with at least one primary artifact are + * returned. + * @return {Promise} a Promise for the global list of datasets + */ + service.groups = function(primary) { + if (typeof primary === 'undefined') { + primary = true; + } + + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + var ret = []; + config.artifacts.forEach(function(entries, name) { + if (primary) { + entries = entries.filter(function(artifact) { + return artifact.primary; + }); + } + + if (entries.length > 0) { + ret.push(name); + } + }); + + resolve(ret); + }, reject); + /* eslint-enable angular/di */ + }); + }; + + /** + * Gets the dataset with the given ID. Note that for deployer instances, there + * will only ever be a single dataset (id #0). In most cases, dataset #0 + * should be treated as the 'primary' dataset (and should almost always be the + * only one configured). + * @param {number} id the index of the dataset to get + * @return {Promise} a Promise to retreive the specified dataset + */ + service.get = function(id) { + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + var dataset = config.datasets[id]; + if (dataset) { + resolve(dataset); + } else { + reject("Dataset not found with ID: " + id); + } + }, function(reason) { + reject(reason); + }); + /* eslint-enable angular/di */ + }); + }; + + /** + * Fetch all artifacts with the given `artifact_name` field. This should be + * the primary method for differentiating between artifacts. If no artifact + * name is given, this returns a flat list of all artifacts (via a Promise). + * @param {string} [name] an `artifact_name` field value + * @return {Promise} a promise for a list of matching artifacts + */ + service.artifacts = function(name) { + return $q(function(resolve, reject) { + /* eslint-disable angular/di */ + service.config().then(function(config) { + if (typeof name === 'undefined') { + var ret = []; + config.datasets.forEach(function(dataset) { + ret.push.apply(ret, dataset.artifacts); + }); + resolve(ret); + } else { + var group = config.artifacts.get(name); + if (group && group.length > 0) { + resolve(group); + } else { + reject('No artifacts found with name: ' + name); + } + } + }, reject); + /* eslint-enable angular/di */ + }); + }; + + var _loadArtifact = function(artifact, resolve, reject, message) { + if (artifact) { + var url = null; + if (deployer) { + url = config.apiRoot + '/blob/' + artifact.id; + } else { + url = 'data/' + artifact.path; } resolve($http({ cache: true, - url: "data/" + dataset.dstat, + url: url, method: 'GET' })); - }); + } else { + reject('No artifact found matching ' + message); + } + }; + + /** + * Fetch the artifact with the given `artifact_name` and `artifact_type` + * fields. If only one parameter is provided, only `artifact_type` is + * considered. + * @param {string} [name] an `artifact_name` field value + * @param {string} type an `artifact_type` field value (e.g. 'subunit') + * @return {Promise} a Promise for the actual data associated with the + * artifact + */ + service.artifact = function(name, type) { + if (arguments.length === 1) { + type = arguments[0]; + + return $q(function(resolve, reject) { + service.artifacts().then(function(all) { + _loadArtifact(all.find(function(a) { + return a.artifact_type === type; + }), resolve, reject, 'type=' + type); + }); + }); + } else { + return $q(function(resolve, reject) { + service.artifacts(name).then(function(group) { + _loadArtifact(group.find(function(a) { + return a.artifact_type === type; + }), resolve, reject, 'name=' + name + ', type=' + type); + }, reject); + }); + } }; return service; diff --git a/app/views/directives/tempest-summary.html b/app/views/directives/tempest-summary.html index 83828c1..b02aca3 100644 --- a/app/views/directives/tempest-summary.html +++ b/app/views/directives/tempest-summary.html @@ -1,7 +1,7 @@