899 lines
37 KiB
JavaScript
899 lines
37 KiB
JavaScript
/*
|
|
* This file is part of Cockpit.
|
|
*
|
|
* Copyright (C) 2016 Red Hat, Inc.
|
|
*
|
|
* Cockpit is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU Lesser General Public License as published by
|
|
* the Free Software Foundation; either version 2.1 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Cockpit is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public License
|
|
* along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/* This is here to support cockpit.jump
|
|
* If this ever needs to be used outsite of cockpit
|
|
* then we'll need abstract this away in kube-client-cockpit
|
|
*/
|
|
/* globals cockpit */
|
|
|
|
(function() {
|
|
var angular = require('angular');
|
|
var d3 = require('d3');
|
|
require('angular-route');
|
|
require('angular-dialog.js');
|
|
|
|
require('./charts');
|
|
require('./date');
|
|
require('./kube-client');
|
|
require('./listing');
|
|
require('./utils');
|
|
|
|
require('../views/nodes-page.html');
|
|
require('../views/node-page.html');
|
|
require('../views/node-body.html');
|
|
require('../views/node-capacity.html');
|
|
require('../views/node-stats.html');
|
|
require('../views/node-add.html');
|
|
require('../views/node-delete.html');
|
|
require('../views/node-alerts.html');
|
|
|
|
angular.module('kubernetes.nodes', [
|
|
'ngRoute',
|
|
'kubeClient',
|
|
'kubernetes.date',
|
|
'kubernetes.listing',
|
|
'kubeUtils',
|
|
'ui.cockpit',
|
|
'ui.charts',
|
|
])
|
|
|
|
.config([
|
|
'$routeProvider',
|
|
function($routeProvider) {
|
|
$routeProvider
|
|
.when('/nodes', {
|
|
templateUrl: 'views/nodes-page.html',
|
|
controller: 'NodeCtrl'
|
|
})
|
|
|
|
.when('/nodes/:target', {
|
|
controller: 'NodeCtrl',
|
|
templateUrl: 'views/node-page.html'
|
|
});
|
|
}
|
|
])
|
|
|
|
/*
|
|
* The controller for the node view.
|
|
*/
|
|
.controller('NodeCtrl', [
|
|
'$scope',
|
|
'kubeLoader',
|
|
'kubeSelect',
|
|
'ListingState',
|
|
'filterService',
|
|
'$routeParams',
|
|
'$location',
|
|
'nodeActions',
|
|
'nodeData',
|
|
'nodeStatsSummary',
|
|
'$timeout',
|
|
'KubeBrowserStorage',
|
|
function($scope, loader, select, ListingState, filterService,
|
|
$routeParams, $location, actions, nodeData, statsSummary,
|
|
$timeout, browser) {
|
|
var target = $routeParams["target"] || "";
|
|
$scope.target = target;
|
|
|
|
$scope.stats = statsSummary.newNodeStatsSummary();
|
|
|
|
loader.listen(function() {
|
|
var selection;
|
|
$scope.nodes = select().kind("Node");
|
|
if (target) {
|
|
selection = select().kind("Node")
|
|
.name(target);
|
|
$scope.item = selection.one();
|
|
} else {
|
|
selection = $scope.nodes;
|
|
}
|
|
if ($scope.stats)
|
|
$scope.stats.trackNodes(selection);
|
|
}, $scope);
|
|
|
|
loader.watch("Node", $scope);
|
|
|
|
$scope.$on("$destroy", function() {
|
|
if ($scope.stats)
|
|
$scope.stats.close();
|
|
$scope.stats = null;
|
|
});
|
|
|
|
$scope.listing = new ListingState($scope);
|
|
|
|
/* All the actions available on the $scope */
|
|
angular.extend($scope, actions);
|
|
angular.extend($scope, nodeData);
|
|
|
|
$scope.$on("activate", function(ev, id) {
|
|
$location.path('/nodes/' + encodeURIComponent(id));
|
|
$scope.$applyAsync();
|
|
});
|
|
|
|
$scope.nodePods = function node_pods(item) {
|
|
var meta = item.metadata || {};
|
|
return select().kind("Pod")
|
|
.host(meta.name);
|
|
};
|
|
|
|
$scope.deleteSelectedNodes = function() {
|
|
var k;
|
|
var selected = [];
|
|
for (k in $scope.listing.selected) {
|
|
if ($scope.nodes[k] && $scope.listing.selected[k])
|
|
selected.push($scope.nodes[k]);
|
|
}
|
|
|
|
if (!selected.length)
|
|
return;
|
|
|
|
return actions.deleteNodes(selected).then(function() {
|
|
$scope.listing.selected = {};
|
|
});
|
|
};
|
|
|
|
/* Redirect after a delete */
|
|
$scope.deleteNode = function(val) {
|
|
var promise = actions.deleteNodes(val);
|
|
|
|
/* If the promise is successful, redirect to another page */
|
|
promise.then(function() {
|
|
if ($scope.target)
|
|
$location.path("/nodes");
|
|
});
|
|
|
|
return promise;
|
|
};
|
|
|
|
$scope.jump = function (node) {
|
|
var host, ip;
|
|
if (!node || !node.metadata)
|
|
return;
|
|
|
|
host = node.metadata.name;
|
|
ip = nodeData.nodeIPAddress(node);
|
|
|
|
if (ip == "127.0.0.1" || ip == "::1") {
|
|
ip = "localhost";
|
|
} else {
|
|
browser.sessionStorage.setItem(
|
|
"v1-session-machine/" + ip,
|
|
JSON.stringify({ "address": ip,
|
|
"label": host,
|
|
visible: true })
|
|
);
|
|
}
|
|
|
|
cockpit.jump("/", ip);
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('nodeBody',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/node-body.html',
|
|
};
|
|
}
|
|
)
|
|
|
|
.directive('nodeCapacity',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/node-capacity.html',
|
|
};
|
|
}
|
|
)
|
|
|
|
.directive('nodeStats',
|
|
function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/node-stats.html',
|
|
};
|
|
}
|
|
)
|
|
|
|
.factory('nodeActions', [
|
|
'$modal',
|
|
function($modal) {
|
|
function addNode() {
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'AddNodeCtrl',
|
|
templateUrl: 'views/node-add.html',
|
|
resolve: {},
|
|
}).result;
|
|
}
|
|
|
|
function deleteNodes(val) {
|
|
var nodes;
|
|
if (angular.isArray(val))
|
|
nodes = val;
|
|
else
|
|
nodes = [ val ];
|
|
|
|
return $modal.open({
|
|
animation: false,
|
|
controller: 'NodeDeleteCtrl',
|
|
templateUrl: 'views/node-delete.html',
|
|
resolve: {
|
|
dialogData: function() {
|
|
return { nodes: nodes };
|
|
}
|
|
},
|
|
}).result;
|
|
}
|
|
|
|
return {
|
|
addNode: addNode,
|
|
deleteNodes: deleteNodes
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("NodeDeleteCtrl", [
|
|
"$q",
|
|
"$scope",
|
|
"$modalInstance",
|
|
"dialogData",
|
|
"kubeMethods",
|
|
"kubeSelect",
|
|
function($q, $scope, $instance, dialogData, methods, select) {
|
|
angular.extend($scope, dialogData);
|
|
|
|
$scope.performDelete = function performDelete() {
|
|
var k;
|
|
var errors = [];
|
|
var nodes = {};
|
|
var promises = [];
|
|
|
|
function handleError(ex) {
|
|
errors.push(ex.message || ex.statusText);
|
|
nodes[k] = $scope.nodes[k];
|
|
return $q.reject();
|
|
}
|
|
|
|
for (k in $scope.nodes) {
|
|
var p = methods.delete($scope.nodes[k])
|
|
.catch(handleError);
|
|
promises.push(p);
|
|
}
|
|
|
|
return $q.all(promises).catch(function () {
|
|
$scope.nodes = select(nodes);
|
|
return $q.reject(errors);
|
|
});
|
|
};
|
|
}
|
|
])
|
|
|
|
.factory('nodeData', [
|
|
"KubeMapNamedArray",
|
|
"KubeTranslate",
|
|
function (mapNamedArray, translate) {
|
|
const _ = translate.gettext;
|
|
|
|
function nodeConditions(node) {
|
|
var status;
|
|
if (!node)
|
|
return;
|
|
|
|
if (!node.conditions) {
|
|
status = node.status || { };
|
|
node.conditions = mapNamedArray(status.conditions, "type");
|
|
}
|
|
return node.conditions;
|
|
}
|
|
|
|
function nodeCondition(node, type) {
|
|
var conditions = nodeConditions(node) || {};
|
|
return conditions[type] || {};
|
|
}
|
|
|
|
function nodeStatus(node) {
|
|
var spec = node ? node.spec : {};
|
|
if (!nodeCondition(node, "Ready").status)
|
|
return _("Unknown");
|
|
|
|
if (nodeCondition(node, "Ready").status != 'True')
|
|
return _("Not Ready");
|
|
|
|
if (spec && spec.unschedulable)
|
|
return _("Scheduling Disabled");
|
|
|
|
return _("Ready");
|
|
}
|
|
|
|
function nodeStatusIcon(node) {
|
|
var state = "";
|
|
/* If no status.conditions then it hasn't even started */
|
|
if (!nodeCondition(node, "Ready").status) {
|
|
state = "wait";
|
|
} else {
|
|
if (nodeCondition(node, "Ready").status != 'True') {
|
|
state = "fail";
|
|
} else if (nodeCondition(node, "OutOfDisk").status == "True" ||
|
|
nodeCondition(node, "OutOfMemory").status == "True") {
|
|
state = "warn";
|
|
}
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function nodeIPAddress(node) {
|
|
if (!node || !node.status)
|
|
return;
|
|
|
|
var addresses = node.status.addresses;
|
|
var address;
|
|
var internal;
|
|
/* If no addresses then it hasn't even started */
|
|
if (addresses) {
|
|
addresses.forEach(function(a) {
|
|
if (a.type == "LegacyHostIP" || a.type == "ExternalIP") {
|
|
address = a.address;
|
|
return false;
|
|
} else if (a.type == "InternalIP") {
|
|
internal = a.address;
|
|
}
|
|
});
|
|
}
|
|
|
|
return address || internal;
|
|
}
|
|
|
|
return {
|
|
nodeStatusIcon: nodeStatusIcon,
|
|
nodeCondition: nodeCondition,
|
|
nodeConditions: nodeConditions,
|
|
nodeStatus: nodeStatus,
|
|
nodeIPAddress: nodeIPAddress
|
|
};
|
|
}
|
|
])
|
|
|
|
.controller("AddNodeCtrl", [
|
|
"$q",
|
|
"$scope",
|
|
"$modalInstance",
|
|
"kubeMethods",
|
|
"KubeTranslate",
|
|
function($q, $scope, $instance, methods, translate) {
|
|
const _ = translate.gettext;
|
|
var fields = {
|
|
"address" : "",
|
|
"name" : "",
|
|
};
|
|
var dirty = false;
|
|
|
|
$scope.fields = fields;
|
|
|
|
function validate() {
|
|
var regex = /^[a-z0-9.-]+$/i;
|
|
var defer = $q.defer();
|
|
var address = fields.address.trim();
|
|
var name = fields.name.trim();
|
|
var ex;
|
|
var failures = [];
|
|
var item;
|
|
|
|
if (!address)
|
|
ex = new Error(_("Please type an address"));
|
|
else if (!regex.test(address))
|
|
ex = new Error(_("The address contains invalid characters"));
|
|
|
|
if (ex) {
|
|
ex.target = "#node-address";
|
|
failures.push(ex);
|
|
}
|
|
|
|
if (name && !regex.test(name)) {
|
|
ex = new Error(_("The name contains invalid characters"));
|
|
ex.target = "#node-name";
|
|
failures.push(ex);
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
defer.reject(failures);
|
|
} else {
|
|
item = {
|
|
"kind": "Node",
|
|
"apiVersion": "v1",
|
|
"metadata": {
|
|
"name": name || address,
|
|
}
|
|
};
|
|
defer.resolve(item);
|
|
}
|
|
|
|
return defer.promise;
|
|
}
|
|
|
|
$scope.nameKeyUp = function nameKeyUp(event) {
|
|
dirty = true;
|
|
if (event.keyCode == 13)
|
|
$scope.performAdd();
|
|
};
|
|
|
|
$scope.addressKeyUp = function addressKeyUp(event) {
|
|
if (event.keyCode == 13)
|
|
$scope.performAdd();
|
|
else if (!dirty)
|
|
fields.name = event.target.value;
|
|
};
|
|
|
|
$scope.performAdd = function performAdd() {
|
|
return validate().then(function(item) {
|
|
return methods.create(item);
|
|
});
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('kubernetesStatusIcon', function() {
|
|
return {
|
|
restrict: 'A',
|
|
link: function($scope, element, attributes) {
|
|
$scope.$watch(attributes["status"], function(status) {
|
|
element
|
|
.toggleClass("spinner spinner-xs", status == "wait")
|
|
.toggleClass("pficon pficon-error-circle-o", status == "fail")
|
|
.toggleClass("pficon pficon-warning-triangle-o", status == "warn");
|
|
});
|
|
}
|
|
};
|
|
})
|
|
|
|
.directive('nodeAlerts', function() {
|
|
return {
|
|
restrict: 'A',
|
|
templateUrl: 'views/node-alerts.html'
|
|
};
|
|
})
|
|
|
|
.factory('nodeStatsSummary', [
|
|
"$q",
|
|
"$interval",
|
|
"$exceptionHandler",
|
|
"KubeRequest",
|
|
"nodeData",
|
|
"KubeStringToBytes",
|
|
"KubeFormat",
|
|
function ($q, $interval, $exceptionHandler, KubeRequest, nodeData,
|
|
kubeStringToBytes, format) {
|
|
function NodeStatsSummary() {
|
|
var self = this;
|
|
|
|
var requests = {};
|
|
var statData = {};
|
|
var callbacks = [];
|
|
var interval;
|
|
|
|
function request(name) {
|
|
if (requests[name])
|
|
return $q.when([]);
|
|
|
|
var path = "/api/v1/nodes/" + encodeURIComponent(name) + "/proxy/stats/summary/";
|
|
var req = KubeRequest("GET", path);
|
|
requests[name] = req;
|
|
|
|
return req.then(function(data) {
|
|
delete requests[name];
|
|
statData[name] = data.data;
|
|
})
|
|
.catch(function(ex) {
|
|
delete requests[name];
|
|
delete statData[name];
|
|
if (ex.status != 503)
|
|
console.warn(ex);
|
|
});
|
|
}
|
|
|
|
function invokeCallbacks(/* ... */) {
|
|
var i, len, func;
|
|
for (i = 0, len = callbacks.length; i < len; i++) {
|
|
func = callbacks[i];
|
|
try {
|
|
if (func)
|
|
func.apply(self, arguments);
|
|
} catch (e) {
|
|
$exceptionHandler(e);
|
|
}
|
|
}
|
|
}
|
|
|
|
function fetchForNames(names) {
|
|
var q = [];
|
|
angular.forEach(names, function(name) {
|
|
q.push(request(name));
|
|
});
|
|
|
|
$q.all(q).then(invokeCallbacks, invokeCallbacks);
|
|
}
|
|
|
|
interval = $interval(function () {
|
|
fetchForNames(Object.keys(statData));
|
|
}, 5000);
|
|
|
|
self.watch = function watch(callback) {
|
|
callbacks.push(callback);
|
|
|
|
return {
|
|
cancel: function() {
|
|
var i, len;
|
|
for (i = 0, len = callbacks.length; i < len; i++) {
|
|
if (callbacks[i] === callback)
|
|
callbacks[i] = null;
|
|
}
|
|
}
|
|
};
|
|
};
|
|
|
|
self.close = function close() {
|
|
var name;
|
|
if (interval)
|
|
$interval.cancel(interval);
|
|
|
|
for (name in requests) {
|
|
var req = requests[name];
|
|
if (req && req.cancel)
|
|
req.cancel();
|
|
}
|
|
};
|
|
|
|
self.trackNodes = function trackNodes(selection) {
|
|
var names = [];
|
|
angular.forEach(selection, function(node) {
|
|
var ready = nodeData.nodeCondition(node, "Ready");
|
|
var meta = node ? node.metadata : {};
|
|
var name = meta ? meta.name : "";
|
|
|
|
if (ready && ready.status === 'True') {
|
|
if (!statData[name])
|
|
names.push(name);
|
|
} else {
|
|
// Unfortunally i'm seen some requests
|
|
// not error so clean them out.
|
|
delete statData[name];
|
|
if (request[name]) {
|
|
request[name].cancel();
|
|
delete request[name];
|
|
}
|
|
}
|
|
});
|
|
fetchForNames(names);
|
|
};
|
|
|
|
self.getSimpleUsage = function getSimpleUsage(node, section) {
|
|
var meta = node ? node.metadata : {};
|
|
var status = node ? node.status : {};
|
|
var name = meta ? meta.name : "";
|
|
|
|
var nodeData = statData[name] ? statData[name].node : {};
|
|
var result = nodeData[section];
|
|
|
|
if (!result)
|
|
return;
|
|
|
|
var allocatable = status ? status.allocatable : {};
|
|
if (!allocatable)
|
|
return;
|
|
|
|
switch (section) {
|
|
case "cpu":
|
|
if (!allocatable.cpu)
|
|
return;
|
|
return {
|
|
used: result.usageNanoCores,
|
|
total: allocatable.cpu * 1000000000
|
|
};
|
|
case "memory":
|
|
if (!allocatable.memory)
|
|
return;
|
|
return {
|
|
used: result.usageBytes,
|
|
total: kubeStringToBytes(allocatable.memory)
|
|
};
|
|
case "fs":
|
|
return {
|
|
used: result.usedBytes,
|
|
total: result.capacityBytes
|
|
};
|
|
default:
|
|
}
|
|
};
|
|
}
|
|
|
|
return {
|
|
newNodeStatsSummary: function(interval) {
|
|
return new NodeStatsSummary(interval);
|
|
},
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('nodeOsGraph', [
|
|
"KubeTranslate",
|
|
function(translate) {
|
|
const _ = translate.gettext;
|
|
|
|
return {
|
|
scope: {
|
|
'nodes' : '='
|
|
},
|
|
template: '<div class="col-xs-12 col-md-6" id="os-counts-graph" donut-pct-chart data="data" bar-size="8" legend="os-counts-legend" large-title="largeTitle"></div><div class="col-xs-12 col-md-6 legend-col"><div id="os-counts-legend"></div></div>',
|
|
restrict: 'A',
|
|
link: function($scope, element, attributes) {
|
|
$scope.data = [];
|
|
$scope.largeTitle = 0;
|
|
|
|
function refresh(items) {
|
|
items = items || [];
|
|
var data = {};
|
|
angular.forEach(items, function(node) {
|
|
var os;
|
|
var color;
|
|
if (node.status && node.status.nodeInfo)
|
|
os = node.status.nodeInfo.osImage;
|
|
|
|
if (!os) {
|
|
os = _("Unknown");
|
|
color = "#bbbbbb";
|
|
}
|
|
|
|
if (data[os])
|
|
data[os].value++;
|
|
else
|
|
data[os] = { value: 1, label: os, color: color };
|
|
});
|
|
|
|
var arr = Object.keys(data).map(function(k) {
|
|
return data[k];
|
|
});
|
|
|
|
arr.sort(function (a, b) {
|
|
if (a.label < b.label)
|
|
return -1;
|
|
if (a.label > b.label)
|
|
return 1;
|
|
return 0;
|
|
});
|
|
|
|
$scope.data = arr;
|
|
$scope.largeTitle = items.length;
|
|
}
|
|
|
|
$scope.$watchCollection('nodes', function(nodes) {
|
|
refresh(nodes);
|
|
});
|
|
}
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('nodeHeatMap', [
|
|
"KubeTranslate",
|
|
"KubeFormat",
|
|
function(translate, format) {
|
|
const _ = translate.gettext;
|
|
return {
|
|
restrict: 'A',
|
|
scope: {
|
|
'nodes' : '=',
|
|
'stats' : '='
|
|
},
|
|
template: '<div class="card-pf-title"><ul class="nav nav-tabs nav-tabs-pf"></ul></div><div threshold-heat-map class="card-pf-body node-heatmap" data="data" clickAction="clickAction()" legend="nodes-heatmap-legend"></div><div id="nodes-heatmap-legend"></div></div>',
|
|
link: function($scope, element, attributes) {
|
|
var outer = d3.select(element[0]);
|
|
var currentTab;
|
|
var tabs = {
|
|
cpu: {
|
|
label: _("CPU"),
|
|
tooltip: function(r) { return format.format(_("CPU Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
|
|
},
|
|
memory: {
|
|
label: _("Memory"),
|
|
tooltip: function(r) { return format.format(_("Memory Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
|
|
},
|
|
fs: {
|
|
label: _("Disk"),
|
|
tooltip: function(r) { return format.format(_("Disk Utilization: $0%"), Math.round((r.used / r.total) * 100)) }
|
|
}
|
|
};
|
|
|
|
outer.select("ul.nav-tabs")
|
|
.selectAll("li")
|
|
.data(Object.keys(tabs))
|
|
.enter()
|
|
.append("li")
|
|
.attr("data-metric", function(d) { return d })
|
|
.append("a")
|
|
.text(function(d) { return tabs[d].label });
|
|
|
|
function changeTab(tab) {
|
|
currentTab = tab;
|
|
outer.selectAll("ul.nav-tabs li")
|
|
.attr("class", function(d) { return tab === d ? "active" : null });
|
|
refreshData();
|
|
}
|
|
|
|
outer.selectAll("ul.nav-tabs li")
|
|
.on("click", function() {
|
|
changeTab(d3.select(this).attr("data-metric"));
|
|
});
|
|
|
|
function refreshData() {
|
|
var nodes = $scope.nodes;
|
|
var data = [];
|
|
|
|
if (!nodes)
|
|
nodes = [];
|
|
|
|
angular.forEach(nodes, function(node) {
|
|
var result, value, name;
|
|
var tooltip = _("Unknown");
|
|
if (node && node.metadata)
|
|
name = node.metadata.name;
|
|
|
|
if (!name)
|
|
return;
|
|
|
|
result = $scope.stats.getSimpleUsage(node, currentTab);
|
|
if (result)
|
|
value = result.used / result.total;
|
|
|
|
if (value === undefined)
|
|
value = -1;
|
|
else
|
|
tooltip = tabs[currentTab].tooltip(result);
|
|
|
|
data.push({ value: value, name: name,
|
|
tooltip: tooltip });
|
|
});
|
|
|
|
data.sort(function (a, b) {
|
|
return b.value - a.value;
|
|
});
|
|
|
|
$scope.$applyAsync(function() {
|
|
$scope.data = data;
|
|
});
|
|
return true;
|
|
}
|
|
|
|
$scope.$on("boxClick", function (ev, name) {
|
|
$scope.$emit("activate", name);
|
|
});
|
|
|
|
var sw = $scope.stats.watch(refreshData);
|
|
$scope.$on("$destroy", function() {
|
|
sw.cancel();
|
|
});
|
|
|
|
changeTab("cpu");
|
|
}
|
|
};
|
|
}
|
|
])
|
|
|
|
.directive('nodeUsageDonutChart', [
|
|
"KubeTranslate",
|
|
"KubeFormat",
|
|
function(translate, format) {
|
|
const _ = translate.gettext;
|
|
return {
|
|
restrict: 'A',
|
|
scope: {
|
|
'node' : '=',
|
|
'stats' : '=',
|
|
},
|
|
template: '<div ng-if="data" donut-pct-chart data="data" bar-size="8" large-title="largeTitle" small-title="smallTitle"></div><div class="text-center" ng-if="data">{{ title }}</div>',
|
|
link: function($scope, element, attributes) {
|
|
var colorFunc = d3.scale.threshold()
|
|
.domain([0.7, 0.8, 0.9])
|
|
.range(['#d4f0fa', '#F9D67A', '#EC7A08', '#CE0000']);
|
|
|
|
var types = {
|
|
cpu: {
|
|
label: _("CPU"),
|
|
smallTitle: function () {},
|
|
largeTitle: function (result) {
|
|
var r = result.used / result.total;
|
|
var p = Math.round(r * 100);
|
|
return format.format("$0%", p);
|
|
},
|
|
},
|
|
memory: {
|
|
label: _("Memory"),
|
|
smallTitle: function (result) {
|
|
return _("Used");
|
|
},
|
|
largeTitle: function (result) {
|
|
return format.formatBytes(result.used);
|
|
},
|
|
},
|
|
fs: {
|
|
label: _("Disk"),
|
|
smallTitle: function (result) {
|
|
return _("Used");
|
|
},
|
|
largeTitle: function (result) {
|
|
return format.formatBytes(result.used);
|
|
},
|
|
}
|
|
};
|
|
|
|
var type = attributes['type'];
|
|
$scope.title = types[type].label;
|
|
|
|
function clear() {
|
|
$scope.$applyAsync(function() {
|
|
$scope.data = null;
|
|
$scope.smallTitle = null;
|
|
$scope.largeTitle = null;
|
|
});
|
|
}
|
|
|
|
function refreshData() {
|
|
var node = $scope.node;
|
|
var result;
|
|
|
|
if (!node)
|
|
return clear();
|
|
|
|
result = $scope.stats.getSimpleUsage(node, type);
|
|
if (result) {
|
|
$scope.$applyAsync(function() {
|
|
$scope.smallTitle = types[type].smallTitle(result);
|
|
$scope.largeTitle = types[type].largeTitle(result);
|
|
var u = Math.round((result.used / result.total) * 100);
|
|
var l = 100 - u;
|
|
var freeText = translate.ngettext("$0% Free",
|
|
"$0% Free", u);
|
|
var usedText = translate.ngettext("$0% Used",
|
|
"$0% Used", u);
|
|
$scope.data = [
|
|
{ value: result.total - result.used,
|
|
tooltip : format.format(freeText, l),
|
|
color: "#bbbbbb" },
|
|
{ value: result.used,
|
|
tooltip : format.format(usedText, u),
|
|
color: colorFunc(result.used / result.total) }
|
|
];
|
|
});
|
|
} else {
|
|
clear();
|
|
}
|
|
return true;
|
|
}
|
|
|
|
var sw = $scope.stats.watch(refreshData);
|
|
$scope.$on("$destroy", function() {
|
|
sw.cancel();
|
|
});
|
|
}
|
|
};
|
|
}
|
|
]);
|
|
}());
|