diff --git a/.gitignore b/.gitignore index aa8b207dbbb31..43253f2328376 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ network_closure.sh .kubeconfig .tags* + +# Web UI +www/master/node_modules/ diff --git a/hack/build-ui.sh b/hack/build-ui.sh index d38715b60b145..647bd5fff5c6a 100755 --- a/hack/build-ui.sh +++ b/hack/build-ui.sh @@ -33,7 +33,7 @@ fi DATAFILE=pkg/ui/datafile.go TMP_DATAFILE=/tmp/datafile.go -go-bindata -nocompress -o $DATAFILE -prefix ${PWD} -pkg ui www/... third_party/swagger-ui/... +go-bindata -nocompress -o $DATAFILE -prefix ${PWD} -pkg ui www/app/... third_party/swagger-ui/... cat hooks/boilerplate.go.txt > $TMP_DATAFILE echo "// generated by hack/build-ui.sh; DO NOT EDIT diff --git a/pkg/ui/datafile.go b/pkg/ui/datafile.go index 5c77e6c7d01d9..bfb4d4b9b57ed 100644 --- a/pkg/ui/datafile.go +++ b/pkg/ui/datafile.go @@ -59,1437 +59,17711 @@ func (fi bindata_file_info) Sys() interface{} { return nil } -var _www_box_ng = []byte(` + + + + + + + +`) + +func www_app_assets_img_icons_ic_menu_svg_bytes() ([]byte, error) { + return _www_app_assets_img_icons_ic_menu_svg, nil +} + +func www_app_assets_img_icons_ic_menu_svg() (*asset, error) { + bytes, err := www_app_assets_img_icons_ic_menu_svg_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/img/icons/ic_menu.svg", size: 791, mode: os.FileMode(436), modTime: time.Unix(1429444413, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_img_icons_ic_menu_24px_svg = []byte(` + + + + + + + + + + + + + + + + + + + + + +`) + +func www_app_assets_img_icons_ic_menu_24px_svg_bytes() ([]byte, error) { + return _www_app_assets_img_icons_ic_menu_24px_svg, nil +} + +func www_app_assets_img_icons_ic_menu_24px_svg() (*asset, error) { + bytes, err := www_app_assets_img_icons_ic_menu_24px_svg_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/img/icons/ic_menu_24px.svg", size: 841, mode: os.FileMode(436), modTime: time.Unix(1429444413, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_img_icons_list_control_down_png = []byte("\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x000\x00\x00\x000\b\x03\x00\x00\x00`\xdc\t\xb5\x00\x00\x00\x19tEXtSoftware\x00Adobe ImageReadyq\xc9e<\x00\x00\x00'PLTE\xa8\xa8\xa8\xfc\xfc\xfc\xc0\xc0\xc0\xb6\xb6\xb6\xf7\xf7\xf7\xaa\xaa\xaa\xf6\xf6\xf6\xe6\xe6\xe6\xe8\xe8赵\xb5\xc3\xc3\xc3\xe5\xe5\xe5\xff\xff\xffZLu\xde\x00\x00\x00\rtRNS\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x00=\xe8\"\x86\x00\x00\x00\x8bIDATx\xda\xec\xd4I\x12\x80 \fD\xd1\x04\x04\x9c\xee\u007f^ˁ\xd2`\xb7\x96k\xc3\xd2\xfao\xa1\t\xca\xfc\xf1\x88\x03\a\xbf\x01}\xd7\x06\xb9\u007f\x02S\x8a\x8d\xc8C*\x1c\x94$bE\x1eD\xac0`\x14\xb1\"\xc7\xf5\xc9H\x81\x06+\xba\xad\x0f\xca\xdf\xc1\nԷ_\xe9*`\u007f\x9b\xc3)p\u007f\x1f\\\x15\xa4\a\x93>\x04\xe9\xd1j\xec\x82\xf4p\x97\xaa@=^\xbe]\xc0\x9el\xeb*p\xcf\xd6[\x03\xe9\xe9}P\xf5\x9f\x80\x03\a\xafg\x11`\x00\xb0\xe4e\a\x17\x87\xea}\x00\x00\x00\x00IEND\xaeB`\x82") + +func www_app_assets_img_icons_list_control_down_png_bytes() ([]byte, error) { + return _www_app_assets_img_icons_list_control_down_png, nil +} + +func www_app_assets_img_icons_list_control_down_png() (*asset, error) { + bytes, err := www_app_assets_img_icons_list_control_down_png_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/img/icons/list_control_down.png", size: 309, mode: os.FileMode(436), modTime: time.Unix(1429444413, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_img_kubernetes_svg = []byte(` + + + + +`) + +func www_app_assets_img_kubernetes_svg_bytes() ([]byte, error) { + return _www_app_assets_img_kubernetes_svg, nil +} + +func www_app_assets_img_kubernetes_svg() (*asset, error) { + bytes, err := www_app_assets_img_kubernetes_svg_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/img/kubernetes.svg", size: 11663, mode: os.FileMode(436), modTime: time.Unix(1429444413, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_js_gitkeep = []byte(``) + +func www_app_assets_js_gitkeep_bytes() ([]byte, error) { + return _www_app_assets_js_gitkeep, nil +} + +func www_app_assets_js_gitkeep() (*asset, error) { + bytes, err := www_app_assets_js_gitkeep_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/js/.gitkeep", size: 0, mode: os.FileMode(436), modTime: time.Unix(1429444413, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_js_app_js = []byte(`var componentNamespaces = ["kubernetesApp.components.dashboard"]; +// APP START +// **************************** +// /www/app/assets/app.js is autogenerated. Do not modify. +// Changes should be made in /master/modules/js or /master/components//js +// **************************** +// ----------------------------------- + +var app = angular.module('kubernetesApp', [ + 'ngRoute', + 'ngMaterial', + 'ngLodash', + 'door3.css', + 'kubernetesApp.config', + 'kubernetesApp.services', + 'angular.filter' +].concat(componentNamespaces)); + +angular.module('kubernetesApp.config', []); +angular.module('kubernetesApp.services', ['kubernetesApp.config']); + +app.config([ + '$routeProvider', + function($routeProvider) { + $routeProvider.when("/404", {templateUrl: "views/partials/404.html"}) + // else 404 + .otherwise({redirectTo: "/404"}); + } +]) + .config([ + '$routeProvider', + 'manifestRoutes', + function($routeProvider, manifestRoutes) { + angular.forEach(manifestRoutes, function(r) { + var route = { + templateUrl: r.templateUrl + }; + if (r.controller) { + route.controller = r.controller; + } + if (r.css) { + route.css = r.css; + } + $routeProvider.when(r.url, route); + }); + } + ]); + +app.directive('includeReplace', + function() { + 'use strict'; + return { + require: 'ngInclude', + restrict: 'A', /* optional */ + link: function(scope, el, attrs) { el.replaceWith(el.children()); } + }; + }) + .directive('compile', + ["$compile", function($compile) { + 'use strict'; + return function(scope, element, attrs) { + scope.$watch(function(scope) { return scope.$eval(attrs.compile); }, + function(value) { + element.html(value); + $compile(element.contents())(scope); + }); + }; + }]) + .directive("kubernetesUiMenu", + function() { + 'use strict'; + return { + templateUrl: "views/partials/kubernetes-ui-menu.tmpl.html" + }; + }) + .directive('menuToggle', function() { + 'use strict'; + return { + scope: {section: '='}, + templateUrl: 'views/partials/menu-toggle.tmpl.html', + link: function($scope, $element) { + var controller = $element.parent().controller(); + + $scope.isOpen = function() { return controller.isOpen($scope.section); }; + $scope.toggle = function() { controller.toggleOpen($scope.section); }; + + var parentNode = $element[0].parentNode.parentNode.parentNode; + if (parentNode.classList.contains('parent-list-item')) { + var heading = parentNode.querySelector('h2'); + $element[0].firstChild.setAttribute('aria-describedby', heading.id); + } + } + }; + }); + +app.filter('startFrom', + function() { + 'use strict'; + return function(input, start) { return input.slice(start); }; + }) + .filter('nospace', function() { + 'use strict'; + return function(value) { return (!value) ? '' : value.replace(/ /g, ''); }; + }); + +app.run(['$route', angular.noop]) + .run(["lodash", function(lodash) { + // Alias lodash + window['_'] = lodash; + }]); + +app.service('SidebarService', [ + '$rootScope', + function($rootScope) { + var service = this; + service.sidebarItems = []; + + service.clearSidebarItems = function() { service.sidebarItems = []; }; + + service.renderSidebar = function() { + var _entries = ''; + service.sidebarItems.forEach(function(entry) { _entries += entry.Html; }); + + if (_entries) { + $rootScope.sidenavLeft = '
' + _entries + '
'; + } + }; + + service.addSidebarItem = function(item) { + + service.sidebarItems.push(item); + + service.sidebarItems.sort(function(a, b) { return (a.order > b.order) ? 1 : ((b.order > a.order) ? -1 : 0); }); + }; + } +]); + + +app.value("tabs", [{"component":"dashboard","title":"Dashboard"}]); +app.constant("manifestRoutes", [{"description":"Dashboard visualization.","url":"/dashboard/","templateUrl":"components/dashboard/pages/home.html"},{"description":"Pods","url":"/dashboard/pods","templateUrl":"components/dashboard/views/listPods.html"},{"description":"Pod Visualizer","url":"/dashboard/visualpods","templateUrl":"components/dashboard/views/listPodsVisualizer.html"},{"description":"Services","url":"/dashboard/services","templateUrl":"components/dashboard/views/listServices.html"},{"description":"Replication Controllers","url":"/dashboard/replicationcontrollers","templateUrl":"components/dashboard/views/listReplicationControllers.html"},{"description":"Events","url":"/dashboard/events","templateUrl":"components/dashboard/views/listEvents.html"},{"description":"Minions","url":"/dashboard/minions","templateUrl":"components/dashboard/views/listMinions.html"},{"description":"Replication Controller","url":"/dashboard/replicationcontrollers/:replicationControllerId","templateUrl":"components/dashboard/views/replication.html"},{"description":"Service","url":"/dashboard/services/:serviceId","templateUrl":"components/dashboard/views/service.html"},{"description":"Explore","url":"/dashboard/groups/:grouping*?/selector/:selector*?","templateUrl":"components/dashboard/views/groups.html"},{"description":"Pod","url":"/dashboard/pods/:podId","templateUrl":"components/dashboard/views/pod.html"}]); + +angular.module("kubernetesApp.config", []) + +.constant("ENV", { + "/": { + "k8sApiServer": "/api/v1beta2", + "k8sDataServer": "/cluster", + "k8sDataPollMinIntervalSec": 10, + "k8sDataPollMaxIntervalSec": 120, + "k8sDataPollErrorThreshold": 5, + "cAdvisorProxy": "", + "cAdvisorPort": "4194" + } +}) + +.constant("ngConstant", true) + +; +/**========================================================= + * Module: config.js + * App routes and resources configuration + =========================================================*/ +/**========================================================= + * Module: constants.js + * Define constants to inject across the application + =========================================================*/ +/**========================================================= + * Module: main.js + * Main Application Controller + =========================================================*/ +/**========================================================= + * Module: tabs-global.js + * Page Controller + =========================================================*/ + +app.controller('TabCtrl', [ + '$scope', + '$location', + 'tabs', + function($scope, $location, tabs) { + $scope.tabs = tabs; + + $scope.switchTab = function(index) { + var location_path = $location.path(); + var tab = tabs[index]; + + if (tab) { + var path = '/%s'.format(tab.component); + if (location_path.indexOf(path) == -1) { + $location.path(path); + } + } + }; + } +]); + +/**========================================================= + * Module: sidebar.js + * Wraps the sidebar and handles collapsed state + =========================================================*/ +(function() { + "use strict"; + + angular.module('kubernetesApp.services') + .service('cAdvisorService', ["$http", "$q", "ENV", function($http, $q, ENV) { + var _baseUrl = function(minionIp) { + var minionPort = ENV['/']['cAdvisorPort'] || "8081"; + var proxy = ENV['/']['cAdvisorProxy'] || "/api/v1beta2/proxy/nodes/"; + + return proxy + minionIp + ':' + minionPort + '/api/v1.0/'; + }; + + this.getMachineInfo = getMachineInfo; + + function getMachineInfo(minionIp) { + var fullUrl = _baseUrl(minionIp) + 'machine'; + var deferred = $q.defer(); + + // hack + $http.get(fullUrl).success(function(data) { + deferred.resolve(data); + }).error(function(data, status) { deferred.reject('There was an error') }); + return deferred.promise; + } + + this.getContainerInfo = getContainerInfo; + // containerId optional + function getContainerInfo(minionIp, containerId) { + containerId = (typeof containerId === "undefined") ? "/" : containerId; + + var fullUrl = _baseUrl(minionIp) + 'containers' + containerId; + var deferred = $q.defer(); + + var request = { + "num_stats": 10, + "num_samples": 0 + }; + + $http.post(fullUrl, request) + .success(function(data) { deferred.resolve(data); }) + .error(function() { deferred.reject('There was an error') }); + return deferred.promise; + } + + this.getDataForMinion = function(minionIp) { + var machineData, containerData; + var deferred = $q.defer(); + + var p = $q.all([getMachineInfo(minionIp), getContainerInfo(minionIp)]) + .then( + function(dataArray) { + machineData = dataArray[0]; + containerData = dataArray[1]; + + var memoryData = parseMemory(machineData, containerData); + var cpuData = parseCpu(machineData, containerData); + var fsData = parseFilesystems(machineData, containerData); + deferred.resolve({ + memoryData: memoryData, + cpuData: cpuData, + filesystemData: fsData, + machineData: machineData, + containerData: containerData + }); + + }, + function(errorData) { deferred.reject(errorData); }); + + return deferred.promise; + }; + + // Utils to process cadvisor data + function humanize(num, size, units) { + var unit; + for (unit = units.pop(); units.length && num >= size; unit = units.pop()) { + num /= size; + } + return [num, unit]; + } + + // Following the IEC naming convention + function humanizeIEC(num) { + var ret = humanize(num, 1024, ["TiB", "GiB", "MiB", "KiB", "Bytes"]); + return ret[0].toFixed(2) + " " + ret[1]; + } + + // Following the Metric naming convention + function humanizeMetric(num) { + var ret = humanize(num, 1000, ["TB", "GB", "MB", "KB", "Bytes"]); + return ret[0].toFixed(2) + " " + ret[1]; + } + + function hasResource(stats, resource) { return stats.stats.length > 0 && stats.stats[0][resource]; } + + // Gets the length of the interval in nanoseconds. + function getInterval(current, previous) { + var cur = new Date(current); + var prev = new Date(previous); + + // ms -> ns. + return (cur.getTime() - prev.getTime()) * 1000000; + } + + function parseCpu(machineInfo, containerInfo) { + var cur = containerInfo.stats[containerInfo.stats.length - 1]; + var results = []; + + var cpuUsage = 0; + if (containerInfo.spec.has_cpu && containerInfo.stats.length >= 2) { + var prev = containerInfo.stats[containerInfo.stats.length - 2]; + var rawUsage = cur.cpu.usage.total - prev.cpu.usage.total; + var intervalInNs = getInterval(cur.timestamp, prev.timestamp); + + // Convert to millicores and take the percentage + cpuUsage = Math.round(((rawUsage / intervalInNs) / machineInfo.num_cores) * 100); + if (cpuUsage > 100) { + cpuUsage = 100; + } + } + + return { + cpuPercentUsage: cpuUsage + }; + } + + function parseFilesystems(machineInfo, containerInfo) { + var cur = containerInfo.stats[containerInfo.stats.length - 1]; + if (!cur.filesystem) { + return; + } + + var filesystemData = []; + for (var i = 0; i < cur.filesystem.length; i++) { + var data = cur.filesystem[i]; + var totalUsage = Math.floor((data.usage * 100.0) / data.capacity); + + var f = { + device: data.device, + filesystemNumber: i + 1, + usage: data.usage, + usageDescription: humanizeMetric(data.usage), + capacity: data.capacity, + capacityDescription: humanizeMetric(data.capacity), + totalUsage: Math.floor((data.usage * 100.0) / data.capacity) + }; + + filesystemData.push(f); + } + return filesystemData; + } + + var oneMegabyte = 1024 * 1024; + var oneGigabyte = 1024 * oneMegabyte; + + function parseMemory(machineInfo, containerInfo) { + if (containerInfo.spec.has_memory && !hasResource(containerInfo, "memory")) { + return; + } + + // var titles = ["Time", "Total", "Hot"]; + var data = []; + for (var i = 0; i < containerInfo.stats.length; i++) { + var cur = containerInfo.stats[i]; + + var elements = []; + elements.push(cur.timestamp); + elements.push(cur.memory.usage / oneMegabyte); + elements.push(cur.memory.working_set / oneMegabyte); + data.push(elements); + } + + // Get the memory limit, saturate to the machine size. + var memory_limit = machineInfo.memory_capacity; + if (containerInfo.spec.memory.limit && (containerInfo.spec.memory.limit < memory_limit)) { + memory_limit = containerInfo.spec.memory.limit; + } + + var cur = containerInfo.stats[containerInfo.stats.length - 1]; + + var r = { + current: { + memoryUsage: cur.memory.usage, + workingMemoryUsage: cur.memory.working_set, + memoryLimit: memory_limit, + memoryUsageDescription: humanizeMetric(cur.memory.usage), + workingMemoryUsageDescription: humanizeMetric(cur.memory.working_set), + memoryLimitDescription: humanizeMetric(memory_limit) + }, + historical: data + }; + + return r; + } + }]); +})(); + +app.provider('k8sApi', + function() { + + var urlBase = ''; + + this.setUrlBase = function(value) { urlBase = value; }; + + var _get = function($http, baseUrl, query) { + var _fullUrl = baseUrl; + if (query !== undefined) { + _fullUrl += '/' + query; + } + + return $http.get(_fullUrl); + }; + + this.$get = ["$http", "$q", function($http, $q) { + var api = {}; + + api.getUrlBase = function() { return urlBase; }; + + api.getPods = function(query) { return _get($http, urlBase + '/pods', query); }; + + api.getMinions = function(query) { return _get($http, urlBase + '/minions', query); }; + + api.getServices = function(query) { return _get($http, urlBase + '/services', query); }; + + api.getReplicationControllers = function(query) { + return _get($http, urlBase + '/replicationControllers', query) + }; + + api.getEvents = function(query) { return _get($http, urlBase + '/events', query); }; + + return api; + }]; + }) + .config(["k8sApiProvider", "ENV", function(k8sApiProvider, ENV) { + if (ENV && ENV['/'] && ENV['/']['k8sApiServer']) { + var proxy = ENV['/']['cAdvisorProxy'] || ''; + k8sApiProvider.setUrlBase(proxy + ENV['/']['k8sApiServer']); + } + }]); + +(function() { + "use strict"; + + var pollK8sDataServiceProvider = function PollK8sDataServiceProvider(_) { + // A set of configuration controlling the polling behavior. + // Their values should be configured in the application before + // creating the service instance. + + var useSampleData = false; + this.setUseSampleData = function(value) { useSampleData = value; }; + + var sampleDataFiles = ["shared/assets/sampleData1.json"]; + this.setSampleDataFiles = function(value) { sampleDataFiles = value; }; + + var dataServer = "http://localhost:5555/cluster"; + this.setDataServer = function(value) { dataServer = value; }; + + var pollMinIntervalSec = 10; + this.setPollMinIntervalSec = function(value) { pollMinIntervalSec = value; }; + + var pollMaxIntervalSec = 120; + this.setPollMaxIntervalSec = function(value) { pollMaxIntervalSec = value; }; + + var pollErrorThreshold = 5; + this.setPollErrorThreshold = function(value) { pollErrorThreshold = value; }; + + this.$get = function($http, $timeout) { + // Now the sequenceNumber will be used for debugging and verification purposes. + var k8sdatamodel = { + "data": undefined, + "sequenceNumber": 0, + "useSampleData": useSampleData + }; + var pollingError = 0; + var promise = undefined; + + // Implement fibonacci back off when the service is down. + var pollInterval = pollMinIntervalSec; + var pollIncrement = pollInterval; + + // Reset polling interval. + var resetCounters = function() { + pollInterval = pollMinIntervalSec; + pollIncrement = pollInterval; + }; + + // Bump error count and polling interval. + var bumpCounters = function() { + // Bump the error count. + pollingError++; + + // TODO: maybe display an error in the UI to the end user. + if (pollingError % pollErrorThreshold === 0) { + console.log("Error: " + pollingError + " consecutive polling errors for " + dataServer + "."); + } + + // Bump the polling interval. + var oldIncrement = pollIncrement; + pollIncrement = pollInterval; + pollInterval += oldIncrement; + + // Reset when limit reached. + if (pollInterval > pollMaxIntervalSec) { + resetCounters(); + } + }; + + var updateModel = function(newModel) { + var dedupe = function(dataModel) { + if (dataModel.resources) { + dataModel.resources = _.uniq(dataModel.resources, function(resource) { return resource.id; }); + } + + if (dataModel.relations) { + dataModel.relations = + _.uniq(dataModel.relations, function(relation) { return relation.source + relation.target; }); + } + }; + + dedupe(newModel); + + var newModelString = JSON.stringify(newModel); + var oldModelString = ""; + if (k8sdatamodel.data) { + oldModelString = JSON.stringify(k8sdatamodel.data); + } + + if (newModelString !== oldModelString) { + k8sdatamodel.data = newModel; + k8sdatamodel.sequenceNumber++; + } + + pollingError = 0; + resetCounters(); + }; + + var nextSampleDataFile = 0; + var getSampleDataFile = function() { + var result = ""; + if (sampleDataFiles.length > 0) { + result = sampleDataFiles[nextSampleDataFile % sampleDataFiles.length]; + ++nextSampleDataFile; + } + + return result; + }; + + var pollOnce = function(scope, repeat) { + var dataSource = (k8sdatamodel.useSampleData) ? getSampleDataFile() : dataServer; + $.getJSON(dataSource) + .done(function(newModel, jqxhr, textStatus) { + if (newModel && newModel.success) { + delete newModel.success; // Remove success indicator. + delete newModel.timestamp; // Remove changing timestamp. + updateModel(newModel); + scope.$apply(); + promise = repeat ? $timeout(function() { pollOnce(scope, true); }, pollInterval * 1000) : undefined; + return; + } + + bumpCounters(); + promise = repeat ? $timeout(function() { pollOnce(scope, true); }, pollInterval * 1000) : undefined; + }) + .fail(function(jqxhr, textStatus, error) { + bumpCounters(); + promise = repeat ? $timeout(function() { pollOnce(scope, true); }, pollInterval * 1000) : undefined; + }); + }; + + var isPolling = function() { return promise ? true : false; }; + + var start = function(scope) { + // If polling has already started, then calling start() again would + // just reset the counters and polling interval, but it will not + // start a new thread polling in parallel to the existing polling + // thread. + resetCounters(); + if (!promise) { + k8sdatamodel.data = undefined; + pollOnce(scope, true); + } + }; + + var stop = function() { + if (promise) { + $timeout.cancel(promise); + promise = undefined; + } + }; + + var refresh = function(scope) { + stop(scope); + resetCounters(); + k8sdatamodel.data = undefined; + pollOnce(scope, false); + }; + + return { + "k8sdatamodel": k8sdatamodel, + "isPolling": isPolling, + "refresh": refresh, + "start": start, + "stop": stop + }; + }; + }; + + angular.module("kubernetesApp.services") + .provider("pollK8sDataService", ["lodash", pollK8sDataServiceProvider]) + .config(["pollK8sDataServiceProvider", "ENV", function(pollK8sDataServiceProvider, ENV) { + if (ENV && ENV['/']) { + if (ENV['/']['k8sDataServer']) { + pollK8sDataServiceProvider.setDataServer(ENV['/']['k8sDataServer']); + } + if (ENV['/']['k8sDataPollIntervalMinSec']) { + pollK8sDataServiceProvider.setPollIntervalSec(ENV['/']['k8sDataPollIntervalMinSec']); + } + if (ENV['/']['k8sDataPollIntervalMaxSec']) { + pollK8sDataServiceProvider.setPollIntervalSec(ENV['/']['k8sDataPollIntervalMaxSec']); + } + if (ENV['/']['k8sDataPollErrorThreshold']) { + pollK8sDataServiceProvider.setPollErrorThreshold(ENV['/']['k8sDataPollErrorThreshold']); + } + } + }]); + +}()); + +/**========================================================= + * Module: toggle-state.js + * Services to share toggle state functionality + =========================================================*/ + + +app.controller('cAdvisorController', [ + '$scope', + '$routeParams', + 'k8sApi', + 'lodash', + 'cAdvisorService', + '$q', + '$interval', + function($scope, $routeParams, k8sApi, lodash, cAdvisorService, $q, $interval) { + $scope.k8sApi = k8sApi; + + $scope.activeMinionDataById = {}; + $scope.maxDataByById = {}; + + $scope.getData = function() { + $scope.loading = true; + + k8sApi.getMinions().success(angular.bind(this, function(res) { + $scope.minions = res; + // console.log(res); + var promises = lodash.map(res.items, function(m) { return cAdvisorService.getDataForMinion(m.id); }); + + $q.all(promises).then( + function(dataArray) { + lodash.each(dataArray, function(data, i) { + var m = res.items[i]; + + var maxData = maxMemCpuInfo(m.id, data.memoryData, data.cpuData, data.filesystemData); + + // console.log("maxData", maxData); + + $scope.activeMinionDataById[m.id] = + transformMemCpuInfo(data.memoryData, data.cpuData, data.filesystemData, maxData, m.hostIP) + }); + + }, + function(errorData) { + // console.log("Error: " + errorData); + $scope.loading = false; + }); + + $scope.loading = false; + })).error(angular.bind(this, this.handleError)); + }; + + function getcAdvisorDataForMinion(m) { + var p = cAdvisorService.getDataForMinion(m.hostIP); + return p; + } + + function handleError(data, status, headers, config) { + // console.log("Error (" + status + "): " + data); + $scope.loading = false; + }; + + // d3 + function getColorForIndex(i, percentage) { + // var colors = ['red', 'blue', 'yellow', 'pink', 'purple', 'green', 'orange']; + // return colors[i]; + var c = "color-" + (i + 1); + if (percentage && percentage >= 90) + c = c + ' color-critical'; + else if (percentage && percentage >= 80) + c = c + ' color-warning'; + + return c; + } + + function getMaxColorForIndex(i, percentage) { + // var colors = ['red', 'blue', 'yellow', 'pink', 'purple', 'green', 'orange']; + // return colors[i]; + var c = "color-max-" + (i + 1); + if (percentage && percentage >= 90) + c = c + ' color-max-critical'; + else if (percentage && percentage >= 80) + c = c + ' color-max-warning'; + + return c; + } + + function maxMemCpuInfo(mId, mem, cpu, fsDataArray) { + if ($scope.maxDataByById[mId] === undefined) $scope.maxDataByById[mId] = {}; + + var currentMem = mem.current; + var currentCpu = cpu; + + var items = []; + + if ($scope.maxDataByById[mId]['cpu'] === undefined || + $scope.maxDataByById[mId]['cpu'] < currentCpu.cpuPercentUsage) { + // console.log("New max cpu " + mId, $scope.maxDataByById[mId].cpu, currentCpu.cpuPercentUsage); + $scope.maxDataByById[mId]['cpu'] = currentCpu.cpuPercentUsage; + } + items.push({ + maxValue: $scope.maxDataByById[mId]['cpu'], + maxTickClassNames: getColorForIndex(0, $scope.maxDataByById[mId]['cpu']), + maxClassNames: getMaxColorForIndex(0, $scope.maxDataByById[mId]['cpu']) + }); + + var memPercentage = Math.floor((currentMem.memoryUsage * 100.0) / currentMem.memoryLimit); + if ($scope.maxDataByById[mId]['mem'] === undefined || $scope.maxDataByById[mId]['mem'] < memPercentage) + $scope.maxDataByById[mId]['mem'] = memPercentage; + items.push({ + maxValue: $scope.maxDataByById[mId]['mem'], + maxTickClassNames: getColorForIndex(1, $scope.maxDataByById[mId]['mem']), + maxClassNames: getMaxColorForIndex(1, $scope.maxDataByById[mId]['mem']) + }); + + for (var i = 0; i < fsDataArray.length; i++) { + var f = fsDataArray[i]; + var fid = 'FS #' + f.filesystemNumber; + if ($scope.maxDataByById[mId][fid] === undefined || $scope.maxDataByById[mId][fid] < f.totalUsage) + $scope.maxDataByById[mId][fid] = f.totalUsage; + items.push({ + maxValue: $scope.maxDataByById[mId][fid], + maxTickClassNames: getColorForIndex(2 + i, $scope.maxDataByById[mId][fid]), + maxClassNames: getMaxColorForIndex(2 + i, $scope.maxDataByById[mId][fid]) + }); + } + + // console.log("Max Data is now " + mId, $scope.maxDataByById[mId]); + return items; + } + + function transformMemCpuInfo(mem, cpu, fsDataArray, maxData, hostName) { + var currentMem = mem.current; + var currentCpu = cpu; + + var items = []; + + items.push({ + label: 'CPU', + stats: currentCpu.cpuPercentUsage + '%', + value: currentCpu.cpuPercentUsage, + classNames: getColorForIndex(0, currentCpu.cpuPercentUsage), + maxData: maxData[0], + hostName: hostName + }); + + var memPercentage = Math.floor((currentMem.memoryUsage * 100.0) / currentMem.memoryLimit); + items.push({ + label: 'Memory', + stats: currentMem.memoryUsageDescription + ' / ' + currentMem.memoryLimitDescription, + value: memPercentage, + classNames: getColorForIndex(1, memPercentage), + maxData: maxData[1], + hostName: hostName + }); + + for (var i = 0; i < fsDataArray.length; i++) { + var f = fsDataArray[i]; + + items.push({ + label: 'FS #' + f.filesystemNumber, + stats: f.usageDescription + ' / ' + f.capacityDescription, + value: f.totalUsage, + classNames: getColorForIndex(2 + i, f.totalUsage), + maxData: maxData[2 + i], + hostName: hostName + + }); + } + + var a = []; + var segments = { + segments: items + }; + a.push(segments); + return a; + }; + + // end d3 + var promise = $interval($scope.getData, 3000); + + // Cancel interval on page changes + $scope.$on('$destroy', function() { + if (angular.isDefined(promise)) { + $interval.cancel(promise); + promise = undefined; + } + }); + + $scope.getData(); + + } +]); + +/**========================================================= + * Module: Dashboard + * Visualizer for clusters + =========================================================*/ + +app.controller('DashboardCtrl', ['$scope', function($scope) {}]); + +/**========================================================= + * Module: Group + * Visualizer for groups + =========================================================*/ + +app.controller('GroupCtrl', [ + '$scope', + '$route', + '$interval', + '$routeParams', + 'k8sApi', + '$rootScope', + '$location', + 'lodash', + function($scope, $route, $interval, $routeParams, k8sApi, $rootScope, $location, _) { + 'use strict'; + $scope.doTheBack = function() { window.history.back(); }; + + $scope.capitalize = function(s) { return _.capitalize(s); }; + + $rootScope.doTheBack = $scope.doTheBack; + + $scope.resetGroupLayout = function(group) { delete group.settings; }; + + $scope.handlePath = function(path) { + var parts = path.split("/"); + // split leaves an empty string at the beginning. + parts = parts.slice(1); + + if (parts.length === 0) { + return; + } + this.handleGroups(parts.slice(1)); + }; + + $scope.getState = function(obj) { return Object.keys(obj)[0]; }; + + $scope.clearSelector = function(grouping) { $location.path("/dashboard/groups/" + grouping + "/selector/"); }; + + $scope.changeGroupBy = function() { + var grouping = $scope.selectedGroupBy; + + var s = _.clone($location.search()); + if ($scope.routeParams.grouping != grouping) + $location.path("/dashboard/groups/" + grouping + "/selector/").search(s); + }; + + $scope.createBarrier = function(count, callback) { + var barrier = count; + var barrierFunction = angular.bind(this, function(data) { + // JavaScript is single threaded so this is safe. + barrier--; + if (barrier === 0) { + if (callback) { + callback(); + } + } + }); + return barrierFunction; + }; + + $scope.handleGroups = function(parts, selector) { + $scope.groupBy = parts; + $scope.loading = true; + $scope.selector = selector; + var args = []; + var type = ""; + if (selector && selector.length > 0) { + $scope.selectorPieces = selector.split(","); + var labels = []; + var fields = []; + for (var i = 0; i < $scope.selectorPieces.length; i++) { + var piece = $scope.selectorPieces[i]; + if (piece[0] == '$') { + fields.push(piece.slice(2)); + } else { + if (piece.indexOf("type=") === 0) { + var labelParts = piece.split("="); + if (labelParts.length > 1) { + type = labelParts[1]; + } + } else { + labels.push(piece); + } + } + } + if (labels.length > 0) { + args.push("labels=" + encodeURI(labels.join(","))); + } + if (fields.length > 0) { + args.push("fields=" + encodeURI(fields.join(","))); + } + } + var query = "?" + args.join("&"); + var list = []; + var count = type.length > 0 ? 1 : 3; + var barrier = $scope.createBarrier(count, function() { + $scope.groups = $scope.groupData(list, 0); + $scope.loading = false; + $scope.groupByOptions = buildGroupByOptions(); + $scope.selectedGroupBy = $routeParams.grouping; + }); + + if (type === "" || type == "pod") { + k8sApi.getPods(query).success(function(data) { + $scope.addLabel("type", "pod", data.items); + for (var i = 0; data.items && i < data.items.length; ++i) { + data.items[i].labels.host = data.items[i].currentState.host; + list.push(data.items[i]); + } + barrier(); + }).error($scope.handleError); + } + if (type === "" || type == "service") { + k8sApi.getServices(query).success(function(data) { + $scope.addLabel("type", "service", data.items); + for (var i = 0; data.items && i < data.items.length; ++i) { + list.push(data.items[i]); + } + barrier(); + }).error($scope.handleError); + } + if (type === "" || type == "replicationController") { + k8sApi.getReplicationControllers(query).success(angular.bind(this, function(data) { + $scope.addLabel("type", "replicationController", data.items); + for (var i = 0; data.items && i < data.items.length; ++i) { + list.push(data.items[i]); + } + barrier(); + })).error($scope.handleError); + } + }; + + $scope.addLabel = function(key, value, items) { + if (!items) { + return; + } + for (var i = 0; i < items.length; i++) { + if (!items[i].labels) { + items[i].labels = []; + } + items[i].labels[key] = value; + } + }; + + $scope.groupData = function(items, index) { + var result = { + "items": {}, + "kind": "grouping" + }; + for (var i = 0; i < items.length; i++) { + key = items[i].labels[$scope.groupBy[index]]; + if (!key) { + key = ""; + } + var list = result.items[key]; + if (!list) { + list = []; + result.items[key] = list; + } + list.push(items[i]); + } + + if (index + 1 < $scope.groupBy.length) { + for (var key in result.items) { + result.items[key] = $scope.groupData(result.items[key], index + 1); + } + } + return result; + }; + $scope.getGroupColor = function(type) { + if (type === 'pod') { + return '#6193F0'; + } else if (type === 'replicationController') { + return '#E008FE'; + } else if (type === 'service') { + return '#7C43FF'; + } + }; + + var groups = $routeParams.grouping; + if (!groups) { + groups = ''; + } + + $scope.routeParams = $routeParams; + $scope.route = $route; + + $scope.handleGroups(groups.split('/'), $routeParams.selector); + + $scope.handleError = function(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope_.loading = false; + }; + + function getDefaultGroupByOptions() { return [{name: 'Type', value: 'type'}, {name: 'Name', value: 'name'}]; } + + function buildGroupByOptions() { + var g = $scope.groups; + var options = getDefaultGroupByOptions(); + var newOptions = _.map(g.items, function(vals) { return _.map(vals, function(v) { return _.keys(v.labels); }); }); + newOptions = + _.reject(_.uniq(_.flattenDeep(newOptions)), function(o) { return o == 'name' || o == 'type' || o == ""; }); + newOptions = _.map(newOptions, function(o) { + return { + name: o, + value: o + }; + }); + + options = options.concat(newOptions); + return options; + } + + $scope.changeFilterBy = function(selector) { + var grouping = $scope.selectedGroupBy; + + var s = _.clone($location.search()); + if ($scope.routeParams.selector != selector) + $location.path("/dashboard/groups/" + $scope.routeParams.grouping + "/selector/" + selector).search(s); + }; + } +]); + +/**========================================================= + * Module: Header + * Visualizer for clusters + =========================================================*/ + +angular.module('kubernetesApp.components.dashboard', []) + .controller('HeaderCtrl', [ + '$scope', + '$location', + function($scope, $location) { + 'use strict'; + $scope.$watch('Pages', function(newValue, oldValue) { + if (typeof newValue !== 'undefined') { + $location.path(newValue); + } + }); + + $scope.subPages = [ + {category: 'dashboard', name: 'Explore', value: '/dashboard/groups/type/selector/'}, + {category: 'dashboard', name: 'Pods', value: '/dashboard/pods'}, + {category: 'dashboard', name: 'Minions', value: '/dashboard/minions'}, + {category: 'dashboard', name: 'Replication Controllers', value: '/dashboard/replicationcontrollers'}, + {category: 'dashboard', name: 'Services', value: '/dashboard/services'}, + {category: 'dashboard', name: 'Events', value: '/dashboard/events'} + ]; + } + ]); + +/**========================================================= + * Module: List Events + * Visualizer list events + =========================================================*/ + +app.controller('ListEventsCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + '$location', + '$filter', + function($scope, $routeParams, k8sApi, $location, $filter) { + 'use strict'; + $scope.getData = getData; + $scope.loading = true; + $scope.k8sApi = k8sApi; + $scope.pods = null; + $scope.groupedPods = null; + $scope.serverView = false; + + $scope.headers = [ + {name: 'Time', field: 'time'}, + {name: 'From', field: 'from'}, + {name: 'Sub Object Path', field: 'subobject'}, + {name: 'Reason', field: 'reason'}, + {name: 'Message', field: 'message'} + ]; + + $scope.custom = { + time: '', + from: 'grey', + subobject: 'grey', + reason: 'grey', + message: 'grey' + }; + $scope.sortable = ['time', 'from', 'subobject']; + $scope.thumbs = 'thumb'; + $scope.count = 10; + + $scope.go = function(d) { $location.path('/dashboard/pods/' + d.id); }; + + $scope.moreClick = function(d, e) { + $location.path('/dashboard/pods/' + d.id); + e.stopPropagation(); + }; + + function handleError(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope.loading = false; + } + + $scope.content = []; + + function getData(dataId) { + $scope.loading = true; + k8sApi.getEvents().success(function(data) { + $scope.loading = false; + + var _fixComma = function(str) { + if (str.substring(0, 1) == ',') { + return str.substring(1); + } else { + return str; + } + }; + + data.items.forEach(function(event) { + + $scope.content.push({ + time: $filter('date')(event.timestamp, 'medium'), + from: event.source, + subobject: event.involvedObject.fieldPath, + reason: event.reason, + message: event.message + }); + + }); + + }).error($scope.handleError); + } + + getData($routeParams.serviceId); + + } +]); + +/**========================================================= + * Module: Minions + * Visualizer for minions + =========================================================*/ + +app.controller('ListMinionsCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + '$location', + function($scope, $routeParams, k8sApi, $location) { + 'use strict'; + $scope.getData = getData; + $scope.loading = true; + $scope.k8sApi = k8sApi; + $scope.pods = null; + $scope.groupedPods = null; + $scope.serverView = false; + + $scope.headers = [{name: 'Name', field: 'name'}, {name: 'IP', field: 'ip'}, {name: 'Status', field: 'status'}]; + + $scope.custom = { + name: '', + status: 'grey', + ip: 'grey' + }; + $scope.sortable = ['name', 'status', 'ip']; + $scope.thumbs = 'thumb'; + $scope.count = 10; + + $scope.go = function(d) { $location.path('/dashboard/pods/' + d.id); }; + + $scope.moreClick = function(d, e) { + $location.path('/dashboard/pods/' + d.id); + e.stopPropagation(); + }; + + function handleError(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope.loading = false; + } + + $scope.content = []; + + function getData(dataId) { + $scope.loading = true; + k8sApi.getMinions().success(function(data) { + $scope.loading = false; + + var _fixComma = function(str) { + if (str.substring(0, 1) == ',') { + return str.substring(1); + } else { + return str; + } + }; + + data.items.forEach(function(minion) { + var _kind = ''; + + if (minion.status.conditions) { + Object.keys(minion.status.conditions) + .forEach(function(key) { _kind += minion.status.conditions[key].kind; }); + } + + $scope.content.push({name: minion.id, ip: minion.hostIP, status: _kind}); + + }); + + }).error($scope.handleError); + } + + getData($routeParams.serviceId); + + } +]); + + + +app.controller('ListPodsCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + 'lodash', + '$location', + function($scope, $routeParams, k8sApi, lodash, $location) { + var _ = lodash; + $scope.getData = getData; + $scope.loading = true; + $scope.k8sApi = k8sApi; + $scope.pods = null; + $scope.groupedPods = null; + $scope.serverView = false; + + $scope.headers = [ + {name: '', field: 'thumb'}, + {name: 'Pod', field: 'pod'}, + {name: 'IP', field: 'ip'}, + {name: 'Status', field: 'status'}, + {name: 'Containers', field: 'containers'}, + {name: 'Images', field: 'images'}, + {name: 'Host', field: 'host'}, + {name: 'Labels', field: 'labels'} + ]; + + $scope.custom = { + pod: '', + ip: 'grey', + containers: 'grey', + images: 'grey', + host: 'grey', + labels: 'grey', + status: 'grey' + }; + $scope.sortable = ['pod', 'ip', 'status']; + $scope.thumbs = 'thumb'; + $scope.count = 10; + + $scope.go = function(d) { $location.path('/dashboard/pods/' + d.id); }; + + $scope.moreClick = function(d, e) { + $location.path('/dashboard/pods/' + d.id); + e.stopPropagation(); + }; + + var orderedPodNames = []; + + function handleError(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope.loading = false; + }; + + function getPodName(pod) { return _.has(pod.labels, 'name') ? pod.labels.name : pod.id; } + + $scope.content = []; + + function getData(dataId) { + $scope.loading = true; + k8sApi.getPods().success(angular.bind(this, function(data) { + $scope.loading = false; + + var _fixComma = function(str) { + if (str.substring(0, 1) == ',') { + return str.substring(1); + } else { + return str; + } + }; + + data.items.forEach(function(pod) { + var _containers = '', _images = '', _labels = '', _uses = ''; + + if (pod.desiredState.manifest) { + Object.keys(pod.desiredState.manifest.containers) + .forEach(function(key) { + _containers += ', ' + pod.desiredState.manifest.containers[key].name; + _images += ', ' + pod.desiredState.manifest.containers[key].image; + }); + } + + Object.keys(pod.labels) + .forEach(function(key) { + if (key == 'name') { + _labels += ', ' + pod.labels[key]; + } + if (key == 'uses') { + _uses += ', ' + pod.labels[key]; + } + }); + + $scope.content.push({ + thumb: '"assets/img/kubernetes.svg"', + pod: pod.id, + ip: pod.currentState.podIP, + containers: _fixComma(_containers), + images: _fixComma(_images), + host: pod.currentState.host, + labels: _fixComma(_labels) + ':' + _fixComma(_uses), + status: pod.currentState.status + }); + + }); + + })).error(angular.bind(this, handleError)); + }; + + $scope.getPodRestarts = function(pod) { + var r = null; + var container = _.first(pod.desiredState.manifest.containers); + if (container) r = pod.currentState.info[container.name].restartCount; + return r; + }; + + $scope.otherLabels = function(labels) { return _.omit(labels, 'name') }; + + $scope.podStatusClass = function(pod) { + + var s = pod.currentState.status.toLowerCase(); + + if (s == 'running' || s == 'succeeded') + return null; + else + return "status-" + s; + }; + + $scope.podIndexFromName = function(pod) { + var name = getPodName(pod); + return _.indexOf(orderedPodNames, name) + 1; + }; + + getData($routeParams.serviceId); + + } +]); + +/**========================================================= + * Module: Replication Controllers + * Visualizer for replication controllers + =========================================================*/ + +app.controller('ListReplicationControllersCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + '$location', + function($scope, $routeParams, k8sApi, $location) { + 'use strict'; + $scope.getData = getData; + $scope.loading = true; + $scope.k8sApi = k8sApi; + $scope.pods = null; + $scope.groupedPods = null; + $scope.serverView = false; + + $scope.headers = [ + {name: 'Controller', field: 'controller'}, + {name: 'Containers', field: 'containers'}, + {name: 'Images', field: 'images'}, + {name: 'Selector', field: 'selector'}, + {name: 'Replicas', field: 'replicas'} + ]; + + $scope.custom = { + controller: '', + containers: 'grey', + images: 'grey', + selector: 'grey', + replicas: 'grey' + }; + $scope.sortable = ['controller', 'containers', 'images']; + $scope.thumbs = 'thumb'; + $scope.count = 10; + + $scope.go = function(d) { $location.path('/dashboard/pods/' + d.id); }; + + $scope.moreClick = function(d, e) { + $location.path('/dashboard/pods/' + d.id); + e.stopPropagation(); + }; + + function handleError(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope.loading = false; + } + + $scope.content = []; + + function getData(dataId) { + $scope.loading = true; + k8sApi.getReplicationControllers().success(function(data) { + $scope.loading = false; + + var _fixComma = function(str) { + if (str.substring(0, 1) == ',') { + return str.substring(1); + } else { + return str; + } + }; + + data.items.forEach(function(replicationController) { + + var _name = '', _image = ''; + + if (replicationController.desiredState.podTemplate.desiredState.manifest.containers) { + Object.keys(replicationController.desiredState.podTemplate.desiredState.manifest.containers) + .forEach(function(key) { + _name += replicationController.desiredState.podTemplate.desiredState.manifest.containers[key].name; + _image += replicationController.desiredState.podTemplate.desiredState.manifest.containers[key].image; + }); + } + + var _name_selector = ''; + + if (replicationController.desiredState.replicaSelector) { + Object.keys(replicationController.desiredState.replicaSelector) + .forEach(function(key) { _name_selector += replicationController.desiredState.replicaSelector[key]; }); + } + + $scope.content.push({ + controller: replicationController.id, + containers: _name, + images: _image, + selector: _name_selector, + replicas: replicationController.currentState.replicas + }); + + }); + + }).error($scope.handleError); + } + + getData($routeParams.serviceId); + + } +]); + +/**========================================================= + * Module: Services + * Visualizer for services + =========================================================*/ + +app.controller('ListServicesCtrl', [ + '$scope', + '$interval', + '$routeParams', + 'k8sApi', + '$rootScope', + function($scope, $interval, $routeParams, k8sApi, $rootScope) { + 'use strict'; + $scope.doTheBack = function() { window.history.back(); }; + + $scope.headers = [ + {name: 'Name', field: 'name'}, + {name: 'Labels', field: 'labels'}, + {name: 'Selector', field: 'selector'}, + {name: 'IP', field: 'ip'}, + {name: 'Port', field: 'port'} + ]; + + $scope.custom = { + name: '', + ip: 'grey', + selector: 'grey', + port: 'grey', + labels: 'grey' + }; + $scope.sortable = ['name', 'ip', 'port']; + $scope.count = 10; + + $scope.content = []; + + $rootScope.doTheBack = $scope.doTheBack; + + $scope.handleError = function(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope_.loading = false; + }; + + $scope.getData = function(dataId) { + $scope.loading = true; + k8sApi.getServices(dataId).success(angular.bind(this, function(data) { + $scope.services = data; + $scope.loading = false; + + var _fixComma = function(str) { + if (str.substring(0, 1) == ',') { + return str.substring(1); + } else { + return str; + } + }; + + var addLabel = function(str, label) { + if (str) { + str = label + str; + } + return str; + }; + + if (data.items.constructor === Array) { + data.items.forEach(function(service) { + + var _name = '', _uses = '', _component = '', _provider = ''; + + if (service.labels !== null && typeof service.labels === 'object') { + Object.keys(service.labels) + .forEach(function(key) { + if (key == 'name') { + _name += ',' + service.labels[key]; + } + if (key == 'component') { + _component += ',' + service.labels[key]; + } + if (key == 'provider') { + _provider += ',' + service.labels[key]; + } + }); + } + + var _selectors = ''; + + if (service.selector !== null && typeof service.selector === 'object') { + Object.keys(service.selector) + .forEach(function(key) { + if (key == 'name') { + _selectors += ',' + service.selector[key]; + } + }); + } + + $scope.content.push({ + name: service.id, + ip: service.portalIP, + port: service.port, + selector: addLabel(_fixComma(_selectors), 'name='), + labels: addLabel(_fixComma(_name), 'name=') + ' ' + addLabel(_fixComma(_component), 'component=') + ' ' + + addLabel(_fixComma(_provider), 'provider=') + }); + }); + } + })).error($scope.handleError); + }; + + $scope.getData($routeParams.serviceId); + } +]); + +/**========================================================= + * Module: Pods + * Visualizer for pods + =========================================================*/ + +app.controller('PodCtrl', [ + '$scope', + '$interval', + '$routeParams', + 'k8sApi', + '$rootScope', + function($scope, $interval, $routeParams, k8sApi, $rootScope) { + 'use strict'; + $scope.doTheBack = function() { window.history.back(); }; + + $rootScope.doTheBack = $scope.doTheBack; + + $scope.handleError = function(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + $scope_.loading = false; + }; + + $scope.handlePod = function(podId) { + $scope.loading = true; + k8sApi.getPods(podId).success(angular.bind(this, function(data) { + $scope.pod = data; + $scope.loading = false; + })).error($scope.handleError); + }; + + $scope.handlePod($routeParams.podId); + } +]); + +/**========================================================= + * Module: Replication + * Visualizer for replication controllers + =========================================================*/ + +function ReplicationController() { +} + +ReplicationController.prototype.getData = function(dataId) { + this.scope.loading = true; + this.k8sApi.getReplicationControllers(dataId).success(angular.bind(this, function(data) { + this.scope.replicationController = data; + this.scope.loading = false; + })).error(angular.bind(this, this.handleError)); +}; + +ReplicationController.prototype.handleError = function(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + this.scope.loading = false; +}; + +app.controller('ReplicationControllerCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + function($scope, $routeParams, k8sApi) { + $scope.controller = new ReplicationController(); + $scope.controller.k8sApi = k8sApi; + $scope.controller.scope = $scope; + $scope.controller.getData($routeParams.replicationControllerId); + } +]); + +/**========================================================= + * Module: Services + * Visualizer for services + =========================================================*/ + +function ServiceController() { +} + +ServiceController.prototype.getData = function(dataId) { + this.scope.loading = true; + this.k8sApi.getServices(dataId).success(angular.bind(this, function(data) { + this.scope.service = data; + this.scope.loading = false; + })).error(angular.bind(this, this.handleError)); +}; + +ServiceController.prototype.handleError = function(data, status, headers, config) { + console.log("Error (" + status + "): " + data); + this.scope.loading = false; +}; + +app.controller('ServiceCtrl', [ + '$scope', + '$routeParams', + 'k8sApi', + '$location', + function($scope, $routeParams, k8sApi, $location) { + $scope.controller = new ServiceController(); + $scope.controller.k8sApi = k8sApi; + $scope.controller.scope = $scope; + $scope.controller.getData($routeParams.serviceId); + + $scope.go = function(d) { $location.path('/dashboard/services/' + d.id); } + + $scope.moreClick = function(d, e) { + $location.path('/dashboard/services/' + d.id); + e.stopPropagation(); + } + } +]); + +(function() { + 'use strict'; + + angular.module('kubernetesApp.components.dashboard') + .directive('d3MinionBarGauge', [ + 'd3DashboardService', + function(d3DashboardService) { + + return { + restrict: 'E', + scope: { + data: '=', + thickness: '@', + graphWidth: '@', + graphHeight: '@' + + }, + link: function(scope, element, attrs) { + + var draw = function(d3) { + var svg = d3.select("svg.chart"); + var legendSvg = d3.select("svg.legend"); + window.onresize = function() { return scope.$apply(); }; + + scope.$watch(function() { return angular.element(window)[0].innerWidth; }, + function() { return scope.render(scope.data); }); + + scope.$watch('data', function(newVals, oldVals) { + return initOrUpdate(newVals, oldVals); + + }, true); + + function initOrUpdate(newVals, oldVals) { + if (oldVals === null || oldVals === undefined) { + return scope.render(newVals); + } else { + return update(oldVals, newVals); + } + } + + var textOffset = 10; + var el = null; + var radius = 100; + var oldData = []; + + function init(options) { + var clone = options.data; + var preparedData = setData(clone); + setup(preparedData, options.width, options.height); + } + + function setup(data, w, h) { + svg = d3.select(element[0]).append("svg").attr("width", "100%"); + + legendSvg = d3.select(element[0]).append("svg").attr("width", "100%"); + + var chart = svg.attr("class", "chart") + .attr("width", w) + .attr("height", h - 25) + .append("svg:g") + .attr("class", "concentricchart") + .attr("transform", "translate(" + ((w / 2)) + "," + h / 4 + ")"); + + var legend = legendSvg.attr("class", "legend").attr("width", w); + + radius = Math.min(w, h) / 2; + + var hostName = legendSvg.append("text") + .attr("class", "hostName") + .attr("transform", "translate(" + ((w - 120) / 2) + "," + 15 + ")"); + + var label_legend_area = legendSvg.append("svg:g") + .attr("class", "label_legend_area") + .attr("transform", "translate(" + ((w - 185) / 2) + "," + 35 + ")"); + + var legend_group = label_legend_area.append("svg:g").attr("class", "legend_group"); + + var label_group = label_legend_area.append("svg:g") + .attr("class", "label_group") + .attr("transform", "translate(" + 25 + "," + 11 + ")"); + + var stats_group = label_legend_area.append("svg:g") + .attr("class", "stats_group") + .attr("transform", "translate(" + 85 + "," + 11 + ")"); + + var path_group = chart.append("svg:g") + .attr("class", "path_group") + .attr("transform", "translate(0," + (h / 4) + ")"); + var value_group = chart.append("svg:g") + .attr("class", "value_group") + .attr("transform", "translate(" + -(w * 0.205) + "," + -(h * 0.10) + ")"); + generateArcs(chart, data); + } + + function update(_oldData, _newData) { + if (_newData === undefined || _newData === null) { + return; + } + + var clone = jQuery.extend(true, {}, _newData); + var cloneOld = jQuery.extend(true, {}, _oldData); + var preparedData = setData(clone); + oldData = setData(cloneOld); + animate(preparedData); + } + + function animate(data) { generateArcs(null, data); } + + function setData(data) { + var diameter = 2 * Math.PI * radius; + var localData = []; + + $.each(data[0].segments, function(ri, value) { + + function calcAngles(v) { + var segmentValueSum = 200; + if (v > segmentValueSum) { + v = segmentValueSum; + } + + var segmentValue = v; + var fraction = segmentValue / segmentValueSum; + var arcBatchLength = fraction * 4 * Math.PI; + var arcPartition = arcBatchLength; + var startAngle = Math.PI * 2; + var endAngle = startAngle + arcPartition; + + return { + startAngle: startAngle, + endAngle: endAngle + }; + } + + var valueData = calcAngles(value.value); + data[0].segments[ri].startAngle = valueData.startAngle; + data[0].segments[ri].endAngle = valueData.endAngle; + + var maxData = value.maxData; + var maxTickData = calcAngles(maxData.maxValue + 0.2); + data[0].segments[ri].maxTickStartAngle = maxTickData.startAngle; + data[0].segments[ri].maxTickEndAngle = maxTickData.endAngle; + + var maxArcData = calcAngles(maxData.maxValue); + data[0].segments[ri].maxArcStartAngle = maxArcData.startAngle; + data[0].segments[ri].maxArcEndAngle = maxArcData.endAngle; + + data[0].segments[ri].index = ri; + }); + localData.push(data[0].segments); + return localData[0]; + } + + function generateArcs(_svg, data) { + var chart = svg; + var transitionTime = 750; + $.each(data, function(index, value) { + if (oldData[index] !== undefined) { + data[index].previousEndAngle = oldData[index].endAngle; + } else { + data[index].previousEndAngle = 0; + } + }); + var thickness = parseInt(scope.thickness, 10); + var ir = (parseInt(scope.graphWidth, 10) / 3); + var path_group = svg.select('.path_group'); + var arc_group = path_group.selectAll(".arc_group").data(data); + var arcEnter = arc_group.enter().append("g").attr("class", "arc_group"); + + arcEnter.append("path").attr("class", "bg-circle").attr("d", getBackgroundArc(thickness, ir)); + + arcEnter.append("path") + .attr("class", function(d, i) { return 'max_tick_arc ' + d.maxData.maxTickClassNames; }); + + arcEnter.append("path") + .attr("class", function(d, i) { return 'max_bg_arc ' + d.maxData.maxClassNames; }); + + arcEnter.append("path").attr("class", function(d, i) { return 'value_arc ' + d.classNames; }); + + var max_tick_arc = arc_group.select(".max_tick_arc"); + + max_tick_arc.transition() + .attr("class", function(d, i) { return 'max_tick_arc ' + d.maxData.maxTickClassNames; }) + .attr("d", function(d) { + var arc = maxArc(thickness, ir); + arc.startAngle(d.maxTickStartAngle); + arc.endAngle(d.maxTickEndAngle); + return arc(d); + }); + + var max_bg_arc = arc_group.select(".max_bg_arc"); + + max_bg_arc.transition() + .attr("class", function(d, i) { return 'max_bg_arc ' + d.maxData.maxClassNames; }) + .attr("d", function(d) { + var arc = maxArc(thickness, ir); + arc.startAngle(d.maxArcStartAngle); + arc.endAngle(d.maxArcEndAngle); + return arc(d); + }); + + var value_arc = arc_group.select(".value_arc"); + + value_arc.transition().ease("exp").attr("class", function(d, i) { + return 'value_arc ' + d.classNames; + }).duration(transitionTime).attrTween("d", function(d) { return arcTween(d, thickness, ir); }); + + arc_group.exit() + .select(".value_arc") + .transition() + .ease("exp") + .duration(transitionTime) + .attrTween("d", function(d) { return arcTween(d, thickness, ir); }) + .remove(); + + drawLabels(chart, data, ir, thickness); + buildLegend(chart, data); + } + + function arcTween(b, thickness, ir) { + var prev = JSON.parse(JSON.stringify(b)); + prev.endAngle = b.previousEndAngle; + var i = d3.interpolate(prev, b); + return function(t) { return getArc(thickness, ir)(i(t)); }; + } + + function maxArc(thickness, ir) { + var arc = d3.svg.arc().innerRadius(function(d) { + return getRadiusRing(ir, d.index); + }).outerRadius(function(d) { return getRadiusRing(ir + thickness, d.index); }); + return arc; + } + + function drawLabels(chart, data, ir, thickness) { + svg.select('.value_group').selectAll("*").remove(); + var counts = data.length; + var value_group = chart.select('.value_group'); + var valueLabels = value_group.selectAll("text.value").data(data); + valueLabels.enter() + .append("svg:text") + .attr("class", "value") + .attr( + "transform", function(d) { return "translate(" + (getRadiusRing(ir, counts - 1)) + ", 0)"; }) + .attr("dx", function(d, i) { return 0; }) + .attr("dy", function(d, i) { return (thickness + 3) * i; }) + .attr("text-anchor", function(d) { return "start"; }) + .text(function(d) { return d.value; }); + valueLabels.transition().duration(300).attrTween( + "d", function(d) { return arcTween(d, thickness, ir); }); + valueLabels.exit().remove(); + } + + function buildLegend(chart, data) { + var svg = legendSvg; + svg.select('.label_group').selectAll("*").remove(); + svg.select('.legend_group').selectAll("*").remove(); + svg.select('.stats_group').selectAll("*").remove(); + + var host_name = svg.select('.hostName'); + var label_group = svg.select('.label_group'); + var stats_group = svg.select('.stats_group'); + + host_name.text(data[0].hostName); + + host_name = svg.selectAll("text.hostName").data(data); + + host_name.attr("text-anchor", function(d) { return "start"; }) + .text(function(d) { return d.hostName; }); + host_name.exit().remove(); + + var labels = label_group.selectAll("text.labels").data(data); + labels.enter() + .append("svg:text") + .attr("class", "labels") + .attr("dy", function(d, i) { return 19 * i; }) + .attr("text-anchor", function(d) { return "start"; }) + .text(function(d) { return d.label; }); + labels.exit().remove(); + + var stats = stats_group.selectAll("text.stats").data(data); + stats.enter() + .append("svg:text") + .attr("class", "stats") + .attr("dy", function(d, i) { return 19 * i; }) + .attr("text-anchor", function(d) { return "start"; }) + .text(function(d) { return d.stats; }); + stats.exit().remove(); + + var legend_group = svg.select('.legend_group'); + var legend = legend_group.selectAll("rect").data(data); + legend.enter() + .append("svg:rect") + .attr("x", 2) + .attr("y", function(d, i) { return 19 * i; }) + .attr("width", 13) + .attr("height", 13) + .attr("class", function(d, i) { return "rect " + d.classNames; }); + + legend.exit().remove(); + } + + function getRadiusRing(ir, i) { return ir - (i * 20); } + + function getArc(thickness, ir) { + var arc = d3.svg.arc() + .innerRadius(function(d) { return getRadiusRing(ir, d.index); }) + .outerRadius(function(d) { return getRadiusRing(ir + thickness, d.index); }) + .startAngle(function(d, i) { return d.startAngle; }) + .endAngle(function(d, i) { return d.endAngle; }); + return arc; + } + + function getBackgroundArc(thickness, ir) { + var arc = d3.svg.arc() + .innerRadius(function(d) { return getRadiusRing(ir, d.index); }) + .outerRadius(function(d) { return getRadiusRing(ir + thickness, d.index); }) + .startAngle(0) + .endAngle(function() { return 2 * Math.PI; }); + return arc; + } + + scope.render = function(data) { + if (data === undefined || data === null) { + return; + } + + svg.selectAll("*").remove(); + + var graph = $(element[0]); + var w = scope.graphWidth; + var h = scope.graphHeight; + + var options = { + data: data, + width: w, + height: h + }; + + init(options); + }; + }; + d3DashboardService.d3().then(draw); + } + }; + } + ]); +}()); + +(function() { + 'use strict'; + + angular.module('kubernetesApp.components.dashboard') + .directive( + 'dashboardHeader', + function() { + 'use strict'; + return { + restrict: 'A', + replace: true, + scope: {user: '='}, + templateUrl: "components/dashboard/pages/header.html", + controller: [ + '$scope', + '$filter', + '$location', + '$rootScope', + function($scope, $filter, $location, $rootScope) { + $scope.$watch('page', function(newValue, oldValue) { + if (typeof newValue !== 'undefined') { + $location.path(newValue); + } + }); + + $scope.subpages = [ + { + category: 'dashboard', + name: 'Groups', + value: '/dashboard/groups/type/selector/', + id: 'groupsView' + }, + {category: 'dashboard', name: 'Pods', value: '/dashboard/pods', id: 'podsView'}, + {category: 'dashboard', name: 'Minions', value: '/dashboard/minions', id: 'minionsView'}, + { + category: 'dashboard', + name: 'Replication Controllers', + value: '/dashboard/replicationcontrollers', + id: 'rcView' + }, + {category: 'dashboard', name: 'Services', value: '/dashboard/services', id: 'servicesView'}, + {category: 'dashboard', name: 'Events', value: '/dashboard/events', id: 'eventsView'}, + ]; + } + ] + }; + }) + .directive('dashboardFooter', + function() { + 'use strict'; + return { + restrict: 'A', + replace: true, + templateUrl: "components/dashboard/pages/footer.html", + controller: ['$scope', '$filter', function($scope, $filter) {}] + }; + }) + .directive('mdTable', function() { + 'use strict'; + return { + restrict: 'E', + scope: { + headers: '=', + content: '=', + sortable: '=', + filters: '=', + customClass: '=customClass', + thumbs: '=', + count: '=' + }, + controller: ["$scope", "$filter", "$window", "$location", function($scope, $filter, $window, $location) { + var orderBy = $filter('orderBy'); + $scope.currentPage = 0; + $scope.nbOfPages = function() { return Math.ceil($scope.content.length / $scope.count); }; + $scope.handleSort = function(field) { + if ($scope.sortable.indexOf(field) > -1) { + return true; + } else { + return false; + } + }; + $scope.go = function(d) { + if (d.pod) { + $location.path('/dashboard/pods/' + d.pod); + } else if (d.name) { + $location.path('/dashboard/services/' + d.name); + } + }; + $scope.order = function(predicate, reverse) { + $scope.content = orderBy($scope.content, predicate, reverse); + $scope.predicate = predicate; + }; + $scope.order($scope.sortable[0], false); + $scope.getNumber = function(num) { return new Array(num); }; + $scope.goToPage = function(page) { $scope.currentPage = page; }; + }], + templateUrl: 'views/partials/md-table.tmpl.html' + }; + }); + +}()); + +angular.module('kubernetesApp.components.dashboard') + .factory('d3DashboardService', [ + '$document', + '$q', + '$rootScope', + function($document, $q, $rootScope) { + var d = $q.defer(); + function onScriptLoad() { + // Load client in the browser + $rootScope.$apply(function() { d.resolve(window.d3); }); + } + // Create a script tag with d3 as the source + // and call our onScriptLoad callback when it + // has been loaded + var scriptTag = $document[0].createElement('script'); + scriptTag.type = 'text/javascript'; + scriptTag.async = true; + scriptTag.src = 'vendor/d3/d3.min.js'; + scriptTag.onreadystatechange = function() { + if (this.readyState == 'complete') onScriptLoad(); + }; + scriptTag.onload = onScriptLoad; + + var s = $document[0].getElementsByTagName('body')[0]; + s.appendChild(scriptTag); + + return { + d3: function() { return d.promise; } + }; + } + ]); + +(function() { + 'use strict'; + + angular.module('pods', []).service('podService', PodDataService); + + /** + * Pod DataService + * Mock async data service. + * + * @returns {{loadAll: Function}} + * @constructor + */ + function PodDataService($q) { + var pods = { + "kind": "PodList", + "creationTimestamp": null, + "selfLink": "/api/v1beta1/pods", + "resourceVersion": 166552, + "apiVersion": "v1beta1", + "items": [{ + "id": "hello", + "uid": "0fe3644e-ab53-11e4-8ae8-061695c59fcf", + "creationTimestamp": "2015-02-03T03:16:36Z", + "selfLink": "/api/v1beta1/pods/hello?namespace=default", + "resourceVersion": 466, + "namespace": "default", + "labels": {"environment": "testing", "name": "hello"}, + "desiredState": { + "manifest": { + "version": "v1beta2", + "id": "", + "volumes": null, + "containers": [{ + "name": "hello", + "image": "quay.io/kelseyhightower/hello", + "ports": [{"hostPort": 80, "containerPort": 80, "protocol": "TCP"}], + "imagePullPolicy": "PullIfNotPresent" + }], + "restartPolicy": {"always": {}}, + "dnsPolicy": "ClusterFirst" + } + }, + "currentState": { + "manifest": {"version": "", "id": "", "volumes": null, "containers": null, "restartPolicy": {}}, + "status": "Running", + "host": "172.31.12.204", + "podIP": "10.244.73.2", + "info": { + "hello": { + "state": {"running": {"startedAt": "2015-02-03T03:16:51Z"}}, + "restartCount": 0, + "image": "quay.io/kelseyhightower/hello", + "containerID": "docker://96ade8ff30a44c4489969eaf343a7899317671b07a9766ecd0963e9b41501256" + }, + "net": { + "state": {"running": {"startedAt": "2015-02-03T03:16:41Z"}}, + "restartCount": 0, + "podIP": "10.244.73.2", + "image": "kubernetes/pause:latest", + "containerID": "docker://93d32603cafbff7165dadb1d4527899c24246bca2f5e6770b8297fd3721b272c" + } + } + } + }] + }; + + // Uses promises + return { + loadAll: function() { + // Simulate async call + return $q.when(pods); + } + }; + } + PodDataService.$inject = ["$q"]; + +})(); + +(function() { + 'use strict'; + + angular.module('replicationControllers', []) + .service('replicationControllerService', ReplicationControllerDataService); + + /** + * Replication Controller DataService + * Mock async data service. + * + * @returns {{loadAll: Function}} + * @constructor + */ + function ReplicationControllerDataService($q) { + var replicationControllers = { + "kind": "ReplicationControllerList", + "creationTimestamp": null, + "selfLink": "/api/v1beta1/replicationControllers", + "resourceVersion": 166552, + "apiVersion": "v1beta1", + "items": [] + }; + + // Uses promises + return { + loadAll: function() { + // Simulate async call + return $q.when(replicationControllers); + } + }; + } + ReplicationControllerDataService.$inject = ["$q"]; + +})(); + +(function() { + 'use strict'; + + angular.module('services', []).service('serviceService', ServiceDataService); + + /** + * Service DataService + * Mock async data service. + * + * @returns {{loadAll: Function}} + * @constructor + */ + function ServiceDataService($q) { + var services = { + "kind": "ServiceList", + "creationTimestamp": null, + "selfLink": "/api/v1beta1/services", + "resourceVersion": 166552, + "apiVersion": "v1beta1", + "items": [ + { + "id": "kubernetes", + "uid": "626dd08d-ab51-11e4-8ae8-061695c59fcf", + "creationTimestamp": "2015-02-03T03:04:36Z", + "selfLink": "/api/v1beta1/services/kubernetes?namespace=default", + "resourceVersion": 11, + "namespace": "default", + "port": 443, + "protocol": "TCP", + "labels": {"component": "apiserver", "provider": "kubernetes"}, + "selector": null, + "containerPort": 0, + "portalIP": "10.244.66.215", + "sessionAffinity": "None" + }, + { + "id": "kubernetes-ro", + "uid": "626f9584-ab51-11e4-8ae8-061695c59fcf", + "creationTimestamp": "2015-02-03T03:04:36Z", + "selfLink": "/api/v1beta1/services/kubernetes-ro?namespace=default", + "resourceVersion": 12, + "namespace": "default", + "port": 80, + "protocol": "TCP", + "labels": {"component": "apiserver", "provider": "kubernetes"}, + "selector": null, + "containerPort": 0, + "portalIP": "10.244.182.142", + "sessionAffinity": "None" + } + ] + }; + + // Uses promises + return { + loadAll: function() { + // Simulate async call + return $q.when(services); + } + }; + } + ServiceDataService.$inject = ["$q"]; + +})(); +`) + +func www_app_assets_js_app_js_bytes() ([]byte, error) { + return _www_app_assets_js_app_js, nil +} + +func www_app_assets_js_app_js() (*asset, error) { + bytes, err := www_app_assets_js_app_js_bytes() + if err != nil { + return nil, err + } + + info := bindata_file_info{name: "www/app/assets/js/app.js", size: 79552, mode: os.FileMode(436), modTime: time.Unix(1429574518, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +var _www_app_assets_js_base_js = []byte(`!function(e,t){"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(e,t){function n(e){var t=e.length,n=Z.type(e);return"function"===n||Z.isWindow(e)?!1:1===e.nodeType&&t?!0:"array"===n||0===t||"number"==typeof t&&t>0&&t-1 in e}function r(e,t,n){if(Z.isFunction(t))return Z.grep(e,function(e,r){return!!t.call(e,r,e)!==n});if(t.nodeType)return Z.grep(e,function(e){return e===t!==n});if("string"==typeof t){if(ae.test(t))return Z.filter(t,e,n);t=Z.filter(t,e)}return Z.grep(e,function(e){return U.call(t,e)>=0!==n})}function i(e,t){for(;(e=e[t])&&1!==e.nodeType;);return e}function o(e){var t=he[e]={};return Z.each(e.match(de)||[],function(e,n){t[n]=!0}),t}function s(){J.removeEventListener("DOMContentLoaded",s,!1),e.removeEventListener("load",s,!1),Z.ready()}function a(){Object.defineProperty(this.cache={},0,{get:function(){return{}}}),this.expando=Z.expando+a.uid++}function u(e,t,n){var r;if(void 0===n&&1===e.nodeType)if(r="data-"+t.replace(be,"-$1").toLowerCase(),n=e.getAttribute(r),"string"==typeof n){try{n="true"===n?!0:"false"===n?!1:"null"===n?null:+n+""===n?+n:xe.test(n)?Z.parseJSON(n):n}catch(i){}ye.set(e,t,n)}else n=void 0;return n}function l(){return!0}function c(){return!1}function f(){try{return J.activeElement}catch(e){}}function p(e,t){return Z.nodeName(e,"table")&&Z.nodeName(11!==t.nodeType?t:t.firstChild,"tr")?e.getElementsByTagName("tbody")[0]||e.appendChild(e.ownerDocument.createElement("tbody")):e}function d(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function h(e){var t=Pe.exec(e.type);return t?e.type=t[1]:e.removeAttribute("type"),e}function g(e,t){for(var n=0,r=e.length;r>n;n++)ve.set(e[n],"globalEval",!t||ve.get(t[n],"globalEval"))}function m(e,t){var n,r,i,o,s,a,u,l;if(1===t.nodeType){if(ve.hasData(e)&&(o=ve.access(e),s=ve.set(t,o),l=o.events)){delete s.handle,s.events={};for(i in l)for(n=0,r=l[i].length;r>n;n++)Z.event.add(t,i,l[i][n])}ye.hasData(e)&&(a=ye.access(e),u=Z.extend({},a),ye.set(t,u))}}function v(e,t){var n=e.getElementsByTagName?e.getElementsByTagName(t||"*"):e.querySelectorAll?e.querySelectorAll(t||"*"):[];return void 0===t||t&&Z.nodeName(e,t)?Z.merge([e],n):n}function y(e,t){var n=t.nodeName.toLowerCase();"input"===n&&Ne.test(e.type)?t.checked=e.checked:("input"===n||"textarea"===n)&&(t.defaultValue=e.defaultValue)}function x(t,n){var r,i=Z(n.createElement(t)).appendTo(n.body),o=e.getDefaultComputedStyle&&(r=e.getDefaultComputedStyle(i[0]))?r.display:Z.css(i[0],"display");return i.detach(),o}function b(e){var t=J,n=$e[e];return n||(n=x(e,t),"none"!==n&&n||(We=(We||Z("