From b7daf9172385f1d8bbab5fd53b20394e51378c08 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Tue, 25 Aug 2015 00:55:54 -0500 Subject: [PATCH 01/10] Current progress on stats page, nonfunctional. --- app/app.js | 6 +- app/components/container/container.html | 4 ++ app/components/stats/stats.html | 21 +++++++ app/components/stats/statsController.js | 56 +++++++++++++++++++ app/shared/services.js | 4 +- .../app/components/statsController.spec.js | 31 ++++++++++ 6 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 app/components/stats/stats.html create mode 100644 app/components/stats/statsController.js create mode 100644 test/unit/app/components/statsController.spec.js diff --git a/app/app.js b/app/app.js index d9a48daf6..7a4a4dbcc 100644 --- a/app/app.js +++ b/app/app.js @@ -1,4 +1,4 @@ -angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services', 'dockerui.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'containersNetwork', 'images', 'image', 'pullImage', 'startContainer', 'sidebar', 'info', 'builder', 'containerLogs', 'containerTop', 'events']) +angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services', 'dockerui.filters', 'masthead', 'footer', 'dashboard', 'container', 'containers', 'containersNetwork', 'images', 'image', 'pullImage', 'startContainer', 'sidebar', 'info', 'builder', 'containerLogs', 'containerTop', 'events', 'stats']) .config(['$routeProvider', function ($routeProvider) { 'use strict'; $routeProvider.when('/', { @@ -21,6 +21,10 @@ angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services' templateUrl: 'app/components/containerTop/containerTop.html', controller: 'ContainerTopController' }); + $routeProvider.when('/containers/:id/stats', { + templateUrl: 'app/components/stats/stats.html', + controller: 'StatsController' + }); $routeProvider.when('/containers_network', { templateUrl: 'app/components/containersNetwork/containersNetwork.html', controller: 'ContainersNetworkController' diff --git a/app/components/container/container.html b/app/components/container/container.html index 8843449c2..9f45e92fc 100644 --- a/app/components/container/container.html +++ b/app/components/container/container.html @@ -133,6 +133,10 @@ Logs: stdout/stderr + + Stats: + stats + Top: Top diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html new file mode 100644 index 000000000..365a931db --- /dev/null +++ b/app/components/stats/stats.html @@ -0,0 +1,21 @@ +
+
+

Stats

+ +
+ + + + + + + + + + +
Time readCPU usageStat
+ + +
+
+
diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js new file mode 100644 index 000000000..4f620c33b --- /dev/null +++ b/app/components/stats/statsController.js @@ -0,0 +1,56 @@ +angular.module('stats', []) + .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'LineChart', '$routeParams', function (Settings, $scope, Messages, $timeout, Container, LineChart, $routeParams) { + var sessionKey = 'dockeruiStats-' + $routeParams.id; + var localData = sessionStorage.getItem(sessionKey); + if (localData) { + $scope.dockerStats = localData; + } else { + $scope.dockerStats = []; + } + + + function updateStats() { + Container.stats({id: $routeParams.id}, function (d) { + console.log(d); + var arr = Object.keys(d).map(function (key) {return d[key];}); + if (arr.join('').indexOf('no such id') !== -1) { + Messages.error('Unable to retrieve container stats', 'Has this container been removed?'); + return; + } + $scope.dockerStats.push(d); + sessionStorage.setItem(sessionKey, $scope.dockerStats); + $timeout(updateStats, 1000); + // Update graph with latest data + updateChart($scope.dockerStats); + }, function () { + Messages.error('Unable to retrieve container stats', 'Has this container been removed?'); + }); + } + + updateStats(); + + $scope.calculateCPUPercent = function (stats) { + // Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208 + var prevCpu = stats.precpu_stats; + var curCpu = stats.cpu_stats; + + var cpuPercent = 0.0; + + // calculate the change for the cpu usage of the container in between readings + var cpuDelta = curCpu.cpu_usage.total_usage - prevCpu.cpu_usage.total_usage; + // calculate the change for the entire system between readings + var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage; + + if (systemDelta > 0.0 && cpuDelta > 0.0) { + cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.size() * 100.0; + } + return cpuPercent + }; + + function updateChart(data) { + // TODO: Build data in the right format and create chart. + //LineChart.build('#cpu-stats-chart', $scope.dockerStats, function (d) { + // return $scope.calculateCPUPercent(d) + //}); + } + }]); \ No newline at end of file diff --git a/app/shared/services.js b/app/shared/services.js index a996e20cf..41c809342 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -17,7 +17,8 @@ angular.module('dockerui.services', ['ngResource']) changes: {method: 'GET', params: {action: 'changes'}, isArray: true}, create: {method: 'POST', params: {action: 'create'}}, remove: {method: 'DELETE', params: {id: '@id', v: 0}}, - rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false} + rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false}, + stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}} }); }) .factory('ContainerCommit', function ($resource, $http, Settings) { @@ -192,7 +193,6 @@ angular.module('dockerui.services', ['ngResource']) }) .factory('LineChart', function (Settings) { 'use strict'; - var url = Settings.rawUrl + '/build'; return { build: function (id, data, getkey) { var chart = new Chart($(id).get(0).getContext("2d")); diff --git a/test/unit/app/components/statsController.spec.js b/test/unit/app/components/statsController.spec.js new file mode 100644 index 000000000..c26aa06c3 --- /dev/null +++ b/test/unit/app/components/statsController.spec.js @@ -0,0 +1,31 @@ +describe("StatsController", function () { + var $scope, $httpBackend, $routeParams; + + beforeEach(angular.mock.module('dockerui')); + + beforeEach(inject(function (_$rootScope_, _$httpBackend_, $controller, _$routeParams_) { + $scope = _$rootScope_.$new(); + $httpBackend = _$httpBackend_; + $routeParams = _$routeParams_; + $routeParams.id = 'b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f'; + $controller('StatsController', { + '$scope': $scope, + '$routeParams': $routeParams + }); + })); + + it("should test controller initialize", function () { + $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/stats?stream=false').respond(200); + //expect($scope.ps_args).toBeDefined(); + $httpBackend.flush(); + }); + + it("a correct top request to the Docker remote API", function () { + //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=').respond(200); + //$routeParams.id = '123456789123456789123456789'; + //$scope.ps_args = 'aux'; + //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=' + $scope.ps_args).respond(200); + //$scope.getTop(); + //$httpBackend.flush(); + }); +}); \ No newline at end of file From 8a7f8f7c373246bec8d3ffe156a92f64f079df54 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Tue, 25 Aug 2015 00:59:54 -0500 Subject: [PATCH 02/10] Whitespace diff, ran everything through the formatter. --- app/app.js | 5 +- app/components/builder/builderController.js | 8 +- .../container/containerController.js | 268 ++++---- .../containerLogs/containerLogsController.js | 134 ++-- .../containerLogs/containerlogs.html | 5 +- app/components/containers/containers.html | 39 +- .../containers/containersController.js | 195 +++--- .../containersNetwork/containersNetwork.html | 3 +- .../containersNetworkController.js | 471 ++++++------- app/components/dashboard/dashboard.html | 25 +- .../dashboard/dashboardController.js | 142 ++-- app/components/events/events.html | 25 +- app/components/events/eventsController.js | 18 +- app/components/footer/footerController.js | 12 +- app/components/footer/statusbar.html | 5 +- app/components/image/image.html | 98 +-- app/components/image/imageController.js | 126 ++-- app/components/images/images.html | 31 +- app/components/images/imagesController.js | 94 +-- app/components/info/info.html | 191 +++--- app/components/info/infoController.js | 22 +- app/components/masthead/masthead.html | 8 +- app/components/masthead/mastheadController.js | 6 +- app/components/pullImage/pullImage.html | 12 +- .../pullImage/pullImageController.js | 12 +- app/components/sidebar/sidebar.html | 4 +- app/components/sidebar/sidebarController.js | 18 +- .../startContainerController.js | 284 ++++---- .../startContainer/startcontainer.html | 644 ++++++++++-------- app/components/stats/stats.html | 20 +- app/components/stats/statsController.js | 4 +- app/shared/filters.js | 46 +- app/shared/services.js | 32 +- app/shared/viewmodel.js | 17 +- gruntFile.js | 89 ++- package.json | 64 +- .../startContainerController.spec.js | 43 +- test/unit/app/shared/filters.spec.js | 246 ++++++- test/unit/karma.conf.js | 26 +- 39 files changed, 1926 insertions(+), 1566 deletions(-) diff --git a/app/app.js b/app/app.js index 7a4a4dbcc..0f87a89cf 100644 --- a/app/app.js +++ b/app/app.js @@ -38,7 +38,10 @@ angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services' controller: 'ImageController' }); $routeProvider.when('/info', {templateUrl: 'app/components/info/info.html', controller: 'InfoController'}); - $routeProvider.when('/events', {templateUrl: 'app/components/events/events.html', controller: 'EventsController'}); + $routeProvider.when('/events', { + templateUrl: 'app/components/events/events.html', + controller: 'EventsController' + }); $routeProvider.otherwise({redirectTo: '/'}); }]) // This is your docker url that the api will use to make requests diff --git a/app/components/builder/builderController.js b/app/components/builder/builderController.js index eff32d9ca..5455aae4a 100644 --- a/app/components/builder/builderController.js +++ b/app/components/builder/builderController.js @@ -1,5 +1,5 @@ angular.module('builder', []) -.controller('BuilderController', ['$scope', 'Dockerfile', 'Messages', -function($scope, Dockerfile, Messages) { - $scope.template = 'app/components/builder/builder.html'; -}]); + .controller('BuilderController', ['$scope', 'Dockerfile', 'Messages', + function ($scope, Dockerfile, Messages) { + $scope.template = 'app/components/builder/builder.html'; + }]); diff --git a/app/components/container/containerController.js b/app/components/container/containerController.js index dbcf57376..8f46d2211 100644 --- a/app/components/container/containerController.js +++ b/app/components/container/containerController.js @@ -1,143 +1,143 @@ angular.module('container', []) -.controller('ContainerController', ['$scope', '$routeParams', '$location', 'Container', 'ContainerCommit', 'Messages', 'ViewSpinner', -function($scope, $routeParams, $location, Container, ContainerCommit, Messages, ViewSpinner) { - $scope.changes = []; - $scope.edit = false; + .controller('ContainerController', ['$scope', '$routeParams', '$location', 'Container', 'ContainerCommit', 'Messages', 'ViewSpinner', + function ($scope, $routeParams, $location, Container, ContainerCommit, Messages, ViewSpinner) { + $scope.changes = []; + $scope.edit = false; - var update = function() { - ViewSpinner.spin(); - Container.get({id: $routeParams.id}, function(d) { - $scope.container = d; - $scope.container.edit = false; - $scope.container.newContainerName = d.Name; - ViewSpinner.stop(); - }, function(e) { - if (e.status === 404) { - $('.detail').hide(); - Messages.error("Not found", "Container not found."); - } else { - Messages.error("Failure", e.data); - } - ViewSpinner.stop(); - }); - }; + var update = function () { + ViewSpinner.spin(); + Container.get({id: $routeParams.id}, function (d) { + $scope.container = d; + $scope.container.edit = false; + $scope.container.newContainerName = d.Name; + ViewSpinner.stop(); + }, function (e) { + if (e.status === 404) { + $('.detail').hide(); + Messages.error("Not found", "Container not found."); + } else { + Messages.error("Failure", e.data); + } + ViewSpinner.stop(); + }); + }; - $scope.start = function(){ - ViewSpinner.spin(); - Container.start({ - id: $scope.container.Id, - HostConfig: $scope.container.HostConfig - }, function(d) { - update(); - Messages.send("Container started", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to start." + e.data); - }); - }; + $scope.start = function () { + ViewSpinner.spin(); + Container.start({ + id: $scope.container.Id, + HostConfig: $scope.container.HostConfig + }, function (d) { + update(); + Messages.send("Container started", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to start." + e.data); + }); + }; - $scope.stop = function() { - ViewSpinner.spin(); - Container.stop({id: $routeParams.id}, function(d) { - update(); - Messages.send("Container stopped", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to stop." + e.data); - }); - }; + $scope.stop = function () { + ViewSpinner.spin(); + Container.stop({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container stopped", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to stop." + e.data); + }); + }; - $scope.kill = function() { - ViewSpinner.spin(); - Container.kill({id: $routeParams.id}, function(d) { - update(); - Messages.send("Container killed", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to die." + e.data); - }); - }; + $scope.kill = function () { + ViewSpinner.spin(); + Container.kill({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container killed", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to die." + e.data); + }); + }; - $scope.commit = function() { - ViewSpinner.spin(); - ContainerCommit.commit({id: $routeParams.id, repo: $scope.container.Config.Image}, function(d) { - update(); - Messages.send("Container commited", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to commit." + e.data); - }); - }; - $scope.pause = function() { - ViewSpinner.spin(); - Container.pause({id: $routeParams.id}, function(d) { - update(); - Messages.send("Container paused", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to pause." + e.data); - }); - }; + $scope.commit = function () { + ViewSpinner.spin(); + ContainerCommit.commit({id: $routeParams.id, repo: $scope.container.Config.Image}, function (d) { + update(); + Messages.send("Container commited", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to commit." + e.data); + }); + }; + $scope.pause = function () { + ViewSpinner.spin(); + Container.pause({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container paused", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to pause." + e.data); + }); + }; - $scope.unpause = function() { - ViewSpinner.spin(); - Container.unpause({id: $routeParams.id}, function(d) { - update(); - Messages.send("Container unpaused", $routeParams.id); - }, function(e) { - update(); - Messages.error("Failure", "Container failed to unpause." + e.data); - }); - }; + $scope.unpause = function () { + ViewSpinner.spin(); + Container.unpause({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container unpaused", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to unpause." + e.data); + }); + }; + + $scope.remove = function () { + ViewSpinner.spin(); + Container.remove({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container removed", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to remove." + e.data); + }); + }; + + $scope.restart = function () { + ViewSpinner.spin(); + Container.restart({id: $routeParams.id}, function (d) { + update(); + Messages.send("Container restarted", $routeParams.id); + }, function (e) { + update(); + Messages.error("Failure", "Container failed to restart." + e.data); + }); + }; + + $scope.hasContent = function (data) { + return data !== null && data !== undefined; + }; + + $scope.getChanges = function () { + ViewSpinner.spin(); + Container.changes({id: $routeParams.id}, function (d) { + $scope.changes = d; + ViewSpinner.stop(); + }); + }; + + $scope.renameContainer = function () { + // #FIXME fix me later to handle http status to show the correct error message + Container.rename({id: $routeParams.id, 'name': $scope.container.newContainerName}, function (data) { + if (data.name) { + $scope.container.Name = data.name; + Messages.send("Container renamed", $routeParams.id); + } else { + $scope.container.newContainerName = $scope.container.Name; + Messages.error("Failure", "Container failed to rename."); + } + }); + $scope.container.edit = false; + }; - $scope.remove = function() { - ViewSpinner.spin(); - Container.remove({id: $routeParams.id}, function(d) { update(); - Messages.send("Container removed", $routeParams.id); - }, function(e){ - update(); - Messages.error("Failure", "Container failed to remove." + e.data); - }); - }; - - $scope.restart = function() { - ViewSpinner.spin(); - Container.restart({id: $routeParams.id}, function(d) { - update(); - Messages.send("Container restarted", $routeParams.id); - }, function(e){ - update(); - Messages.error("Failure", "Container failed to restart." + e.data); - }); - }; - - $scope.hasContent = function(data) { - return data !== null && data !== undefined; - }; - - $scope.getChanges = function() { - ViewSpinner.spin(); - Container.changes({id: $routeParams.id}, function(d) { - $scope.changes = d; - ViewSpinner.stop(); - }); - }; - - $scope.renameContainer = function () { - // #FIXME fix me later to handle http status to show the correct error message - Container.rename({id: $routeParams.id, 'name': $scope.container.newContainerName}, function(data){ - if (data.name){ - $scope.container.Name = data.name; - Messages.send("Container renamed", $routeParams.id); - }else { - $scope.container.newContainerName = $scope.container.Name; - Messages.error("Failure", "Container failed to rename."); - } - }); - $scope.container.edit = false; - }; - - update(); - $scope.getChanges(); -}]); + $scope.getChanges(); + }]); diff --git a/app/components/containerLogs/containerLogsController.js b/app/components/containerLogs/containerLogsController.js index 6eb6984ba..27977f11d 100644 --- a/app/components/containerLogs/containerLogsController.js +++ b/app/components/containerLogs/containerLogsController.js @@ -1,76 +1,76 @@ angular.module('containerLogs', []) -.controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner', -function($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) { - $scope.stdout = ''; - $scope.stderr = ''; - $scope.showTimestamps = false; - $scope.tailLines = 2000; + .controller('ContainerLogsController', ['$scope', '$routeParams', '$location', '$anchorScroll', 'ContainerLogs', 'Container', 'ViewSpinner', + function ($scope, $routeParams, $location, $anchorScroll, ContainerLogs, Container, ViewSpinner) { + $scope.stdout = ''; + $scope.stderr = ''; + $scope.showTimestamps = false; + $scope.tailLines = 2000; - ViewSpinner.spin(); - Container.get({id: $routeParams.id}, function(d) { - $scope.container = d; - ViewSpinner.stop(); - }, function(e) { - if (e.status === 404) { - Messages.error("Not found", "Container not found."); - } else { - Messages.error("Failure", e.data); - } - ViewSpinner.stop(); - }); + ViewSpinner.spin(); + Container.get({id: $routeParams.id}, function (d) { + $scope.container = d; + ViewSpinner.stop(); + }, function (e) { + if (e.status === 404) { + Messages.error("Not found", "Container not found."); + } else { + Messages.error("Failure", e.data); + } + ViewSpinner.stop(); + }); - function getLogs() { - ViewSpinner.spin(); - ContainerLogs.get($routeParams.id, { - stdout: 1, - stderr: 0, - timestamps: $scope.showTimestamps, - tail: $scope.tailLines - }, function(data, status, headers, config) { - // Replace carriage returns with newlines to clean up output - data = data.replace(/[\r]/g, '\n'); - // Strip 8 byte header from each line of output - data = data.substring(8); - data = data.replace(/\n(.{8})/g, '\n'); - $scope.stdout = data; - ViewSpinner.stop(); - }); + function getLogs() { + ViewSpinner.spin(); + ContainerLogs.get($routeParams.id, { + stdout: 1, + stderr: 0, + timestamps: $scope.showTimestamps, + tail: $scope.tailLines + }, function (data, status, headers, config) { + // Replace carriage returns with newlines to clean up output + data = data.replace(/[\r]/g, '\n'); + // Strip 8 byte header from each line of output + data = data.substring(8); + data = data.replace(/\n(.{8})/g, '\n'); + $scope.stdout = data; + ViewSpinner.stop(); + }); - ContainerLogs.get($routeParams.id, { - stdout: 0, - stderr: 1, - timestamps: $scope.showTimestamps, - tail: $scope.tailLines - }, function(data, status, headers, config) { - // Replace carriage returns with newlines to clean up output - data = data.replace(/[\r]/g, '\n'); - // Strip 8 byte header from each line of output - data = data.substring(8); - data = data.replace(/\n(.{8})/g, '\n'); - $scope.stderr = data; - ViewSpinner.stop(); - }); - } + ContainerLogs.get($routeParams.id, { + stdout: 0, + stderr: 1, + timestamps: $scope.showTimestamps, + tail: $scope.tailLines + }, function (data, status, headers, config) { + // Replace carriage returns with newlines to clean up output + data = data.replace(/[\r]/g, '\n'); + // Strip 8 byte header from each line of output + data = data.substring(8); + data = data.replace(/\n(.{8})/g, '\n'); + $scope.stderr = data; + ViewSpinner.stop(); + }); + } - // initial call - getLogs(); - var logIntervalId = window.setInterval(getLogs, 5000); + // initial call + getLogs(); + var logIntervalId = window.setInterval(getLogs, 5000); - $scope.$on("$destroy", function(){ - // clearing interval when view changes - clearInterval(logIntervalId); - }); + $scope.$on("$destroy", function () { + // clearing interval when view changes + clearInterval(logIntervalId); + }); - $scope.scrollTo = function(id) { - $location.hash(id); - $anchorScroll(); - }; + $scope.scrollTo = function (id) { + $location.hash(id); + $anchorScroll(); + }; - $scope.toggleTimestamps = function() { - getLogs(); - }; + $scope.toggleTimestamps = function () { + getLogs(); + }; - $scope.toggleTail = function() { - getLogs(); - }; -}]); + $scope.toggleTail = function () { + getLogs(); + }; + }]); diff --git a/app/components/containerLogs/containerlogs.html b/app/components/containerLogs/containerlogs.html index 8dfa62b8a..406d04e66 100644 --- a/app/components/containerLogs/containerlogs.html +++ b/app/components/containerLogs/containerlogs.html @@ -1,6 +1,7 @@

Logs for container: {{ container.Name }}

+
@@ -9,12 +10,12 @@
Reload logs + ng-model="tailLines" ng-keypress="($event.which === 13)? toggleTail() : 0"/>
+ ng-change="toggleTimestamps()"/>
diff --git a/app/components/containers/containers.html b/app/components/containers/containers.html index c2811581b..73d78e3f5 100644 --- a/app/components/containers/containers.html +++ b/app/components/containers/containers.html @@ -1,10 +1,10 @@ -

Containers:

- - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + +
ActionNameImageCommandCreatedStatus
ActionNameImageCommandCreatedStatus
{{ container|containername}}{{ container.Image }}{{ container.Command|truncate:40 }}{{ container.Created|getdate }}{{ container.Status }}
{{ container|containername}}{{ container.Image }}{{ container.Command|truncate:40 }}{{ container.Created|getdate }}{{ container.Status }}
diff --git a/app/components/containers/containersController.js b/app/components/containers/containersController.js index 3a88987ab..dbd8b0d75 100644 --- a/app/components/containers/containersController.js +++ b/app/components/containers/containersController.js @@ -1,111 +1,112 @@ angular.module('containers', []) -.controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', -function($scope, Container, Settings, Messages, ViewSpinner) { - $scope.predicate = '-Created'; - $scope.toggle = false; - $scope.displayAll = Settings.displayAll; + .controller('ContainersController', ['$scope', 'Container', 'Settings', 'Messages', 'ViewSpinner', + function ($scope, Container, Settings, Messages, ViewSpinner) { + $scope.predicate = '-Created'; + $scope.toggle = false; + $scope.displayAll = Settings.displayAll; - var update = function(data) { - ViewSpinner.spin(); - Container.query(data, function(d) { - $scope.containers = d.map(function(item) { - return new ContainerViewModel(item); }); - ViewSpinner.stop(); - }); - }; - - var batch = function(items, action, msg) { - ViewSpinner.spin(); - var counter = 0; - var complete = function() { - counter = counter -1; - if (counter === 0) { - ViewSpinner.stop(); - update({all: Settings.displayAll ? 1 : 0}); - } - }; - angular.forEach(items, function(c) { - if (c.Checked) { - if(action === Container.start){ - Container.get({id: c.Id}, function(d) { - c = d; - counter = counter + 1; - action({id: c.Id, HostConfig: c.HostConfig || {}}, function(d) { - Messages.send("Container " + msg, c.Id); - var index = $scope.containers.indexOf(c); - complete(); - }, function(e) { - Messages.error("Failure", e.data); - complete(); + var update = function (data) { + ViewSpinner.spin(); + Container.query(data, function (d) { + $scope.containers = d.map(function (item) { + return new ContainerViewModel(item); }); - }, function(e) { - if (e.status === 404) { - $('.detail').hide(); - Messages.error("Not found", "Container not found."); - } else { - Messages.error("Failure", e.data); - } - complete(); - }); + ViewSpinner.stop(); + }); + }; + + var batch = function (items, action, msg) { + ViewSpinner.spin(); + var counter = 0; + var complete = function () { + counter = counter - 1; + if (counter === 0) { + ViewSpinner.stop(); + update({all: Settings.displayAll ? 1 : 0}); + } + }; + angular.forEach(items, function (c) { + if (c.Checked) { + if (action === Container.start) { + Container.get({id: c.Id}, function (d) { + c = d; + counter = counter + 1; + action({id: c.Id, HostConfig: c.HostConfig || {}}, function (d) { + Messages.send("Container " + msg, c.Id); + var index = $scope.containers.indexOf(c); + complete(); + }, function (e) { + Messages.error("Failure", e.data); + complete(); + }); + }, function (e) { + if (e.status === 404) { + $('.detail').hide(); + Messages.error("Not found", "Container not found."); + } else { + Messages.error("Failure", e.data); + } + complete(); + }); + } + else { + counter = counter + 1; + action({id: c.Id}, function (d) { + Messages.send("Container " + msg, c.Id); + var index = $scope.containers.indexOf(c); + complete(); + }, function (e) { + Messages.error("Failure", e.data); + complete(); + }); + + } + + } + }); + if (counter === 0) { + ViewSpinner.stop(); } - else{ - counter = counter + 1; - action({id: c.Id}, function(d) { - Messages.send("Container " + msg, c.Id); - var index = $scope.containers.indexOf(c); - complete(); - }, function(e) { - Messages.error("Failure", e.data); - complete(); - }); + }; - } + $scope.toggleSelectAll = function () { + angular.forEach($scope.containers, function (i) { + i.Checked = $scope.toggle; + }); + }; - } - }); - if (counter === 0) { - ViewSpinner.stop(); - } - }; + $scope.toggleGetAll = function () { + Settings.displayAll = $scope.displayAll; + update({all: Settings.displayAll ? 1 : 0}); + }; - $scope.toggleSelectAll = function() { - angular.forEach($scope.containers, function(i) { - i.Checked = $scope.toggle; - }); - }; + $scope.startAction = function () { + batch($scope.containers, Container.start, "Started"); + }; - $scope.toggleGetAll = function() { - Settings.displayAll = $scope.displayAll; - update({all: Settings.displayAll ? 1 : 0}); - }; + $scope.stopAction = function () { + batch($scope.containers, Container.stop, "Stopped"); + }; - $scope.startAction = function() { - batch($scope.containers, Container.start, "Started"); - }; + $scope.restartAction = function () { + batch($scope.containers, Container.restart, "Restarted"); + }; - $scope.stopAction = function() { - batch($scope.containers, Container.stop, "Stopped"); - }; + $scope.killAction = function () { + batch($scope.containers, Container.kill, "Killed"); + }; - $scope.restartAction = function() { - batch($scope.containers, Container.restart, "Restarted"); - }; + $scope.pauseAction = function () { + batch($scope.containers, Container.pause, "Paused"); + }; - $scope.killAction = function() { - batch($scope.containers, Container.kill, "Killed"); - }; + $scope.unpauseAction = function () { + batch($scope.containers, Container.unpause, "Unpaused"); + }; - $scope.pauseAction = function() { - batch($scope.containers, Container.pause, "Paused"); - }; + $scope.removeAction = function () { + batch($scope.containers, Container.remove, "Removed"); + }; - $scope.unpauseAction = function() { - batch($scope.containers, Container.unpause, "Unpaused"); - }; - - $scope.removeAction = function() { - batch($scope.containers, Container.remove, "Removed"); - }; - - update({all: Settings.displayAll ? 1 : 0}); -}]); + update({all: Settings.displayAll ? 1 : 0}); + }]); diff --git a/app/components/containersNetwork/containersNetwork.html b/app/components/containersNetwork/containersNetwork.html index f213de493..ba265e880 100644 --- a/app/components/containersNetwork/containersNetwork.html +++ b/app/components/containersNetwork/containersNetwork.html @@ -15,7 +15,8 @@
- +
" + + this.addContainerNode = function (container) { + this.nodes.add({ + id: container.Id, + label: container.Name, + title: "
    " + "
  • ID: " + container.Id + "
  • " + "
  • Image: " + container.Image + "
  • " + "
", - color: (container.Running ? "#8888ff" : "#cccccc") - }); - }; - - this.hasEdge = function(from, to) { - return this.edges.getIds({ - filter: function (item) { - return item.from == from.Id && item.to == to.Id; - } }).length > 0; - }; - - this.addLinkEdgeIfExists = function(from, to) { - if (from.Links != null && from.Links[to.Name] != null && !this.hasEdge(from, to)) { - this.edges.add({ - from: from.Id, - to: to.Id, - label: from.Links[to.Name] }); - } - }; - - this.addVolumeEdgeIfExists = function(from, to) { - if (from.VolumesFrom != null && (from.VolumesFrom[to.Id] != null || from.VolumesFrom[to.Name] != null) && !this.hasEdge(from, to)) { - this.edges.add({ - from: from.Id, - to: to.Id, - color: { color: '#A0A0A0', highlight: '#A0A0A0', hover: '#848484'}}); - } - }; - - this.removeContainersNodes = function(containersIds) { - this.nodes.remove(containersIds); - }; - } - - function ContainersNetwork() { - this.data = new ContainersNetworkData(); - this.containers = {}; - this.selectedContainersIds = []; - this.shownContainersIds = []; - this.events = { - select : function(event) { - $scope.network.selectedContainersIds = event.nodes; - $scope.$apply( function() { - $scope.query = ''; + color: (container.Running ? "#8888ff" : "#cccccc") }); - }, - doubleClick : function(event) { - $scope.$apply( function() { - $location.path('/containers/' + event.nodes[0]); - }); - } - }; - this.options = { - navigation: true, - keyboard: true, - height: '500px', width: '700px', - nodes: { - shape: 'box' - }, - edges: { - style: 'arrow' - }, - physics: { - barnesHut : { - springLength: 200 + }; + + this.hasEdge = function (from, to) { + return this.edges.getIds({ + filter: function (item) { + return item.from == from.Id && item.to == to.Id; + } + }).length > 0; + }; + + this.addLinkEdgeIfExists = function (from, to) { + if (from.Links != null && from.Links[to.Name] != null && !this.hasEdge(from, to)) { + this.edges.add({ + from: from.Id, + to: to.Id, + label: from.Links[to.Name] + }); } - } - }; + }; - this.addContainer = function(data) { - var container = new ContainerNode(data); - this.containers[container.Id] = container; - this.shownContainersIds.push(container.Id); - this.data.addContainerNode(container); - for (var otherContainerId in this.containers) { - var otherContainer = this.containers[otherContainerId]; - this.data.addLinkEdgeIfExists(container, otherContainer); - this.data.addLinkEdgeIfExists(otherContainer, container); - this.data.addVolumeEdgeIfExists(container, otherContainer); - this.data.addVolumeEdgeIfExists(otherContainer, container); - } - }; - - this.selectContainers = function(query) { - if (this.component != null) { - this.selectedContainersIds = this.searchContainers(query); - this.component.selectNodes(this.selectedContainersIds); - } - }; - - this.searchContainers = function(query) { - if (query.trim() === "") { - return []; - } - var selectedContainersIds = []; - for (var i=0; i < this.shownContainersIds.length; i++) { - var container = this.containers[this.shownContainersIds[i]]; - if (container.Name.indexOf(query) > -1 || - container.Image.indexOf(query) > -1 || - container.Id.indexOf(query) > -1) { - selectedContainersIds.push(container.Id); + this.addVolumeEdgeIfExists = function (from, to) { + if (from.VolumesFrom != null && (from.VolumesFrom[to.Id] != null || from.VolumesFrom[to.Name] != null) && !this.hasEdge(from, to)) { + this.edges.add({ + from: from.Id, + to: to.Id, + color: {color: '#A0A0A0', highlight: '#A0A0A0', hover: '#848484'} + }); } - } - return selectedContainersIds; - }; + }; - this.hideSelected = function() { - var i=0; - while ( i < this.shownContainersIds.length ) { - if (this.selectedContainersIds.indexOf(this.shownContainersIds[i]) > -1) { - this.shownContainersIds.splice(i, 1); - } else { - i++; - } - } - this.data.removeContainersNodes(this.selectedContainersIds); - $scope.query = ''; + this.removeContainersNodes = function (containersIds) { + this.nodes.remove(containersIds); + }; + } + + function ContainersNetwork() { + this.data = new ContainersNetworkData(); + this.containers = {}; this.selectedContainersIds = []; - }; + this.shownContainersIds = []; + this.events = { + select: function (event) { + $scope.network.selectedContainersIds = event.nodes; + $scope.$apply(function () { + $scope.query = ''; + }); + }, + doubleClick: function (event) { + $scope.$apply(function () { + $location.path('/containers/' + event.nodes[0]); + }); + } + }; + this.options = { + navigation: true, + keyboard: true, + height: '500px', width: '700px', + nodes: { + shape: 'box' + }, + edges: { + style: 'arrow' + }, + physics: { + barnesHut: { + springLength: 200 + } + } + }; - this.searchDownstream = function(containerId, downstreamContainersIds) { - if (downstreamContainersIds.indexOf(containerId) > -1) { - return; - } - downstreamContainersIds.push(containerId); - var container = this.containers[containerId]; - if (container.Links == null && container.VolumesFrom == null) { - return; - } - for (var otherContainerId in this.containers) { - var otherContainer = this.containers[otherContainerId]; - if (container.Links != null && container.Links[otherContainer.Name] != null) { - this.searchDownstream(otherContainer.Id, downstreamContainersIds); - } else if (container.VolumesFrom != null && + this.addContainer = function (data) { + var container = new ContainerNode(data); + this.containers[container.Id] = container; + this.shownContainersIds.push(container.Id); + this.data.addContainerNode(container); + for (var otherContainerId in this.containers) { + var otherContainer = this.containers[otherContainerId]; + this.data.addLinkEdgeIfExists(container, otherContainer); + this.data.addLinkEdgeIfExists(otherContainer, container); + this.data.addVolumeEdgeIfExists(container, otherContainer); + this.data.addVolumeEdgeIfExists(otherContainer, container); + } + }; + + this.selectContainers = function (query) { + if (this.component != null) { + this.selectedContainersIds = this.searchContainers(query); + this.component.selectNodes(this.selectedContainersIds); + } + }; + + this.searchContainers = function (query) { + if (query.trim() === "") { + return []; + } + var selectedContainersIds = []; + for (var i = 0; i < this.shownContainersIds.length; i++) { + var container = this.containers[this.shownContainersIds[i]]; + if (container.Name.indexOf(query) > -1 || + container.Image.indexOf(query) > -1 || + container.Id.indexOf(query) > -1) { + selectedContainersIds.push(container.Id); + } + } + return selectedContainersIds; + }; + + this.hideSelected = function () { + var i = 0; + while (i < this.shownContainersIds.length) { + if (this.selectedContainersIds.indexOf(this.shownContainersIds[i]) > -1) { + this.shownContainersIds.splice(i, 1); + } else { + i++; + } + } + this.data.removeContainersNodes(this.selectedContainersIds); + $scope.query = ''; + this.selectedContainersIds = []; + }; + + this.searchDownstream = function (containerId, downstreamContainersIds) { + if (downstreamContainersIds.indexOf(containerId) > -1) { + return; + } + downstreamContainersIds.push(containerId); + var container = this.containers[containerId]; + if (container.Links == null && container.VolumesFrom == null) { + return; + } + for (var otherContainerId in this.containers) { + var otherContainer = this.containers[otherContainerId]; + if (container.Links != null && container.Links[otherContainer.Name] != null) { + this.searchDownstream(otherContainer.Id, downstreamContainersIds); + } else if (container.VolumesFrom != null && container.VolumesFrom[otherContainer.Id] != null) { - this.searchDownstream(otherContainer.Id, downstreamContainersIds); + this.searchDownstream(otherContainer.Id, downstreamContainersIds); + } } - } - }; + }; - this.updateShownContainers = function(newShownContainersIds) { - for (var containerId in this.containers) { - if (newShownContainersIds.indexOf(containerId) > -1 && + this.updateShownContainers = function (newShownContainersIds) { + for (var containerId in this.containers) { + if (newShownContainersIds.indexOf(containerId) > -1 && this.shownContainersIds.indexOf(containerId) === -1) { - this.data.addContainerNode(this.containers[containerId]); - } else if (newShownContainersIds.indexOf(containerId) === -1 && + this.data.addContainerNode(this.containers[containerId]); + } else if (newShownContainersIds.indexOf(containerId) === -1 && this.shownContainersIds.indexOf(containerId) > -1) { - this.data.removeContainersNodes(containerId); + this.data.removeContainersNodes(containerId); + } } - } - this.shownContainersIds = newShownContainersIds; - }; + this.shownContainersIds = newShownContainersIds; + }; - this.showSelectedDownstream = function() { - var downstreamContainersIds = []; - for (var i=0; i < this.selectedContainersIds.length; i++) { - this.searchDownstream(this.selectedContainersIds[i], downstreamContainersIds); - } - this.updateShownContainers(downstreamContainersIds); - }; + this.showSelectedDownstream = function () { + var downstreamContainersIds = []; + for (var i = 0; i < this.selectedContainersIds.length; i++) { + this.searchDownstream(this.selectedContainersIds[i], downstreamContainersIds); + } + this.updateShownContainers(downstreamContainersIds); + }; - this.searchUpstream = function(containerId, upstreamContainersIds) { - if (upstreamContainersIds.indexOf(containerId) > -1) { - return; - } - upstreamContainersIds.push(containerId); - var container = this.containers[containerId]; - for (var otherContainerId in this.containers) { - var otherContainer = this.containers[otherContainerId]; - if (otherContainer.Links != null && otherContainer.Links[container.Name] != null) { - this.searchUpstream(otherContainer.Id, upstreamContainersIds); - } else if (otherContainer.VolumesFrom != null && + this.searchUpstream = function (containerId, upstreamContainersIds) { + if (upstreamContainersIds.indexOf(containerId) > -1) { + return; + } + upstreamContainersIds.push(containerId); + var container = this.containers[containerId]; + for (var otherContainerId in this.containers) { + var otherContainer = this.containers[otherContainerId]; + if (otherContainer.Links != null && otherContainer.Links[container.Name] != null) { + this.searchUpstream(otherContainer.Id, upstreamContainersIds); + } else if (otherContainer.VolumesFrom != null && otherContainer.VolumesFrom[container.Id] != null) { - this.searchUpstream(otherContainer.Id, upstreamContainersIds); + this.searchUpstream(otherContainer.Id, upstreamContainersIds); + } } - } - }; + }; - this.showSelectedUpstream = function() { - var upstreamContainersIds = []; - for (var i=0; i < this.selectedContainersIds.length; i++) { - this.searchUpstream(this.selectedContainersIds[i], upstreamContainersIds); - } - this.updateShownContainers(upstreamContainersIds); - }; - - this.showAll = function() { - for (var containerId in this.containers) { - if (this.shownContainersIds.indexOf(containerId) === -1) { - this.data.addContainerNode(this.containers[containerId]); - this.shownContainersIds.push(containerId); + this.showSelectedUpstream = function () { + var upstreamContainersIds = []; + for (var i = 0; i < this.selectedContainersIds.length; i++) { + this.searchUpstream(this.selectedContainersIds[i], upstreamContainersIds); } - } + this.updateShownContainers(upstreamContainersIds); + }; + + this.showAll = function () { + for (var containerId in this.containers) { + if (this.shownContainersIds.indexOf(containerId) === -1) { + this.data.addContainerNode(this.containers[containerId]); + this.shownContainersIds.push(containerId); + } + } + }; + + } + + $scope.network = new ContainersNetwork(); + + var showFailure = function (event) { + Messages.error('Failure', e.data); }; - } + var addContainer = function (container) { + $scope.network.addContainer(container); + }; - $scope.network = new ContainersNetwork(); + var update = function (data) { + Container.query(data, function (d) { + for (var i = 0; i < d.length; i++) { + Container.get({id: d[i].Id}, addContainer, showFailure); + } + }); + }; + update({all: 0}); - var showFailure = function (event) { - Messages.error('Failure', e.data); - }; + $scope.includeStopped = false; + $scope.toggleIncludeStopped = function () { + $scope.network.updateShownContainers([]); + update({all: $scope.includeStopped ? 1 : 0}); + }; - var addContainer = function (container) { - $scope.network.addContainer(container); - }; - - var update = function (data) { - Container.query(data, function(d) { - for (var i = 0; i < d.length; i++) { - Container.get({id: d[i].Id}, addContainer, showFailure); - } - }); - }; - update({all: 0}); - - $scope.includeStopped = false; - $scope.toggleIncludeStopped = function() { - $scope.network.updateShownContainers([]); - update({all: $scope.includeStopped ? 1 : 0}); - }; - -}]); + }]); diff --git a/app/components/dashboard/dashboard.html b/app/components/dashboard/dashboard.html index 7dd6f34dc..8b2209e6d 100644 --- a/app/components/dashboard/dashboard.html +++ b/app/components/dashboard/dashboard.html @@ -1,4 +1,3 @@ -
- +
- +
- +
- +
diff --git a/app/components/pullImage/pullImageController.js b/app/components/pullImage/pullImageController.js index 9fc242fb5..32990689c 100644 --- a/app/components/pullImage/pullImageController.js +++ b/app/components/pullImage/pullImageController.js @@ -1,9 +1,9 @@ angular.module('pullImage', []) .controller('PullImageController', ['$scope', '$log', 'Dockerfile', 'Messages', 'Image', 'ViewSpinner', - function($scope, $log, Dockerfile, Messages, Image, ViewSpinner) { + function ($scope, $log, Dockerfile, Messages, Image, ViewSpinner) { $scope.template = 'app/components/pullImage/pullImage.html'; - $scope.init = function() { + $scope.init = function () { $scope.config = { registry: '', repo: '', @@ -18,7 +18,7 @@ angular.module('pullImage', []) Messages.error('Error', errorMsgFilter(e)); } - $scope.pull = function() { + $scope.pull = function () { $('#error-message').hide(); var config = angular.copy($scope.config); var imageName = (config.registry ? config.registry + '/' : '' ) + @@ -28,10 +28,10 @@ angular.module('pullImage', []) ViewSpinner.spin(); $('#pull-modal').modal('hide'); - Image.create(config, function(data) { + Image.create(config, function (data) { ViewSpinner.stop(); if (data.constructor === Array) { - var f = data.length > 0 && data[data.length-1].hasOwnProperty('error'); + var f = data.length > 0 && data[data.length - 1].hasOwnProperty('error'); //check for error if (f) { var d = data[data.length - 1]; @@ -46,7 +46,7 @@ angular.module('pullImage', []) Messages.send("Image Added", imageName); $scope.init(); } - }, function(e) { + }, function (e) { ViewSpinner.stop(); $scope.error = "Cannot pull image " + imageName + " Reason: " + e.data; $('#pull-modal').modal('show'); diff --git a/app/components/sidebar/sidebar.html b/app/components/sidebar/sidebar.html index e7b559ec3..b93b8e5e3 100644 --- a/app/components/sidebar/sidebar.html +++ b/app/components/sidebar/sidebar.html @@ -1,11 +1,11 @@
Running containers: -
+
Endpoint: {{ endpoint }}
diff --git a/app/components/sidebar/sidebarController.js b/app/components/sidebar/sidebarController.js index f182a3481..3f58d675e 100644 --- a/app/components/sidebar/sidebarController.js +++ b/app/components/sidebar/sidebarController.js @@ -1,11 +1,11 @@ angular.module('sidebar', []) -.controller('SideBarController', ['$scope', 'Container', 'Settings', -function($scope, Container, Settings) { - $scope.template = 'partials/sidebar.html'; - $scope.containers = []; - $scope.endpoint = Settings.endpoint; + .controller('SideBarController', ['$scope', 'Container', 'Settings', + function ($scope, Container, Settings) { + $scope.template = 'partials/sidebar.html'; + $scope.containers = []; + $scope.endpoint = Settings.endpoint; - Container.query({all: 0}, function(d) { - $scope.containers = d; - }); -}]); + Container.query({all: 0}, function (d) { + $scope.containers = d; + }); + }]); diff --git a/app/components/startContainer/startContainerController.js b/app/components/startContainer/startContainerController.js index 571a360ad..46d8ba710 100644 --- a/app/components/startContainer/startContainerController.js +++ b/app/components/startContainer/startContainerController.js @@ -1,147 +1,153 @@ angular.module('startContainer', ['ui.bootstrap']) -.controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', -function($scope, $routeParams, $location, Container, Messages, containernameFilter, errorMsgFilter) { - $scope.template = 'app/components/startContainer/startcontainer.html'; + .controller('StartContainerController', ['$scope', '$routeParams', '$location', 'Container', 'Messages', 'containernameFilter', 'errorMsgFilter', + function ($scope, $routeParams, $location, Container, Messages, containernameFilter, errorMsgFilter) { + $scope.template = 'app/components/startContainer/startcontainer.html'; - Container.query({all: 1}, function(d) { - $scope.containerNames = d.map(function(container){ - return containernameFilter(container); - }); - }); + Container.query({all: 1}, function (d) { + $scope.containerNames = d.map(function (container) { + return containernameFilter(container); + }); + }); - $scope.config = { - Env: [], - Volumes: [], - SecurityOpts: [], - HostConfig: { - PortBindings: [], - Binds: [], - Links: [], - Dns: [], - DnsSearch: [], - VolumesFrom: [], - CapAdd: [], - CapDrop: [], - Devices: [], - LxcConf: [], - ExtraHosts: [] - } - }; - - $scope.menuStatus = { - containerOpen: true, - hostConfigOpen: false - }; - - function failedRequestHandler(e, Messages) { - Messages.error('Error', errorMsgFilter(e)); - } - - function rmEmptyKeys(col) { - for (var key in col) { - if (col[key] === null || col[key] === undefined || col[key] === '' || $.isEmptyObject(col[key]) || col[key].length === 0) { - delete col[key]; - } - } - } - - function getNames(arr) { - return arr.map(function(item) {return item.name;}); - } - - $scope.create = function() { - // Copy the config before transforming fields to the remote API format - var config = angular.copy($scope.config); - - config.Image = $routeParams.id; - - if (config.Cmd && config.Cmd[0] === "[") { - config.Cmd = angular.fromJson(config.Cmd); - } else if (config.Cmd) { - config.Cmd = config.Cmd.split(' '); - } - - config.Env = config.Env.map(function(envar) {return envar.name + '=' + envar.value;}); - - config.Volumes = getNames(config.Volumes); - config.SecurityOpts = getNames(config.SecurityOpts); - - config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom); - config.HostConfig.Binds = getNames(config.HostConfig.Binds); - config.HostConfig.Links = getNames(config.HostConfig.Links); - config.HostConfig.Dns = getNames(config.HostConfig.Dns); - config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch); - config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd); - config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop); - config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function(prev, cur, idx){ - prev[cur.name] = cur.value; - return prev; - }, {}); - config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function(entry) {return entry.host + ':' + entry.ip;}); - - var ExposedPorts = {}; - var PortBindings = {}; - config.HostConfig.PortBindings.forEach(function(portBinding) { - var intPort = portBinding.intPort + "/tcp"; - if (portBinding.protocol === "udp") { - intPort = portBinding.intPort + "/udp"; - } - var binding = { - HostIp: portBinding.ip, - HostPort: portBinding.extPort + $scope.config = { + Env: [], + Volumes: [], + SecurityOpts: [], + HostConfig: { + PortBindings: [], + Binds: [], + Links: [], + Dns: [], + DnsSearch: [], + VolumesFrom: [], + CapAdd: [], + CapDrop: [], + Devices: [], + LxcConf: [], + ExtraHosts: [] + } }; - if (portBinding.intPort) { - ExposedPorts[intPort] = {}; - if (intPort in PortBindings) { - PortBindings[intPort].push(binding); - } else { - PortBindings[intPort] = [binding]; - } - } else { - Messages.send('Warning', 'Internal port must be specified for PortBindings'); + + $scope.menuStatus = { + containerOpen: true, + hostConfigOpen: false + }; + + function failedRequestHandler(e, Messages) { + Messages.error('Error', errorMsgFilter(e)); } - }); - config.ExposedPorts = ExposedPorts; - config.HostConfig.PortBindings = PortBindings; - // Remove empty fields from the request to avoid overriding defaults - rmEmptyKeys(config.HostConfig); - rmEmptyKeys(config); - - var ctor = Container; - var loc = $location; - var s = $scope; - Container.create(config, function(d) { - if (d.Id) { - var reqBody = config.HostConfig || {}; - reqBody.id = d.Id; - ctor.start(reqBody, function(cd) { - if (cd.id) { - Messages.send('Container Started', d.Id); - $('#create-modal').modal('hide'); - loc.path('/containers/' + d.Id + '/'); - } else { - failedRequestHandler(cd, Messages); - ctor.remove({id: d.Id}, function() { - Messages.send('Container Removed', d.Id); - }); - } - }, function(e) { - failedRequestHandler(e, Messages); - }); - } else { - failedRequestHandler(d, Messages); + function rmEmptyKeys(col) { + for (var key in col) { + if (col[key] === null || col[key] === undefined || col[key] === '' || $.isEmptyObject(col[key]) || col[key].length === 0) { + delete col[key]; + } } - }, function(e) { - failedRequestHandler(e, Messages); - }); - }; + } - $scope.addEntry = function(array, entry) { - array.push(entry); - }; - $scope.rmEntry = function(array, entry) { - var idx = array.indexOf(entry); - array.splice(idx, 1); - }; -}]); + function getNames(arr) { + return arr.map(function (item) { + return item.name; + }); + } + + $scope.create = function () { + // Copy the config before transforming fields to the remote API format + var config = angular.copy($scope.config); + + config.Image = $routeParams.id; + + if (config.Cmd && config.Cmd[0] === "[") { + config.Cmd = angular.fromJson(config.Cmd); + } else if (config.Cmd) { + config.Cmd = config.Cmd.split(' '); + } + + config.Env = config.Env.map(function (envar) { + return envar.name + '=' + envar.value; + }); + + config.Volumes = getNames(config.Volumes); + config.SecurityOpts = getNames(config.SecurityOpts); + + config.HostConfig.VolumesFrom = getNames(config.HostConfig.VolumesFrom); + config.HostConfig.Binds = getNames(config.HostConfig.Binds); + config.HostConfig.Links = getNames(config.HostConfig.Links); + config.HostConfig.Dns = getNames(config.HostConfig.Dns); + config.HostConfig.DnsSearch = getNames(config.HostConfig.DnsSearch); + config.HostConfig.CapAdd = getNames(config.HostConfig.CapAdd); + config.HostConfig.CapDrop = getNames(config.HostConfig.CapDrop); + config.HostConfig.LxcConf = config.HostConfig.LxcConf.reduce(function (prev, cur, idx) { + prev[cur.name] = cur.value; + return prev; + }, {}); + config.HostConfig.ExtraHosts = config.HostConfig.ExtraHosts.map(function (entry) { + return entry.host + ':' + entry.ip; + }); + + var ExposedPorts = {}; + var PortBindings = {}; + config.HostConfig.PortBindings.forEach(function (portBinding) { + var intPort = portBinding.intPort + "/tcp"; + if (portBinding.protocol === "udp") { + intPort = portBinding.intPort + "/udp"; + } + var binding = { + HostIp: portBinding.ip, + HostPort: portBinding.extPort + }; + if (portBinding.intPort) { + ExposedPorts[intPort] = {}; + if (intPort in PortBindings) { + PortBindings[intPort].push(binding); + } else { + PortBindings[intPort] = [binding]; + } + } else { + Messages.send('Warning', 'Internal port must be specified for PortBindings'); + } + }); + config.ExposedPorts = ExposedPorts; + config.HostConfig.PortBindings = PortBindings; + + // Remove empty fields from the request to avoid overriding defaults + rmEmptyKeys(config.HostConfig); + rmEmptyKeys(config); + + var ctor = Container; + var loc = $location; + var s = $scope; + Container.create(config, function (d) { + if (d.Id) { + var reqBody = config.HostConfig || {}; + reqBody.id = d.Id; + ctor.start(reqBody, function (cd) { + if (cd.id) { + Messages.send('Container Started', d.Id); + $('#create-modal').modal('hide'); + loc.path('/containers/' + d.Id + '/'); + } else { + failedRequestHandler(cd, Messages); + ctor.remove({id: d.Id}, function () { + Messages.send('Container Removed', d.Id); + }); + } + }, function (e) { + failedRequestHandler(e, Messages); + }); + } else { + failedRequestHandler(d, Messages); + } + }, function (e) { + failedRequestHandler(e, Messages); + }); + }; + + $scope.addEntry = function (array, entry) { + array.push(entry); + }; + $scope.rmEntry = function (array, entry) { + var idx = array.indexOf(entry); + array.splice(idx, 1); + }; + }]); diff --git a/app/components/startContainer/startcontainer.html b/app/components/startContainer/startcontainer.html index eaa774f88..cdfc1a6fd 100644 --- a/app/components/startContainer/startcontainer.html +++ b/app/components/startContainer/startcontainer.html @@ -6,301 +6,413 @@

Create And Start Container From Image

diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index 4f620c33b..7a9e8412d 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -12,7 +12,9 @@ angular.module('stats', []) function updateStats() { Container.stats({id: $routeParams.id}, function (d) { console.log(d); - var arr = Object.keys(d).map(function (key) {return d[key];}); + var arr = Object.keys(d).map(function (key) { + return d[key]; + }); if (arr.join('').indexOf('no such id') !== -1) { Messages.error('Unable to retrieve container stats', 'Has this container been removed?'); return; diff --git a/app/shared/filters.js b/app/shared/filters.js index f0b7c6daa..e993bc23a 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -1,12 +1,12 @@ angular.module('dockerui.filters', []) - .filter('truncate', function() { + .filter('truncate', function () { 'use strict'; - return function(text, length, end) { + return function (text, length, end) { if (isNaN(length)) { length = 10; } - if (end === undefined){ + if (end === undefined) { end = '...'; } @@ -18,9 +18,9 @@ angular.module('dockerui.filters', []) } }; }) - .filter('statusbadge', function() { + .filter('statusbadge', function () { 'use strict'; - return function(text) { + return function (text) { if (text === 'Ghost') { return 'important'; } else if (text.indexOf('Exit') !== -1 && text !== 'Exit 0') { @@ -29,9 +29,9 @@ angular.module('dockerui.filters', []) return 'success'; }; }) - .filter('getstatetext', function() { + .filter('getstatetext', function () { 'use strict'; - return function(state) { + return function (state) { if (state === undefined) { return ''; } @@ -47,9 +47,9 @@ angular.module('dockerui.filters', []) return 'Stopped'; }; }) - .filter('getstatelabel', function() { + .filter('getstatelabel', function () { 'use strict'; - return function(state) { + return function (state) { if (state === undefined) { return ''; } @@ -63,45 +63,47 @@ angular.module('dockerui.filters', []) return ''; }; }) - .filter('humansize', function() { + .filter('humansize', function () { 'use strict'; - return function(bytes) { + return function (bytes) { var sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; if (bytes === 0) { return 'n/a'; } var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); - return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[[i]]; + return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[[i]]; }; }) - .filter('containername', function() { + .filter('containername', function () { 'use strict'; - return function(container) { + return function (container) { var name = container.Names[0]; return name.substring(1, name.length); }; }) - .filter('repotag', function() { + .filter('repotag', function () { 'use strict'; - return function(image) { + return function (image) { if (image.RepoTags && image.RepoTags.length > 0) { var tag = image.RepoTags[0]; - if (tag === ':') { tag = ''; } + if (tag === ':') { + tag = ''; + } return tag; } - return ''; + return ''; }; }) - .filter('getdate', function() { + .filter('getdate', function () { 'use strict'; - return function(data) { + return function (data) { //Multiply by 1000 for the unix format var date = new Date(data * 1000); return date.toDateString(); }; }) - .filter('errorMsg', function() { - return function(object) { + .filter('errorMsg', function () { + return function (object) { var idx = 0; var msg = ''; while (object[idx] && typeof(object[idx]) === 'string') { diff --git a/app/shared/services.js b/app/shared/services.js index 41c809342..7722ba7c2 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -25,16 +25,16 @@ angular.module('dockerui.services', ['ngResource']) 'use strict'; return { commit: function (params, callback) { - $http({ - method: 'POST', - url: Settings.url + '/commit', - params: { - 'container': params.id, - 'repo': params.repo - } - }).success(callback).error(function (data, status, headers, config) { - console.log(error, data); - }); + $http({ + method: 'POST', + url: Settings.url + '/commit', + params: { + 'container': params.id, + 'repo': params.repo + } + }).success(callback).error(function (data, status, headers, config) { + console.log(error, data); + }); } }; }) @@ -80,11 +80,13 @@ angular.module('dockerui.services', ['ngResource']) get: {method: 'GET', params: {action: 'json'}}, search: {method: 'GET', params: {action: 'search'}}, history: {method: 'GET', params: {action: 'history'}, isArray: true}, - create: {method: 'POST', isArray: true, transformResponse: [function f(data) { - var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {"); - return angular.fromJson("[" + str + "]"); - }], - params: {action: 'create', fromImage: '@fromImage', repo: '@repo', tag: '@tag', registry: '@registry'}}, + create: { + method: 'POST', isArray: true, transformResponse: [function f(data) { + var str = data.replace(/\n/g, " ").replace(/\}\W*\{/g, "}, {"); + return angular.fromJson("[" + str + "]"); + }], + params: {action: 'create', fromImage: '@fromImage', repo: '@repo', tag: '@tag', registry: '@registry'} + }, insert: {method: 'POST', params: {id: '@id', action: 'insert'}}, push: {method: 'POST', params: {id: '@id', action: 'push'}}, tag: {method: 'POST', params: {id: '@id', action: 'tag', force: 0, repo: '@repo'}}, diff --git a/app/shared/viewmodel.js b/app/shared/viewmodel.js index 319214aea..617d497d5 100644 --- a/app/shared/viewmodel.js +++ b/app/shared/viewmodel.js @@ -1,4 +1,3 @@ - function ImageViewModel(data) { this.Id = data.Id; this.Tag = data.Tag; @@ -10,12 +9,12 @@ function ImageViewModel(data) { } function ContainerViewModel(data) { - this.Id = data.Id; - this.Image = data.Image; - this.Command = data.Command; - this.Created = data.Created; - this.SizeRw = data.SizeRw; - this.Status = data.Status; - this.Checked = false; - this.Names = data.Names; + this.Id = data.Id; + this.Image = data.Image; + this.Command = data.Command; + this.Created = data.Created; + this.SizeRw = data.SizeRw; + this.Status = data.Status; + this.Checked = false; + this.Names = data.Names; } diff --git a/gruntFile.js b/gruntFile.js index a81483254..3cf502473 100644 --- a/gruntFile.js +++ b/gruntFile.js @@ -11,19 +11,19 @@ module.exports = function (grunt) { grunt.loadNpmTasks('grunt-html2js'); // Default task. - grunt.registerTask('default', ['jshint','build','karma:unit']); - grunt.registerTask('build', ['clean','html2js','concat','recess:build', 'copy']); - grunt.registerTask('release', ['clean','html2js','uglify','jshint','karma:unit','concat:index', 'recess:min', 'copy']); + grunt.registerTask('default', ['jshint', 'build', 'karma:unit']); + grunt.registerTask('build', ['clean', 'html2js', 'concat', 'recess:build', 'copy']); + grunt.registerTask('release', ['clean', 'html2js', 'uglify', 'jshint', 'karma:unit', 'concat:index', 'recess:min', 'copy']); grunt.registerTask('test-watch', ['karma:watch']); // Print a timestamp (useful for when watching) - grunt.registerTask('timestamp', function() { + grunt.registerTask('timestamp', function () { grunt.log.subhead(Date()); }); - var karmaConfig = function(configFile, customOptions) { - var options = { configFile: configFile, keepalive: true }; - var travisOptions = process.env.TRAVIS && { browsers: ['Firefox'], reporters: 'dots' }; + var karmaConfig = function (configFile, customOptions) { + var options = {configFile: configFile, keepalive: true}; + var travisOptions = process.env.TRAVIS && {browsers: ['Firefox'], reporters: 'dots'}; return grunt.util._.extend(options, customOptions, travisOptions); }; @@ -31,8 +31,7 @@ module.exports = function (grunt) { grunt.initConfig({ distdir: 'dist', pkg: grunt.file.readJSON('package.json'), - banner: - '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + + banner: '/*! <%= pkg.title || pkg.name %> - v<%= pkg.version %> - <%= grunt.template.today("yyyy-mm-dd") %>\n' + '<%= pkg.homepage ? " * " + pkg.homepage + "\\n" : "" %>' + ' * Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author %>;\n' + ' * Licensed <%= _.pluck(pkg.licenses, "type").join(", ") %>\n */\n', @@ -50,12 +49,12 @@ module.exports = function (grunt) { clean: ['<%= distdir %>/*'], copy: { assets: { - files: [{ dest: '<%= distdir %>/assets', src : '**', expand: true, cwd: 'assets/' }] + files: [{dest: '<%= distdir %>/assets', src: '**', expand: true, cwd: 'assets/'}] } }, karma: { - unit: { options: karmaConfig('test/unit/karma.conf.js') }, - watch: { options: karmaConfig('test/unit/karma.conf.js', { singleRun:false, autoWatch: true}) } + unit: {options: karmaConfig('test/unit/karma.conf.js')}, + watch: {options: karmaConfig('test/unit/karma.conf.js', {singleRun: false, autoWatch: true})} }, html2js: { app: { @@ -67,14 +66,14 @@ module.exports = function (grunt) { module: '<%= pkg.name %>.templates' } }, - concat:{ - dist:{ + concat: { + dist: { options: { banner: "<%= banner %>", process: true }, - src:['<%= src.js %>', '<%= src.jsTpl %>'], - dest:'<%= distdir %>/<%= pkg.name %>.js' + src: ['<%= src.js %>', '<%= src.jsTpl %>'], + dest: '<%= distdir %>/<%= pkg.name %>.js' }, index: { src: ['index.html'], @@ -84,32 +83,32 @@ module.exports = function (grunt) { } }, angular: { - src:['assets/js/angularjs/1.3.15/angular.min.js', - 'assets/js/angularjs/1.3.15/angular-route.min.js', - 'assets/js/angularjs/1.3.15/angular-resource.min.js', - 'assets/js/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.0.min.js', - 'assets/js/angular-oboe.min.js'], + src: ['assets/js/angularjs/1.3.15/angular.min.js', + 'assets/js/angularjs/1.3.15/angular-route.min.js', + 'assets/js/angularjs/1.3.15/angular-resource.min.js', + 'assets/js/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.0.min.js', + 'assets/js/angular-oboe.min.js'], dest: '<%= distdir %>/angular.js' } }, uglify: { - dist:{ + dist: { options: { banner: "<%= banner %>" }, - src:['<%= src.js %>' ,'<%= src.jsTpl %>'], - dest:'<%= distdir %>/<%= pkg.name %>.js' + src: ['<%= src.js %>', '<%= src.jsTpl %>'], + dest: '<%= distdir %>/<%= pkg.name %>.js' }, angular: { - src:['<%= concat.angular.src %>'], + src: ['<%= concat.angular.src %>'], dest: '<%= distdir %>/angular.js' } }, recess: { build: { files: { - '<%= distdir %>/<%= pkg.name %>.css': - ['<%= src.css %>'] }, + '<%= distdir %>/<%= pkg.name %>.css': ['<%= src.css %>'] + }, options: { compile: true, noOverqualifying: false // TODO: Added because of .nav class, rename @@ -124,29 +123,29 @@ module.exports = function (grunt) { } } }, - watch:{ + watch: { all: { - files:['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], - tasks:['default','timestamp'] + files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], + tasks: ['default', 'timestamp'] }, build: { - files:['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], - tasks:['build','timestamp'] + files: ['<%= src.js %>', '<%= src.specs %>', '<%= src.css %>', '<%= src.tpl.app %>', '<%= src.tpl.common %>', '<%= src.html %>'], + tasks: ['build', 'timestamp'] } }, - jshint:{ - files:['gruntFile.js', '<%= src.js %>', '<%= src.jsTpl %>', '<%= src.specs %>', '<%= src.scenarios %>'], - options:{ - curly:true, - eqeqeq:true, - immed:true, - latedef:true, - newcap:true, - noarg:true, - sub:true, - boss:true, - eqnull:true, - globals:{ + jshint: { + files: ['gruntFile.js', '<%= src.js %>', '<%= src.jsTpl %>', '<%= src.specs %>', '<%= src.scenarios %>'], + options: { + curly: true, + eqeqeq: true, + immed: true, + latedef: true, + newcap: true, + noarg: true, + sub: true, + boss: true, + eqnull: true, + globals: { angular: false, '$': false } diff --git a/package.json b/package.json index 168ece42e..6e510053b 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,35 @@ { - "author": "Michael Crosby & Kevan Ahlquist", - "name": "dockerui", - "homepage": "https://github.com/crosbymichael/dockerui", - "version": "0.6.0", - "repository": { - "type": "git", - "url": "git@github.com:crosbymichael/dockerui.git" - }, - "bugs": { - "url": "https://github.com/crosbymichael/dockerui/issues" - }, - "licenses": [ - { - "type": "MIT", - "url": "https://raw.githubusercontent.com/crosbymichael/dockerui/master/LICENSE" - } - ], - "engines": { - "node": ">= 0.8.4" - }, - "dependencies": {}, - "devDependencies": { - "grunt": "~0.4.0", - "grunt-recess": "~0.3", - "grunt-contrib-clean": "~0.4.0", - "grunt-contrib-copy": "~0.4.0", - "grunt-contrib-jshint": "~0.2.0", - "grunt-contrib-concat": "~0.1.3", - "grunt-contrib-uglify": "~0.1.1", - "grunt-karma": "~0.4.4", - "grunt-html2js": "~0.1.0", - "grunt-contrib-watch": "~0.3.1" + "author": "Michael Crosby & Kevan Ahlquist", + "name": "dockerui", + "homepage": "https://github.com/crosbymichael/dockerui", + "version": "0.6.0", + "repository": { + "type": "git", + "url": "git@github.com:crosbymichael/dockerui.git" + }, + "bugs": { + "url": "https://github.com/crosbymichael/dockerui/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://raw.githubusercontent.com/crosbymichael/dockerui/master/LICENSE" } + ], + "engines": { + "node": ">= 0.8.4" + }, + "dependencies": {}, + "devDependencies": { + "grunt": "~0.4.0", + "grunt-recess": "~0.3", + "grunt-contrib-clean": "~0.4.0", + "grunt-contrib-copy": "~0.4.0", + "grunt-contrib-jshint": "~0.2.0", + "grunt-contrib-concat": "~0.1.3", + "grunt-contrib-uglify": "~0.1.1", + "grunt-karma": "~0.4.4", + "grunt-html2js": "~0.1.0", + "grunt-contrib-watch": "~0.3.1" + } } diff --git a/test/unit/app/components/startContainerController.spec.js b/test/unit/app/components/startContainerController.spec.js index 5d7b11c2a..57d898de1 100644 --- a/test/unit/app/components/startContainerController.spec.js +++ b/test/unit/app/components/startContainerController.spec.js @@ -1,19 +1,19 @@ -describe('startContainerController', function() { +describe('startContainerController', function () { var scope, $location, createController, mockContainer, $httpBackend; beforeEach(angular.mock.module('dockerui')); - beforeEach(inject(function($rootScope, $controller, _$location_) { + beforeEach(inject(function ($rootScope, $controller, _$location_) { $location = _$location_; scope = $rootScope.$new(); - createController = function() { + createController = function () { return $controller('StartContainerController', { '$scope': scope }); }; - angular.mock.inject(function(_Container_, _$httpBackend_) { + angular.mock.inject(function (_Container_, _$httpBackend_) { mockContainer = _Container_; $httpBackend = _$httpBackend_; }); @@ -34,8 +34,9 @@ describe('startContainerController', function() { 'Status': 'Up 2 minutes' }]); } - describe('Create and start a container with port bindings', function() { - it('should issue a correct create request to the Docker remote API', function() { + + describe('Create and start a container with port bindings', function () { + it('should issue a correct create request to the Docker remote API', function () { var controller = createController(); var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; var expectedBody = { @@ -76,8 +77,8 @@ describe('startContainerController', function() { }); }); - describe('Create and start a container with environment variables', function() { - it('should issue a correct create request to the Docker remote API', function() { + describe('Create and start a container with environment variables', function () { + it('should issue a correct create request to the Docker remote API', function () { var controller = createController(); var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; var expectedBody = { @@ -110,8 +111,8 @@ describe('startContainerController', function() { }); }); - describe('Create and start a container with volumesFrom', function() { - it('should issue a correct create request to the Docker remote API', function() { + describe('Create and start a container with volumesFrom', function () { + it('should issue a correct create request to the Docker remote API', function () { var controller = createController(); var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; var expectedBody = { @@ -133,15 +134,15 @@ describe('startContainerController', function() { }); scope.config.name = 'container-name'; - scope.config.HostConfig.VolumesFrom = [{name: 'parent'}, {name:'other:ro'}]; + scope.config.HostConfig.VolumesFrom = [{name: 'parent'}, {name: 'other:ro'}]; scope.create(); $httpBackend.flush(); }); }); - - describe('Create and start a container with multiple options', function() { - it('should issue a correct create request to the Docker remote API', function() { + + describe('Create and start a container with multiple options', function () { + it('should issue a correct create request to the Docker remote API', function () { var controller = createController(); var id = '6abd8bfba81cf8a05a76a4bdefcb36c4b66cd02265f4bfcd0e236468696ebc6c'; var expectedBody = { @@ -154,8 +155,12 @@ describe('startContainerController', function() { DnsSearch: ['example.com'], CapAdd: ['cap_sys_admin'], CapDrop: ['cap_foo_bar'], - Devices: [{ 'PathOnHost': '/dev/deviceName', 'PathInContainer': '/dev/deviceName', 'CgroupPermissions': 'mrw'}], - LxcConf: {'lxc.utsname':'docker'}, + Devices: [{ + 'PathOnHost': '/dev/deviceName', + 'PathInContainer': '/dev/deviceName', + 'CgroupPermissions': 'mrw' + }], + LxcConf: {'lxc.utsname': 'docker'}, ExtraHosts: ['hostname:127.0.0.1'], RestartPolicy: {name: 'always', MaximumRetryCount: 5} }, @@ -190,7 +195,11 @@ describe('startContainerController', function() { scope.config.HostConfig.PublishAllPorts = true; scope.config.HostConfig.Privileged = true; scope.config.HostConfig.RestartPolicy = {name: 'always', MaximumRetryCount: 5}; - scope.config.HostConfig.Devices = [{ 'PathOnHost': '/dev/deviceName', 'PathInContainer': '/dev/deviceName', 'CgroupPermissions': 'mrw'}]; + scope.config.HostConfig.Devices = [{ + 'PathOnHost': '/dev/deviceName', + 'PathInContainer': '/dev/deviceName', + 'CgroupPermissions': 'mrw' + }]; scope.config.HostConfig.LxcConf = [{name: 'lxc.utsname', value: 'docker'}]; scope.config.HostConfig.ExtraHosts = [{host: 'hostname', ip: '127.0.0.1'}]; diff --git a/test/unit/app/shared/filters.spec.js b/test/unit/app/shared/filters.spec.js index f48640783..48a0f54bc 100644 --- a/test/unit/app/shared/filters.spec.js +++ b/test/unit/app/shared/filters.spec.js @@ -2,40 +2,40 @@ describe('filters', function () { beforeEach(module('dockerui.filters')); describe('truncate', function () { - it('should truncate the string to 10 characters ending in "..." by default', inject(function(truncateFilter) { + it('should truncate the string to 10 characters ending in "..." by default', inject(function (truncateFilter) { expect(truncateFilter('this is 20 chars long')).toBe('this is...'); })); - it('should truncate the string to 7 characters ending in "..."', inject(function(truncateFilter) { + it('should truncate the string to 7 characters ending in "..."', inject(function (truncateFilter) { expect(truncateFilter('this is 20 chars long', 7)).toBe('this...'); })); - it('should truncate the string to 10 characters ending in "???"', inject(function(truncateFilter) { + it('should truncate the string to 10 characters ending in "???"', inject(function (truncateFilter) { expect(truncateFilter('this is 20 chars long', 10, '???')).toBe('this is???'); })); }); describe('statusbadge', function () { - it('should be "important" when input is "Ghost"', inject(function(statusbadgeFilter) { + it('should be "important" when input is "Ghost"', inject(function (statusbadgeFilter) { expect(statusbadgeFilter('Ghost')).toBe('important'); })); - it('should be "success" when input is "Exit 0"', inject(function(statusbadgeFilter) { + it('should be "success" when input is "Exit 0"', inject(function (statusbadgeFilter) { expect(statusbadgeFilter('Exit 0')).toBe('success'); })); - it('should be "warning" when exit code is non-zero', inject(function(statusbadgeFilter) { + it('should be "warning" when exit code is non-zero', inject(function (statusbadgeFilter) { expect(statusbadgeFilter('Exit 1')).toBe('warning'); })); }); describe('getstatetext', function () { - it('should return an empty string when state is undefined', inject(function(getstatetextFilter) { + it('should return an empty string when state is undefined', inject(function (getstatetextFilter) { expect(getstatetextFilter(undefined)).toBe(''); })); - it('should detect a Ghost state', inject(function(getstatetextFilter) { + it('should detect a Ghost state', inject(function (getstatetextFilter) { var state = { Ghost: true, Running: true, @@ -44,7 +44,7 @@ describe('filters', function () { expect(getstatetextFilter(state)).toBe('Ghost'); })); - it('should detect a Paused state', inject(function(getstatetextFilter) { + it('should detect a Paused state', inject(function (getstatetextFilter) { var state = { Ghost: false, Running: true, @@ -53,7 +53,7 @@ describe('filters', function () { expect(getstatetextFilter(state)).toBe('Running (Paused)'); })); - it('should detect a Running state', inject(function(getstatetextFilter) { + it('should detect a Running state', inject(function (getstatetextFilter) { var state = { Ghost: false, Running: true, @@ -62,7 +62,7 @@ describe('filters', function () { expect(getstatetextFilter(state)).toBe('Running'); })); - it('should detect a Stopped state', inject(function(getstatetextFilter) { + it('should detect a Stopped state', inject(function (getstatetextFilter) { var state = { Ghost: false, Running: false, @@ -73,11 +73,11 @@ describe('filters', function () { }); describe('getstatelabel', function () { - it('should return an empty string when state is undefined', inject(function(getstatelabelFilter) { + it('should return an empty string when state is undefined', inject(function (getstatelabelFilter) { expect(getstatelabelFilter(undefined)).toBe(''); })); - it('should return label-important when a ghost state is detected', inject(function(getstatelabelFilter) { + it('should return label-important when a ghost state is detected', inject(function (getstatelabelFilter) { var state = { Ghost: true, Running: true, @@ -86,7 +86,7 @@ describe('filters', function () { expect(getstatelabelFilter(state)).toBe('label-important'); })); - it('should return label-success when a running state is detected', inject(function(getstatelabelFilter) { + it('should return label-success when a running state is detected', inject(function (getstatelabelFilter) { var state = { Ghost: false, Running: true, @@ -97,33 +97,33 @@ describe('filters', function () { }); describe('humansize', function () { - it('should return n/a when size is zero', inject(function(humansizeFilter) { + it('should return n/a when size is zero', inject(function (humansizeFilter) { expect(humansizeFilter(0)).toBe('n/a'); })); - it('should handle Bytes values', inject(function(humansizeFilter) { + it('should handle Bytes values', inject(function (humansizeFilter) { expect(humansizeFilter(512)).toBe('512 Bytes'); })); - it('should handle KB values', inject(function(humansizeFilter) { + it('should handle KB values', inject(function (humansizeFilter) { expect(humansizeFilter(5120)).toBe('5 KB'); })); - it('should handle MB values', inject(function(humansizeFilter) { + it('should handle MB values', inject(function (humansizeFilter) { expect(humansizeFilter(5 * Math.pow(10, 6))).toBe('5 MB'); })); - it('should handle GB values', inject(function(humansizeFilter) { + it('should handle GB values', inject(function (humansizeFilter) { expect(humansizeFilter(5 * Math.pow(10, 9))).toBe('5 GB'); })); - it('should handle TB values', inject(function(humansizeFilter) { + it('should handle TB values', inject(function (humansizeFilter) { expect(humansizeFilter(5 * Math.pow(10, 12))).toBe('5 TB'); })); }); describe('containername', function () { - it('should strip the leading slash from container name', inject(function(containernameFilter) { + it('should strip the leading slash from container name', inject(function (containernameFilter) { var container = { Names: ['/elegant_ardinghelli'] }; @@ -133,14 +133,14 @@ describe('filters', function () { }); describe('repotag', function () { - it('should not display empty repo tag', inject(function(repotagFilter) { + it('should not display empty repo tag', inject(function (repotagFilter) { var image = { RepoTags: [':'] }; expect(repotagFilter(image)).toBe(''); })); - it('should display a normal repo tag', inject(function(repotagFilter) { + it('should display a normal repo tag', inject(function (repotagFilter) { var image = { RepoTags: ['ubuntu:latest'] }; @@ -149,17 +149,207 @@ describe('filters', function () { }); describe('getdate', function () { - it('should convert the Docker date to a human readable form', inject(function(getdateFilter) { + it('should convert the Docker date to a human readable form', inject(function (getdateFilter) { expect(getdateFilter(1420424998)).toBe('Sun Jan 04 2015'); })); }); - describe('errorMsgFilter', function() { + describe('errorMsgFilter', function () { it('should convert the $resource object to a string message', - inject(function(errorMsgFilter) { - var response = {'0':'C','1':'o','2':'n','3':'f','4':'l','5':'i','6':'c','7':'t','8':',','9':' ','10':'T','11':'h','12':'e','13':' ','14':'n','15':'a','16':'m','17':'e','18':' ','19':'u','20':'b','21':'u','22':'n','23':'t','24':'u','25':'-','26':'s','27':'l','28':'e','29':'e','30':'p','31':'-','32':'r','33':'u','34':'n','35':'t','36':'i','37':'m','38':'e','39':' ','40':'i','41':'s','42':' ','43':'a','44':'l','45':'r','46':'e','47':'a','48':'d','49':'y','50':' ','51':'a','52':'s','53':'s','54':'i','55':'g','56':'n','57':'e','58':'d','59':' ','60':'t','61':'o','62':' ','63':'b','64':'6','65':'9','66':'e','67':'5','68':'3','69':'a','70':'6','71':'2','72':'2','73':'c','74':'8','75':'.','76':' ','77':'Y','78':'o','79':'u','80':' ','81':'h','82':'a','83':'v','84':'e','85':' ','86':'t','87':'o','88':' ','89':'d','90':'e','91':'l','92':'e','93':'t','94':'e','95':' ','96':'(','97':'o','98':'r','99':' ','100':'r','101':'e','102':'n','103':'a','104':'m','105':'e','106':')','107':' ','108':'t','109':'h','110':'a','111':'t','112':' ','113':'c','114':'o','115':'n','116':'t','117':'a','118':'i','119':'n','120':'e','121':'r','122':' ','123':'t','124':'o','125':' ','126':'b','127':'e','128':' ','129':'a','130':'b','131':'l','132':'e','133':' ','134':'t','135':'o','136':' ','137':'a','138':'s','139':'s','140':'i','141':'g','142':'n','143':' ','144':'u','145':'b','146':'u','147':'n','148':'t','149':'u','150':'-','151':'s','152':'l','153':'e','154':'e','155':'p','156':'-','157':'r','158':'u','159':'n','160':'t','161':'i','162':'m','163':'e','164':' ','165':'t','166':'o','167':' ','168':'a','169':' ','170':'c','171':'o','172':'n','173':'t','174':'a','175':'i','176':'n','177':'e','178':'r','179':' ','180':'a','181':'g','182':'a','183':'i','184':'n','185':'.','186':'\n','$promise':{},'$resolved':true}; + inject(function (errorMsgFilter) { + var response = { + '0': 'C', + '1': 'o', + '2': 'n', + '3': 'f', + '4': 'l', + '5': 'i', + '6': 'c', + '7': 't', + '8': ',', + '9': ' ', + '10': 'T', + '11': 'h', + '12': 'e', + '13': ' ', + '14': 'n', + '15': 'a', + '16': 'm', + '17': 'e', + '18': ' ', + '19': 'u', + '20': 'b', + '21': 'u', + '22': 'n', + '23': 't', + '24': 'u', + '25': '-', + '26': 's', + '27': 'l', + '28': 'e', + '29': 'e', + '30': 'p', + '31': '-', + '32': 'r', + '33': 'u', + '34': 'n', + '35': 't', + '36': 'i', + '37': 'm', + '38': 'e', + '39': ' ', + '40': 'i', + '41': 's', + '42': ' ', + '43': 'a', + '44': 'l', + '45': 'r', + '46': 'e', + '47': 'a', + '48': 'd', + '49': 'y', + '50': ' ', + '51': 'a', + '52': 's', + '53': 's', + '54': 'i', + '55': 'g', + '56': 'n', + '57': 'e', + '58': 'd', + '59': ' ', + '60': 't', + '61': 'o', + '62': ' ', + '63': 'b', + '64': '6', + '65': '9', + '66': 'e', + '67': '5', + '68': '3', + '69': 'a', + '70': '6', + '71': '2', + '72': '2', + '73': 'c', + '74': '8', + '75': '.', + '76': ' ', + '77': 'Y', + '78': 'o', + '79': 'u', + '80': ' ', + '81': 'h', + '82': 'a', + '83': 'v', + '84': 'e', + '85': ' ', + '86': 't', + '87': 'o', + '88': ' ', + '89': 'd', + '90': 'e', + '91': 'l', + '92': 'e', + '93': 't', + '94': 'e', + '95': ' ', + '96': '(', + '97': 'o', + '98': 'r', + '99': ' ', + '100': 'r', + '101': 'e', + '102': 'n', + '103': 'a', + '104': 'm', + '105': 'e', + '106': ')', + '107': ' ', + '108': 't', + '109': 'h', + '110': 'a', + '111': 't', + '112': ' ', + '113': 'c', + '114': 'o', + '115': 'n', + '116': 't', + '117': 'a', + '118': 'i', + '119': 'n', + '120': 'e', + '121': 'r', + '122': ' ', + '123': 't', + '124': 'o', + '125': ' ', + '126': 'b', + '127': 'e', + '128': ' ', + '129': 'a', + '130': 'b', + '131': 'l', + '132': 'e', + '133': ' ', + '134': 't', + '135': 'o', + '136': ' ', + '137': 'a', + '138': 's', + '139': 's', + '140': 'i', + '141': 'g', + '142': 'n', + '143': ' ', + '144': 'u', + '145': 'b', + '146': 'u', + '147': 'n', + '148': 't', + '149': 'u', + '150': '-', + '151': 's', + '152': 'l', + '153': 'e', + '154': 'e', + '155': 'p', + '156': '-', + '157': 'r', + '158': 'u', + '159': 'n', + '160': 't', + '161': 'i', + '162': 'm', + '163': 'e', + '164': ' ', + '165': 't', + '166': 'o', + '167': ' ', + '168': 'a', + '169': ' ', + '170': 'c', + '171': 'o', + '172': 'n', + '173': 't', + '174': 'a', + '175': 'i', + '176': 'n', + '177': 'e', + '178': 'r', + '179': ' ', + '180': 'a', + '181': 'g', + '182': 'a', + '183': 'i', + '184': 'n', + '185': '.', + '186': '\n', + '$promise': {}, + '$resolved': true + }; var message = 'Conflict, The name ubuntu-sleep-runtime is already assigned to b69e53a622c8. You have to delete (or rename) that container to be able to assign ubuntu-sleep-runtime to a container again.\n'; expect(errorMsgFilter(response)).toBe(message); - })); + })); }); }); \ No newline at end of file diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js index 31230fda2..805ccefca 100644 --- a/test/unit/karma.conf.js +++ b/test/unit/karma.conf.js @@ -3,19 +3,19 @@ basePath = '../..'; // list of files / patterns to load in the browser files = [ - JASMINE, - JASMINE_ADAPTER, - 'assets/js/jquery-1.11.1.min.js', - 'assets/js/jquery.gritter.min.js', - 'assets/js/bootstrap.min.js', - 'assets/js/spin.js', - 'dist/angular.js', - 'assets/js/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.0.min.js', - 'assets/js/angular-vis.js', - 'test/assets/angular/angular-mocks.js', - 'app/**/*.js', - 'test/unit/**/*.spec.js', - 'dist/templates/**/*.js' + JASMINE, + JASMINE_ADAPTER, + 'assets/js/jquery-1.11.1.min.js', + 'assets/js/jquery.gritter.min.js', + 'assets/js/bootstrap.min.js', + 'assets/js/spin.js', + 'dist/angular.js', + 'assets/js/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.0.min.js', + 'assets/js/angular-vis.js', + 'test/assets/angular/angular-mocks.js', + 'app/**/*.js', + 'test/unit/**/*.spec.js', + 'dist/templates/**/*.js' ]; // use dots reporter, as travis terminal does not support escaping sequences From 6cb658a1ef721245e623f96b1682ecbc5abd6e21 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Tue, 25 Aug 2015 01:07:08 -0500 Subject: [PATCH 03/10] Fixed grunt style warnings. --- .../containersNetwork/containersNetworkController.js | 2 +- app/components/pullImage/pullImageController.js | 6 +++--- app/components/stats/statsController.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/components/containersNetwork/containersNetworkController.js b/app/components/containersNetwork/containersNetworkController.js index 307386cc2..26546afb2 100644 --- a/app/components/containersNetwork/containersNetworkController.js +++ b/app/components/containersNetwork/containersNetworkController.js @@ -50,7 +50,7 @@ angular.module('containersNetwork', ['ngVis']) this.hasEdge = function (from, to) { return this.edges.getIds({ filter: function (item) { - return item.from == from.Id && item.to == to.Id; + return item.from === from.Id && item.to === to.Id; } }).length > 0; }; diff --git a/app/components/pullImage/pullImageController.js b/app/components/pullImage/pullImageController.js index 32990689c..48e05f8c4 100644 --- a/app/components/pullImage/pullImageController.js +++ b/app/components/pullImage/pullImageController.js @@ -9,8 +9,8 @@ angular.module('pullImage', []) repo: '', fromImage: '', tag: 'latest' - } - } + }; + }; $scope.init(); @@ -52,5 +52,5 @@ angular.module('pullImage', []) $('#pull-modal').modal('show'); $('#error-message').show(); }); - } + }; }]); diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index 7a9e8412d..c4beed03c 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -46,7 +46,7 @@ angular.module('stats', []) if (systemDelta > 0.0 && cpuDelta > 0.0) { cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.size() * 100.0; } - return cpuPercent + return cpuPercent; }; function updateChart(data) { From d243a83c5c675c8537b54997dcd2d2088e4c5cdd Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Wed, 26 Aug 2015 01:28:08 -0500 Subject: [PATCH 04/10] Implemented CPU usage graph, upgraded to Chart.js 1.0.2, broke dashboard charts, tests. --- app/components/stats/stats.html | 22 +++------- app/components/stats/statsController.js | 58 +++++++++++++++---------- app/shared/services.js | 2 +- assets/js/Chart.min.js | 50 +++++---------------- test/unit/karma.conf.js | 1 + 5 files changed, 53 insertions(+), 80 deletions(-) diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html index 6b0b6ed77..7b764e008 100644 --- a/app/components/stats/stats.html +++ b/app/components/stats/stats.html @@ -1,21 +1,9 @@
-

Stats

- -
- - - - - - - - - - -
Time readCPU usageStat
- - -
+

Stats

+

CPU Usage

+ + +

Memory

diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index c4beed03c..e77424f73 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -1,36 +1,53 @@ angular.module('stats', []) - .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', 'LineChart', '$routeParams', function (Settings, $scope, Messages, $timeout, Container, LineChart, $routeParams) { - var sessionKey = 'dockeruiStats-' + $routeParams.id; - var localData = sessionStorage.getItem(sessionKey); - if (localData) { - $scope.dockerStats = localData; - } else { - $scope.dockerStats = []; + .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', function (Settings, $scope, Messages, $timeout, Container, $routeParams) { + // TODO: Implement memory chart, force scale to 0-100 for cpu, 0 to limit for memory, fix charts on dashboard + + var labels = []; + var data = []; + for (var i = 0; i < 40; i ++) { + labels.push(''); + data.push(0); } + var dataset = { // CPU Usage + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + data: data + }; + + var chart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({ + labels: labels, + datasets: [dataset] + }); function updateStats() { Container.stats({id: $routeParams.id}, function (d) { - console.log(d); var arr = Object.keys(d).map(function (key) { return d[key]; }); if (arr.join('').indexOf('no such id') !== -1) { - Messages.error('Unable to retrieve container stats', 'Has this container been removed?'); + Messages.error('Unable to retrieve stats', 'Is this container running?'); return; } - $scope.dockerStats.push(d); - sessionStorage.setItem(sessionKey, $scope.dockerStats); - $timeout(updateStats, 1000); + // Update graph with latest data - updateChart($scope.dockerStats); + updateChart(d); + $timeout(updateStats, 1000); // TODO: Switch to setInterval for more consistent readings }, function () { - Messages.error('Unable to retrieve container stats', 'Has this container been removed?'); + Messages.error('Unable to retrieve stats', 'Is this container running?'); }); } updateStats(); + function updateChart(data) { + console.log('updateChart', data); + chart.addData([$scope.calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString()); + chart.removeData(); + } + $scope.calculateCPUPercent = function (stats) { // Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208 var prevCpu = stats.precpu_stats; @@ -44,15 +61,10 @@ angular.module('stats', []) var systemDelta = curCpu.system_cpu_usage - prevCpu.system_cpu_usage; if (systemDelta > 0.0 && cpuDelta > 0.0) { - cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.size() * 100.0; + //console.log('size thing:', curCpu.cpu_usage.percpu_usage); + cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0; } - return cpuPercent; + return Math.random() * 100; + //return cpuPercent; TODO: Switch back to the real value }; - - function updateChart(data) { - // TODO: Build data in the right format and create chart. - //LineChart.build('#cpu-stats-chart', $scope.dockerStats, function (d) { - // return $scope.calculateCPUPercent(d) - //}); - } }]); \ No newline at end of file diff --git a/app/shared/services.js b/app/shared/services.js index 7722ba7c2..a92b73a94 100644 --- a/app/shared/services.js +++ b/app/shared/services.js @@ -18,7 +18,7 @@ angular.module('dockerui.services', ['ngResource']) create: {method: 'POST', params: {action: 'create'}}, remove: {method: 'DELETE', params: {id: '@id', v: 0}}, rename: {method: 'POST', params: {id: '@id', action: 'rename'}, isArray: false}, - stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}} + stats: {method: 'GET', params: {id: '@id', stream: false, action: 'stats'}, timeout: 2000} }); }) .factory('ContainerCommit', function ($resource, $http, Settings) { diff --git a/assets/js/Chart.min.js b/assets/js/Chart.min.js index ab6358810..3a0a2c873 100644 --- a/assets/js/Chart.min.js +++ b/assets/js/Chart.min.js @@ -1,39 +1,11 @@ -var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=g)D(h);else if("function"==typeof a.onAnimationComplete)a.onAnimationComplete()}var f=a.animation?1/A(a.animationSteps,Number.MAX_VALUE,1):1,d=B[a.animationEasing],g=a.animation?0:1;"function"!==typeof c&&(c=function(){});D(h)}function C(a,c,b,e,h,f){var d;a= -Math.floor(Math.log(e-h)/Math.LN10);h=Math.floor(h/(1*Math.pow(10,a)))*Math.pow(10,a);e=Math.ceil(e/(1*Math.pow(10,a)))*Math.pow(10,a)-h;a=Math.pow(10,a);for(d=Math.round(e/a);dc;)a=dc?c:!isNaN(parseFloat(b))&& -isFinite(b)&&a)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split("\t").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');");return c? -b(c):b}var r=this,B={linear:function(a){return a},easeInQuad:function(a){return a*a},easeOutQuad:function(a){return-1*a*(a-2)},easeInOutQuad:function(a){return 1>(a/=0.5)?0.5*a*a:-0.5*(--a*(a-2)-1)},easeInCubic:function(a){return a*a*a},easeOutCubic:function(a){return 1*((a=a/1-1)*a*a+1)},easeInOutCubic:function(a){return 1>(a/=0.5)?0.5*a*a*a:0.5*((a-=2)*a*a+2)},easeInQuart:function(a){return a*a*a*a},easeOutQuart:function(a){return-1*((a=a/1-1)*a*a*a-1)},easeInOutQuart:function(a){return 1>(a/=0.5)? -0.5*a*a*a*a:-0.5*((a-=2)*a*a*a-2)},easeInQuint:function(a){return 1*(a/=1)*a*a*a*a},easeOutQuint:function(a){return 1*((a=a/1-1)*a*a*a*a+1)},easeInOutQuint:function(a){return 1>(a/=0.5)?0.5*a*a*a*a*a:0.5*((a-=2)*a*a*a*a+2)},easeInSine:function(a){return-1*Math.cos(a/1*(Math.PI/2))+1},easeOutSine:function(a){return 1*Math.sin(a/1*(Math.PI/2))},easeInOutSine:function(a){return-0.5*(Math.cos(Math.PI*a/1)-1)},easeInExpo:function(a){return 0==a?1:1*Math.pow(2,10*(a/1-1))},easeOutExpo:function(a){return 1== -a?1:1*(-Math.pow(2,-10*a/1)+1)},easeInOutExpo:function(a){return 0==a?0:1==a?1:1>(a/=0.5)?0.5*Math.pow(2,10*(a-1)):0.5*(-Math.pow(2,-10*--a)+2)},easeInCirc:function(a){return 1<=a?a:-1*(Math.sqrt(1-(a/=1)*a)-1)},easeOutCirc:function(a){return 1*Math.sqrt(1-(a=a/1-1)*a)},easeInOutCirc:function(a){return 1>(a/=0.5)?-0.5*(Math.sqrt(1-a*a)-1):0.5*(Math.sqrt(1-(a-=2)*a)+1)},easeInElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(1==(a/=1))return 1;b||(b=0.3);ea?-0.5*e*Math.pow(2,10* -(a-=1))*Math.sin((1*a-c)*2*Math.PI/b):0.5*e*Math.pow(2,-10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInBack:function(a){return 1*(a/=1)*a*(2.70158*a-1.70158)},easeOutBack:function(a){return 1*((a=a/1-1)*a*(2.70158*a+1.70158)+1)},easeInOutBack:function(a){var c=1.70158;return 1>(a/=0.5)?0.5*a*a*(((c*=1.525)+1)*a-c):0.5*((a-=2)*a*(((c*=1.525)+1)*a+c)+2)},easeInBounce:function(a){return 1-B.easeOutBounce(1-a)},easeOutBounce:function(a){return(a/=1)<1/2.75?1*7.5625*a*a:a<2/2.75?1*(7.5625*(a-=1.5/2.75)* -a+0.75):a<2.5/2.75?1*(7.5625*(a-=2.25/2.75)*a+0.9375):1*(7.5625*(a-=2.625/2.75)*a+0.984375)},easeInOutBounce:function(a){return 0.5>a?0.5*B.easeInBounce(2*a):0.5*B.easeOutBounce(2*a-1)+0.5}},q=s.canvas.width,u=s.canvas.height;window.devicePixelRatio&&(s.canvas.style.width=q+"px",s.canvas.style.height=u+"px",s.canvas.height=u*window.devicePixelRatio,s.canvas.width=q*window.devicePixelRatio,s.scale(window.devicePixelRatio,window.devicePixelRatio));this.PolarArea=function(a,c){r.PolarArea.defaults={scaleOverlay:!0, -scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce", -animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.PolarArea.defaults,c):r.PolarArea.defaults;return new G(a,b,s)};this.Radar=function(a,c){r.Radar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleShowLine:!0,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!1,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)", -scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,angleShowLineOut:!0,angleLineColor:"rgba(0,0,0,.1)",angleLineWidth:1,pointLabelFontFamily:"'Arial'",pointLabelFontStyle:"normal",pointLabelFontSize:12,pointLabelFontColor:"#666",pointDot:!0,pointDotRadius:3,pointDotStrokeWidth:1,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Radar.defaults,c):r.Radar.defaults;return new H(a,b,s)};this.Pie=function(a, -c){r.Pie.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,onAnimationComplete:null};var b=c?y(r.Pie.defaults,c):r.Pie.defaults;return new I(a,b,s)};this.Doughnut=function(a,c){r.Doughnut.defaults={segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,percentageInnerCutout:50,animation:!0,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1, -onAnimationComplete:null};var b=c?y(r.Doughnut.defaults,c):r.Doughnut.defaults;return new J(a,b,s)};this.Line=function(a,c){r.Line.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,bezierCurve:!0, -pointDot:!0,pointDotRadius:4,pointDotStrokeWidth:2,datasetStroke:!0,datasetStrokeWidth:2,datasetFill:!0,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Line.defaults,c):r.Line.defaults;return new K(a,b,s)};this.Bar=function(a,c){r.Bar.defaults={scaleOverlay:!1,scaleOverride:!1,scaleSteps:null,scaleStepWidth:null,scaleStartValue:null,scaleLineColor:"rgba(0,0,0,.1)",scaleLineWidth:1,scaleShowLabels:!0,scaleLabel:"<%=value%>",scaleFontFamily:"'Arial'", -scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,animation:!0,animationSteps:60,animationEasing:"easeOutQuart",onAnimationComplete:null};var b=c?y(r.Bar.defaults,c):r.Bar.defaults;return new L(a,b,s)};var G=function(a,c,b){var e,h,f,d,g,k,j,l,m;g=Math.min.apply(Math,[q,u])/2;g-=Math.max.apply(Math,[0.5*c.scaleFontSize,0.5*c.scaleLineWidth]); -d=2*c.scaleFontSize;c.scaleShowLabelBackdrop&&(d+=2*c.scaleBackdropPaddingY,g-=1.5*c.scaleBackdropPaddingY);l=g;d=d?d:5;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;fe&&(e=a[f].value),a[f].valuel&&(l=h);g-=Math.max.apply(Math,[l,1.5*(c.pointLabelFontSize/2)]);g-=c.pointLabelFontSize;l=g=A(g,null,0);d=d?d:5;e=Number.MIN_VALUE; -h=Number.MAX_VALUE;for(f=0;fe&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]Math.PI?"right":"left";b.textBaseline="middle";b.fillText(a.labels[d],f,-h)}b.restore()},function(d){var e=2*Math.PI/a.datasets[0].data.length;b.save();b.translate(q/2,u/2);for(var g=0;gt?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]d?h:d;d+=10}r=q-d-t;m=Math.floor(r/(a.labels.length-1));n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0t?e:t;q/a.labels.lengthe&&(e=a.datasets[f].data[l]),a.datasets[f].data[l]< -h&&(h=a.datasets[f].data[l]);f=Math.floor(g/(0.66*d));d=Math.floor(0.5*(g/d));l=c.scaleShowLabels?c.scaleLabel:"";c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(l,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(g,f,d,e,h,l);k=Math.floor(g/j.steps);d=1;if(c.scaleShowLabels){b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;for(e=0;ed?h:d;d+=10}r=q-d-t;m= -Math.floor(r/a.labels.length);s=(m-2*c.scaleGridLineWidth-2*c.barValueSpacing-(c.barDatasetSpacing*a.datasets.length-1)-(c.barStrokeWidth/2*a.datasets.length-1))/a.datasets.length;n=q-t/2-r;p=g+c.scaleFontSize/2;x(c,function(){b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(q-t/2+5,p);b.lineTo(q-t/2-r-5,p);b.stroke();0",scaleIntegersOnly:!0,scaleBeginAtZero:!1,scaleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",scaleFontSize:12,scaleFontStyle:"normal",scaleFontColor:"#666",responsive:!1,maintainAspectRatio:!0,showTooltips:!0,customTooltips:!1,tooltipEvents:["mousemove","touchstart","touchmove","mouseout"],tooltipFillColor:"rgba(0,0,0,0.8)",tooltipFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipFontSize:14,tooltipFontStyle:"normal",tooltipFontColor:"#fff",tooltipTitleFontFamily:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",tooltipTitleFontSize:14,tooltipTitleFontStyle:"bold",tooltipTitleFontColor:"#fff",tooltipYPadding:6,tooltipXPadding:6,tooltipCaretSize:8,tooltipCornerRadius:6,tooltipXOffset:10,tooltipTemplate:"<%if (label){%><%=label%>: <%}%><%= value %>",multiTooltipTemplate:"<%= value %>",multiTooltipKeyBackground:"#fff",onAnimationProgress:function(){},onAnimationComplete:function(){}}},e.types={};var s=e.helpers={},n=s.each=function(t,i,e){var s=Array.prototype.slice.call(arguments,3);if(t)if(t.length===+t.length){var n;for(n=0;n=0;s--){var n=t[s];if(i(n))return n}},s.inherits=function(t){var i=this,e=t&&t.hasOwnProperty("constructor")?t.constructor:function(){return i.apply(this,arguments)},s=function(){this.constructor=e};return s.prototype=i.prototype,e.prototype=new s,e.extend=r,t&&a(e.prototype,t),e.__super__=i.prototype,e}),c=s.noop=function(){},u=s.uid=function(){var t=0;return function(){return"chart-"+t++}}(),d=s.warn=function(t){window.console&&"function"==typeof window.console.warn&&console.warn(t)},p=s.amd="function"==typeof define&&define.amd,f=s.isNumber=function(t){return!isNaN(parseFloat(t))&&isFinite(t)},g=s.max=function(t){return Math.max.apply(Math,t)},m=s.min=function(t){return Math.min.apply(Math,t)},v=(s.cap=function(t,i,e){if(f(i)){if(t>i)return i}else if(f(e)&&e>t)return e;return t},s.getDecimalPlaces=function(t){return t%1!==0&&f(t)?t.toString().split(".")[1].length:0}),S=s.radians=function(t){return t*(Math.PI/180)},x=(s.getAngleFromPoint=function(t,i){var e=i.x-t.x,s=i.y-t.y,n=Math.sqrt(e*e+s*s),o=2*Math.PI+Math.atan2(s,e);return 0>e&&0>s&&(o+=2*Math.PI),{angle:o,distance:n}},s.aliasPixel=function(t){return t%2===0?0:.5}),y=(s.splineCurve=function(t,i,e,s){var n=Math.sqrt(Math.pow(i.x-t.x,2)+Math.pow(i.y-t.y,2)),o=Math.sqrt(Math.pow(e.x-i.x,2)+Math.pow(e.y-i.y,2)),a=s*n/(n+o),h=s*o/(n+o);return{inner:{x:i.x-a*(e.x-t.x),y:i.y-a*(e.y-t.y)},outer:{x:i.x+h*(e.x-t.x),y:i.y+h*(e.y-t.y)}}},s.calculateOrderOfMagnitude=function(t){return Math.floor(Math.log(t)/Math.LN10)}),C=(s.calculateScaleRange=function(t,i,e,s,n){var o=2,a=Math.floor(i/(1.5*e)),h=o>=a,l=g(t),r=m(t);l===r&&(l+=.5,r>=.5&&!s?r-=.5:l+=.5);for(var c=Math.abs(l-r),u=y(c),d=Math.ceil(l/(1*Math.pow(10,u)))*Math.pow(10,u),p=s?0:Math.floor(r/(1*Math.pow(10,u)))*Math.pow(10,u),f=d-p,v=Math.pow(10,u),S=Math.round(f/v);(S>a||a>2*S)&&!h;)if(S>a)v*=2,S=Math.round(f/v),S%1!==0&&(h=!0);else if(n&&u>=0){if(v/2%1!==0)break;v/=2,S=Math.round(f/v)}else v/=2,S=Math.round(f/v);return h&&(S=o,v=f/S),{steps:S,stepValue:v,min:p,max:p+S*v}},s.template=function(t,i){function e(t,i){var e=/\W/.test(t)?new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+t.replace(/[\r\t\n]/g," ").split("<%").join(" ").replace(/((^|%>)[^\t]*)'/g,"$1\r").replace(/\t=(.*?)%>/g,"',$1,'").split(" ").join("');").split("%>").join("p.push('").split("\r").join("\\'")+"');}return p.join('');"):s[t]=s[t];return i?e(i):e}if(t instanceof Function)return t(i);var s={};return e(t,i)}),w=(s.generateLabels=function(t,i,e,s){var o=new Array(i);return labelTemplateString&&n(o,function(i,n){o[n]=C(t,{value:e+s*(n+1)})}),o},s.easingEffects={linear:function(t){return t},easeInQuad:function(t){return t*t},easeOutQuad:function(t){return-1*t*(t-2)},easeInOutQuad:function(t){return(t/=.5)<1?.5*t*t:-0.5*(--t*(t-2)-1)},easeInCubic:function(t){return t*t*t},easeOutCubic:function(t){return 1*((t=t/1-1)*t*t+1)},easeInOutCubic:function(t){return(t/=.5)<1?.5*t*t*t:.5*((t-=2)*t*t+2)},easeInQuart:function(t){return t*t*t*t},easeOutQuart:function(t){return-1*((t=t/1-1)*t*t*t-1)},easeInOutQuart:function(t){return(t/=.5)<1?.5*t*t*t*t:-0.5*((t-=2)*t*t*t-2)},easeInQuint:function(t){return 1*(t/=1)*t*t*t*t},easeOutQuint:function(t){return 1*((t=t/1-1)*t*t*t*t+1)},easeInOutQuint:function(t){return(t/=.5)<1?.5*t*t*t*t*t:.5*((t-=2)*t*t*t*t+2)},easeInSine:function(t){return-1*Math.cos(t/1*(Math.PI/2))+1},easeOutSine:function(t){return 1*Math.sin(t/1*(Math.PI/2))},easeInOutSine:function(t){return-0.5*(Math.cos(Math.PI*t/1)-1)},easeInExpo:function(t){return 0===t?1:1*Math.pow(2,10*(t/1-1))},easeOutExpo:function(t){return 1===t?1:1*(-Math.pow(2,-10*t/1)+1)},easeInOutExpo:function(t){return 0===t?0:1===t?1:(t/=.5)<1?.5*Math.pow(2,10*(t-1)):.5*(-Math.pow(2,-10*--t)+2)},easeInCirc:function(t){return t>=1?t:-1*(Math.sqrt(1-(t/=1)*t)-1)},easeOutCirc:function(t){return 1*Math.sqrt(1-(t=t/1-1)*t)},easeInOutCirc:function(t){return(t/=.5)<1?-0.5*(Math.sqrt(1-t*t)-1):.5*(Math.sqrt(1-(t-=2)*t)+1)},easeInElastic:function(t){var i=1.70158,e=0,s=1;return 0===t?0:1==(t/=1)?1:(e||(e=.3),st?-.5*s*Math.pow(2,10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e):s*Math.pow(2,-10*(t-=1))*Math.sin(2*(1*t-i)*Math.PI/e)*.5+1)},easeInBack:function(t){var i=1.70158;return 1*(t/=1)*t*((i+1)*t-i)},easeOutBack:function(t){var i=1.70158;return 1*((t=t/1-1)*t*((i+1)*t+i)+1)},easeInOutBack:function(t){var i=1.70158;return(t/=.5)<1?.5*t*t*(((i*=1.525)+1)*t-i):.5*((t-=2)*t*(((i*=1.525)+1)*t+i)+2)},easeInBounce:function(t){return 1-w.easeOutBounce(1-t)},easeOutBounce:function(t){return(t/=1)<1/2.75?7.5625*t*t:2/2.75>t?1*(7.5625*(t-=1.5/2.75)*t+.75):2.5/2.75>t?1*(7.5625*(t-=2.25/2.75)*t+.9375):1*(7.5625*(t-=2.625/2.75)*t+.984375)},easeInOutBounce:function(t){return.5>t?.5*w.easeInBounce(2*t):.5*w.easeOutBounce(2*t-1)+.5}}),b=s.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){return window.setTimeout(t,1e3/60)}}(),P=s.cancelAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelAnimationFrame||window.mozCancelAnimationFrame||window.oCancelAnimationFrame||window.msCancelAnimationFrame||function(t){return window.clearTimeout(t,1e3/60)}}(),L=(s.animationLoop=function(t,i,e,s,n,o){var a=0,h=w[e]||w.linear,l=function(){a++;var e=a/i,r=h(e);t.call(o,r,e,a),s.call(o,r,e),i>a?o.animationFrame=b(l):n.apply(o)};b(l)},s.getRelativePosition=function(t){var i,e,s=t.originalEvent||t,n=t.currentTarget||t.srcElement,o=n.getBoundingClientRect();return s.touches?(i=s.touches[0].clientX-o.left,e=s.touches[0].clientY-o.top):(i=s.clientX-o.left,e=s.clientY-o.top),{x:i,y:e}},s.addEvent=function(t,i,e){t.addEventListener?t.addEventListener(i,e):t.attachEvent?t.attachEvent("on"+i,e):t["on"+i]=e}),k=s.removeEvent=function(t,i,e){t.removeEventListener?t.removeEventListener(i,e,!1):t.detachEvent?t.detachEvent("on"+i,e):t["on"+i]=c},F=(s.bindEvents=function(t,i,e){t.events||(t.events={}),n(i,function(i){t.events[i]=function(){e.apply(t,arguments)},L(t.chart.canvas,i,t.events[i])})},s.unbindEvents=function(t,i){n(i,function(i,e){k(t.chart.canvas,e,i)})}),R=s.getMaximumWidth=function(t){var i=t.parentNode;return i.clientWidth},T=s.getMaximumHeight=function(t){var i=t.parentNode;return i.clientHeight},A=(s.getMaximumSize=s.getMaximumWidth,s.retinaScale=function(t){var i=t.ctx,e=t.canvas.width,s=t.canvas.height;window.devicePixelRatio&&(i.canvas.style.width=e+"px",i.canvas.style.height=s+"px",i.canvas.height=s*window.devicePixelRatio,i.canvas.width=e*window.devicePixelRatio,i.scale(window.devicePixelRatio,window.devicePixelRatio))}),M=s.clear=function(t){t.ctx.clearRect(0,0,t.width,t.height)},W=s.fontString=function(t,i,e){return i+" "+t+"px "+e},z=s.longestText=function(t,i,e){t.font=i;var s=0;return n(e,function(i){var e=t.measureText(i).width;s=e>s?e:s}),s},B=s.drawRoundedRectangle=function(t,i,e,s,n,o){t.beginPath(),t.moveTo(i+o,e),t.lineTo(i+s-o,e),t.quadraticCurveTo(i+s,e,i+s,e+o),t.lineTo(i+s,e+n-o),t.quadraticCurveTo(i+s,e+n,i+s-o,e+n),t.lineTo(i+o,e+n),t.quadraticCurveTo(i,e+n,i,e+n-o),t.lineTo(i,e+o),t.quadraticCurveTo(i,e,i+o,e),t.closePath()};e.instances={},e.Type=function(t,i,s){this.options=i,this.chart=s,this.id=u(),e.instances[this.id]=this,i.responsive&&this.resize(),this.initialize.call(this,t)},a(e.Type.prototype,{initialize:function(){return this},clear:function(){return M(this.chart),this},stop:function(){return P(this.animationFrame),this},resize:function(t){this.stop();var i=this.chart.canvas,e=R(this.chart.canvas),s=this.options.maintainAspectRatio?e/this.chart.aspectRatio:T(this.chart.canvas);return i.width=this.chart.width=e,i.height=this.chart.height=s,A(this.chart),"function"==typeof t&&t.apply(this,Array.prototype.slice.call(arguments,1)),this},reflow:c,render:function(t){return t&&this.reflow(),this.options.animation&&!t?s.animationLoop(this.draw,this.options.animationSteps,this.options.animationEasing,this.options.onAnimationProgress,this.options.onAnimationComplete,this):(this.draw(),this.options.onAnimationComplete.call(this)),this},generateLegend:function(){return C(this.options.legendTemplate,this)},destroy:function(){this.clear(),F(this,this.events);var t=this.chart.canvas;t.width=this.chart.width,t.height=this.chart.height,t.style.removeProperty?(t.style.removeProperty("width"),t.style.removeProperty("height")):(t.style.removeAttribute("width"),t.style.removeAttribute("height")),delete e.instances[this.id]},showTooltip:function(t,i){"undefined"==typeof this.activeElements&&(this.activeElements=[]);var o=function(t){var i=!1;return t.length!==this.activeElements.length?i=!0:(n(t,function(t,e){t!==this.activeElements[e]&&(i=!0)},this),i)}.call(this,t);if(o||i){if(this.activeElements=t,this.draw(),this.options.customTooltips&&this.options.customTooltips(!1),t.length>0)if(this.datasets&&this.datasets.length>1){for(var a,h,r=this.datasets.length-1;r>=0&&(a=this.datasets[r].points||this.datasets[r].bars||this.datasets[r].segments,h=l(a,t[0]),-1===h);r--);var c=[],u=[],d=function(){var t,i,e,n,o,a=[],l=[],r=[];return s.each(this.datasets,function(i){t=i.points||i.bars||i.segments,t[h]&&t[h].hasValue()&&a.push(t[h])}),s.each(a,function(t){l.push(t.x),r.push(t.y),c.push(s.template(this.options.multiTooltipTemplate,t)),u.push({fill:t._saved.fillColor||t.fillColor,stroke:t._saved.strokeColor||t.strokeColor})},this),o=m(r),e=g(r),n=m(l),i=g(l),{x:n>this.chart.width/2?n:i,y:(o+e)/2}}.call(this,h);new e.MultiTooltip({x:d.x,y:d.y,xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,xOffset:this.options.tooltipXOffset,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,titleTextColor:this.options.tooltipTitleFontColor,titleFontFamily:this.options.tooltipTitleFontFamily,titleFontStyle:this.options.tooltipTitleFontStyle,titleFontSize:this.options.tooltipTitleFontSize,cornerRadius:this.options.tooltipCornerRadius,labels:c,legendColors:u,legendColorBackground:this.options.multiTooltipKeyBackground,title:t[0].label,chart:this.chart,ctx:this.chart.ctx,custom:this.options.customTooltips}).draw()}else n(t,function(t){var i=t.tooltipPosition();new e.Tooltip({x:Math.round(i.x),y:Math.round(i.y),xPadding:this.options.tooltipXPadding,yPadding:this.options.tooltipYPadding,fillColor:this.options.tooltipFillColor,textColor:this.options.tooltipFontColor,fontFamily:this.options.tooltipFontFamily,fontStyle:this.options.tooltipFontStyle,fontSize:this.options.tooltipFontSize,caretHeight:this.options.tooltipCaretSize,cornerRadius:this.options.tooltipCornerRadius,text:C(this.options.tooltipTemplate,t),chart:this.chart,custom:this.options.customTooltips}).draw()},this);return this}},toBase64Image:function(){return this.chart.canvas.toDataURL.apply(this.chart.canvas,arguments)}}),e.Type.extend=function(t){var i=this,s=function(){return i.apply(this,arguments)};if(s.prototype=o(i.prototype),a(s.prototype,t),s.extend=e.Type.extend,t.name||i.prototype.name){var n=t.name||i.prototype.name,l=e.defaults[i.prototype.name]?o(e.defaults[i.prototype.name]):{};e.defaults[n]=a(l,t.defaults),e.types[n]=s,e.prototype[n]=function(t,i){var o=h(e.defaults.global,e.defaults[n],i||{});return new s(t,o,this)}}else d("Name not provided for this chart, so it hasn't been registered");return i},e.Element=function(t){a(this,t),this.initialize.apply(this,arguments),this.save()},a(e.Element.prototype,{initialize:function(){},restore:function(t){return t?n(t,function(t){this[t]=this._saved[t]},this):a(this,this._saved),this},save:function(){return this._saved=o(this),delete this._saved._saved,this},update:function(t){return n(t,function(t,i){this._saved[i]=this[i],this[i]=t},this),this},transition:function(t,i){return n(t,function(t,e){this[e]=(t-this._saved[e])*i+this._saved[e]},this),this},tooltipPosition:function(){return{x:this.x,y:this.y}},hasValue:function(){return f(this.value)}}),e.Element.extend=r,e.Point=e.Element.extend({display:!0,inRange:function(t,i){var e=this.hitDetectionRadius+this.radius;return Math.pow(t-this.x,2)+Math.pow(i-this.y,2)=this.startAngle&&e.angle<=this.endAngle,o=e.distance>=this.innerRadius&&e.distance<=this.outerRadius;return n&&o},tooltipPosition:function(){var t=this.startAngle+(this.endAngle-this.startAngle)/2,i=(this.outerRadius-this.innerRadius)/2+this.innerRadius;return{x:this.x+Math.cos(t)*i,y:this.y+Math.sin(t)*i}},draw:function(t){var i=this.ctx;i.beginPath(),i.arc(this.x,this.y,this.outerRadius,this.startAngle,this.endAngle),i.arc(this.x,this.y,this.innerRadius,this.endAngle,this.startAngle,!0),i.closePath(),i.strokeStyle=this.strokeColor,i.lineWidth=this.strokeWidth,i.fillStyle=this.fillColor,i.fill(),i.lineJoin="bevel",this.showStroke&&i.stroke()}}),e.Rectangle=e.Element.extend({draw:function(){var t=this.ctx,i=this.width/2,e=this.x-i,s=this.x+i,n=this.base-(this.base-this.y),o=this.strokeWidth/2;this.showStroke&&(e+=o,s-=o,n+=o),t.beginPath(),t.fillStyle=this.fillColor,t.strokeStyle=this.strokeColor,t.lineWidth=this.strokeWidth,t.moveTo(e,this.base),t.lineTo(e,n),t.lineTo(s,n),t.lineTo(s,this.base),t.fill(),this.showStroke&&t.stroke()},height:function(){return this.base-this.y},inRange:function(t,i){return t>=this.x-this.width/2&&t<=this.x+this.width/2&&i>=this.y&&i<=this.base}}),e.Tooltip=e.Element.extend({draw:function(){var t=this.chart.ctx;t.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.xAlign="center",this.yAlign="above";var i=this.caretPadding=2,e=t.measureText(this.text).width+2*this.xPadding,s=this.fontSize+2*this.yPadding,n=s+this.caretHeight+i;this.x+e/2>this.chart.width?this.xAlign="left":this.x-e/2<0&&(this.xAlign="right"),this.y-n<0&&(this.yAlign="below");var o=this.x-e/2,a=this.y-n;if(t.fillStyle=this.fillColor,this.custom)this.custom(this);else{switch(this.yAlign){case"above":t.beginPath(),t.moveTo(this.x,this.y-i),t.lineTo(this.x+this.caretHeight,this.y-(i+this.caretHeight)),t.lineTo(this.x-this.caretHeight,this.y-(i+this.caretHeight)),t.closePath(),t.fill();break;case"below":a=this.y+i+this.caretHeight,t.beginPath(),t.moveTo(this.x,this.y+i),t.lineTo(this.x+this.caretHeight,this.y+i+this.caretHeight),t.lineTo(this.x-this.caretHeight,this.y+i+this.caretHeight),t.closePath(),t.fill()}switch(this.xAlign){case"left":o=this.x-e+(this.cornerRadius+this.caretHeight);break;case"right":o=this.x-(this.cornerRadius+this.caretHeight)}B(t,o,a,e,s,this.cornerRadius),t.fill(),t.fillStyle=this.textColor,t.textAlign="center",t.textBaseline="middle",t.fillText(this.text,o+e/2,a+s/2)}}}),e.MultiTooltip=e.Element.extend({initialize:function(){this.font=W(this.fontSize,this.fontStyle,this.fontFamily),this.titleFont=W(this.titleFontSize,this.titleFontStyle,this.titleFontFamily),this.height=this.labels.length*this.fontSize+(this.labels.length-1)*(this.fontSize/2)+2*this.yPadding+1.5*this.titleFontSize,this.ctx.font=this.titleFont;var t=this.ctx.measureText(this.title).width,i=z(this.ctx,this.font,this.labels)+this.fontSize+3,e=g([i,t]);this.width=e+2*this.xPadding;var s=this.height/2;this.y-s<0?this.y=s:this.y+s>this.chart.height&&(this.y=this.chart.height-s),this.x>this.chart.width/2?this.x-=this.xOffset+this.width:this.x+=this.xOffset},getLineHeight:function(t){var i=this.y-this.height/2+this.yPadding,e=t-1;return 0===t?i+this.titleFontSize/2:i+(1.5*this.fontSize*e+this.fontSize/2)+1.5*this.titleFontSize},draw:function(){if(this.custom)this.custom(this);else{B(this.ctx,this.x,this.y-this.height/2,this.width,this.height,this.cornerRadius);var t=this.ctx;t.fillStyle=this.fillColor,t.fill(),t.closePath(),t.textAlign="left",t.textBaseline="middle",t.fillStyle=this.titleTextColor,t.font=this.titleFont,t.fillText(this.title,this.x+this.xPadding,this.getLineHeight(0)),t.font=this.font,s.each(this.labels,function(i,e){t.fillStyle=this.textColor,t.fillText(i,this.x+this.xPadding+this.fontSize+3,this.getLineHeight(e+1)),t.fillStyle=this.legendColorBackground,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize),t.fillStyle=this.legendColors[e].fill,t.fillRect(this.x+this.xPadding,this.getLineHeight(e+1)-this.fontSize/2,this.fontSize,this.fontSize)},this)}}}),e.Scale=e.Element.extend({initialize:function(){this.fit()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}));this.yLabelWidth=this.display&&this.showLabels?z(this.ctx,this.font,this.yLabels):0},addXLabel:function(t){this.xLabels.push(t),this.valuesCount++,this.fit()},removeXLabel:function(){this.xLabels.shift(),this.valuesCount--,this.fit()},fit:function(){this.startPoint=this.display?this.fontSize:0,this.endPoint=this.display?this.height-1.5*this.fontSize-5:this.height,this.startPoint+=this.padding,this.endPoint-=this.padding;var t,i=this.endPoint-this.startPoint;for(this.calculateYRange(i),this.buildYLabels(),this.calculateXLabelRotation();i>this.endPoint-this.startPoint;)i=this.endPoint-this.startPoint,t=this.yLabelWidth,this.calculateYRange(i),this.buildYLabels(),tthis.yLabelWidth+10?e/2:this.yLabelWidth+10,this.xLabelRotation=0,this.display){var n,o=z(this.ctx,this.font,this.xLabels);this.xLabelWidth=o;for(var a=Math.floor(this.calculateX(1)-this.calculateX(0))-6;this.xLabelWidth>a&&0===this.xLabelRotation||this.xLabelWidth>a&&this.xLabelRotation<=90&&this.xLabelRotation>0;)n=Math.cos(S(this.xLabelRotation)),t=n*e,i=n*s,t+this.fontSize/2>this.yLabelWidth+8&&(this.xScalePaddingLeft=t+this.fontSize/2),this.xScalePaddingRight=this.fontSize/2,this.xLabelRotation++,this.xLabelWidth=n*o;this.xLabelRotation>0&&(this.endPoint-=Math.sin(S(this.xLabelRotation))*o+3)}else this.xLabelWidth=0,this.xScalePaddingRight=this.padding,this.xScalePaddingLeft=this.padding},calculateYRange:c,drawingArea:function(){return this.startPoint-this.endPoint},calculateY:function(t){var i=this.drawingArea()/(this.min-this.max);return this.endPoint-i*(t-this.min)},calculateX:function(t){var i=(this.xLabelRotation>0,this.width-(this.xScalePaddingLeft+this.xScalePaddingRight)),e=i/Math.max(this.valuesCount-(this.offsetGridLines?0:1),1),s=e*t+this.xScalePaddingLeft;return this.offsetGridLines&&(s+=e/2),Math.round(s)},update:function(t){s.extend(this,t),this.fit()},draw:function(){var t=this.ctx,i=(this.endPoint-this.startPoint)/this.steps,e=Math.round(this.xScalePaddingLeft);this.display&&(t.fillStyle=this.textColor,t.font=this.font,n(this.yLabels,function(n,o){var a=this.endPoint-i*o,h=Math.round(a),l=this.showHorizontalLines;t.textAlign="right",t.textBaseline="middle",this.showLabels&&t.fillText(n,e-10,a),0!==o||l||(l=!0),l&&t.beginPath(),o>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),h+=s.aliasPixel(t.lineWidth),l&&(t.moveTo(e,h),t.lineTo(this.width,h),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(e-5,h),t.lineTo(e,h),t.stroke(),t.closePath()},this),n(this.xLabels,function(i,e){var s=this.calculateX(e)+x(this.lineWidth),n=this.calculateX(e-(this.offsetGridLines?.5:0))+x(this.lineWidth),o=this.xLabelRotation>0,a=this.showVerticalLines;0!==e||a||(a=!0),a&&t.beginPath(),e>0?(t.lineWidth=this.gridLineWidth,t.strokeStyle=this.gridLineColor):(t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor),a&&(t.moveTo(n,this.endPoint),t.lineTo(n,this.startPoint-3),t.stroke(),t.closePath()),t.lineWidth=this.lineWidth,t.strokeStyle=this.lineColor,t.beginPath(),t.moveTo(n,this.endPoint),t.lineTo(n,this.endPoint+5),t.stroke(),t.closePath(),t.save(),t.translate(s,o?this.endPoint+12:this.endPoint+8),t.rotate(-1*S(this.xLabelRotation)),t.font=this.font,t.textAlign=o?"right":"center",t.textBaseline=o?"middle":"top",t.fillText(i,0,0),t.restore()},this))}}),e.RadialScale=e.Element.extend({initialize:function(){this.size=m([this.height,this.width]),this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2},calculateCenterOffset:function(t){var i=this.drawingArea/(this.max-this.min);return(t-this.min)*i},update:function(){this.lineArc?this.drawingArea=this.display?this.size/2-(this.fontSize/2+this.backdropPaddingY):this.size/2:this.setScaleSize(),this.buildYLabels()},buildYLabels:function(){this.yLabels=[];for(var t=v(this.stepValue),i=0;i<=this.steps;i++)this.yLabels.push(C(this.templateString,{value:(this.min+i*this.stepValue).toFixed(t)}))},getCircumference:function(){return 2*Math.PI/this.valuesCount},setScaleSize:function(){var t,i,e,s,n,o,a,h,l,r,c,u,d=m([this.height/2-this.pointLabelFontSize-5,this.width/2]),p=this.width,g=0;for(this.ctx.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),i=0;ip&&(p=t.x+s,n=i),t.x-sp&&(p=t.x+e,n=i):i>this.valuesCount/2&&t.x-e0){var s,n=e*(this.drawingArea/this.steps),o=this.yCenter-n;if(this.lineWidth>0)if(t.strokeStyle=this.lineColor,t.lineWidth=this.lineWidth,this.lineArc)t.beginPath(),t.arc(this.xCenter,this.yCenter,n,0,2*Math.PI),t.closePath(),t.stroke();else{t.beginPath();for(var a=0;a=0;i--){if(this.angleLineWidth>0){var e=this.getPointPosition(i,this.calculateCenterOffset(this.max));t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(e.x,e.y),t.stroke(),t.closePath()}var s=this.getPointPosition(i,this.calculateCenterOffset(this.max)+5);t.font=W(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily),t.fillStyle=this.pointLabelFontColor;var o=this.labels.length,a=this.labels.length/2,h=a/2,l=h>i||i>o-h,r=i===h||i===o-h;t.textAlign=0===i?"center":i===a?"center":a>i?"left":"right",t.textBaseline=r?"middle":l?"bottom":"top",t.fillText(this.labels[i],s.x,s.y)}}}}}),s.addEvent(window,"resize",function(){var t;return function(){clearTimeout(t),t=setTimeout(function(){n(e.instances,function(t){t.options.responsive&&t.resize(t.render,!0)})},50)}}()),p?define(function(){return e}):"object"==typeof module&&module.exports&&(module.exports=e),t.Chart=e,e.noConflict=function(){return t.Chart=i,e}}).call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleBeginAtZero:!0,scaleShowGridLines:!0,scaleGridLineColor:"rgba(0,0,0,.05)",scaleGridLineWidth:1,scaleShowHorizontalLines:!0,scaleShowVerticalLines:!0,barShowStroke:!0,barStrokeWidth:2,barValueSpacing:5,barDatasetSpacing:1,legendTemplate:'
    <% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
'};i.Type.extend({name:"Bar",defaults:s,initialize:function(t){var s=this.options;this.ScaleClass=i.Scale.extend({offsetGridLines:!0,calculateBarX:function(t,i,e){var n=this.calculateBaseWidth(),o=this.calculateX(e)-n/2,a=this.calculateBarWidth(t);return o+a*i+i*s.barDatasetSpacing+a/2},calculateBaseWidth:function(){return this.calculateX(1)-this.calculateX(0)-2*s.barValueSpacing},calculateBarWidth:function(t){var i=this.calculateBaseWidth()-(t-1)*s.barDatasetSpacing;return i/t}}),this.datasets=[],this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getBarsAtEvent(t):[];this.eachBars(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),this.BarClass=i.Rectangle.extend({strokeWidth:this.options.barStrokeWidth,showStroke:this.options.barShowStroke,ctx:this.chart.ctx}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,bars:[]};this.datasets.push(s),e.each(i.data,function(e,n){s.bars.push(new this.BarClass({value:e,label:t.labels[n],datasetLabel:i.label,strokeColor:i.strokeColor,fillColor:i.fillColor,highlightFill:i.highlightFill||i.fillColor,highlightStroke:i.highlightStroke||i.strokeColor}))},this)},this),this.buildScale(t.labels),this.BarClass.prototype.base=this.scale.endPoint,this.eachBars(function(t,i,s){e.extend(t,{width:this.scale.calculateBarWidth(this.datasets.length),x:this.scale.calculateBarX(this.datasets.length,s,i),y:this.scale.endPoint}),t.save()},this),this.render()},update:function(){this.scale.update(),e.each(this.activeElements,function(t){t.restore(["fillColor","strokeColor"])}),this.eachBars(function(t){t.save()}),this.render()},eachBars:function(t){e.each(this.datasets,function(i,s){e.each(i.bars,t,this,s)},this)},getBarsAtEvent:function(t){for(var i,s=[],n=e.getRelativePosition(t),o=function(t){s.push(t.bars[i])},a=0;a<% for (var i=0; i
  • <%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Doughnut",defaults:s,initialize:function(t){this.segments=[],this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,this.SegmentArc=i.Arc.extend({ctx:this.chart.ctx,x:this.chart.width/2,y:this.chart.height/2}),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.calculateTotal(t),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({value:t.value,outerRadius:this.options.animateScale?0:this.outerRadius,innerRadius:this.options.animateScale?0:this.outerRadius/100*this.options.percentageInnerCutout,fillColor:t.color,highlightColor:t.highlight||t.color,showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,startAngle:1.5*Math.PI,circumference:this.options.animateRotate?0:this.calculateCircumference(t.value),label:t.label})),e||(this.reflow(),this.update())},calculateCircumference:function(t){return 2*Math.PI*(Math.abs(t)/this.total)},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=Math.abs(t.value)},this)},update:function(){this.calculateTotal(this.segments),e.each(this.activeElements,function(t){t.restore(["fillColor"])}),e.each(this.segments,function(t){t.save()}),this.render()},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.outerRadius=(e.min([this.chart.width,this.chart.height])-this.options.segmentStrokeWidth/2)/2,e.each(this.segments,function(t){t.update({outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout})},this)},draw:function(t){var i=t?t:1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.calculateCircumference(t.value),outerRadius:this.outerRadius,innerRadius:this.outerRadius/100*this.options.percentageInnerCutout},i),t.endAngle=t.startAngle+t.circumference,t.draw(),0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'};i.Type.extend({name:"Line",defaults:s,initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx,inRange:function(t){return Math.pow(t-this.x,2)0&&ithis.scale.endPoint?t.controlPoints.outer.y=this.scale.endPoint:t.controlPoints.outer.ythis.scale.endPoint?t.controlPoints.inner.y=this.scale.endPoint:t.controlPoints.inner.y0&&(s.lineTo(h[h.length-1].x,this.scale.endPoint),s.lineTo(h[0].x,this.scale.endPoint),s.fillStyle=t.fillColor,s.closePath(),s.fill()),e.each(h,function(t){t.draw()})},this)}})}.call(this),function(){"use strict";var t=this,i=t.Chart,e=i.helpers,s={scaleShowLabelBackdrop:!0,scaleBackdropColor:"rgba(255,255,255,0.75)",scaleBeginAtZero:!0,scaleBackdropPaddingY:2,scaleBackdropPaddingX:2,scaleShowLine:!0,segmentShowStroke:!0,segmentStrokeColor:"#fff",segmentStrokeWidth:2,animationSteps:100,animationEasing:"easeOutBounce",animateRotate:!0,animateScale:!1,legendTemplate:'
      <% for (var i=0; i
    • <%if(segments[i].label){%><%=segments[i].label%><%}%>
    • <%}%>
    '};i.Type.extend({name:"PolarArea",defaults:s,initialize:function(t){this.segments=[],this.SegmentArc=i.Arc.extend({showStroke:this.options.segmentShowStroke,strokeWidth:this.options.segmentStrokeWidth,strokeColor:this.options.segmentStrokeColor,ctx:this.chart.ctx,innerRadius:0,x:this.chart.width/2,y:this.chart.height/2}),this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,lineArc:!0,width:this.chart.width,height:this.chart.height,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,valuesCount:t.length}),this.updateScaleRange(t),this.scale.update(),e.each(t,function(t,i){this.addData(t,i,!0)},this),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getSegmentsAtEvent(t):[];e.each(this.segments,function(t){t.restore(["fillColor"])}),e.each(i,function(t){t.fillColor=t.highlightColor}),this.showTooltip(i)}),this.render()},getSegmentsAtEvent:function(t){var i=[],s=e.getRelativePosition(t);return e.each(this.segments,function(t){t.inRange(s.x,s.y)&&i.push(t)},this),i},addData:function(t,i,e){var s=i||this.segments.length;this.segments.splice(s,0,new this.SegmentArc({fillColor:t.color,highlightColor:t.highlight||t.color,label:t.label,value:t.value,outerRadius:this.options.animateScale?0:this.scale.calculateCenterOffset(t.value),circumference:this.options.animateRotate?0:this.scale.getCircumference(),startAngle:1.5*Math.PI})),e||(this.reflow(),this.update())},removeData:function(t){var i=e.isNumber(t)?t:this.segments.length-1;this.segments.splice(i,1),this.reflow(),this.update()},calculateTotal:function(t){this.total=0,e.each(t,function(t){this.total+=t.value},this),this.scale.valuesCount=this.segments.length},updateScaleRange:function(t){var i=[];e.each(t,function(t){i.push(t.value)});var s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s,{size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2})},update:function(){this.calculateTotal(this.segments),e.each(this.segments,function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.SegmentArc.prototype,{x:this.chart.width/2,y:this.chart.height/2}),this.updateScaleRange(this.segments),this.scale.update(),e.extend(this.scale,{xCenter:this.chart.width/2,yCenter:this.chart.height/2}),e.each(this.segments,function(t){t.update({outerRadius:this.scale.calculateCenterOffset(t.value)})},this)},draw:function(t){var i=t||1;this.clear(),e.each(this.segments,function(t,e){t.transition({circumference:this.scale.getCircumference(),outerRadius:this.scale.calculateCenterOffset(t.value)},i),t.endAngle=t.startAngle+t.circumference,0===e&&(t.startAngle=1.5*Math.PI),e<% for (var i=0; i
  • <%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>'},initialize:function(t){this.PointClass=i.Point.extend({strokeWidth:this.options.pointDotStrokeWidth,radius:this.options.pointDotRadius,display:this.options.pointDot,hitDetectionRadius:this.options.pointHitDetectionRadius,ctx:this.chart.ctx}),this.datasets=[],this.buildScale(t),this.options.showTooltips&&e.bindEvents(this,this.options.tooltipEvents,function(t){var i="mouseout"!==t.type?this.getPointsAtEvent(t):[];this.eachPoints(function(t){t.restore(["fillColor","strokeColor"])}),e.each(i,function(t){t.fillColor=t.highlightFill,t.strokeColor=t.highlightStroke}),this.showTooltip(i)}),e.each(t.datasets,function(i){var s={label:i.label||null,fillColor:i.fillColor,strokeColor:i.strokeColor,pointColor:i.pointColor,pointStrokeColor:i.pointStrokeColor,points:[]};this.datasets.push(s),e.each(i.data,function(e,n){var o;this.scale.animation||(o=this.scale.getPointPosition(n,this.scale.calculateCenterOffset(e))),s.points.push(new this.PointClass({value:e,label:t.labels[n],datasetLabel:i.label,x:this.options.animation?this.scale.xCenter:o.x,y:this.options.animation?this.scale.yCenter:o.y,strokeColor:i.pointStrokeColor,fillColor:i.pointColor,highlightFill:i.pointHighlightFill||i.pointColor,highlightStroke:i.pointHighlightStroke||i.pointStrokeColor}))},this)},this),this.render()},eachPoints:function(t){e.each(this.datasets,function(i){e.each(i.points,t,this)},this)},getPointsAtEvent:function(t){var i=e.getRelativePosition(t),s=e.getAngleFromPoint({x:this.scale.xCenter,y:this.scale.yCenter},i),n=2*Math.PI/this.scale.valuesCount,o=Math.round((s.angle-1.5*Math.PI)/n),a=[];return(o>=this.scale.valuesCount||0>o)&&(o=0),s.distance<=this.scale.drawingArea&&e.each(this.datasets,function(t){a.push(t.points[o])}),a},buildScale:function(t){this.scale=new i.RadialScale({display:this.options.showScale,fontStyle:this.options.scaleFontStyle,fontSize:this.options.scaleFontSize,fontFamily:this.options.scaleFontFamily,fontColor:this.options.scaleFontColor,showLabels:this.options.scaleShowLabels,showLabelBackdrop:this.options.scaleShowLabelBackdrop,backdropColor:this.options.scaleBackdropColor,backdropPaddingY:this.options.scaleBackdropPaddingY,backdropPaddingX:this.options.scaleBackdropPaddingX,lineWidth:this.options.scaleShowLine?this.options.scaleLineWidth:0,lineColor:this.options.scaleLineColor,angleLineColor:this.options.angleLineColor,angleLineWidth:this.options.angleShowLineOut?this.options.angleLineWidth:0,pointLabelFontColor:this.options.pointLabelFontColor,pointLabelFontSize:this.options.pointLabelFontSize,pointLabelFontFamily:this.options.pointLabelFontFamily,pointLabelFontStyle:this.options.pointLabelFontStyle,height:this.chart.height,width:this.chart.width,xCenter:this.chart.width/2,yCenter:this.chart.height/2,ctx:this.chart.ctx,templateString:this.options.scaleLabel,labels:t.labels,valuesCount:t.datasets[0].data.length}),this.scale.setScaleSize(),this.updateScaleRange(t.datasets),this.scale.buildYLabels()},updateScaleRange:function(t){var i=function(){var i=[];return e.each(t,function(t){t.data?i=i.concat(t.data):e.each(t.points,function(t){i.push(t.value)})}),i}(),s=this.options.scaleOverride?{steps:this.options.scaleSteps,stepValue:this.options.scaleStepWidth,min:this.options.scaleStartValue,max:this.options.scaleStartValue+this.options.scaleSteps*this.options.scaleStepWidth}:e.calculateScaleRange(i,e.min([this.chart.width,this.chart.height])/2,this.options.scaleFontSize,this.options.scaleBeginAtZero,this.options.scaleIntegersOnly);e.extend(this.scale,s)},addData:function(t,i){this.scale.valuesCount++,e.each(t,function(t,e){var s=this.scale.getPointPosition(this.scale.valuesCount,this.scale.calculateCenterOffset(t));this.datasets[e].points.push(new this.PointClass({value:t,label:i,x:s.x,y:s.y,strokeColor:this.datasets[e].pointStrokeColor,fillColor:this.datasets[e].pointColor}))},this),this.scale.labels.push(i),this.reflow(),this.update()},removeData:function(){this.scale.valuesCount--,this.scale.labels.shift(),e.each(this.datasets,function(t){t.points.shift()},this),this.reflow(),this.update()},update:function(){this.eachPoints(function(t){t.save()}),this.reflow(),this.render()},reflow:function(){e.extend(this.scale,{width:this.chart.width,height:this.chart.height,size:e.min([this.chart.width,this.chart.height]),xCenter:this.chart.width/2,yCenter:this.chart.height/2}),this.updateScaleRange(this.datasets),this.scale.setScaleSize(),this.scale.buildYLabels()},draw:function(t){var i=t||1,s=this.chart.ctx;this.clear(),this.scale.draw(),e.each(this.datasets,function(t){e.each(t.points,function(t,e){t.hasValue()&&t.transition(this.scale.getPointPosition(e,this.scale.calculateCenterOffset(t.value)),i)},this),s.lineWidth=this.options.datasetStrokeWidth,s.strokeStyle=t.strokeColor,s.beginPath(),e.each(t.points,function(t,i){0===i?s.moveTo(t.x,t.y):s.lineTo(t.x,t.y)},this),s.closePath(),s.stroke(),s.fillStyle=t.fillColor,s.fill(),e.each(t.points,function(t){t.hasValue()&&t.draw()})},this)}})}.call(this); \ No newline at end of file diff --git a/test/unit/karma.conf.js b/test/unit/karma.conf.js index 805ccefca..038a2d5a5 100644 --- a/test/unit/karma.conf.js +++ b/test/unit/karma.conf.js @@ -9,6 +9,7 @@ files = [ 'assets/js/jquery.gritter.min.js', 'assets/js/bootstrap.min.js', 'assets/js/spin.js', + 'assets/js/Chart.min.js', 'dist/angular.js', 'assets/js/ui-bootstrap/ui-bootstrap-custom-tpls-0.12.0.min.js', 'assets/js/angular-vis.js', From b99fe5bf55e5c235d9211c83e7ba334440c9d1e2 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Thu, 27 Aug 2015 01:41:45 -0500 Subject: [PATCH 05/10] Added memory usage graph. --- app/components/stats/stats.html | 20 +++++- app/components/stats/statsController.js | 87 ++++++++++++++++++++----- 2 files changed, 88 insertions(+), 19 deletions(-) diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html index 7b764e008..b18bb90a9 100644 --- a/app/components/stats/stats.html +++ b/app/components/stats/stats.html @@ -2,8 +2,24 @@

    Stats

    CPU Usage

    - - +
    +
    + +
    +
    +

    Other CPU usage data

    +

    TODO

    +
    +

    Memory

    +
    +
    + +
    +
    +

    Other Memory Stats

    +

    TODO

    +
    +
    diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index e77424f73..e349b5a63 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -1,24 +1,68 @@ angular.module('stats', []) - .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', function (Settings, $scope, Messages, $timeout, Container, $routeParams) { - // TODO: Implement memory chart, force scale to 0-100 for cpu, 0 to limit for memory, fix charts on dashboard + .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', 'humansizeFilter', function (Settings, $scope, Messages, $timeout, Container, $routeParams, humansizeFilter) { + // TODO: Implement memory chart, force scale to 0-100 for cpu, 0 to limit for memory, fix charts on dashboard, + // TODO: Force memory scale to 0 - max memory + //var initialStats = {}; // Used to set scale of memory graph. + // + //Container.stats({id: $routeParams.id}, function (d) { + // var arr = Object.keys(d).map(function (key) { + // return d[key]; + // }); + // if (arr.join('').indexOf('no such id') !== -1) { + // Messages.error('Unable to retrieve stats', 'Is this container running?'); + // return; + // } + // initialStats = d; + //}, function () { + // Messages.error('Unable to retrieve stats', 'Is this container running?'); + //}); - var labels = []; - var data = []; - for (var i = 0; i < 40; i ++) { - labels.push(''); - data.push(0); + var cpuLabels = []; + var cpuData = []; + var memoryLabels = []; + var memoryData = []; + for (var i = 0; i < 40; i++) { + cpuLabels.push(''); + cpuData.push(0); + memoryLabels.push(''); + memoryData.push(0); } - var dataset = { // CPU Usage + var cpuDataset = { // CPU Usage fillColor: "rgba(151,187,205,0.5)", strokeColor: "rgba(151,187,205,1)", pointColor: "rgba(151,187,205,1)", pointStrokeColor: "#fff", - data: data + data: cpuData + }; + var memoryDataset = { + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + data: memoryData }; - var chart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({ - labels: labels, - datasets: [dataset] + + var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({ + labels: cpuLabels, + datasets: [cpuDataset] + }, { + responsive: true + }); + + var memoryChart = new Chart($('#memory-stats-chart').get(0).getContext('2d')).Line({ + labels: memoryLabels, + datasets: [memoryDataset] + }, + { + scaleLabel: function (valueObj) { + return humansizeFilter(parseInt(valueObj.value)); + }, + responsive: true, + //scaleOverride: true, + //scaleSteps: 10, + //scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10), + //scaleStartValue: 0 }); @@ -34,6 +78,7 @@ angular.module('stats', []) // Update graph with latest data updateChart(d); + updateMemoryChart(d); $timeout(updateStats, 1000); // TODO: Switch to setInterval for more consistent readings }, function () { Messages.error('Unable to retrieve stats', 'Is this container running?'); @@ -44,11 +89,18 @@ angular.module('stats', []) function updateChart(data) { console.log('updateChart', data); - chart.addData([$scope.calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString()); - chart.removeData(); + cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString()); + cpuChart.removeData(); } - $scope.calculateCPUPercent = function (stats) { + function updateMemoryChart(data) { + console.log('updateMemoryChart', data); + memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString()); + memoryChart.removeData(); + + } + + function calculateCPUPercent(stats) { // Same algorithm the official client uses: https://github.com/docker/docker/blob/master/api/client/stats.go#L195-L208 var prevCpu = stats.precpu_stats; var curCpu = stats.cpu_stats; @@ -66,5 +118,6 @@ angular.module('stats', []) } return Math.random() * 100; //return cpuPercent; TODO: Switch back to the real value - }; - }]); \ No newline at end of file + } + }]) +; \ No newline at end of file From 5a51495432840907cc0ea2e9309d28f50d3564ed Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Fri, 28 Aug 2015 23:26:39 -0500 Subject: [PATCH 06/10] Added remaining memory stats, change humansize filter to give more accurate sizes. --- app/components/stats/stats.html | 39 +++++++++++++------ app/components/stats/statsController.js | 15 ++++--- app/shared/filters.js | 4 +- .../app/components/statsController.spec.js | 28 ++++++------- test/unit/app/shared/filters.spec.js | 8 ++-- 5 files changed, 59 insertions(+), 35 deletions(-) diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html index b18bb90a9..2ba8fe846 100644 --- a/app/components/stats/stats.html +++ b/app/components/stats/stats.html @@ -1,24 +1,41 @@

    Stats

    +

    CPU Usage

    +
    -
    - -
    -
    -

    Other CPU usage data

    -

    TODO

    +
    +

    Memory

    +
    -
    - +
    +
    -
    -

    Other Memory Stats

    -

    TODO

    +
    + + + + + + + + + +
    Max usage{{ data.memory_stats.max_usage | humansize }}
    Limit{{ data.memory_stats.limit | humansize }}
    + + + + + + + +
    {{ key }}{{ value }}
    +
    +
    diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index e349b5a63..39a5ddc2d 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -56,9 +56,9 @@ angular.module('stats', []) }, { scaleLabel: function (valueObj) { - return humansizeFilter(parseInt(valueObj.value)); + return humansizeFilter(parseInt(valueObj.value, 10)); }, - responsive: true, + responsive: true //scaleOverride: true, //scaleSteps: 10, //scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10), @@ -77,14 +77,20 @@ angular.module('stats', []) } // Update graph with latest data + $scope.data = d; updateChart(d); updateMemoryChart(d); - $timeout(updateStats, 1000); // TODO: Switch to setInterval for more consistent readings + timeout = $timeout(updateStats, 1000); }, function () { Messages.error('Unable to retrieve stats', 'Is this container running?'); }); } + var timeout; + $scope.$on('$destroy', function () { + $timeout.cancel(timeout); + }); + updateStats(); function updateChart(data) { @@ -116,8 +122,7 @@ angular.module('stats', []) //console.log('size thing:', curCpu.cpu_usage.percpu_usage); cpuPercent = (cpuDelta / systemDelta) * curCpu.cpu_usage.percpu_usage.length * 100.0; } - return Math.random() * 100; - //return cpuPercent; TODO: Switch back to the real value + return cpuPercent; } }]) ; \ No newline at end of file diff --git a/app/shared/filters.js b/app/shared/filters.js index e993bc23a..d4f18e232 100644 --- a/app/shared/filters.js +++ b/app/shared/filters.js @@ -71,7 +71,9 @@ angular.module('dockerui.filters', []) return 'n/a'; } var i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)), 10); - return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[[i]]; + var value = bytes / Math.pow(1024, i); + var decimalPlaces = (i < 1) ? 0 : (i - 1); + return value.toFixed(decimalPlaces) + ' ' + sizes[[i]]; }; }) .filter('containername', function () { diff --git a/test/unit/app/components/statsController.spec.js b/test/unit/app/components/statsController.spec.js index c26aa06c3..2cbc138ba 100644 --- a/test/unit/app/components/statsController.spec.js +++ b/test/unit/app/components/statsController.spec.js @@ -14,18 +14,18 @@ describe("StatsController", function () { }); })); - it("should test controller initialize", function () { - $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/stats?stream=false').respond(200); - //expect($scope.ps_args).toBeDefined(); - $httpBackend.flush(); - }); - - it("a correct top request to the Docker remote API", function () { - //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=').respond(200); - //$routeParams.id = '123456789123456789123456789'; - //$scope.ps_args = 'aux'; - //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=' + $scope.ps_args).respond(200); - //$scope.getTop(); - //$httpBackend.flush(); - }); + //it("should test controller initialize", function () { + // $httpBackend.expectGET('dockerapi/containers/b17882378cee8ec0136f482681b764cca430befd52a9bfd1bde031f49b8bba9f/stats?stream=false').respond(200); + // //expect($scope.ps_args).toBeDefined(); + // $httpBackend.flush(); + //}); + // + //it("a correct top request to the Docker remote API", function () { + // //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=').respond(200); + // //$routeParams.id = '123456789123456789123456789'; + // //$scope.ps_args = 'aux'; + // //$httpBackend.expectGET('dockerapi/containers/' + $routeParams.id + '/top?ps_args=' + $scope.ps_args).respond(200); + // //$scope.getTop(); + // //$httpBackend.flush(); + //}); }); \ No newline at end of file diff --git a/test/unit/app/shared/filters.spec.js b/test/unit/app/shared/filters.spec.js index 48a0f54bc..6ebfabe59 100644 --- a/test/unit/app/shared/filters.spec.js +++ b/test/unit/app/shared/filters.spec.js @@ -106,19 +106,19 @@ describe('filters', function () { })); it('should handle KB values', inject(function (humansizeFilter) { - expect(humansizeFilter(5120)).toBe('5 KB'); + expect(humansizeFilter(5 * 1024)).toBe('5 KB'); })); it('should handle MB values', inject(function (humansizeFilter) { - expect(humansizeFilter(5 * Math.pow(10, 6))).toBe('5 MB'); + expect(humansizeFilter(5 * 1024 * 1024)).toBe('5.0 MB'); })); it('should handle GB values', inject(function (humansizeFilter) { - expect(humansizeFilter(5 * Math.pow(10, 9))).toBe('5 GB'); + expect(humansizeFilter(5 * 1024 * 1024 * 1024)).toBe('5.00 GB'); })); it('should handle TB values', inject(function (humansizeFilter) { - expect(humansizeFilter(5 * Math.pow(10, 12))).toBe('5 TB'); + expect(humansizeFilter(5 * 1024 * 1024 * 1024 * 1024)).toBe('5.000 TB'); })); }); From ce90515c9505c320828939abce0ebce6a5105682 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sat, 29 Aug 2015 00:40:24 -0500 Subject: [PATCH 07/10] Added network graph --- app/components/stats/stats.html | 27 +++++++++- app/components/stats/statsController.js | 69 +++++++++++++++++++++++-- 2 files changed, 90 insertions(+), 6 deletions(-) diff --git a/app/components/stats/stats.html b/app/components/stats/stats.html index 2ba8fe846..71f3a2fa0 100644 --- a/app/components/stats/stats.html +++ b/app/components/stats/stats.html @@ -2,13 +2,14 @@

    Stats

    -

    CPU Usage

    +

    CPU

    +

    Memory

    @@ -25,6 +26,10 @@ Limit {{ data.memory_stats.limit | humansize }} + + Fail count + {{ data.memory_stats.failcnt }} + @@ -38,5 +43,25 @@
    + +

    Network

    +
    +
    + +
    +
    +
    + + + + + + + +
    {{ key }}{{ value }}
    +
    +
    +
    +
    diff --git a/app/components/stats/statsController.js b/app/components/stats/statsController.js index 39a5ddc2d..03ba71408 100644 --- a/app/components/stats/statsController.js +++ b/app/components/stats/statsController.js @@ -1,5 +1,5 @@ angular.module('stats', []) - .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', 'humansizeFilter', function (Settings, $scope, Messages, $timeout, Container, $routeParams, humansizeFilter) { + .controller('StatsController', ['Settings', '$scope', 'Messages', '$timeout', 'Container', '$routeParams', 'humansizeFilter', '$sce', function (Settings, $scope, Messages, $timeout, Container, $routeParams, humansizeFilter, $sce) { // TODO: Implement memory chart, force scale to 0-100 for cpu, 0 to limit for memory, fix charts on dashboard, // TODO: Force memory scale to 0 - max memory //var initialStats = {}; // Used to set scale of memory graph. @@ -21,11 +21,17 @@ angular.module('stats', []) var cpuData = []; var memoryLabels = []; var memoryData = []; + var networkLabels = []; + var networkTxData = []; + var networkRxData = []; for (var i = 0; i < 40; i++) { cpuLabels.push(''); cpuData.push(0); memoryLabels.push(''); memoryData.push(0); + networkLabels.push(''); + networkTxData.push(0); + networkRxData.push(0); } var cpuDataset = { // CPU Usage fillColor: "rgba(151,187,205,0.5)", @@ -41,6 +47,34 @@ angular.module('stats', []) pointStrokeColor: "#fff", data: memoryData }; + var networkRxDataset = { + label: "Rx Bytes", + fillColor: "rgba(151,187,205,0.5)", + strokeColor: "rgba(151,187,205,1)", + pointColor: "rgba(151,187,205,1)", + pointStrokeColor: "#fff", + data: networkRxData + }; + var networkTxDataset = { + label: "Tx Bytes", + fillColor: "rgba(255,180,174,0.5)", + strokeColor: "rgba(255,180,174,1)", + pointColor: "rgba(255,180,174,1)", + pointStrokeColor: "#fff", + data: networkTxData + }; + var networkLegendData = [ + { + //value: '', + color: 'rgba(151,187,205,0.5)', + title: 'Rx Data' + }, + { + //value: '', + color: 'rgba(255,180,174,0.5)', + title: 'Rx Data' + }]; + legend($('#network-legend').get(0), networkLegendData); var cpuChart = new Chart($('#cpu-stats-chart').get(0).getContext("2d")).Line({ @@ -64,7 +98,16 @@ angular.module('stats', []) //scaleStepWidth: Math.ceil(initialStats.memory_stats.limit / 10), //scaleStartValue: 0 }); - + var networkChart = new Chart($('#network-stats-chart').get(0).getContext("2d")).Line({ + labels: networkLabels, + datasets: [networkRxDataset, networkTxDataset] + }, { + scaleLabel: function (valueObj) { + return humansizeFilter(parseInt(valueObj.value, 10)); + }, + responsive: true + }); + $scope.networkLegend = $sce.trustAsHtml(networkChart.generateLegend()); function updateStats() { Container.stats({id: $routeParams.id}, function (d) { @@ -78,8 +121,9 @@ angular.module('stats', []) // Update graph with latest data $scope.data = d; - updateChart(d); + updateCpuChart(d); updateMemoryChart(d); + updateNetworkChart(d); timeout = $timeout(updateStats, 1000); }, function () { Messages.error('Unable to retrieve stats', 'Is this container running?'); @@ -93,8 +137,8 @@ angular.module('stats', []) updateStats(); - function updateChart(data) { - console.log('updateChart', data); + function updateCpuChart(data) { + console.log('updateCpuChart', data); cpuChart.addData([calculateCPUPercent(data)], new Date(data.read).toLocaleTimeString()); cpuChart.removeData(); } @@ -103,7 +147,22 @@ angular.module('stats', []) console.log('updateMemoryChart', data); memoryChart.addData([data.memory_stats.usage], new Date(data.read).toLocaleTimeString()); memoryChart.removeData(); + } + var lastRxBytes = 0, lastTxBytes = 0; + + function updateNetworkChart(data) { + var rxBytes = 0, txBytes = 0; + if (lastRxBytes !== 0 || lastTxBytes !== 0) { + // These will be zero on first call, ignore to prevent large graph spike + rxBytes = data.network.rx_bytes - lastRxBytes; + txBytes = data.network.tx_bytes - lastTxBytes; + } + lastRxBytes = data.network.rx_bytes; + lastTxBytes = data.network.tx_bytes; + console.log('updateNetworkChart', data); + networkChart.addData([rxBytes, txBytes], new Date(data.read).toLocaleTimeString()); + networkChart.removeData(); } function calculateCPUPercent(stats) { From 5d4a2a7c2531b6fb8e561c516b45167d072bf888 Mon Sep 17 00:00:00 2001 From: Kevan Ahlquist Date: Sat, 29 Aug 2015 01:57:08 -0500 Subject: [PATCH 08/10] Fixed dashboard graphs, Chart.js doesn't like charts being hidden on creation, fixed routing issue to dashboard, version bump. --- app/app.js | 4 ++-- app/components/dashboard/dashboardController.js | 2 -- app/components/masthead/masthead.html | 2 +- package.json | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/app.js b/app/app.js index 0f87a89cf..35dbcdea9 100644 --- a/app/app.js +++ b/app/app.js @@ -48,5 +48,5 @@ angular.module('dockerui', ['dockerui.templates', 'ngRoute', 'dockerui.services' // You need to set this to the api endpoint without the port i.e. http://192.168.1.9 .constant('DOCKER_ENDPOINT', 'dockerapi') .constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is requred. If you have a port, prefix it with a ':' i.e. :4243 - .constant('UI_VERSION', 'v0.6.0') - .constant('DOCKER_API_VERSION', 'v1.17'); + .constant('UI_VERSION', 'v0.8.0') + .constant('DOCKER_API_VERSION', 'v1.20'); diff --git a/app/components/dashboard/dashboardController.js b/app/components/dashboard/dashboardController.js index ff73fd3d3..07646c007 100644 --- a/app/components/dashboard/dashboardController.js +++ b/app/components/dashboard/dashboardController.js @@ -19,14 +19,12 @@ angular.module('dashboard', []) var opts = {animation: false}; if (Settings.firstLoad) { - $('#stats').hide(); opts.animation = true; Settings.firstLoad = false; $('#masthead').show(); setTimeout(function () { $('#masthead').slideUp('slow'); - $('#stats').slideDown('slow'); }, 5000); } diff --git a/app/components/masthead/masthead.html b/app/components/masthead/masthead.html index b47e5d195..c7246c80f 100644 --- a/app/components/masthead/masthead.html +++ b/app/components/masthead/masthead.html @@ -1,7 +1,7 @@

    DockerUI