diff --git a/Gruntfile.js b/Gruntfile.js index 4422cb87..f088e055 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -7,13 +7,13 @@ module.exports = function(grunt) { } }, watch: { - scripts: { - files: ['src/kopf/**/*.*','src/kopf/*.*'], - tasks: ['build'], - options: { - spawn: false, - }, - }, + scripts: { + files: ['src/kopf/**/*.*','src/kopf/*.*'], + tasks: ['build'], + options: { + spawn: false, + }, + }, }, copy: { main: { @@ -50,6 +50,7 @@ module.exports = function(grunt) { 'src/kopf/elastic/cluster_settings.js', 'src/kopf/elastic/cluster.js', 'src/kopf/elastic/elastic_client.js', + 'src/kopf/elastic/es_connection.js', 'src/kopf/elastic/index.js', 'src/kopf/elastic/editable_index_settings.js', 'src/kopf/elastic/node.js', @@ -75,8 +76,10 @@ module.exports = function(grunt) { // SERVICES 'src/kopf/services/alerts.js', 'src/kopf/services/settings.js', + 'src/kopf/services/aceeditor.js', // MODELS 'src/kopf/models/ace_editor.js', + 'src/kopf/models/gist.js', // UTIL 'src/kopf/util.js', @@ -152,7 +155,10 @@ module.exports = function(grunt) { }, qunit: { all: ['tests/all.html'] - } + }, + karma: { + unit: { configFile: 'tests/karma.config.js', keepalive: true }, + }, }); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-contrib-concat'); @@ -161,7 +167,9 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-contrib-watch'); grunt.loadNpmTasks('grunt-contrib-jshint'); grunt.loadNpmTasks('grunt-contrib-qunit'); + grunt.loadNpmTasks('grunt-karma'); + grunt.registerTask('dev', ['karma', 'watch']) + grunt.registerTask('test', ['karma']) grunt.registerTask('build', ['clean', 'jshint', 'qunit', 'copy', 'concat']); grunt.registerTask('server', ['clean', 'jshint', 'qunit', 'copy', 'concat','connect:server']); - }; diff --git a/dist/kopf.css b/dist/kopf.css index e55c979e..463140bf 100644 --- a/dist/kopf.css +++ b/dist/kopf.css @@ -122,7 +122,7 @@ h4 { color: #666; font-variant:small-caps; font-weight: 500; } h5 { color: #777; font-variant:small-caps; font-weight: 500; } h6 { color: #999; font-variant:small-caps; font-weight: 500; } -#page-wrap { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -60px; padding: 0px 0px 60px; } +#page-wrap { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -60px; padding: 0px 25px 60px; } .settings-section-header { top: 15px; @@ -195,10 +195,14 @@ html,body { .alert-block { font-size: 12px; font-weight: 300; - padding-top: 0px; + padding-top: 10px; padding-bottom: 0px; text-align: left; } + +.alert { + margin-bottom: 0px; +} .alert-server-response { padding-top: 25px; } @@ -400,9 +404,44 @@ body { color: #666; } .cluster-health-content { - background: #fff; font-size: 10px; + color: #666; + font-family: "Lucida Console", Monaco, monospace; +} + +.cluster-health-form-group { + padding-left: 5px; + float: right; +} +.gist-timestamp-col { + width: 70px; +} +.gist-link-col { + width: 240px; +} +.gist-title-col { + width: auto; +} +.cluster-health-filters { + padding-top: 6px; +} +table.overview { + table-layout:fixed; +} +table.overview tr td { width: 16.67%; } + +.index-menu-icon { + padding-right: 5px; +} + +.dropdown-menu .divider { + margin: 5px 0px; } + +.cluster-map-header-index-icon { + padding-right: 5px; +} + .cluster-map-header-cluster-actions { font-size: 32px; vertical-align: middle !important; @@ -422,16 +461,27 @@ a.cluster-map-header-cluster-action { text-decoration: none; } .cluster-map-header-index-cell { - width: 230px !important; - max-width: 230px !important; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; border-bottom: 0px !important; } + +.closed-index { + color: #D0D0D0 !important; + font-weight: 300 !important; +} + +.closed-index-cell { + background: #F9F9F9; +} + .cluster-map-header-index-name { - word-wrap:break-word; + overflow: hidden; + white-space: nowrap; font-weight: 500; + text-overflow: ellipsis; + display: block; } .cluster-map-header-index-alias { word-wrap:break-word; @@ -442,6 +492,19 @@ a.cluster-map-header-cluster-action { .cluster-map-header-index-actions { padding: 6px; } +.cluster-map-header-index-name { + color: #555555; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} +a.cluster-map-header-index-name:hover { + color: #555555; + font-size: 13px; + font-weight: 500; + text-decoration: none; +} + .cluster-map-header-index-action { color: #555555; font-size: 12px; @@ -457,18 +520,28 @@ a.cluster-map-header-index-action:hover { .cluster-map-node-name { word-wrap:break-word; font-weight: 500; + font-size: 13px; + text-overflow: ellipsis; + overflow: hidden; + display: block; } .cluster-map-node-types { - padding-top: 5px; - padding-left: 0px; - font-size: 12px; + padding-top: 4px; } .cluster-map-node-type { - padding-right: 12px; + font-weight: 300; + padding-left: 5px; + font-size: 12px; +} +.cluster-overview-filter { + padding-bottom: 10px; +} +.node-type-icon { + padding-left: 2px; + padding-right: 2px; } .cluster-map-node-cell { width: 220px; - max-width: 220px; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; @@ -497,8 +570,8 @@ a.cluster-map-header-index-action:hover { } .cluster-map-pagination { padding-top: 5px; - font-weight: 400; - height: 30px; + font-weight: 300; + font-size: 12px; } .cluster-map-node-action { color: #555555; @@ -522,35 +595,30 @@ a.cluster-map-node-action { } .cluster-map-stats-section { background: #f9f9f9; - text-align: justify; - padding-left: 15px; - padding-right: 15px; - border: 1px solid #dddddd; - border-radius: 3px 3px 3px 3px; - height: 50px; - margin-bottom: 40px; - margin-top: 20px; + padding-left: 10px; + border: 1px solid #ccc; + margin-top: 10px; + border-radius: 4px 4px 4px 4px; + margin-bottom: 30px; } .cluster-map-stat-block { display: inline-block; - padding-top: 3px; } .cluster-map-stats-section:after { content: ""; width: 100%; - display: inline-block; } .cluster-map-stat-number { color: #555555; - font-size: 28px; + font-size: 20px; text-align: center; - font-weight: 600; + font-weight: 500; } .cluster-map-stat-description { color: #555555; - font-size: 18px; + font-size: 12px; text-align: center; - font-weight: 200; + font-weight: 300; } .shard { text-align: center; @@ -623,8 +691,6 @@ a.header-action:hover { } .header-index-cell { - max-width: 230px; - min-width: 230px; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; @@ -671,8 +737,6 @@ a.header-index-action-mini:hover { text-decoration: none; } .shards { - max-width: 230px; - min-width: 230px; max-height: 40px !important; vertical-align: top !important; word-wrap:break-word; @@ -752,6 +816,70 @@ a.header-index-action-mini:hover { .json-collapsed { display: none; } +.dropdown-menu { + margin-top: 0px; + margin-left: 25px; +} + +.navbar-nav { + margin-top: 0px; +} + +.navbar-collapse { + border-top: 0px; +} + +.navbar-brand { + padding: 0px 15px 0px 0px; + margin-left: 25px; + font-size: 38px; + font-variant: small-caps; + line-height: 32px; + font-weight: 100; + color: #dff0d8; +} +.navbar-toggle { + padding: 6px 9px; + margin-right: 25px; +} +.navbar-toggle .icon-bar { + width: 10px; + height: 1px; +} + +.navbar-toggle-red .icon-bar { + background: #f2dede !important; +} + +.navbar-toggle-yellow .icon-bar { + background: #fcf8e3 !important; +} + +.navbar-toggle-green .icon-bar { + background: #dff0d8 !important; +} + +.navbar-toggle { + height: 22px; + width: 30px; +} + +.navbar-toggle-red { + height: 22px; + width: 30px; + border: 1px solid #c09853; +} +.navbar-toggle-yellow { + height: 22px; + width: 30px; + border: 1px solid #fcf8e3; +} +.navbar-toggle-green { + height: 22px; + width: 30px; + border: 1px solid #dff0d8; +} + .navbar-fixed-top { margin-right: 0px !important; } @@ -860,12 +988,13 @@ a.header-index-action-mini:hover { } .navbar-app-settings { color: #000000; - width: 250px; + width: auto; padding-left: 10px; padding-right: 10px; } .navbar-host-input { - width: 250px; + padding-right: 25px; + width: 280px; padding-top: 2px; vertical-align: middle; } @@ -878,6 +1007,7 @@ a.header-index-action-mini:hover { vertical-align: middle; padding-top: 4px; padding-bottom: 4px; + min-width: 300px; } .navbar-nav > li { height: 38px !important; @@ -959,6 +1089,10 @@ select.input-sm-twoxheight { padding-left: 15px; } +.row-nomargin-button{ + margin-right: 15px; +} + .form-label-repository { padding-left: 15px; } \ No newline at end of file diff --git a/dist/kopf.js b/dist/kopf.js index ea0e0284..a7f2658d 100644 --- a/dist/kopf.js +++ b/dist/kopf.js @@ -67,10 +67,15 @@ function ClusterChanges() { this.nodeJoins = null; this.nodeLeaves = null; + this.indicesCreated = null; + this.indicesDeleted = null; this.hasChanges=function() { - return (isDefined(this.nodeJoins) || - isDefined(this.nodeLeaves) + return ( + isDefined(this.nodeJoins) || + isDefined(this.nodeLeaves) || + isDefined(this.indicesCreated) || + isDefined(this.indicesDeleted) ); }; @@ -97,11 +102,43 @@ function ClusterChanges() { this.hasLeaves=function() { return isDefined(this.nodeLeaves); }; + + this.hasCreatedIndices=function() { + return isDefined(this.indicesCreated); + }; + + this.hasDeletedIndices=function() { + return isDefined(this.indicesDeleted); + }; + + this.addCreatedIndex=function(index) { + if (!isDefined(this.indicesCreated)) { + this.indicesCreated = []; + } + this.indicesCreated.push(index); + }; + + this.addDeletedIndex=function(index) { + if (!isDefined(this.indicesDeleted)) { + this.indicesDeleted = []; + } + this.indicesDeleted.push(index); + }; } function ClusterHealth(health) { this.status = health.status; - this.name = health.cluster_name; + this.cluster_name = health.cluster_name; + this.initializing_shards = health.initializing_shards; + this.active_primary_shards = health.active_primary_shards; + this.active_shards = health.active_shards; + this.relocating_shards = health.relocating_shards; + this.unassigned_shards = health.unassigned_shards; + this.number_of_nodes = health.number_of_nodes; + this.number_of_data_nodes = health.number_of_data_nodes; + this.timed_out = health.timed_out; + this.shards = this.active_shards + this.relocating_shards + this.unassigned_shards + this.initializing_shards; + this.fetched_at = getTimeString(new Date()); } function ClusterSettings(settings) { // FIXME: 0.90/1.0 check @@ -162,15 +199,20 @@ function Cluster(state,status,nodes,settings) { var unassigned_shards = 0; var total_size = 0; var num_docs = 0; + var special_indices = 0; this.indices = Object.keys(iMetadata).map( function(x) { var index = new Index(x,iRoutingTable[x], iMetadata[x], iStatus[x]); + if (index.isSpecial()) { + special_indices++; + } unassigned_shards += index.unassigned.length; total_size += parseInt(index.total_size); num_docs += index.num_docs; return index; } ).sort(function(a,b) { return a.compare(b); }); + this.special_indices = special_indices; this.num_docs = num_docs; this.unassigned_shards = unassigned_shards; this.total_indices = this.indices.length; @@ -179,18 +221,17 @@ function Cluster(state,status,nodes,settings) { this.successful_shards = status._shards.successful; this.total_size = readablizeBytes(total_size); this.getNodes=function(name, data, master, client) { - return $.map(this.nodes,function(n) { - if (name.trim().length > 0 && n.name.toLowerCase().indexOf(name.trim().toLowerCase()) == -1) { - return null; - } - return (data && n.data || master && n.master || client && n.client) ? n : null; + return $.map(this.nodes,function(node) { + return node.matches(name, data, master, client) ? node : null; }); }; this.getChanges=function(new_cluster) { var nodes = this.nodes; + var indices = this.indices; var changes = new ClusterChanges(); if (isDefined(new_cluster)) { + // checks for node differences nodes.forEach(function(node) { for (var i = 0; i < new_cluster.nodes.length; i++) { if (new_cluster.nodes[i].equals(node)) { @@ -215,15 +256,41 @@ function Cluster(state,status,nodes,settings) { } }); } + + // checks for indices differences + indices.forEach(function(index) { + for (var i = 0; i < new_cluster.indices.length; i++) { + if (new_cluster.indices[i].equals(index)) { + index = null; + break; + } + } + if (isDefined(index)) { + changes.addDeletedIndex(index); + } + }); + if (new_cluster.indices.length != indices.length || !changes.hasCreatedIndices()) { + new_cluster.indices.forEach(function(index) { + for (var i = 0; i < indices.length; i++) { + if (indices[i].equals(index)) { + index = null; + break; + } + } + if (isDefined(index)) { + changes.addCreatedIndex(index); + } + }); + } } return changes; }; } } -function ElasticClient(host,username,password) { - this.host = host; - this.username = username; - this.password = password; +function ElasticClient(connection) { + this.host = connection.host; + this.username = connection.username; + this.password = connection.password; this.createAuthToken=function(username,password) { var auth = null; @@ -233,10 +300,10 @@ function ElasticClient(host,username,password) { return auth; }; - var auth = this.createAuthToken(username,password); + var auth = this.createAuthToken(connection.username, connection.password); var fetch_version = $.ajax({ type: 'GET', - url: host, + url: connection.host, beforeSend: function(xhr) { if (isDefined(auth)) { xhr.setRequestHeader("Authorization", auth); @@ -441,8 +508,8 @@ function ElasticClient(host,username,password) { this.executeElasticRequest('DELETE', "/_snapshot/" + repository + "/" +snapshot, {}, callback_success, callback_error); }; - this.restoreSnapshot=function(repository, snapshot, callback_success, callback_error){ - this.executeElasticRequest('POST', "/_snapshot/" + repository + "/" +snapshot + "/_restore", {}, callback_success, callback_error); + this.restoreSnapshot=function(repository, snapshot, body, callback_success, callback_error){ + this.executeElasticRequest('POST', "/_snapshot/" + repository + "/" +snapshot + "/_restore", body, callback_success, callback_error); }; this.createSnapshot=function(repository, snapshot, body, callback_success, callback_error){ @@ -560,50 +627,77 @@ function ElasticClient(host,username,password) { ); }; - this.getClusterDiagnosis=function(callback_success,callback_error) { + this.getClusterDiagnosis=function(health, state, stats, hotthreads, callback_success,callback_error) { var host = this.host; var auth = this.createAuthToken(this.username,this.password); - $.when( - $.ajax({ - type: 'GET', - url: host+"/_cluster/state", - dataType: 'json', - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }), - $.ajax({ - type: 'GET', - url: host+"/_nodes/stats?all=true", - dataType: 'json', - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }), - $.ajax({ - type: 'GET', - url: host+"/_nodes/hot_threads", - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }) - ).then( - function(state, stats, hot_threads) { - callback_success(state[0], stats[0], hot_threads[0]); - }, - function(failed_request) { - callback_error(failed_request); - } + var deferreds = []; + if (health) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_cluster/health", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (state) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_cluster/state", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (stats) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_nodes/stats?all=true", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (hotthreads) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_nodes/hot_threads", + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) ); + } + $.when.apply($, deferreds).then( + function() { + callback_success(arguments); + }, + function(failed_request) { + callback_error(failed_request); + } + ); }; } @@ -619,17 +713,34 @@ function ElasticClient(host,username,password) { +// Expects URL according to /^(https|http):\/\/(\w+):(\w+)@(.*)/i; +// Examples: +// http://localhost:9200 +// http://user:password@localhost:9200 +// https://localhost:9200 +function ESConnection(url) { + var protected_url = /^(https|http):\/\/(\w+):(\w+)@(.*)/i; + this.host = "http://localhost:9200"; // default + if (notEmpty(url)) { + var connection_parts = protected_url.exec(url); + if (isDefined(connection_parts)) { + this.host = connection_parts[1] + "://" + url_parts[4]; + this.username = connection_parts[2]; + this.password = connection_parts[3]; + } else { + this.host = url; + } + } +} function Index(index_name,index_info, index_metadata, index_status) { this.name = index_name; var index_shards = {}; this.shards = index_shards; - this.state = index_metadata.state; this.metadata = {}; - this.aliases = index_metadata.aliases; - this.total_aliases = isDefined(index_metadata.aliases) ? index_metadata.aliases.length : 0; - this.visibleAliases=function() { - return this.total_aliases > 5 ? this.aliases.slice(0,5) : this.aliases; - }; + this.aliases = getProperty(index_metadata,'aliases', []); + + this.visibleAliases=function() { return this.aliases.length > 5 ? this.aliases.slice(0,5) : this.aliases; }; + this.settings = index_metadata.settings; // FIXME: 0.90/1.0 check this.editable_settings = new EditableIndexSettings(index_metadata.settings); @@ -638,55 +749,49 @@ function Index(index_name,index_info, index_metadata, index_status) { this.metadata.mappings = this.mappings; // FIXME: 0.90/1.0 check - if (isDefined(index_metadata.settings['index.number_of_shards'])) { - this.num_of_shards = index_metadata.settings['index.number_of_shards']; - this.num_of_replicas = parseInt(index_metadata.settings['index.number_of_replicas']); - } else { - this.num_of_shards = index_metadata.settings.index.number_of_shards; - this.num_of_replicas = parseInt(index_metadata.settings.index.number_of_replicas); - } + this.num_of_shards = getProperty(index_metadata.settings, 'index.number_of_shards'); + this.num_of_replicas = parseInt(getProperty(index_metadata.settings, 'index.number_of_replicas')); + this.state = index_metadata.state; + + this.num_docs = getProperty(index_status, 'docs.num_docs', 0); + this.max_doc = getProperty(index_status, 'docs.max_doc', 0); + this.deleted_docs = getProperty(index_status, 'docs.deleted_docs', 0); + this.size = getProperty(index_status, 'index.primary_size_in_bytes', 0); + this.total_size = getProperty(index_status, 'index.size_in_bytes', 0); + this.size_in_bytes = readablizeBytes(this.size); + this.total_size_in_bytes = readablizeBytes(this.total_size); - this.state_class = index_metadata.state === "open" ? "success" : "active"; - this.visible = true; var unassigned = []; // adds shard information - if (isDefined(index_status)) { - $.map(index_status.shards, function(shards, shard_num) { - $.map(shards, function(shard_info, shard_copy) { - if (!isDefined(index_shards[shard_info.routing.node])) { - index_shards[shard_info.routing.node] = []; - } - index_shards[shard_info.routing.node].push(new Shard(shard_info)); - }); - }); - this.metadata.stats = index_status; - } - // adds unassigned shards information - if (index_info) { - Object.keys(index_info.shards).forEach(function(x) { - var shards_info = index_info.shards[x]; - shards_info.forEach(function(shard_info) { - if (shard_info.state === 'UNASSIGNED') { - unassigned.push(new UnassignedShard(shard_info)); + + if (isDefined(index_info)) { + $.map(index_info.shards, function(shards, shard_num) { + $.map(shards, function(shard_routing, shard_copy) { + if (shard_routing.node === null) { + unassigned.push(new UnassignedShard(shard_routing)); + } else { + if (!isDefined(index_shards[shard_routing.node])) { + index_shards[shard_routing.node] = []; + } + var shard_status = null; + if (isDefined(index_status) && isDefined(index_status.shards[shard_routing.shard])) { + index_status.shards[shard_routing.shard].forEach(function(status) { + if (status.routing.node == shard_routing.node && status.routing.shard == shard_routing.shard) { + shard_status = status; + } + }); + } + index_shards[shard_routing.node].push(new Shard(shard_routing, shard_status)); } }); }); } - this.unassigned = unassigned; - var has_status = this.state === 'open' && isDefined(index_status); - this.num_docs = has_status && isDefined(index_status.docs) ? index_status.docs.num_docs : 0; - this.max_doc = has_status && isDefined(index_status.docs) ? index_status.docs.max_doc : 0; - this.deleted_docs = has_status && isDefined(index_status.docs) ? index_status.docs.deleted_docs : 0; - this.size = has_status ? index_status.index.primary_size_in_bytes : 0; - this.total_size = has_status ? index_status.index.size_in_bytes : 0; - this.size_in_bytes = readablizeBytes(this.size); - this.total_size_in_bytes = readablizeBytes(this.total_size); this.settingsAsString=function() { - return hierachyJson(JSON.stringify(this.metadata, undefined, "")); + return prettyPrintObject(this.metadata); }; this.compare=function(b) { // TODO: take into account index properties? return this.name.localeCompare(b.name); @@ -747,6 +852,25 @@ function Index(index_name,index_info, index_metadata, index_status) { return []; } }; + + this.isSpecial=function() { + return ( + this.name.indexOf(".") === 0 || + this.name.indexOf("_") === 0 + ); + }; + + this.equals=function(index) { + return index.name == this.name; + }; + + this.closed=function() { + return this.state === "close"; + }; + + this.open=function() { + return this.state === "open"; + }; } function EditableIndexSettings(settings) { // FIXME: 0.90/1.0 check @@ -821,13 +945,8 @@ function Node(node_id, node_info, node_stats) { this.stats = node_stats; // FIXME: 0.90/1.0 check - if (isDefined(this.stats.jvm.mem.heap_used)) { - this.heap_used = this.stats.jvm.mem.heap_used; - this.heap_committed = this.stats.jvm.mem.heap_committed; - } else { - this.heap_used = readablizeBytes(this.stats.jvm.mem.heap_used_in_bytes); - this.heap_committed = readablizeBytes(this.stats.jvm.mem.heap_committed_in_bytes); - } + this.heap_used = readablizeBytes(getProperty(this.stats,'jvm.mem.heap_used_in_bytes')); + this.heap_committed = readablizeBytes(getProperty(this.stats, 'jvm.mem.heap_committed_in_bytes')); this.setCurrentMaster=function() { this.current_master = true; @@ -837,36 +956,32 @@ function Node(node_id, node_info, node_stats) { return node.id === this.id; }; - this.compare=function(other) { // TODO: take into account node specs? - if (other.current_master) { - return 1; - } - if (this.current_master) { - return -1; - } - if (other.master && !this.master) { - return 1; - } - if (this.master && !other.master) { - return -1; - } - - if (other.data && !this.data) { - return 1; - } - if (this.data && !other.data) { - return -1; + this.compare=function(other) { + if (other.current_master) return 1; // current master comes first + if (this.current_master) return -1; // current master comes first + if (other.master && !this.master) return 1; // master eligible comes first + if (this.master && !other.master) return -1; // master eligible comes first + if (other.data && !this.data) return 1; // data node comes first + if (this.data && !other.data) return -1; // data node comes first + return this.name.localeCompare(other.name); // if all the same, lex. sort + }; + + this.matches=function(name, data, master, client) { + if (notEmpty(name)) { + if (this.name.toLowerCase().indexOf(name.trim().toLowerCase()) == -1) { + return false; + } } - return this.name.localeCompare(other.name); + return (data && this.data || master && this.master || client && this.client); }; } -function Shard(shard_info) { - this.info = shard_info; - this.primary = shard_info.routing.primary; - this.shard = shard_info.routing.shard; - this.state = shard_info.routing.state; - this.node = shard_info.routing.node; - this.index = shard_info.routing.index; +function Shard(shard_routing, shard_info) { + this.info = isDefined(shard_info) ? shard_info : shard_routing; + this.primary = shard_routing.primary; + this.shard = shard_routing.shard; + this.state = shard_routing.state; + this.node = shard_routing.node; + this.index = shard_routing.index; this.id = this.node + "_" + this.shard + "_" + this.index; } @@ -1033,10 +1148,11 @@ function AliasesPagination(page, results) { function ClusterNavigation() { this.page = 1; - this.page_size = 4; // TODO: allow to change it? + this.page_size = 5; // TODO: move it to a single place? this.query = ""; this.previous_query = null; + this.hide_special = true; this.data = true; this.master = true; @@ -1051,37 +1167,6 @@ function ModalControls() { this.title = ''; this.info = ''; } - -function hierachyJson(json) { - var jsonObject = JSON.parse(json); - var resultObject = {}; - Object.keys(jsonObject).forEach(function(key) { - var parts = key.split("."); - var property = null; - var reference = resultObject; - var previous = null; - for (var i = 0; i 30) { + $scope.gist_history.length = 30; + } + localStorage.kopf_gist_history = JSON.stringify($scope.gist_history); + }; + + $scope.loadHistory=function() { + var history = []; + if (isDefined(localStorage.kopf_gist_history)) { + try { + history = JSON.parse(localStorage.kopf_gist_history).map(function(h) { + return new Gist().loadFromJSON(h); + }); + } catch (error) { + localStorage.kopf_gist_history = null; + } + } + return history; + }; + + $scope.gist_history = $scope.loadHistory(); + } function ClusterOverviewController($scope, $location, $timeout, IndexSettingsService, ConfirmDialogService, AlertService, SettingsService) { $scope.settings_service = SettingsService; $scope.idxSettingsSrv = IndexSettingsService; $scope.dialog_service = ConfirmDialogService; $scope.pagination= new ClusterNavigation(); - $scope.alert_service = AlertService; $scope.getNodes=function() { if (isDefined($scope.cluster)) { @@ -1409,13 +1550,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.shutdownNode(node_id, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Node [" + node_id + "] successfully shutdown", response); + AlertService.success("Node [" + node_id + "] successfully shutdown", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while shutting down node",error); + AlertService.error("Error while shutting down node",error); }); } ); @@ -1433,12 +1574,12 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.optimizeIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully optimized", response); + AlertService.success("Index was successfully optimized", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while optimizing index", error); + AlertService.error("Error while optimizing index", error); }); } ); @@ -1454,14 +1595,11 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer function() { $scope.client.deleteIndex(index, function(response) { - $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully deleted", response); - }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting index", error); + AlertService.error("Error while deleting index", error); }); } ); @@ -1478,13 +1616,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.clearCache(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index cache was successfully cleared", response); + AlertService.success("Index cache was successfully cleared", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while clearing index cache", error); + AlertService.error("Error while clearing index cache", error); }); } ); @@ -1501,12 +1639,12 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.refreshIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully refreshed", response); + AlertService.success("Index was successfully refreshed", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while refreshing index", error); + AlertService.error("Error while refreshing index", error); }); } ); @@ -1518,13 +1656,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.enableShardAllocation( function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Shard allocation was successfully enabled", response); + AlertService.success("Shard allocation was successfully enabled", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while enabling shard allocation", error); + AlertService.error("Error while enabling shard allocation", error); }); } ); @@ -1534,13 +1672,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.disableShardAllocation( function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Shard allocation was successfully disabled", response); + AlertService.success("Shard allocation was successfully disabled", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while disabling shard allocation", error); + AlertService.error("Error while disabling shard allocation", error); }); } ); @@ -1557,13 +1695,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.closeIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully closed", response); + AlertService.success("Index was successfully closed", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while closing index", error); + AlertService.error("Error while closing index", error); }); } ); @@ -1581,13 +1719,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.openIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully opened", response); + AlertService.success("Index was successfully opened", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while opening index", error); + AlertService.error("Error while opening index", error); }); } ); @@ -1665,10 +1803,20 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer return page; }; + $scope.index=function(index) { + var page = $scope.getPage(); + if (isDefined(page[index])) { + return page[index]; + } else { + return null; + } + }; + $scope.getResults=function() { var indices = isDefined($scope.cluster) ? $scope.cluster.indices : []; var query = $scope.pagination.query; var state = $scope.pagination.state; + var hide_special = $scope.pagination.hide_special; return $.map(indices,function(i) { if (isDefined(query) && query.length > 0) { if (i.name.toLowerCase().indexOf(query.trim().toLowerCase()) == -1) { @@ -1678,18 +1826,16 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer if (state.length > 0 && state != i.state) { return null; } + if (hide_special && i.isSpecial()) { + return null; + } return i; }); }; } function ClusterSettingsController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; - $scope.$on('loadClusterSettingsEvent', function() { $('#cluster_settings_option a').tab('show'); $('#cluster_settings_tabs a:first').tab('show'); @@ -1703,20 +1849,19 @@ function ClusterSettingsController($scope, $location, $timeout, AlertService) { var response = $scope.client.updateClusterSettings(JSON.stringify(new_settings, undefined, ""), function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Cluster settings were successfully updated",response); + AlertService.success("Cluster settings were successfully updated",response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while updating cluster settings",error); + AlertService.error("Error while updating cluster settings",error); }); } ); }; } function CreateIndexController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; $scope.settings = ''; $scope.shards = ''; $scope.replicas = ''; @@ -1725,10 +1870,6 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { $scope.editor = new AceEditor('index-settings-editor'); - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; - $scope.$on('loadCreateIndex', function() { $('#create_index_option a').tab('show'); $scope.prepareCreateIndex(); @@ -1766,9 +1907,6 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { } $scope.client.createIndex($scope.name, JSON.stringify(settings, undefined, ""), function(response) { - $scope.updateModel(function() { - AlertService.success('Index successfully created', response); - }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { @@ -1790,12 +1928,20 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { } function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogService, AlertService, SettingsService) { $scope.dialog = ConfirmDialogService; - $scope.version = "0.5.1"; + $scope.version = "0.5.2"; $scope.username = null; $scope.password = null; - $scope.alerts_service = AlertService; + $scope.alert_service = AlertService; + + $scope.home_screen=function() { + $('#cluster_option a').tab('show'); + }; $scope.setConnected=function(status) { + if (!status) { + $scope.cluster = null; + $scope.cluster_health = null; + } $scope.is_connected = status; }; @@ -1809,38 +1955,39 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic }; $scope.setHost=function(url) { - var exp = /^(https|http):\/\/(\w+):(\w+)@(.*)/i; - // expected: "http://user:password@host", "http", "user", "password", "host"] - var url_parts = exp.exec(url); - if (isDefined(url_parts)) { - $scope.host = url_parts[1] + "://" + url_parts[4]; - $scope.username = url_parts[2]; - $scope.password = url_parts[3]; - } else { - $scope.username = null; - $scope.password = null; - $scope.host = url; + if (url.indexOf("http://") !== 0 && url.indexOf("https://") !== 0) { + url = "http://" + url; } + $scope.connection = new ESConnection(url); $scope.setConnected(false); try { - $scope.client = new ElasticClient($scope.host,$scope.username,$scope.password); - $scope.broadcastMessage('hostChanged',{}); + $scope.client = new ElasticClient($scope.connection); + $scope.home_screen(); } catch (error) { + $scope.client = null; AlertService.error(error); } - }; - if ($location.host() === "") { // when opening from filesystem - $scope.setHost("http://localhost:9200"); - } else { - var location = $scope.readParameter('location'); - if (isDefined(location)) { - $scope.setHost(location); + $scope.connect=function() { + // when opening from filesystem, just try default ES location + if ($location.host() === "") { + $scope.setHost("http://localhost:9200"); } else { - $scope.setHost($location.protocol() + "://" + $location.host() + ":" + $location.port()); - } - } + var location = $scope.readParameter('location'); + // reads ES location from url parameter + if (isDefined(location)) { + $scope.setHost(location); + } else { // uses current location as ES location + var absUrl = $location.absUrl(); + var cutIndex = absUrl.indexOf("/_plugin/kopf"); + $scope.setHost(absUrl.substring(0,cutIndex)); + } + } + }; + + $scope.connect(); + $scope.modal = new ModalControls(); $scope.alert = null; $scope.is_connected = false; @@ -1857,6 +2004,14 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic var leaves = changes.nodeLeaves.map(function(node) { return node.name + "[" + node.transport_address + "]"; }); AlertService.warn(changes.nodeLeaves.length + " node(s) left the cluster", leaves); } + if (changes.hasCreatedIndices()) { + var created = changes.indicesCreated.map(function(index) { return index.name; }); + AlertService.info(changes.indicesCreated.length + " indices created: [" + created.join(",") + "]"); + } + if (changes.hasDeletedIndices()) { + var deleted = changes.indicesDeleted.map(function(index) { return index.name; }); + AlertService.info(changes.indicesDeleted.length + " indices deleted: [" + deleted.join(",") + "]"); + } } } }; @@ -1913,20 +2068,12 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic return $('#' + tab).hasClass('active'); }; - $scope.getHost=function() { - return $scope.host; - }; - $scope.displayInfo=function(title,info) { $scope.modal.title = title; $scope.modal.info = $sce.trustAsHtml(JSONTree.create(info)); $('#modal_info').modal({show:true,backdrop:true}); }; - $scope.isInModal=function() { - return ($('.modal-backdrop').length > 0); - }; - $scope.getCurrentTime=function() { return getTimeString(new Date()); }; @@ -1941,15 +2088,10 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic $scope.updateModel=function(action) { $scope.$apply(action); }; - + } function IndexSettingsController($scope, $location, $timeout, IndexSettingsService, AlertService) { - $scope.alert_service = AlertService; $scope.service = IndexSettingsService; - - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; $scope.save=function() { var index = $scope.service.index; @@ -1963,13 +2105,13 @@ function IndexSettingsController($scope, $location, $timeout, IndexSettingsServi $scope.client.updateIndexSettings(index.name, JSON.stringify(new_settings, undefined, ""), function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index settings were successfully updated", response); + AlertService.success("Index settings were successfully updated", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while updating index settings", error); + AlertService.error("Error while updating index settings", error); }); } ); @@ -1977,13 +2119,14 @@ function IndexSettingsController($scope, $location, $timeout, IndexSettingsServi } function NavbarController($scope, $location, $timeout, AlertService, SettingsService) { $scope.settings_service = SettingsService; - $scope.alert_service = AlertService; $scope.new_refresh = $scope.settings_service.getRefreshInterval(); - $scope.connectToHost=function() { - if (isDefined($scope.new_host) && $scope.new_host.length > 0) { - $scope.setHost($scope.new_host); - $scope.refreshClusterState(); + $scope.connectToHost=function(event) { + if (event.keyCode == 13) { + if (isDefined($scope.new_host) && $scope.new_host.length > 0) { + $scope.setHost($scope.new_host); + $scope.refreshClusterState(); + } } }; @@ -1993,10 +2136,9 @@ function NavbarController($scope, $location, $timeout, AlertService, SettingsSer } -function RestController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; +function RestController($scope, $location, $timeout, AlertService, AceEditorService) { - $scope.request = new Request($scope.getHost() + "/_search","GET","{}"); + $scope.request = new Request($scope.connection.host + "/_search","GET","{}"); $scope.validation_error = null; $scope.loadHistory=function() { @@ -2009,16 +2151,19 @@ function RestController($scope, $location, $timeout, AlertService) { } catch (error) { localStorage.kopf_request_history = null; } - } + } return history; }; $scope.history = $scope.loadHistory(); $scope.history_request = null; - - $scope.editor = new AceEditor('rest-client-editor'); + + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('rest-client-editor'); + } + $scope.editor.setValue($scope.request.body); - + $scope.loadFromHistory=function(history_request) { $scope.request.url = history_request.url; $scope.request.body = history_request.body; @@ -2040,7 +2185,7 @@ function RestController($scope, $location, $timeout, AlertService) { if ($scope.history.length > 30) { $scope.history.length = 30; } - localStorage.kopf_request_history = JSON.stringify($scope.history); + localStorage.kopf_request_history = JSON.stringify($scope.history); } }; @@ -2050,7 +2195,7 @@ function RestController($scope, $location, $timeout, AlertService) { if (!isDefined($scope.editor.error) && notEmpty($scope.request.url)) { // TODO: deal with basic auth here if ($scope.request.method == 'GET' && '{}' !== $scope.request.body) { - $scope.alert_service.info("You are executing a GET request with body content. Maybe you meant to use POST or PUT?"); + AlertService.info("You are executing a GET request with body content. Maybe you meant to use POST or PUT?"); } $scope.client.executeRequest($scope.request.method,$scope.request.url,null,null,$scope.request.body, function(response) { @@ -2069,9 +2214,9 @@ function RestController($scope, $location, $timeout, AlertService) { function(error) { $scope.updateModel(function() { if (error.status !== 0) { - $scope.alert_service.error("Request was not successful: " + error.statusText); + AlertService.error("Request was not successful: " + error.statusText); } else { - $scope.alert_service.error($scope.request.url + " is unreachable"); + AlertService.error($scope.request.url + " is unreachable"); } }); try { @@ -2084,11 +2229,10 @@ function RestController($scope, $location, $timeout, AlertService) { } }; } -function PercolatorController($scope, $location, $timeout, ConfirmDialogService, AlertService) { +function PercolatorController($scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { $scope.dialog_service = ConfirmDialogService; - $scope.editor = new AceEditor('percolator-query-editor'); - + $scope.editor = undefined; $scope.total = 0; $scope.queries = []; $scope.page = 1; @@ -2101,8 +2245,15 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.$on('loadPercolatorEvent', function() { $scope.indices = $scope.cluster.indices; + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('percolator-query-editor'); + } + }; + $scope.previousPage=function() { $scope.page -= 1; $scope.loadPercolatorQueries(); @@ -2154,7 +2305,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.client.deletePercolatorQuery(query.index, query.id, function(response) { var refreshIndex = $scope.client.is1() ? query.index : '_percolator'; - $scope.client.refreshIndex(refreshIndex, + $scope.client.refreshIndex(refreshIndex, function(response) { $scope.updateModel(function() { AlertService.success("Query successfully deleted", response); @@ -2184,7 +2335,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.client.createPercolatorQuery($scope.new_query.index.name, $scope.new_query.id, $scope.new_query.source, function(response) { var refreshIndex = $scope.client.is1() ? $scope.new_query.index.name : '_percolator'; - $scope.client.refreshIndex(refreshIndex, + $scope.client.refreshIndex(refreshIndex, function(response) { $scope.updateModel(function() { AlertService.success("Percolator Query successfully created", response); @@ -2237,7 +2388,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, }); } } - ); + ); } catch (error) { AlertService.error("Filter is not a valid JSON"); return; @@ -2260,26 +2411,35 @@ function PercolateQuery(query_info) { } }; } -function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogService, AlertService) { +function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { - $scope.alert_service = AlertService; $scope.dialog_service = ConfirmDialogService; - - $scope.editor = new AceEditor('repository-settings-editor'); - $scope.repositories = []; $scope.repositories_names = []; $scope.snapshots = []; $scope.indices = []; + $scope.restorable_indices = []; $scope.new_repo = {}; $scope.new_snap = {}; + $scope.restore_snap = {}; + $scope.editor = undefined; $scope.$on('loadRepositoryEvent', function() { $scope.reload(); + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('repository-settings-editor'); + } + }; + $scope.loadIndices=function() { - $scope.indices = $scope.cluster.indices; + if( angular.isDefined($scope.cluster) ) + { + $scope.indices = $scope.cluster.indices || []; + } }; $scope.reload=function(){ @@ -2290,6 +2450,14 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.loadIndices(); }; + $scope.optionalParam=function(body, object, paramname){ + if(angular.isDefined(object[paramname])) + { + body[paramname] = object[paramname]; + } + return body; + }; + $scope.deleteRepository=function(name, value){ $scope.dialog_service.open( "are you sure you want to delete repository " + name + "?", @@ -2298,12 +2466,12 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ function() { $scope.client.deleteRepository(name, function(response) { - $scope.alert_service.success("Repository successfully deleted", response); + AlertService.success("Repository successfully deleted", response); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting repositor", error); + AlertService.error("Error while deleting repositor", error); }); } ); @@ -2311,6 +2479,38 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ ); }; + $scope.restoreSnapshot=function(){ + + var body = {} + // dont add to body if not present, these are optional, all indices included by default + if(angular.isDefined($scope.restore_snap.indices) && $scope.restore_snap.indices.length > 0) + { + body["indices"] = $scope.restore_snap.indices.join(","); + } + + if(angular.isDefined($scope.restore_snap.include_global_state)) + { + //TODO : when es fixes bug [https://github.com/elasticsearch/elasticsearch/issues/4949], this extra "true/false" -> true/false handling will go away + body["include_global_state"] = ($scope.restore_snap.include_global_state == 'true'); + } + + $scope.optionalParam(body, $scope.restore_snap, "ignore_unavailable"); + $scope.optionalParam(body, $scope.restore_snap, "rename_replacement"); + $scope.optionalParam(body, $scope.restore_snap, "rename_pattern"); + + $scope.client.restoreSnapshot($scope.restore_snap.snapshot.repository, $scope.restore_snap.snapshot.snapshot, JSON.stringify(body), + function(response) { + AlertService.success("Snapshot Restored Started"); + $scope.reload(); + }, + function(error) { + $scope.updateModel(function() { + AlertService.error("Error while started restor of snapshot", error); + }); + } + ); + }; + $scope.createRepository=function(){ $scope.new_repo.settings = $scope.editor.format(); if ($scope.editor.error === null){ @@ -2321,42 +2521,45 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.client.createRepository($scope.new_repo.name, JSON.stringify(body), function(response) { - $scope.alert_service.success("Repository created"); + AlertService.success("Repository created"); $scope.loadRepositories(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while creating repository", error); + AlertService.error("Error while creating repository", error); }); } ); } }; + $scope._parseRepositories=function(response, deferred){ + $scope.updateModel(function() { + $scope.repositories = response; + $scope.repositories_names = []; + $.each($scope.repositories, function(key, value){ + $scope.repositories_names.push({"name":key, "value":key}); + }); + }); + deferred.resolve(true); + }; + $scope.loadRepositories=function() { var deferred = $q.defer(); try { $scope.client.getRepositories( function(response) { - $scope.updateModel(function() { - $scope.repositories = response; - $.each($scope.repositories, function(key, value){ - $scope.repositories_names.push({"name":key, "value":key}); - }); - }); - deferred.resolve(true); + $scope._parseRepositories(response, deferred) }, function(error) { - if (!(error['responseJSON'] != null )) { - $scope.updateModel(function() { - $scope.alert_service.error("Error while reading repositories", error); - }); - } + $scope.updateModel(function() { + AlertService.error("Error while reading repositories", error); + }); deferred.reject(true); } ) } catch (error) { - $scope.alert_service.error("Failed to load repositories"); + AlertService.error("Failed to load repositories"); deferred.reject(false); } return deferred.promise @@ -2368,13 +2571,13 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ // name and repo required if(!angular.isDefined($scope.new_snap.repository)) { - $scope.alert_service.warn("Repository is required"); + AlertService.warn("Repository is required"); return } if(!angular.isDefined($scope.new_snap.name)) { - $scope.alert_service.warn("Snapshot name is required"); + AlertService.warn("Snapshot name is required"); return } @@ -2384,24 +2587,22 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ body["indices"] = $scope.new_snap.indices.join(","); } - if(angular.isDefined($scope.new_snap.ignore_unavailable)) - { - body["ignore_unavailable"] = $scope.new_snap.ignore_unavailable; - } - if(angular.isDefined($scope.new_snap.include_global_state)) { - body["include_global_state"] = true; //$scope.new_snap.include_global_state; + //TODO : when es fixes bug [https://github.com/elasticsearch/elasticsearch/issues/4949], this extra "true/false" -> true/false handling will go away + body["include_global_state"] = ($scope.new_snap.include_global_state == 'true'); } + $scope.optionalParam(body, $scope.new_snap, "ignore_unavailable"); + $scope.client.createSnapshot($scope.new_snap.repository, $scope.new_snap.name, JSON.stringify(body), function(response) { - $scope.alert_service.success("Snapshot created"); + AlertService.success("Snapshot created"); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while creating snapshot", error); + AlertService.error("Error while creating snapshot", error); }); } ); @@ -2417,12 +2618,12 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ snapshot.repository, snapshot.snapshot, function(response) { - $scope.alert_service.success("Snapshot successfully deleted", response); + AlertService.success("Snapshot successfully deleted", response); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting snapshot", error); + AlertService.error("Error while deleting snapshot", error); }); } ); @@ -2432,64 +2633,49 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.allSnapshots=function(repositories) { var all = []; - $.each( repositories, function( index, value ){ - $scope.fetchSnapshots(index).then( + $.each( repositories, function( repository, value ){ + $scope.fetchSnapshots(repository).then( function(data){ - $.merge($scope.snapshots, data ); + $.merge(all, data ); }); }); $scope.snapshots = all; }; + $scope._parseSnapshots=function(repository, response, deferred) { + var arr = response["snapshots"]; + + // add the repository name to each snapshot object + // + if(arr && arr.constructor==Array && arr.length!==0){ + $.each(arr, function(index, value){ + value["repository"] = repository; + }); + } + deferred.resolve(response["snapshots"]); + }; + $scope.fetchSnapshots=function(repository) { var deferred = $q.defer(); try { $scope.client.getSnapshots(repository, function(response) { - var arr = response["snapshots"]; - if(arr && arr.constructor==Array && arr.length!=0){ - $.each(arr, function(index, value){ - value["repository"] = repository; - }); - } - deferred.resolve(response["snapshots"]); + $scope._parseSnapshots(repository, response, deferred); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while fetching snapshots", error); + AlertService.error("Error while fetching snapshots", error); }); deferred.resolve([]); } ) } catch (error) { - $scope.alert_service.error("Failed to load snapshots"); + AlertService.error("Failed to load snapshots"); deferred.resolve([]); } return deferred.promise; }; - $scope.loadSnapshots=function(repository) { - try { - $scope.client.getSnapshots(repository, - function(response) { - $scope.updateModel(function() { - $scope.snapshots = response["snapshots"]; - }); - }, - function(error) { - if (!(error['responseJSON'] != null )) { - $scope.updateModel(function() { - $scope.alert_service.error("Error while reading snapshots", error); - }); - } - } - ) - } catch (error) { - $scope.alert_service.error("Failed to load snapshots"); - return; - } - }; - } function ConfirmDialogController($scope, $location, $timeout, ConfirmDialogService) { @@ -2505,15 +2691,9 @@ function ConfirmDialogController($scope, $location, $timeout, ConfirmDialogServi }; } -function WarmupController($scope, $location, $timeout, ConfirmDialogService, AlertService) { - $scope.alert_service = AlertService; +function WarmupController($scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { $scope.dialog_service = ConfirmDialogService; - - $scope.editor = ace.edit("warmup-query-editor"); - $scope.editor.setFontSize("10px"); - $scope.editor.setTheme("ace/theme/kopf"); - $scope.editor.getSession().setMode("ace/mode/json"); - + $scope.editor = undefined; $scope.indices = []; $scope.warmers = {}; $scope.index = null; @@ -2527,8 +2707,15 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.$on('loadWarmupEvent', function() { $scope.loadIndices(); + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('warmup-query-editor'); + } + }; + $scope.totalWarmers=function() { return Object.keys($scope.warmers).length; }; @@ -2543,12 +2730,12 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.client.registerWarmupQuery($scope.new_index.name, $scope.new_types, $scope.new_warmer_id, $scope.new_source, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Warmup query successfully registered", response); + AlertService.success("Warmup query successfully registered", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Request did not return a valid JSON", error); + AlertService.error("Request did not return a valid JSON", error); }); } ); @@ -2564,13 +2751,13 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.client.deleteWarmupQuery($scope.index.name, warmer_id, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Warmup query successfully deleted", response); + AlertService.success("Warmup query successfully deleted", response); $scope.loadIndexWarmers(); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting warmup query", error); + AlertService.error("Error while deleting warmup query", error); }); } ); @@ -2592,7 +2779,7 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while fetching warmup queries", error); + AlertService.error("Error while fetching warmup queries", error); }); } ); @@ -2658,25 +2845,25 @@ kopf.factory('AlertService', function() { // creates an error alert this.error=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 15000; - this.addAlert(new Alert(message, response, "error", "alert-danger", "icon-warning-sign"), timeout); + return this.addAlert(new Alert(message, response, "error", "alert-danger", "icon-warning-sign"), timeout); }; // creates an info alert this.info=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 5000; - this.addAlert(new Alert(message, response, "info", "alert-info", "icon-info"), timeout); + return this.addAlert(new Alert(message, response, "info", "alert-info", "icon-info"), timeout); }; // creates success alert this.success=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 5000; - this.addAlert(new Alert(message, response, "success", "alert-success", "icon-ok"), timeout); + return this.addAlert(new Alert(message, response, "success", "alert-success", "icon-ok"), timeout); }; // creates a warn alert this.warn=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 10000; - this.addAlert(new Alert(message, response, "warn", "alert-warning", "icon-info"), timeout); + return this.addAlert(new Alert(message, response, "warn", "alert-warning", "icon-info"), timeout); }; this.addAlert=function(alert, timeout) { @@ -2686,6 +2873,7 @@ kopf.factory('AlertService', function() { if (this.alerts.length >= this.max_alerts) { this.alerts.length = 3; } + return alert.id; }; return this; @@ -2709,6 +2897,15 @@ kopf.factory('SettingsService', function() { return this; }); + +kopf.factory('AceEditorService', function() { + + this.init=function(name) { + return new AceEditor(name); + }; + + return this; +}); function AceEditor(target) { // ace editor this.editor = ace.edit(target); @@ -2745,6 +2942,19 @@ function AceEditor(target) { return content; }; } +function Gist(title, url) { + this.timestamp = getTimeString(new Date()); + this.title = title; + this.url = url; + + this.loadFromJSON=function(json) { + this.title = json.title; + this.url = json.url; + this.timestamp = json.timestamp; + return this; + }; + +} function readablizeBytes(bytes) { if (bytes > 0) { var s = ['b', 'KB', 'MB', 'GB', 'TB', 'PB']; @@ -2760,25 +2970,16 @@ function readablizeBytes(bytes) { // Example: get the value of object[a][b][c][d] // where property_path is [a,b,c,d] function getProperty(object, property_path, default_value) { - var value = default_value; - if (isDefined(object[property_path])) { - return object[property_path]; - } - var path_parts = property_path.split('.'); - var ref = object; - for (var i = 0; i < path_parts.length; i++) { - var property = path_parts[i]; - if (isDefined(ref[property])) { - ref = ref[property]; - } else { - ref = null; - break; + if (isDefined(object)) { + if (isDefined(object[property_path])) { + return object[property_path]; + } + var path_parts = property_path.split('.'); // path as nested properties + for (var i = 0; i < path_parts.length && isDefined(object); i++) { + object = object[path_parts[i]]; } } - if (isDefined(ref)) { - value = ref; - } - return value; + return isDefined(object) ? object : default_value; } // Checks if value is both non null and undefined @@ -2795,4 +2996,34 @@ function notEmpty(value) { // Returns the given date as a String formatted as hh:MM:ss function getTimeString(date) { return ('0' + date.getHours()).slice(-2) + ":" + ('0' + date.getMinutes()).slice(-2) + ":" + ('0' + date.getSeconds()).slice(-2); +} + +function prettyPrintObject(object) { + var prettyObject = {}; + Object.keys(object).forEach(function(key) { + var parts = key.split("."); + var property = null; + var reference = prettyObject; + var previous = null; + for (var i = 0; i
-
+
diff --git a/package.json b/package.json index 432a61d0..2f218e15 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "elasticsearch-kopf", - "version": "0.5.1", + "version": "0.5.2", "description": "kopf - simple web administration tool for ElasticSearch", "main": "index.html", "scripts": { @@ -23,6 +23,41 @@ "grunt-contrib-clean": "~0.5.0", "grunt-contrib-watch": "~0.5.3", "grunt-contrib-jshint": "~0.8.0", - "grunt-contrib-qunit": "~0.4.0" + "grunt-contrib-qunit": "~0.4.0", + "karma-script-launcher": "~0.1.0", + "karma-chrome-launcher": "~0.1.2", + "karma-firefox-launcher": "~0.1.3", + "karma-html2js-preprocessor": "~0.1.0", + "karma-jasmine": "~0.1.5", + "requirejs": "~2.1.10", + "karma-requirejs": "~0.2.1", + "karma-coffee-preprocessor": "~0.1.2", + "karma-phantomjs-launcher": "~0.1.2", + "karma": "~0.10.9", + "grunt-karma": "~0.6.2" + }, + "directories": { + "test": "tests" + }, + "dependencies": { + "grunt": "~0.4.2", + "grunt-contrib-clean": "~0.5.0", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "grunt-contrib-jshint": "~0.8.0", + "grunt-contrib-qunit": "~0.4.0", + "grunt-contrib-watch": "~0.5.3", + "grunt-karma": "~0.6.2", + "karma-coffee-preprocessor": "~0.1.2", + "karma-html2js-preprocessor": "~0.1.0", + "karma-requirejs": "~0.2.1", + "karma-firefox-launcher": "~0.1.3", + "karma-script-launcher": "~0.1.0", + "karma-jasmine": "~0.1.5", + "karma-phantomjs-launcher": "~0.1.2", + "karma-chrome-launcher": "~0.1.2", + "requirejs": "~2.1.10", + "karma": "~0.10.9" } } diff --git a/partials/aliases.html b/partials/aliases.html index ead0d45e..0d480819 100644 --- a/partials/aliases.html +++ b/partials/aliases.html @@ -1,5 +1,4 @@
-

aliases management

diff --git a/partials/analysis.html b/partials/analysis.html index b51cf30e..add424c3 100644 --- a/partials/analysis.html +++ b/partials/analysis.html @@ -1,5 +1,4 @@
-

analysis

diff --git a/partials/cluster_health.html b/partials/cluster_health.html index e92df1c8..0a86255b 100644 --- a/partials/cluster_health.html +++ b/partials/cluster_health.html @@ -1,45 +1,133 @@
-

cluster health information

-
-
-
- - - +
+
+ +
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+
+ + + + + + +
{{h.timestamp}}{{h.title}}
+
+
+
-
-
-

cluster state

-
-
{{cluster_health.state}}
+
+ +
+
+ +
+
-

nodes stats

-
-
{{cluster_health.stats}}
+
+ +
+
+ +
+
-

hot threads

-
-
{{cluster_health.hot_threads}}
+
+ +
+
+ +
+
-
-
{{state}}
-
-
- - - - - +
+ +
+
+ {{results.hot_threads}} +
+
diff --git a/partials/cluster_overview.html b/partials/cluster_overview.html index 96e6466f..4a60db42 100644 --- a/partials/cluster_overview.html +++ b/partials/cluster_overview.html @@ -1,60 +1,78 @@
-

cluster administration

-
-
-
-
- +
+
+
+
filter indices
+
+
+ +
+
+ +
+
+ +
-
- -
-
- -
-
-
- -
-
- +
+
+
filter nodes
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
-
- -
+
- +
+
+
+
+ {{cluster_health.fetched_at}} - {{cluster_health.cluster_name}} +
-
+
- + {{firstResult()}}-{{lastResult()}} of {{total()}} - +
-
- +
-
+
\ No newline at end of file diff --git a/partials/cluster_settings.html b/partials/cluster_settings.html index 40914b94..1defce56 100644 --- a/partials/cluster_settings.html +++ b/partials/cluster_settings.html @@ -1,5 +1,4 @@
-

cluster settings

@@ -20,7 +19,7 @@

cluster settings

- +
diff --git a/partials/cluster_stats.html b/partials/cluster_stats.html index 398f81aa..a36652d0 100644 --- a/partials/cluster_stats.html +++ b/partials/cluster_stats.html @@ -1,32 +1,56 @@ -
- - {{cluster.number_of_nodes}} - nodes - - - {{cluster.total_indices}} - indices - - - {{cluster.shards}} - shards - - - {{cluster.failed_shards}} - failed shards - - - {{cluster.unassigned_shards}} - unassigned shards - - - {{cluster.num_docs}} - docs - - - {{cluster.total_size}} - total size - +
+
+
+
+
+
+
+ {{cluster_health.number_of_nodes}} + nodes +
+ +
+ {{cluster.total_indices}} + indices +
+ +
+ {{cluster_health.shards}} + shards +
+ +
+ {{cluster_health.initializing_shards}} + initializing +
+
+
+
+
+
+ {{cluster_health.relocating_shards}} + relocating +
+ +
+ {{cluster_health.unassigned_shards}} + unassigned +
+ +
+ {{cluster.num_docs}} + docs +
+ +
+ {{cluster.total_size}} +
+
+
+
+
+
+
diff --git a/partials/percolator.html b/partials/percolator.html index 071ac3e0..d509b195 100644 --- a/partials/percolator.html +++ b/partials/percolator.html @@ -1,5 +1,4 @@
-

percolator

diff --git a/partials/repository.html b/partials/repository.html index 5272e80e..c9ead8a2 100644 --- a/partials/repository.html +++ b/partials/repository.html @@ -1,5 +1,4 @@
-

repositories

@@ -85,7 +84,7 @@

- +
@@ -161,7 +160,7 @@

- @@ -173,7 +172,130 @@

- + + + + + +
+ +
+
+
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+ +
+ +
+
+
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+ +
+ + +
+
+
+
+
+
+
+
+ + +
+
+
+
+ + + +
+
+
+
+
+
+
+ + +
diff --git a/partials/rest_client.html b/partials/rest_client.html index 8af5f61f..260a82e4 100644 --- a/partials/rest_client.html +++ b/partials/rest_client.html @@ -1,5 +1,4 @@
-

rest client

diff --git a/partials/shard_map_body.html b/partials/shard_map_body.html index c6e7fab4..cb79e034 100644 --- a/partials/shard_map_body.html +++ b/partials/shard_map_body.html @@ -13,7 +13,9 @@
-
{{node.name}}
+
+ {{node.name}} +
@@ -32,18 +34,15 @@
- -
- - {{shard.shard}} - {{shard.shard}} - -
- + + + + + - +
@@ -61,11 +60,9 @@
- -
- - {{shard.shard}} - -
- + + + + + diff --git a/partials/shard_map_header.html b/partials/shard_map_header.html index 4bccb2c9..53a973da 100644 --- a/partials/shard_map_header.html +++ b/partials/shard_map_header.html @@ -2,23 +2,23 @@
-
+ -
+ -
+ -
+
@@ -26,70 +26,9 @@
- - -
-
-
{{index.name}}
-
-
-
-
-
- shards: {{index.num_of_shards}} * {{index.num_of_replicas + 1}} | - docs: {{index.num_docs}} | - size: {{index.size_in_bytes}} -
-
-
- -
- -
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- -
-
-
- {{alias}} -
-
-
-
-
-
- {{index.total_aliases}} total aliases -
-
-
- + + + + + \ No newline at end of file diff --git a/partials/warmup.html b/partials/warmup.html index e6b00b8f..50796cb2 100644 --- a/partials/warmup.html +++ b/partials/warmup.html @@ -1,5 +1,4 @@
-

warmup queries

diff --git a/src/kopf/controllers.js b/src/kopf/controllers.js index e865b787..bbca5847 100644 --- a/src/kopf/controllers.js +++ b/src/kopf/controllers.js @@ -146,10 +146,11 @@ function AliasesPagination(page, results) { function ClusterNavigation() { this.page = 1; - this.page_size = 4; // TODO: allow to change it? + this.page_size = 5; // TODO: move it to a single place? this.query = ""; this.previous_query = null; + this.hide_special = true; this.data = true; this.master = true; @@ -163,35 +164,4 @@ function ModalControls() { this.active = false; this.title = ''; this.info = ''; -} - -function hierachyJson(json) { - var jsonObject = JSON.parse(json); - var resultObject = {}; - Object.keys(jsonObject).forEach(function(key) { - var parts = key.split("."); - var property = null; - var reference = resultObject; - var previous = null; - for (var i = 0; i 30) { + $scope.gist_history.length = 30; + } + localStorage.kopf_gist_history = JSON.stringify($scope.gist_history); + }; + + $scope.loadHistory=function() { + var history = []; + if (isDefined(localStorage.kopf_gist_history)) { + try { + history = JSON.parse(localStorage.kopf_gist_history).map(function(h) { + return new Gist().loadFromJSON(h); + }); + } catch (error) { + localStorage.kopf_gist_history = null; + } + } + return history; + }; + + $scope.gist_history = $scope.loadHistory(); + } \ No newline at end of file diff --git a/src/kopf/controllers/cluster_overview.js b/src/kopf/controllers/cluster_overview.js index 49494d82..616fbbbd 100644 --- a/src/kopf/controllers/cluster_overview.js +++ b/src/kopf/controllers/cluster_overview.js @@ -3,7 +3,6 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.idxSettingsSrv = IndexSettingsService; $scope.dialog_service = ConfirmDialogService; $scope.pagination= new ClusterNavigation(); - $scope.alert_service = AlertService; $scope.getNodes=function() { if (isDefined($scope.cluster)) { @@ -27,13 +26,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.shutdownNode(node_id, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Node [" + node_id + "] successfully shutdown", response); + AlertService.success("Node [" + node_id + "] successfully shutdown", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while shutting down node",error); + AlertService.error("Error while shutting down node",error); }); } ); @@ -51,12 +50,12 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.optimizeIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully optimized", response); + AlertService.success("Index was successfully optimized", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while optimizing index", error); + AlertService.error("Error while optimizing index", error); }); } ); @@ -72,14 +71,11 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer function() { $scope.client.deleteIndex(index, function(response) { - $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully deleted", response); - }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting index", error); + AlertService.error("Error while deleting index", error); }); } ); @@ -96,13 +92,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.clearCache(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index cache was successfully cleared", response); + AlertService.success("Index cache was successfully cleared", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while clearing index cache", error); + AlertService.error("Error while clearing index cache", error); }); } ); @@ -119,12 +115,12 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.refreshIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully refreshed", response); + AlertService.success("Index was successfully refreshed", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while refreshing index", error); + AlertService.error("Error while refreshing index", error); }); } ); @@ -136,13 +132,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.enableShardAllocation( function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Shard allocation was successfully enabled", response); + AlertService.success("Shard allocation was successfully enabled", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while enabling shard allocation", error); + AlertService.error("Error while enabling shard allocation", error); }); } ); @@ -152,13 +148,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer var response = $scope.client.disableShardAllocation( function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Shard allocation was successfully disabled", response); + AlertService.success("Shard allocation was successfully disabled", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while disabling shard allocation", error); + AlertService.error("Error while disabling shard allocation", error); }); } ); @@ -175,13 +171,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.closeIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully closed", response); + AlertService.success("Index was successfully closed", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while closing index", error); + AlertService.error("Error while closing index", error); }); } ); @@ -199,13 +195,13 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer $scope.client.openIndex(index, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index was successfully opened", response); + AlertService.success("Index was successfully opened", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while opening index", error); + AlertService.error("Error while opening index", error); }); } ); @@ -283,10 +279,20 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer return page; }; + $scope.index=function(index) { + var page = $scope.getPage(); + if (isDefined(page[index])) { + return page[index]; + } else { + return null; + } + }; + $scope.getResults=function() { var indices = isDefined($scope.cluster) ? $scope.cluster.indices : []; var query = $scope.pagination.query; var state = $scope.pagination.state; + var hide_special = $scope.pagination.hide_special; return $.map(indices,function(i) { if (isDefined(query) && query.length > 0) { if (i.name.toLowerCase().indexOf(query.trim().toLowerCase()) == -1) { @@ -296,6 +302,9 @@ function ClusterOverviewController($scope, $location, $timeout, IndexSettingsSer if (state.length > 0 && state != i.state) { return null; } + if (hide_special && i.isSpecial()) { + return null; + } return i; }); }; diff --git a/src/kopf/controllers/cluster_settings.js b/src/kopf/controllers/cluster_settings.js index 68dba019..98c03406 100644 --- a/src/kopf/controllers/cluster_settings.js +++ b/src/kopf/controllers/cluster_settings.js @@ -1,10 +1,5 @@ function ClusterSettingsController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; - $scope.$on('loadClusterSettingsEvent', function() { $('#cluster_settings_option a').tab('show'); $('#cluster_settings_tabs a:first').tab('show'); @@ -18,13 +13,13 @@ function ClusterSettingsController($scope, $location, $timeout, AlertService) { var response = $scope.client.updateClusterSettings(JSON.stringify(new_settings, undefined, ""), function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Cluster settings were successfully updated",response); + AlertService.success("Cluster settings were successfully updated",response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while updating cluster settings",error); + AlertService.error("Error while updating cluster settings",error); }); } ); diff --git a/src/kopf/controllers/create_index.js b/src/kopf/controllers/create_index.js index f264a2e8..42ad8411 100644 --- a/src/kopf/controllers/create_index.js +++ b/src/kopf/controllers/create_index.js @@ -1,5 +1,4 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; $scope.settings = ''; $scope.shards = ''; $scope.replicas = ''; @@ -8,10 +7,6 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { $scope.editor = new AceEditor('index-settings-editor'); - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; - $scope.$on('loadCreateIndex', function() { $('#create_index_option a').tab('show'); $scope.prepareCreateIndex(); @@ -49,9 +44,6 @@ function CreateIndexController($scope, $location, $timeout, AlertService) { } $scope.client.createIndex($scope.name, JSON.stringify(settings, undefined, ""), function(response) { - $scope.updateModel(function() { - AlertService.success('Index successfully created', response); - }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { diff --git a/src/kopf/controllers/global.js b/src/kopf/controllers/global.js index f7ab768e..197fdb5e 100644 --- a/src/kopf/controllers/global.js +++ b/src/kopf/controllers/global.js @@ -1,11 +1,19 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogService, AlertService, SettingsService) { $scope.dialog = ConfirmDialogService; - $scope.version = "0.5.1"; + $scope.version = "0.5.2"; $scope.username = null; $scope.password = null; - $scope.alerts_service = AlertService; + $scope.alert_service = AlertService; + + $scope.home_screen=function() { + $('#cluster_option a').tab('show'); + }; $scope.setConnected=function(status) { + if (!status) { + $scope.cluster = null; + $scope.cluster_health = null; + } $scope.is_connected = status; }; @@ -19,38 +27,39 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic }; $scope.setHost=function(url) { - var exp = /^(https|http):\/\/(\w+):(\w+)@(.*)/i; - // expected: "http://user:password@host", "http", "user", "password", "host"] - var url_parts = exp.exec(url); - if (isDefined(url_parts)) { - $scope.host = url_parts[1] + "://" + url_parts[4]; - $scope.username = url_parts[2]; - $scope.password = url_parts[3]; - } else { - $scope.username = null; - $scope.password = null; - $scope.host = url; + if (url.indexOf("http://") !== 0 && url.indexOf("https://") !== 0) { + url = "http://" + url; } + $scope.connection = new ESConnection(url); $scope.setConnected(false); try { - $scope.client = new ElasticClient($scope.host,$scope.username,$scope.password); - $scope.broadcastMessage('hostChanged',{}); + $scope.client = new ElasticClient($scope.connection); + $scope.home_screen(); } catch (error) { + $scope.client = null; AlertService.error(error); } - }; - if ($location.host() === "") { // when opening from filesystem - $scope.setHost("http://localhost:9200"); - } else { - var location = $scope.readParameter('location'); - if (isDefined(location)) { - $scope.setHost(location); + $scope.connect=function() { + // when opening from filesystem, just try default ES location + if ($location.host() === "") { + $scope.setHost("http://localhost:9200"); } else { - $scope.setHost($location.protocol() + "://" + $location.host() + ":" + $location.port()); - } - } + var location = $scope.readParameter('location'); + // reads ES location from url parameter + if (isDefined(location)) { + $scope.setHost(location); + } else { // uses current location as ES location + var absUrl = $location.absUrl(); + var cutIndex = absUrl.indexOf("/_plugin/kopf"); + $scope.setHost(absUrl.substring(0,cutIndex)); + } + } + }; + + $scope.connect(); + $scope.modal = new ModalControls(); $scope.alert = null; $scope.is_connected = false; @@ -67,6 +76,14 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic var leaves = changes.nodeLeaves.map(function(node) { return node.name + "[" + node.transport_address + "]"; }); AlertService.warn(changes.nodeLeaves.length + " node(s) left the cluster", leaves); } + if (changes.hasCreatedIndices()) { + var created = changes.indicesCreated.map(function(index) { return index.name; }); + AlertService.info(changes.indicesCreated.length + " indices created: [" + created.join(",") + "]"); + } + if (changes.hasDeletedIndices()) { + var deleted = changes.indicesDeleted.map(function(index) { return index.name; }); + AlertService.info(changes.indicesDeleted.length + " indices deleted: [" + deleted.join(",") + "]"); + } } } }; @@ -123,20 +140,12 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic return $('#' + tab).hasClass('active'); }; - $scope.getHost=function() { - return $scope.host; - }; - $scope.displayInfo=function(title,info) { $scope.modal.title = title; $scope.modal.info = $sce.trustAsHtml(JSONTree.create(info)); $('#modal_info').modal({show:true,backdrop:true}); }; - $scope.isInModal=function() { - return ($('.modal-backdrop').length > 0); - }; - $scope.getCurrentTime=function() { return getTimeString(new Date()); }; @@ -151,5 +160,5 @@ function GlobalController($scope, $location, $timeout, $sce, ConfirmDialogServic $scope.updateModel=function(action) { $scope.$apply(action); }; - + } \ No newline at end of file diff --git a/src/kopf/controllers/index_settings.js b/src/kopf/controllers/index_settings.js index 69619a2d..3b399331 100644 --- a/src/kopf/controllers/index_settings.js +++ b/src/kopf/controllers/index_settings.js @@ -1,10 +1,5 @@ function IndexSettingsController($scope, $location, $timeout, IndexSettingsService, AlertService) { - $scope.alert_service = AlertService; $scope.service = IndexSettingsService; - - $scope.back=function() { - $('#cluster_option a').tab('show'); - }; $scope.save=function() { var index = $scope.service.index; @@ -18,13 +13,13 @@ function IndexSettingsController($scope, $location, $timeout, IndexSettingsServi $scope.client.updateIndexSettings(index.name, JSON.stringify(new_settings, undefined, ""), function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Index settings were successfully updated", response); + AlertService.success("Index settings were successfully updated", response); }); $scope.refreshClusterState(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while updating index settings", error); + AlertService.error("Error while updating index settings", error); }); } ); diff --git a/src/kopf/controllers/navbar.js b/src/kopf/controllers/navbar.js index 0fbea28d..ce157ee7 100644 --- a/src/kopf/controllers/navbar.js +++ b/src/kopf/controllers/navbar.js @@ -1,12 +1,13 @@ function NavbarController($scope, $location, $timeout, AlertService, SettingsService) { $scope.settings_service = SettingsService; - $scope.alert_service = AlertService; $scope.new_refresh = $scope.settings_service.getRefreshInterval(); - $scope.connectToHost=function() { - if (isDefined($scope.new_host) && $scope.new_host.length > 0) { - $scope.setHost($scope.new_host); - $scope.refreshClusterState(); + $scope.connectToHost=function(event) { + if (event.keyCode == 13) { + if (isDefined($scope.new_host) && $scope.new_host.length > 0) { + $scope.setHost($scope.new_host); + $scope.refreshClusterState(); + } } }; diff --git a/src/kopf/controllers/percolator.js b/src/kopf/controllers/percolator.js index 53223659..2394ca97 100644 --- a/src/kopf/controllers/percolator.js +++ b/src/kopf/controllers/percolator.js @@ -1,8 +1,7 @@ -function PercolatorController($scope, $location, $timeout, ConfirmDialogService, AlertService) { +function PercolatorController($scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { $scope.dialog_service = ConfirmDialogService; - $scope.editor = new AceEditor('percolator-query-editor'); - + $scope.editor = undefined; $scope.total = 0; $scope.queries = []; $scope.page = 1; @@ -15,8 +14,15 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.$on('loadPercolatorEvent', function() { $scope.indices = $scope.cluster.indices; + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('percolator-query-editor'); + } + }; + $scope.previousPage=function() { $scope.page -= 1; $scope.loadPercolatorQueries(); @@ -68,7 +74,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.client.deletePercolatorQuery(query.index, query.id, function(response) { var refreshIndex = $scope.client.is1() ? query.index : '_percolator'; - $scope.client.refreshIndex(refreshIndex, + $scope.client.refreshIndex(refreshIndex, function(response) { $scope.updateModel(function() { AlertService.success("Query successfully deleted", response); @@ -98,7 +104,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, $scope.client.createPercolatorQuery($scope.new_query.index.name, $scope.new_query.id, $scope.new_query.source, function(response) { var refreshIndex = $scope.client.is1() ? $scope.new_query.index.name : '_percolator'; - $scope.client.refreshIndex(refreshIndex, + $scope.client.refreshIndex(refreshIndex, function(response) { $scope.updateModel(function() { AlertService.success("Percolator Query successfully created", response); @@ -151,7 +157,7 @@ function PercolatorController($scope, $location, $timeout, ConfirmDialogService, }); } } - ); + ); } catch (error) { AlertService.error("Filter is not a valid JSON"); return; diff --git a/src/kopf/controllers/repository.js b/src/kopf/controllers/repository.js index 0cd191ea..44fc2199 100644 --- a/src/kopf/controllers/repository.js +++ b/src/kopf/controllers/repository.js @@ -1,23 +1,32 @@ -function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogService, AlertService) { +function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { - $scope.alert_service = AlertService; $scope.dialog_service = ConfirmDialogService; - - $scope.editor = new AceEditor('repository-settings-editor'); - $scope.repositories = []; $scope.repositories_names = []; $scope.snapshots = []; $scope.indices = []; + $scope.restorable_indices = []; $scope.new_repo = {}; $scope.new_snap = {}; + $scope.restore_snap = {}; + $scope.editor = undefined; $scope.$on('loadRepositoryEvent', function() { $scope.reload(); + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('repository-settings-editor'); + } + }; + $scope.loadIndices=function() { - $scope.indices = $scope.cluster.indices; + if( angular.isDefined($scope.cluster) ) + { + $scope.indices = $scope.cluster.indices || []; + } }; $scope.reload=function(){ @@ -28,6 +37,14 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.loadIndices(); }; + $scope.optionalParam=function(body, object, paramname){ + if(angular.isDefined(object[paramname])) + { + body[paramname] = object[paramname]; + } + return body; + }; + $scope.deleteRepository=function(name, value){ $scope.dialog_service.open( "are you sure you want to delete repository " + name + "?", @@ -36,12 +53,12 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ function() { $scope.client.deleteRepository(name, function(response) { - $scope.alert_service.success("Repository successfully deleted", response); + AlertService.success("Repository successfully deleted", response); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting repositor", error); + AlertService.error("Error while deleting repositor", error); }); } ); @@ -49,6 +66,38 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ ); }; + $scope.restoreSnapshot=function(){ + + var body = {} + // dont add to body if not present, these are optional, all indices included by default + if(angular.isDefined($scope.restore_snap.indices) && $scope.restore_snap.indices.length > 0) + { + body["indices"] = $scope.restore_snap.indices.join(","); + } + + if(angular.isDefined($scope.restore_snap.include_global_state)) + { + //TODO : when es fixes bug [https://github.com/elasticsearch/elasticsearch/issues/4949], this extra "true/false" -> true/false handling will go away + body["include_global_state"] = ($scope.restore_snap.include_global_state == 'true'); + } + + $scope.optionalParam(body, $scope.restore_snap, "ignore_unavailable"); + $scope.optionalParam(body, $scope.restore_snap, "rename_replacement"); + $scope.optionalParam(body, $scope.restore_snap, "rename_pattern"); + + $scope.client.restoreSnapshot($scope.restore_snap.snapshot.repository, $scope.restore_snap.snapshot.snapshot, JSON.stringify(body), + function(response) { + AlertService.success("Snapshot Restored Started"); + $scope.reload(); + }, + function(error) { + $scope.updateModel(function() { + AlertService.error("Error while started restor of snapshot", error); + }); + } + ); + }; + $scope.createRepository=function(){ $scope.new_repo.settings = $scope.editor.format(); if ($scope.editor.error === null){ @@ -59,42 +108,45 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.client.createRepository($scope.new_repo.name, JSON.stringify(body), function(response) { - $scope.alert_service.success("Repository created"); + AlertService.success("Repository created"); $scope.loadRepositories(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while creating repository", error); + AlertService.error("Error while creating repository", error); }); } ); } }; + $scope._parseRepositories=function(response, deferred){ + $scope.updateModel(function() { + $scope.repositories = response; + $scope.repositories_names = []; + $.each($scope.repositories, function(key, value){ + $scope.repositories_names.push({"name":key, "value":key}); + }); + }); + deferred.resolve(true); + }; + $scope.loadRepositories=function() { var deferred = $q.defer(); try { $scope.client.getRepositories( function(response) { - $scope.updateModel(function() { - $scope.repositories = response; - $.each($scope.repositories, function(key, value){ - $scope.repositories_names.push({"name":key, "value":key}); - }); - }); - deferred.resolve(true); + $scope._parseRepositories(response, deferred) }, function(error) { - if (!(error['responseJSON'] != null )) { - $scope.updateModel(function() { - $scope.alert_service.error("Error while reading repositories", error); - }); - } + $scope.updateModel(function() { + AlertService.error("Error while reading repositories", error); + }); deferred.reject(true); } ) } catch (error) { - $scope.alert_service.error("Failed to load repositories"); + AlertService.error("Failed to load repositories"); deferred.reject(false); } return deferred.promise @@ -106,13 +158,13 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ // name and repo required if(!angular.isDefined($scope.new_snap.repository)) { - $scope.alert_service.warn("Repository is required"); + AlertService.warn("Repository is required"); return } if(!angular.isDefined($scope.new_snap.name)) { - $scope.alert_service.warn("Snapshot name is required"); + AlertService.warn("Snapshot name is required"); return } @@ -122,24 +174,22 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ body["indices"] = $scope.new_snap.indices.join(","); } - if(angular.isDefined($scope.new_snap.ignore_unavailable)) - { - body["ignore_unavailable"] = $scope.new_snap.ignore_unavailable; - } - if(angular.isDefined($scope.new_snap.include_global_state)) { - body["include_global_state"] = true; //$scope.new_snap.include_global_state; + //TODO : when es fixes bug [https://github.com/elasticsearch/elasticsearch/issues/4949], this extra "true/false" -> true/false handling will go away + body["include_global_state"] = ($scope.new_snap.include_global_state == 'true'); } + $scope.optionalParam(body, $scope.new_snap, "ignore_unavailable"); + $scope.client.createSnapshot($scope.new_snap.repository, $scope.new_snap.name, JSON.stringify(body), function(response) { - $scope.alert_service.success("Snapshot created"); + AlertService.success("Snapshot created"); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while creating snapshot", error); + AlertService.error("Error while creating snapshot", error); }); } ); @@ -155,12 +205,12 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ snapshot.repository, snapshot.snapshot, function(response) { - $scope.alert_service.success("Snapshot successfully deleted", response); + AlertService.success("Snapshot successfully deleted", response); $scope.reload(); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting snapshot", error); + AlertService.error("Error while deleting snapshot", error); }); } ); @@ -170,62 +220,47 @@ function RepositoryController($q, $scope, $location, $timeout, ConfirmDialogServ $scope.allSnapshots=function(repositories) { var all = []; - $.each( repositories, function( index, value ){ - $scope.fetchSnapshots(index).then( + $.each( repositories, function( repository, value ){ + $scope.fetchSnapshots(repository).then( function(data){ - $.merge($scope.snapshots, data ); + $.merge(all, data ); }); }); $scope.snapshots = all; }; + $scope._parseSnapshots=function(repository, response, deferred) { + var arr = response["snapshots"]; + + // add the repository name to each snapshot object + // + if(arr && arr.constructor==Array && arr.length!==0){ + $.each(arr, function(index, value){ + value["repository"] = repository; + }); + } + deferred.resolve(response["snapshots"]); + }; + $scope.fetchSnapshots=function(repository) { var deferred = $q.defer(); try { $scope.client.getSnapshots(repository, function(response) { - var arr = response["snapshots"]; - if(arr && arr.constructor==Array && arr.length!=0){ - $.each(arr, function(index, value){ - value["repository"] = repository; - }); - } - deferred.resolve(response["snapshots"]); + $scope._parseSnapshots(repository, response, deferred); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while fetching snapshots", error); + AlertService.error("Error while fetching snapshots", error); }); deferred.resolve([]); } ) } catch (error) { - $scope.alert_service.error("Failed to load snapshots"); + AlertService.error("Failed to load snapshots"); deferred.resolve([]); } return deferred.promise; }; - $scope.loadSnapshots=function(repository) { - try { - $scope.client.getSnapshots(repository, - function(response) { - $scope.updateModel(function() { - $scope.snapshots = response["snapshots"]; - }); - }, - function(error) { - if (!(error['responseJSON'] != null )) { - $scope.updateModel(function() { - $scope.alert_service.error("Error while reading snapshots", error); - }); - } - } - ) - } catch (error) { - $scope.alert_service.error("Failed to load snapshots"); - return; - } - }; - } diff --git a/src/kopf/controllers/rest.js b/src/kopf/controllers/rest.js index 156a8df8..52915a1f 100644 --- a/src/kopf/controllers/rest.js +++ b/src/kopf/controllers/rest.js @@ -1,7 +1,6 @@ -function RestController($scope, $location, $timeout, AlertService) { - $scope.alert_service = AlertService; +function RestController($scope, $location, $timeout, AlertService, AceEditorService) { - $scope.request = new Request($scope.getHost() + "/_search","GET","{}"); + $scope.request = new Request($scope.connection.host + "/_search","GET","{}"); $scope.validation_error = null; $scope.loadHistory=function() { @@ -14,16 +13,19 @@ function RestController($scope, $location, $timeout, AlertService) { } catch (error) { localStorage.kopf_request_history = null; } - } + } return history; }; $scope.history = $scope.loadHistory(); $scope.history_request = null; - - $scope.editor = new AceEditor('rest-client-editor'); + + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('rest-client-editor'); + } + $scope.editor.setValue($scope.request.body); - + $scope.loadFromHistory=function(history_request) { $scope.request.url = history_request.url; $scope.request.body = history_request.body; @@ -45,7 +47,7 @@ function RestController($scope, $location, $timeout, AlertService) { if ($scope.history.length > 30) { $scope.history.length = 30; } - localStorage.kopf_request_history = JSON.stringify($scope.history); + localStorage.kopf_request_history = JSON.stringify($scope.history); } }; @@ -55,7 +57,7 @@ function RestController($scope, $location, $timeout, AlertService) { if (!isDefined($scope.editor.error) && notEmpty($scope.request.url)) { // TODO: deal with basic auth here if ($scope.request.method == 'GET' && '{}' !== $scope.request.body) { - $scope.alert_service.info("You are executing a GET request with body content. Maybe you meant to use POST or PUT?"); + AlertService.info("You are executing a GET request with body content. Maybe you meant to use POST or PUT?"); } $scope.client.executeRequest($scope.request.method,$scope.request.url,null,null,$scope.request.body, function(response) { @@ -74,9 +76,9 @@ function RestController($scope, $location, $timeout, AlertService) { function(error) { $scope.updateModel(function() { if (error.status !== 0) { - $scope.alert_service.error("Request was not successful: " + error.statusText); + AlertService.error("Request was not successful: " + error.statusText); } else { - $scope.alert_service.error($scope.request.url + " is unreachable"); + AlertService.error($scope.request.url + " is unreachable"); } }); try { diff --git a/src/kopf/controllers/warmup.js b/src/kopf/controllers/warmup.js index 675fdfc6..e727c53e 100644 --- a/src/kopf/controllers/warmup.js +++ b/src/kopf/controllers/warmup.js @@ -1,12 +1,6 @@ -function WarmupController($scope, $location, $timeout, ConfirmDialogService, AlertService) { - $scope.alert_service = AlertService; +function WarmupController($scope, $location, $timeout, ConfirmDialogService, AlertService, AceEditorService) { $scope.dialog_service = ConfirmDialogService; - - $scope.editor = ace.edit("warmup-query-editor"); - $scope.editor.setFontSize("10px"); - $scope.editor.setTheme("ace/theme/kopf"); - $scope.editor.getSession().setMode("ace/mode/json"); - + $scope.editor = undefined; $scope.indices = []; $scope.warmers = {}; $scope.index = null; @@ -20,8 +14,15 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.$on('loadWarmupEvent', function() { $scope.loadIndices(); + $scope.initEditor(); }); + $scope.initEditor=function(){ + if(!angular.isDefined($scope.editor)){ + $scope.editor = AceEditorService.init('warmup-query-editor'); + } + }; + $scope.totalWarmers=function() { return Object.keys($scope.warmers).length; }; @@ -36,12 +37,12 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.client.registerWarmupQuery($scope.new_index.name, $scope.new_types, $scope.new_warmer_id, $scope.new_source, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Warmup query successfully registered", response); + AlertService.success("Warmup query successfully registered", response); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Request did not return a valid JSON", error); + AlertService.error("Request did not return a valid JSON", error); }); } ); @@ -57,13 +58,13 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale $scope.client.deleteWarmupQuery($scope.index.name, warmer_id, function(response) { $scope.updateModel(function() { - $scope.alert_service.success("Warmup query successfully deleted", response); + AlertService.success("Warmup query successfully deleted", response); $scope.loadIndexWarmers(); }); }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while deleting warmup query", error); + AlertService.error("Error while deleting warmup query", error); }); } ); @@ -85,7 +86,7 @@ function WarmupController($scope, $location, $timeout, ConfirmDialogService, Ale }, function(error) { $scope.updateModel(function() { - $scope.alert_service.error("Error while fetching warmup queries", error); + AlertService.error("Error while fetching warmup queries", error); }); } ); diff --git a/src/kopf/css/cluster_health.css b/src/kopf/css/cluster_health.css index 7e667ef8..c3a031bc 100644 --- a/src/kopf/css/cluster_health.css +++ b/src/kopf/css/cluster_health.css @@ -7,6 +7,24 @@ color: #666; } .cluster-health-content { - background: #fff; font-size: 10px; + color: #666; + font-family: "Lucida Console", Monaco, monospace; +} + +.cluster-health-form-group { + padding-left: 5px; + float: right; +} +.gist-timestamp-col { + width: 70px; +} +.gist-link-col { + width: 240px; +} +.gist-title-col { + width: auto; +} +.cluster-health-filters { + padding-top: 6px; } \ No newline at end of file diff --git a/src/kopf/css/cluster_overview.css b/src/kopf/css/cluster_overview.css index 0ea557f3..a4f5243e 100644 --- a/src/kopf/css/cluster_overview.css +++ b/src/kopf/css/cluster_overview.css @@ -1,3 +1,20 @@ +table.overview { + table-layout:fixed; +} +table.overview tr td { width: 16.67%; } + +.index-menu-icon { + padding-right: 5px; +} + +.dropdown-menu .divider { + margin: 5px 0px; +} + +.cluster-map-header-index-icon { + padding-right: 5px; +} + .cluster-map-header-cluster-actions { font-size: 32px; vertical-align: middle !important; @@ -17,16 +34,27 @@ a.cluster-map-header-cluster-action { text-decoration: none; } .cluster-map-header-index-cell { - width: 230px !important; - max-width: 230px !important; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; border-bottom: 0px !important; } + +.closed-index { + color: #D0D0D0 !important; + font-weight: 300 !important; +} + +.closed-index-cell { + background: #F9F9F9; +} + .cluster-map-header-index-name { - word-wrap:break-word; + overflow: hidden; + white-space: nowrap; font-weight: 500; + text-overflow: ellipsis; + display: block; } .cluster-map-header-index-alias { word-wrap:break-word; @@ -37,6 +65,19 @@ a.cluster-map-header-cluster-action { .cluster-map-header-index-actions { padding: 6px; } +.cluster-map-header-index-name { + color: #555555; + font-size: 13px; + font-weight: 500; + cursor: pointer; +} +a.cluster-map-header-index-name:hover { + color: #555555; + font-size: 13px; + font-weight: 500; + text-decoration: none; +} + .cluster-map-header-index-action { color: #555555; font-size: 12px; @@ -52,18 +93,28 @@ a.cluster-map-header-index-action:hover { .cluster-map-node-name { word-wrap:break-word; font-weight: 500; + font-size: 13px; + text-overflow: ellipsis; + overflow: hidden; + display: block; } .cluster-map-node-types { - padding-top: 5px; - padding-left: 0px; - font-size: 12px; + padding-top: 4px; } .cluster-map-node-type { - padding-right: 12px; + font-weight: 300; + padding-left: 5px; + font-size: 12px; +} +.cluster-overview-filter { + padding-bottom: 10px; +} +.node-type-icon { + padding-left: 2px; + padding-right: 2px; } .cluster-map-node-cell { width: 220px; - max-width: 220px; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; @@ -92,8 +143,8 @@ a.cluster-map-header-index-action:hover { } .cluster-map-pagination { padding-top: 5px; - font-weight: 400; - height: 30px; + font-weight: 300; + font-size: 12px; } .cluster-map-node-action { color: #555555; @@ -117,35 +168,30 @@ a.cluster-map-node-action { } .cluster-map-stats-section { background: #f9f9f9; - text-align: justify; - padding-left: 15px; - padding-right: 15px; - border: 1px solid #dddddd; - border-radius: 3px 3px 3px 3px; - height: 50px; - margin-bottom: 40px; - margin-top: 20px; + padding-left: 10px; + border: 1px solid #ccc; + margin-top: 10px; + border-radius: 4px 4px 4px 4px; + margin-bottom: 30px; } .cluster-map-stat-block { display: inline-block; - padding-top: 3px; } .cluster-map-stats-section:after { content: ""; width: 100%; - display: inline-block; } .cluster-map-stat-number { color: #555555; - font-size: 28px; + font-size: 20px; text-align: center; - font-weight: 600; + font-weight: 500; } .cluster-map-stat-description { color: #555555; - font-size: 18px; + font-size: 12px; text-align: center; - font-weight: 200; + font-weight: 300; } .shard { text-align: center; @@ -218,8 +264,6 @@ a.header-action:hover { } .header-index-cell { - max-width: 230px; - min-width: 230px; max-height: 40px !important; vertical-align: top !important; background: #f9f9f9; @@ -266,8 +310,6 @@ a.header-index-action-mini:hover { text-decoration: none; } .shards { - max-width: 230px; - min-width: 230px; max-height: 40px !important; vertical-align: top !important; word-wrap:break-word; diff --git a/src/kopf/css/common.css b/src/kopf/css/common.css index 301dd8f5..b6b60085 100644 --- a/src/kopf/css/common.css +++ b/src/kopf/css/common.css @@ -114,7 +114,7 @@ h4 { color: #666; font-variant:small-caps; font-weight: 500; } h5 { color: #777; font-variant:small-caps; font-weight: 500; } h6 { color: #999; font-variant:small-caps; font-weight: 500; } -#page-wrap { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -60px; padding: 0px 0px 60px; } +#page-wrap { min-height: 100%; height: auto !important; height: 100%; margin: 0 auto -60px; padding: 0px 25px 60px; } .settings-section-header { top: 15px; @@ -187,10 +187,14 @@ html,body { .alert-block { font-size: 12px; font-weight: 300; - padding-top: 0px; + padding-top: 10px; padding-bottom: 0px; text-align: left; } + +.alert { + margin-bottom: 0px; +} .alert-server-response { padding-top: 25px; } diff --git a/src/kopf/css/navbar.css b/src/kopf/css/navbar.css index 96e8fa9b..a0ad5d42 100644 --- a/src/kopf/css/navbar.css +++ b/src/kopf/css/navbar.css @@ -1,3 +1,67 @@ +.dropdown-menu { + margin-top: 0px; + margin-left: 25px; +} + +.navbar-nav { + margin-top: 0px; +} + +.navbar-collapse { + border-top: 0px; +} + +.navbar-brand { + padding: 0px 15px 0px 0px; + margin-left: 25px; + font-size: 38px; + font-variant: small-caps; + line-height: 32px; + font-weight: 100; + color: #dff0d8; +} +.navbar-toggle { + padding: 6px 9px; + margin-right: 25px; +} +.navbar-toggle .icon-bar { + width: 10px; + height: 1px; +} + +.navbar-toggle-red .icon-bar { + background: #f2dede !important; +} + +.navbar-toggle-yellow .icon-bar { + background: #fcf8e3 !important; +} + +.navbar-toggle-green .icon-bar { + background: #dff0d8 !important; +} + +.navbar-toggle { + height: 22px; + width: 30px; +} + +.navbar-toggle-red { + height: 22px; + width: 30px; + border: 1px solid #c09853; +} +.navbar-toggle-yellow { + height: 22px; + width: 30px; + border: 1px solid #fcf8e3; +} +.navbar-toggle-green { + height: 22px; + width: 30px; + border: 1px solid #dff0d8; +} + .navbar-fixed-top { margin-right: 0px !important; } @@ -106,12 +170,13 @@ } .navbar-app-settings { color: #000000; - width: 250px; + width: auto; padding-left: 10px; padding-right: 10px; } .navbar-host-input { - width: 250px; + padding-right: 25px; + width: 280px; padding-top: 2px; vertical-align: middle; } @@ -124,6 +189,7 @@ vertical-align: middle; padding-top: 4px; padding-bottom: 4px; + min-width: 300px; } .navbar-nav > li { height: 38px !important; diff --git a/src/kopf/css/repository.css b/src/kopf/css/repository.css index dbdfe8b1..9803d4da 100644 --- a/src/kopf/css/repository.css +++ b/src/kopf/css/repository.css @@ -18,6 +18,10 @@ select.input-sm-twoxheight { padding-left: 15px; } +.row-nomargin-button{ + margin-right: 15px; +} + .form-label-repository { padding-left: 15px; } \ No newline at end of file diff --git a/src/kopf/elastic/cluster.js b/src/kopf/elastic/cluster.js index 239fee23..c9e5cefc 100644 --- a/src/kopf/elastic/cluster.js +++ b/src/kopf/elastic/cluster.js @@ -29,15 +29,20 @@ function Cluster(state,status,nodes,settings) { var unassigned_shards = 0; var total_size = 0; var num_docs = 0; + var special_indices = 0; this.indices = Object.keys(iMetadata).map( function(x) { var index = new Index(x,iRoutingTable[x], iMetadata[x], iStatus[x]); + if (index.isSpecial()) { + special_indices++; + } unassigned_shards += index.unassigned.length; total_size += parseInt(index.total_size); num_docs += index.num_docs; return index; } ).sort(function(a,b) { return a.compare(b); }); + this.special_indices = special_indices; this.num_docs = num_docs; this.unassigned_shards = unassigned_shards; this.total_indices = this.indices.length; @@ -46,18 +51,17 @@ function Cluster(state,status,nodes,settings) { this.successful_shards = status._shards.successful; this.total_size = readablizeBytes(total_size); this.getNodes=function(name, data, master, client) { - return $.map(this.nodes,function(n) { - if (name.trim().length > 0 && n.name.toLowerCase().indexOf(name.trim().toLowerCase()) == -1) { - return null; - } - return (data && n.data || master && n.master || client && n.client) ? n : null; + return $.map(this.nodes,function(node) { + return node.matches(name, data, master, client) ? node : null; }); }; this.getChanges=function(new_cluster) { var nodes = this.nodes; + var indices = this.indices; var changes = new ClusterChanges(); if (isDefined(new_cluster)) { + // checks for node differences nodes.forEach(function(node) { for (var i = 0; i < new_cluster.nodes.length; i++) { if (new_cluster.nodes[i].equals(node)) { @@ -82,6 +86,32 @@ function Cluster(state,status,nodes,settings) { } }); } + + // checks for indices differences + indices.forEach(function(index) { + for (var i = 0; i < new_cluster.indices.length; i++) { + if (new_cluster.indices[i].equals(index)) { + index = null; + break; + } + } + if (isDefined(index)) { + changes.addDeletedIndex(index); + } + }); + if (new_cluster.indices.length != indices.length || !changes.hasCreatedIndices()) { + new_cluster.indices.forEach(function(index) { + for (var i = 0; i < indices.length; i++) { + if (indices[i].equals(index)) { + index = null; + break; + } + } + if (isDefined(index)) { + changes.addCreatedIndex(index); + } + }); + } } return changes; }; diff --git a/src/kopf/elastic/cluster_changes.js b/src/kopf/elastic/cluster_changes.js index fa86b2e0..1f9e173c 100644 --- a/src/kopf/elastic/cluster_changes.js +++ b/src/kopf/elastic/cluster_changes.js @@ -2,10 +2,15 @@ function ClusterChanges() { this.nodeJoins = null; this.nodeLeaves = null; + this.indicesCreated = null; + this.indicesDeleted = null; this.hasChanges=function() { - return (isDefined(this.nodeJoins) || - isDefined(this.nodeLeaves) + return ( + isDefined(this.nodeJoins) || + isDefined(this.nodeLeaves) || + isDefined(this.indicesCreated) || + isDefined(this.indicesDeleted) ); }; @@ -32,5 +37,27 @@ function ClusterChanges() { this.hasLeaves=function() { return isDefined(this.nodeLeaves); }; + + this.hasCreatedIndices=function() { + return isDefined(this.indicesCreated); + }; + + this.hasDeletedIndices=function() { + return isDefined(this.indicesDeleted); + }; + + this.addCreatedIndex=function(index) { + if (!isDefined(this.indicesCreated)) { + this.indicesCreated = []; + } + this.indicesCreated.push(index); + }; + + this.addDeletedIndex=function(index) { + if (!isDefined(this.indicesDeleted)) { + this.indicesDeleted = []; + } + this.indicesDeleted.push(index); + }; } \ No newline at end of file diff --git a/src/kopf/elastic/cluster_health.js b/src/kopf/elastic/cluster_health.js index 3dfbe9a0..4e245a28 100644 --- a/src/kopf/elastic/cluster_health.js +++ b/src/kopf/elastic/cluster_health.js @@ -1,4 +1,14 @@ function ClusterHealth(health) { this.status = health.status; - this.name = health.cluster_name; + this.cluster_name = health.cluster_name; + this.initializing_shards = health.initializing_shards; + this.active_primary_shards = health.active_primary_shards; + this.active_shards = health.active_shards; + this.relocating_shards = health.relocating_shards; + this.unassigned_shards = health.unassigned_shards; + this.number_of_nodes = health.number_of_nodes; + this.number_of_data_nodes = health.number_of_data_nodes; + this.timed_out = health.timed_out; + this.shards = this.active_shards + this.relocating_shards + this.unassigned_shards + this.initializing_shards; + this.fetched_at = getTimeString(new Date()); } \ No newline at end of file diff --git a/src/kopf/elastic/elastic_client.js b/src/kopf/elastic/elastic_client.js index a683a868..469ac628 100644 --- a/src/kopf/elastic/elastic_client.js +++ b/src/kopf/elastic/elastic_client.js @@ -1,7 +1,7 @@ -function ElasticClient(host,username,password) { - this.host = host; - this.username = username; - this.password = password; +function ElasticClient(connection) { + this.host = connection.host; + this.username = connection.username; + this.password = connection.password; this.createAuthToken=function(username,password) { var auth = null; @@ -11,10 +11,10 @@ function ElasticClient(host,username,password) { return auth; }; - var auth = this.createAuthToken(username,password); + var auth = this.createAuthToken(connection.username, connection.password); var fetch_version = $.ajax({ type: 'GET', - url: host, + url: connection.host, beforeSend: function(xhr) { if (isDefined(auth)) { xhr.setRequestHeader("Authorization", auth); @@ -219,8 +219,8 @@ function ElasticClient(host,username,password) { this.executeElasticRequest('DELETE', "/_snapshot/" + repository + "/" +snapshot, {}, callback_success, callback_error); }; - this.restoreSnapshot=function(repository, snapshot, callback_success, callback_error){ - this.executeElasticRequest('POST', "/_snapshot/" + repository + "/" +snapshot + "/_restore", {}, callback_success, callback_error); + this.restoreSnapshot=function(repository, snapshot, body, callback_success, callback_error){ + this.executeElasticRequest('POST', "/_snapshot/" + repository + "/" +snapshot + "/_restore", body, callback_success, callback_error); }; this.createSnapshot=function(repository, snapshot, body, callback_success, callback_error){ @@ -338,50 +338,77 @@ function ElasticClient(host,username,password) { ); }; - this.getClusterDiagnosis=function(callback_success,callback_error) { + this.getClusterDiagnosis=function(health, state, stats, hotthreads, callback_success,callback_error) { var host = this.host; var auth = this.createAuthToken(this.username,this.password); - $.when( - $.ajax({ - type: 'GET', - url: host+"/_cluster/state", - dataType: 'json', - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }), - $.ajax({ - type: 'GET', - url: host+"/_nodes/stats?all=true", - dataType: 'json', - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }), - $.ajax({ - type: 'GET', - url: host+"/_nodes/hot_threads", - data: {}, - beforeSend: function(xhr) { - if (isDefined(auth)) { - xhr.setRequestHeader("Authorization", auth); - } - } - }) - ).then( - function(state, stats, hot_threads) { - callback_success(state[0], stats[0], hot_threads[0]); - }, - function(failed_request) { - callback_error(failed_request); - } + var deferreds = []; + if (health) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_cluster/health", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (state) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_cluster/state", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (stats) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_nodes/stats?all=true", + dataType: 'json', + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) + ); + } + if (hotthreads) { + deferreds.push( + $.ajax({ + type: 'GET', + url: host+"/_nodes/hot_threads", + data: {}, + beforeSend: function(xhr) { + if (isDefined(auth)) { + xhr.setRequestHeader("Authorization", auth); + } + } + }) ); + } + $.when.apply($, deferreds).then( + function() { + callback_success(arguments); + }, + function(failed_request) { + callback_error(failed_request); + } + ); }; } diff --git a/src/kopf/elastic/es_connection.js b/src/kopf/elastic/es_connection.js new file mode 100644 index 00000000..195668b7 --- /dev/null +++ b/src/kopf/elastic/es_connection.js @@ -0,0 +1,19 @@ +// Expects URL according to /^(https|http):\/\/(\w+):(\w+)@(.*)/i; +// Examples: +// http://localhost:9200 +// http://user:password@localhost:9200 +// https://localhost:9200 +function ESConnection(url) { + var protected_url = /^(https|http):\/\/(\w+):(\w+)@(.*)/i; + this.host = "http://localhost:9200"; // default + if (notEmpty(url)) { + var connection_parts = protected_url.exec(url); + if (isDefined(connection_parts)) { + this.host = connection_parts[1] + "://" + url_parts[4]; + this.username = connection_parts[2]; + this.password = connection_parts[3]; + } else { + this.host = url; + } + } +} \ No newline at end of file diff --git a/src/kopf/elastic/index.js b/src/kopf/elastic/index.js index 94b0aacd..e0101219 100644 --- a/src/kopf/elastic/index.js +++ b/src/kopf/elastic/index.js @@ -2,13 +2,11 @@ function Index(index_name,index_info, index_metadata, index_status) { this.name = index_name; var index_shards = {}; this.shards = index_shards; - this.state = index_metadata.state; this.metadata = {}; - this.aliases = index_metadata.aliases; - this.total_aliases = isDefined(index_metadata.aliases) ? index_metadata.aliases.length : 0; - this.visibleAliases=function() { - return this.total_aliases > 5 ? this.aliases.slice(0,5) : this.aliases; - }; + this.aliases = getProperty(index_metadata,'aliases', []); + + this.visibleAliases=function() { return this.aliases.length > 5 ? this.aliases.slice(0,5) : this.aliases; }; + this.settings = index_metadata.settings; // FIXME: 0.90/1.0 check this.editable_settings = new EditableIndexSettings(index_metadata.settings); @@ -17,55 +15,49 @@ function Index(index_name,index_info, index_metadata, index_status) { this.metadata.mappings = this.mappings; // FIXME: 0.90/1.0 check - if (isDefined(index_metadata.settings['index.number_of_shards'])) { - this.num_of_shards = index_metadata.settings['index.number_of_shards']; - this.num_of_replicas = parseInt(index_metadata.settings['index.number_of_replicas']); - } else { - this.num_of_shards = index_metadata.settings.index.number_of_shards; - this.num_of_replicas = parseInt(index_metadata.settings.index.number_of_replicas); - } + this.num_of_shards = getProperty(index_metadata.settings, 'index.number_of_shards'); + this.num_of_replicas = parseInt(getProperty(index_metadata.settings, 'index.number_of_replicas')); + this.state = index_metadata.state; + + this.num_docs = getProperty(index_status, 'docs.num_docs', 0); + this.max_doc = getProperty(index_status, 'docs.max_doc', 0); + this.deleted_docs = getProperty(index_status, 'docs.deleted_docs', 0); + this.size = getProperty(index_status, 'index.primary_size_in_bytes', 0); + this.total_size = getProperty(index_status, 'index.size_in_bytes', 0); + this.size_in_bytes = readablizeBytes(this.size); + this.total_size_in_bytes = readablizeBytes(this.total_size); - this.state_class = index_metadata.state === "open" ? "success" : "active"; - this.visible = true; var unassigned = []; // adds shard information - if (isDefined(index_status)) { - $.map(index_status.shards, function(shards, shard_num) { - $.map(shards, function(shard_info, shard_copy) { - if (!isDefined(index_shards[shard_info.routing.node])) { - index_shards[shard_info.routing.node] = []; - } - index_shards[shard_info.routing.node].push(new Shard(shard_info)); - }); - }); - this.metadata.stats = index_status; - } - // adds unassigned shards information - if (index_info) { - Object.keys(index_info.shards).forEach(function(x) { - var shards_info = index_info.shards[x]; - shards_info.forEach(function(shard_info) { - if (shard_info.state === 'UNASSIGNED') { - unassigned.push(new UnassignedShard(shard_info)); + + if (isDefined(index_info)) { + $.map(index_info.shards, function(shards, shard_num) { + $.map(shards, function(shard_routing, shard_copy) { + if (shard_routing.node === null) { + unassigned.push(new UnassignedShard(shard_routing)); + } else { + if (!isDefined(index_shards[shard_routing.node])) { + index_shards[shard_routing.node] = []; + } + var shard_status = null; + if (isDefined(index_status) && isDefined(index_status.shards[shard_routing.shard])) { + index_status.shards[shard_routing.shard].forEach(function(status) { + if (status.routing.node == shard_routing.node && status.routing.shard == shard_routing.shard) { + shard_status = status; + } + }); + } + index_shards[shard_routing.node].push(new Shard(shard_routing, shard_status)); } }); }); } - this.unassigned = unassigned; - var has_status = this.state === 'open' && isDefined(index_status); - this.num_docs = has_status && isDefined(index_status.docs) ? index_status.docs.num_docs : 0; - this.max_doc = has_status && isDefined(index_status.docs) ? index_status.docs.max_doc : 0; - this.deleted_docs = has_status && isDefined(index_status.docs) ? index_status.docs.deleted_docs : 0; - this.size = has_status ? index_status.index.primary_size_in_bytes : 0; - this.total_size = has_status ? index_status.index.size_in_bytes : 0; - this.size_in_bytes = readablizeBytes(this.size); - this.total_size_in_bytes = readablizeBytes(this.total_size); this.settingsAsString=function() { - return hierachyJson(JSON.stringify(this.metadata, undefined, "")); + return prettyPrintObject(this.metadata); }; this.compare=function(b) { // TODO: take into account index properties? return this.name.localeCompare(b.name); @@ -126,4 +118,23 @@ function Index(index_name,index_info, index_metadata, index_status) { return []; } }; + + this.isSpecial=function() { + return ( + this.name.indexOf(".") === 0 || + this.name.indexOf("_") === 0 + ); + }; + + this.equals=function(index) { + return index.name == this.name; + }; + + this.closed=function() { + return this.state === "close"; + }; + + this.open=function() { + return this.state === "open"; + }; } \ No newline at end of file diff --git a/src/kopf/elastic/node.js b/src/kopf/elastic/node.js index 06c67e19..417c2014 100644 --- a/src/kopf/elastic/node.js +++ b/src/kopf/elastic/node.js @@ -15,13 +15,8 @@ function Node(node_id, node_info, node_stats) { this.stats = node_stats; // FIXME: 0.90/1.0 check - if (isDefined(this.stats.jvm.mem.heap_used)) { - this.heap_used = this.stats.jvm.mem.heap_used; - this.heap_committed = this.stats.jvm.mem.heap_committed; - } else { - this.heap_used = readablizeBytes(this.stats.jvm.mem.heap_used_in_bytes); - this.heap_committed = readablizeBytes(this.stats.jvm.mem.heap_committed_in_bytes); - } + this.heap_used = readablizeBytes(getProperty(this.stats,'jvm.mem.heap_used_in_bytes')); + this.heap_committed = readablizeBytes(getProperty(this.stats, 'jvm.mem.heap_committed_in_bytes')); this.setCurrentMaster=function() { this.current_master = true; @@ -31,26 +26,22 @@ function Node(node_id, node_info, node_stats) { return node.id === this.id; }; - this.compare=function(other) { // TODO: take into account node specs? - if (other.current_master) { - return 1; - } - if (this.current_master) { - return -1; - } - if (other.master && !this.master) { - return 1; - } - if (this.master && !other.master) { - return -1; - } - - if (other.data && !this.data) { - return 1; - } - if (this.data && !other.data) { - return -1; + this.compare=function(other) { + if (other.current_master) return 1; // current master comes first + if (this.current_master) return -1; // current master comes first + if (other.master && !this.master) return 1; // master eligible comes first + if (this.master && !other.master) return -1; // master eligible comes first + if (other.data && !this.data) return 1; // data node comes first + if (this.data && !other.data) return -1; // data node comes first + return this.name.localeCompare(other.name); // if all the same, lex. sort + }; + + this.matches=function(name, data, master, client) { + if (notEmpty(name)) { + if (this.name.toLowerCase().indexOf(name.trim().toLowerCase()) == -1) { + return false; + } } - return this.name.localeCompare(other.name); + return (data && this.data || master && this.master || client && this.client); }; } \ No newline at end of file diff --git a/src/kopf/elastic/shard.js b/src/kopf/elastic/shard.js index bc9fa0a3..196f7c85 100644 --- a/src/kopf/elastic/shard.js +++ b/src/kopf/elastic/shard.js @@ -1,10 +1,10 @@ -function Shard(shard_info) { - this.info = shard_info; - this.primary = shard_info.routing.primary; - this.shard = shard_info.routing.shard; - this.state = shard_info.routing.state; - this.node = shard_info.routing.node; - this.index = shard_info.routing.index; +function Shard(shard_routing, shard_info) { + this.info = isDefined(shard_info) ? shard_info : shard_routing; + this.primary = shard_routing.primary; + this.shard = shard_routing.shard; + this.state = shard_routing.state; + this.node = shard_routing.node; + this.index = shard_routing.index; this.id = this.node + "_" + this.shard + "_" + this.index; } diff --git a/src/kopf/models/gist.js b/src/kopf/models/gist.js new file mode 100644 index 00000000..06ea13b4 --- /dev/null +++ b/src/kopf/models/gist.js @@ -0,0 +1,13 @@ +function Gist(title, url) { + this.timestamp = getTimeString(new Date()); + this.title = title; + this.url = url; + + this.loadFromJSON=function(json) { + this.title = json.title; + this.url = json.url; + this.timestamp = json.timestamp; + return this; + }; + +} \ No newline at end of file diff --git a/src/kopf/services/aceeditor.js b/src/kopf/services/aceeditor.js new file mode 100644 index 00000000..85ec78a0 --- /dev/null +++ b/src/kopf/services/aceeditor.js @@ -0,0 +1,9 @@ + +kopf.factory('AceEditorService', function() { + + this.init=function(name) { + return new AceEditor(name); + }; + + return this; +}); \ No newline at end of file diff --git a/src/kopf/services/alerts.js b/src/kopf/services/alerts.js index b708c38b..7733ec5e 100644 --- a/src/kopf/services/alerts.js +++ b/src/kopf/services/alerts.js @@ -40,25 +40,25 @@ kopf.factory('AlertService', function() { // creates an error alert this.error=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 15000; - this.addAlert(new Alert(message, response, "error", "alert-danger", "icon-warning-sign"), timeout); + return this.addAlert(new Alert(message, response, "error", "alert-danger", "icon-warning-sign"), timeout); }; // creates an info alert this.info=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 5000; - this.addAlert(new Alert(message, response, "info", "alert-info", "icon-info"), timeout); + return this.addAlert(new Alert(message, response, "info", "alert-info", "icon-info"), timeout); }; // creates success alert this.success=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 5000; - this.addAlert(new Alert(message, response, "success", "alert-success", "icon-ok"), timeout); + return this.addAlert(new Alert(message, response, "success", "alert-success", "icon-ok"), timeout); }; // creates a warn alert this.warn=function(message, response, timeout) { timeout = isDefined(timeout) ? timeout : 10000; - this.addAlert(new Alert(message, response, "warn", "alert-warning", "icon-info"), timeout); + return this.addAlert(new Alert(message, response, "warn", "alert-warning", "icon-info"), timeout); }; this.addAlert=function(alert, timeout) { @@ -68,6 +68,7 @@ kopf.factory('AlertService', function() { if (this.alerts.length >= this.max_alerts) { this.alerts.length = 3; } + return alert.id; }; return this; diff --git a/src/kopf/util.js b/src/kopf/util.js index 124f775d..8c23af8b 100644 --- a/src/kopf/util.js +++ b/src/kopf/util.js @@ -13,25 +13,16 @@ function readablizeBytes(bytes) { // Example: get the value of object[a][b][c][d] // where property_path is [a,b,c,d] function getProperty(object, property_path, default_value) { - var value = default_value; - if (isDefined(object[property_path])) { - return object[property_path]; - } - var path_parts = property_path.split('.'); - var ref = object; - for (var i = 0; i < path_parts.length; i++) { - var property = path_parts[i]; - if (isDefined(ref[property])) { - ref = ref[property]; - } else { - ref = null; - break; + if (isDefined(object)) { + if (isDefined(object[property_path])) { + return object[property_path]; + } + var path_parts = property_path.split('.'); // path as nested properties + for (var i = 0; i < path_parts.length && isDefined(object); i++) { + object = object[path_parts[i]]; } } - if (isDefined(ref)) { - value = ref; - } - return value; + return isDefined(object) ? object : default_value; } // Checks if value is both non null and undefined @@ -48,4 +39,34 @@ function notEmpty(value) { // Returns the given date as a String formatted as hh:MM:ss function getTimeString(date) { return ('0' + date.getHours()).slice(-2) + ":" + ('0' + date.getMinutes()).slice(-2) + ":" + ('0' + date.getSeconds()).slice(-2); +} + +function prettyPrintObject(object) { + var prettyObject = {}; + Object.keys(object).forEach(function(key) { + var parts = key.split("."); + var property = null; + var reference = prettyObject; + var previous = null; + for (var i = 0; i
+ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/angular-mocks.js b/tests/angular-mocks.js new file mode 100644 index 00000000..c85e18a9 --- /dev/null +++ b/tests/angular-mocks.js @@ -0,0 +1,2115 @@ +/** + * @license AngularJS v1.2.3 + * (c) 2010-2014 Google, Inc. http://angularjs.org + * License: MIT + */ +(function(window, angular, undefined) { + +'use strict'; + +/** + * @ngdoc overview + * @name angular.mock + * @description + * + * Namespace from 'angular-mocks.js' which contains testing related code. + */ +angular.mock = {}; + +/** + * ! This is a private undocumented service ! + * + * @name ngMock.$browser + * + * @description + * This service is a mock implementation of {@link ng.$browser}. It provides fake + * implementation for commonly used browser apis that are hard to test, e.g. setTimeout, xhr, + * cookies, etc... + * + * The api of this service is the same as that of the real {@link ng.$browser $browser}, except + * that there are several helper methods available which can be used in tests. + */ +angular.mock.$BrowserProvider = function() { + this.$get = function() { + return new angular.mock.$Browser(); + }; +}; + +angular.mock.$Browser = function() { + var self = this; + + this.isMock = true; + self.$$url = "http://server/"; + self.$$lastUrl = self.$$url; // used by url polling fn + self.pollFns = []; + + // TODO(vojta): remove this temporary api + self.$$completeOutstandingRequest = angular.noop; + self.$$incOutstandingRequestCount = angular.noop; + + + // register url polling fn + + self.onUrlChange = function(listener) { + self.pollFns.push( + function() { + if (self.$$lastUrl != self.$$url) { + self.$$lastUrl = self.$$url; + listener(self.$$url); + } + } + ); + + return listener; + }; + + self.cookieHash = {}; + self.lastCookieHash = {}; + self.deferredFns = []; + self.deferredNextId = 0; + + self.defer = function(fn, delay) { + delay = delay || 0; + self.deferredFns.push({time:(self.defer.now + delay), fn:fn, id: self.deferredNextId}); + self.deferredFns.sort(function(a,b){ return a.time - b.time;}); + return self.deferredNextId++; + }; + + + /** + * @name ngMock.$browser#defer.now + * @propertyOf ngMock.$browser + * + * @description + * Current milliseconds mock time. + */ + self.defer.now = 0; + + + self.defer.cancel = function(deferId) { + var fnIndex; + + angular.forEach(self.deferredFns, function(fn, index) { + if (fn.id === deferId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + self.deferredFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + + /** + * @name ngMock.$browser#defer.flush + * @methodOf ngMock.$browser + * + * @description + * Flushes all pending requests and executes the defer callbacks. + * + * @param {number=} number of milliseconds to flush. See {@link #defer.now} + */ + self.defer.flush = function(delay) { + if (angular.isDefined(delay)) { + self.defer.now += delay; + } else { + if (self.deferredFns.length) { + self.defer.now = self.deferredFns[self.deferredFns.length-1].time; + } else { + throw new Error('No deferred tasks to be flushed'); + } + } + + while (self.deferredFns.length && self.deferredFns[0].time <= self.defer.now) { + self.deferredFns.shift().fn(); + } + }; + + self.$$baseHref = ''; + self.baseHref = function() { + return this.$$baseHref; + }; +}; +angular.mock.$Browser.prototype = { + +/** + * @name ngMock.$browser#poll + * @methodOf ngMock.$browser + * + * @description + * run all fns in pollFns + */ + poll: function poll() { + angular.forEach(this.pollFns, function(pollFn){ + pollFn(); + }); + }, + + addPollFn: function(pollFn) { + this.pollFns.push(pollFn); + return pollFn; + }, + + url: function(url, replace) { + if (url) { + this.$$url = url; + return this; + } + + return this.$$url; + }, + + cookies: function(name, value) { + if (name) { + if (angular.isUndefined(value)) { + delete this.cookieHash[name]; + } else { + if (angular.isString(value) && //strings only + value.length <= 4096) { //strict cookie storage limits + this.cookieHash[name] = value; + } + } + } else { + if (!angular.equals(this.cookieHash, this.lastCookieHash)) { + this.lastCookieHash = angular.copy(this.cookieHash); + this.cookieHash = angular.copy(this.cookieHash); + } + return this.cookieHash; + } + }, + + notifyWhenNoOutstandingRequests: function(fn) { + fn(); + } +}; + + +/** + * @ngdoc object + * @name ngMock.$exceptionHandlerProvider + * + * @description + * Configures the mock implementation of {@link ng.$exceptionHandler} to rethrow or to log errors + * passed into the `$exceptionHandler`. + */ + +/** + * @ngdoc object + * @name ngMock.$exceptionHandler + * + * @description + * Mock implementation of {@link ng.$exceptionHandler} that rethrows or logs errors passed + * into it. See {@link ngMock.$exceptionHandlerProvider $exceptionHandlerProvider} for configuration + * information. + * + * + *
+ *   describe('$exceptionHandlerProvider', function() {
+ *
+ *     it('should capture log messages and exceptions', function() {
+ *
+ *       module(function($exceptionHandlerProvider) {
+ *         $exceptionHandlerProvider.mode('log');
+ *       });
+ *
+ *       inject(function($log, $exceptionHandler, $timeout) {
+ *         $timeout(function() { $log.log(1); });
+ *         $timeout(function() { $log.log(2); throw 'banana peel'; });
+ *         $timeout(function() { $log.log(3); });
+ *         expect($exceptionHandler.errors).toEqual([]);
+ *         expect($log.assertEmpty());
+ *         $timeout.flush();
+ *         expect($exceptionHandler.errors).toEqual(['banana peel']);
+ *         expect($log.log.logs).toEqual([[1], [2], [3]]);
+ *       });
+ *     });
+ *   });
+ * 
+ */ + +angular.mock.$ExceptionHandlerProvider = function() { + var handler; + + /** + * @ngdoc method + * @name ngMock.$exceptionHandlerProvider#mode + * @methodOf ngMock.$exceptionHandlerProvider + * + * @description + * Sets the logging mode. + * + * @param {string} mode Mode of operation, defaults to `rethrow`. + * + * - `rethrow`: If any errors are passed into the handler in tests, it typically + * means that there is a bug in the application or test, so this mock will + * make these tests fail. + * - `log`: Sometimes it is desirable to test that an error is thrown, for this case the `log` + * mode stores an array of errors in `$exceptionHandler.errors`, to allow later + * assertion of them. See {@link ngMock.$log#assertEmpty assertEmpty()} and + * {@link ngMock.$log#reset reset()} + */ + this.mode = function(mode) { + switch(mode) { + case 'rethrow': + handler = function(e) { + throw e; + }; + break; + case 'log': + var errors = []; + + handler = function(e) { + if (arguments.length == 1) { + errors.push(e); + } else { + errors.push([].slice.call(arguments, 0)); + } + }; + + handler.errors = errors; + break; + default: + throw new Error("Unknown mode '" + mode + "', only 'log'/'rethrow' modes are allowed!"); + } + }; + + this.$get = function() { + return handler; + }; + + this.mode('rethrow'); +}; + + +/** + * @ngdoc service + * @name ngMock.$log + * + * @description + * Mock implementation of {@link ng.$log} that gathers all logged messages in arrays + * (one array per logging level). These arrays are exposed as `logs` property of each of the + * level-specific log function, e.g. for level `error` the array is exposed as `$log.error.logs`. + * + */ +angular.mock.$LogProvider = function() { + var debug = true; + + function concat(array1, array2, index) { + return array1.concat(Array.prototype.slice.call(array2, index)); + } + + this.debugEnabled = function(flag) { + if (angular.isDefined(flag)) { + debug = flag; + return this; + } else { + return debug; + } + }; + + this.$get = function () { + var $log = { + log: function() { $log.log.logs.push(concat([], arguments, 0)); }, + warn: function() { $log.warn.logs.push(concat([], arguments, 0)); }, + info: function() { $log.info.logs.push(concat([], arguments, 0)); }, + error: function() { $log.error.logs.push(concat([], arguments, 0)); }, + debug: function() { + if (debug) { + $log.debug.logs.push(concat([], arguments, 0)); + } + } + }; + + /** + * @ngdoc method + * @name ngMock.$log#reset + * @methodOf ngMock.$log + * + * @description + * Reset all of the logging arrays to empty. + */ + $log.reset = function () { + /** + * @ngdoc property + * @name ngMock.$log#log.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#log}. + * + * @example + *
+       * $log.log('Some Log');
+       * var first = $log.log.logs.unshift();
+       * 
+ */ + $log.log.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#info.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#info}. + * + * @example + *
+       * $log.info('Some Info');
+       * var first = $log.info.logs.unshift();
+       * 
+ */ + $log.info.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#warn.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#warn}. + * + * @example + *
+       * $log.warn('Some Warning');
+       * var first = $log.warn.logs.unshift();
+       * 
+ */ + $log.warn.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#error.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#error}. + * + * @example + *
+       * $log.log('Some Error');
+       * var first = $log.error.logs.unshift();
+       * 
+ */ + $log.error.logs = []; + /** + * @ngdoc property + * @name ngMock.$log#debug.logs + * @propertyOf ngMock.$log + * + * @description + * Array of messages logged using {@link ngMock.$log#debug}. + * + * @example + *
+       * $log.debug('Some Error');
+       * var first = $log.debug.logs.unshift();
+       * 
+ */ + $log.debug.logs = []; + }; + + /** + * @ngdoc method + * @name ngMock.$log#assertEmpty + * @methodOf ngMock.$log + * + * @description + * Assert that the all of the logging methods have no logged messages. If messages present, an + * exception is thrown. + */ + $log.assertEmpty = function() { + var errors = []; + angular.forEach(['error', 'warn', 'info', 'log', 'debug'], function(logLevel) { + angular.forEach($log[logLevel].logs, function(log) { + angular.forEach(log, function (logItem) { + errors.push('MOCK $log (' + logLevel + '): ' + String(logItem) + '\n' + + (logItem.stack || '')); + }); + }); + }); + if (errors.length) { + errors.unshift("Expected $log to be empty! Either a message was logged unexpectedly, or "+ + "an expected log message was not checked and removed:"); + errors.push(''); + throw new Error(errors.join('\n---------\n')); + } + }; + + $log.reset(); + return $log; + }; +}; + + +/** + * @ngdoc service + * @name ngMock.$interval + * + * @description + * Mock implementation of the $interval service. + * + * Use {@link ngMock.$interval#methods_flush `$interval.flush(millis)`} to + * move forward by `millis` milliseconds and trigger any functions scheduled to run in that + * time. + * + * @param {function()} fn A function that should be called repeatedly. + * @param {number} delay Number of milliseconds between each function call. + * @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat + * indefinitely. + * @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise + * will invoke `fn` within the {@link ng.$rootScope.Scope#methods_$apply $apply} block. + * @returns {promise} A promise which will be notified on each iteration. + */ +angular.mock.$IntervalProvider = function() { + this.$get = ['$rootScope', '$q', + function($rootScope, $q) { + var repeatFns = [], + nextRepeatId = 0, + now = 0; + + var $interval = function(fn, delay, count, invokeApply) { + var deferred = $q.defer(), + promise = deferred.promise, + iteration = 0, + skipApply = (angular.isDefined(invokeApply) && !invokeApply); + + count = (angular.isDefined(count)) ? count : 0, + promise.then(null, null, fn); + + promise.$$intervalId = nextRepeatId; + + function tick() { + deferred.notify(iteration++); + + if (count > 0 && iteration >= count) { + var fnIndex; + deferred.resolve(iteration); + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns.splice(fnIndex, 1); + } + } + + if (!skipApply) $rootScope.$apply(); + } + + repeatFns.push({ + nextTime:(now + delay), + delay: delay, + fn: tick, + id: nextRepeatId, + deferred: deferred + }); + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + + nextRepeatId++; + return promise; + }; + + $interval.cancel = function(promise) { + var fnIndex; + + angular.forEach(repeatFns, function(fn, index) { + if (fn.id === promise.$$intervalId) fnIndex = index; + }); + + if (fnIndex !== undefined) { + repeatFns[fnIndex].deferred.reject('canceled'); + repeatFns.splice(fnIndex, 1); + return true; + } + + return false; + }; + + /** + * @ngdoc method + * @name ngMock.$interval#flush + * @methodOf ngMock.$interval + * @description + * + * Runs interval tasks scheduled to be run in the next `millis` milliseconds. + * + * @param {number=} millis maximum timeout amount to flush up until. + * + * @return {number} The amount of time moved forward. + */ + $interval.flush = function(millis) { + now += millis; + while (repeatFns.length && repeatFns[0].nextTime <= now) { + var task = repeatFns[0]; + task.fn(); + task.nextTime += task.delay; + repeatFns.sort(function(a,b){ return a.nextTime - b.nextTime;}); + } + return millis; + }; + + return $interval; + }]; +}; + + +/* jshint -W101 */ +/* The R_ISO8061_STR regex is never going to fit into the 100 char limit! + * This directive should go inside the anonymous function but a bug in JSHint means that it would + * not be enacted early enough to prevent the warning. + */ +var R_ISO8061_STR = /^(\d{4})-?(\d\d)-?(\d\d)(?:T(\d\d)(?:\:?(\d\d)(?:\:?(\d\d)(?:\.(\d{3}))?)?)?(Z|([+-])(\d\d):?(\d\d)))?$/; + +function jsonStringToDate(string) { + var match; + if (match = string.match(R_ISO8061_STR)) { + var date = new Date(0), + tzHour = 0, + tzMin = 0; + if (match[9]) { + tzHour = int(match[9] + match[10]); + tzMin = int(match[9] + match[11]); + } + date.setUTCFullYear(int(match[1]), int(match[2]) - 1, int(match[3])); + date.setUTCHours(int(match[4]||0) - tzHour, + int(match[5]||0) - tzMin, + int(match[6]||0), + int(match[7]||0)); + return date; + } + return string; +} + +function int(str) { + return parseInt(str, 10); +} + +function padNumber(num, digits, trim) { + var neg = ''; + if (num < 0) { + neg = '-'; + num = -num; + } + num = '' + num; + while(num.length < digits) num = '0' + num; + if (trim) + num = num.substr(num.length - digits); + return neg + num; +} + + +/** + * @ngdoc object + * @name angular.mock.TzDate + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available mock class of `Date`. + * + * Mock of the Date type which has its timezone specified via constructor arg. + * + * The main purpose is to create Date-like instances with timezone fixed to the specified timezone + * offset, so that we can test code that depends on local timezone settings without dependency on + * the time zone settings of the machine where the code is running. + * + * @param {number} offset Offset of the *desired* timezone in hours (fractions will be honored) + * @param {(number|string)} timestamp Timestamp representing the desired time in *UTC* + * + * @example + * !!!! WARNING !!!!! + * This is not a complete Date object so only methods that were implemented can be called safely. + * To make matters worse, TzDate instances inherit stuff from Date via a prototype. + * + * We do our best to intercept calls to "unimplemented" methods, but since the list of methods is + * incomplete we might be missing some non-standard methods. This can result in errors like: + * "Date.prototype.foo called on incompatible Object". + * + *
+ * var newYearInBratislava = new TzDate(-1, '2009-12-31T23:00:00Z');
+ * newYearInBratislava.getTimezoneOffset() => -60;
+ * newYearInBratislava.getFullYear() => 2010;
+ * newYearInBratislava.getMonth() => 0;
+ * newYearInBratislava.getDate() => 1;
+ * newYearInBratislava.getHours() => 0;
+ * newYearInBratislava.getMinutes() => 0;
+ * newYearInBratislava.getSeconds() => 0;
+ * 
+ * + */ +angular.mock.TzDate = function (offset, timestamp) { + var self = new Date(0); + if (angular.isString(timestamp)) { + var tsStr = timestamp; + + self.origDate = jsonStringToDate(timestamp); + + timestamp = self.origDate.getTime(); + if (isNaN(timestamp)) + throw { + name: "Illegal Argument", + message: "Arg '" + tsStr + "' passed into TzDate constructor is not a valid date string" + }; + } else { + self.origDate = new Date(timestamp); + } + + var localOffset = new Date(timestamp).getTimezoneOffset(); + self.offsetDiff = localOffset*60*1000 - offset*1000*60*60; + self.date = new Date(timestamp + self.offsetDiff); + + self.getTime = function() { + return self.date.getTime() - self.offsetDiff; + }; + + self.toLocaleDateString = function() { + return self.date.toLocaleDateString(); + }; + + self.getFullYear = function() { + return self.date.getFullYear(); + }; + + self.getMonth = function() { + return self.date.getMonth(); + }; + + self.getDate = function() { + return self.date.getDate(); + }; + + self.getHours = function() { + return self.date.getHours(); + }; + + self.getMinutes = function() { + return self.date.getMinutes(); + }; + + self.getSeconds = function() { + return self.date.getSeconds(); + }; + + self.getMilliseconds = function() { + return self.date.getMilliseconds(); + }; + + self.getTimezoneOffset = function() { + return offset * 60; + }; + + self.getUTCFullYear = function() { + return self.origDate.getUTCFullYear(); + }; + + self.getUTCMonth = function() { + return self.origDate.getUTCMonth(); + }; + + self.getUTCDate = function() { + return self.origDate.getUTCDate(); + }; + + self.getUTCHours = function() { + return self.origDate.getUTCHours(); + }; + + self.getUTCMinutes = function() { + return self.origDate.getUTCMinutes(); + }; + + self.getUTCSeconds = function() { + return self.origDate.getUTCSeconds(); + }; + + self.getUTCMilliseconds = function() { + return self.origDate.getUTCMilliseconds(); + }; + + self.getDay = function() { + return self.date.getDay(); + }; + + // provide this method only on browsers that already have it + if (self.toISOString) { + self.toISOString = function() { + return padNumber(self.origDate.getUTCFullYear(), 4) + '-' + + padNumber(self.origDate.getUTCMonth() + 1, 2) + '-' + + padNumber(self.origDate.getUTCDate(), 2) + 'T' + + padNumber(self.origDate.getUTCHours(), 2) + ':' + + padNumber(self.origDate.getUTCMinutes(), 2) + ':' + + padNumber(self.origDate.getUTCSeconds(), 2) + '.' + + padNumber(self.origDate.getUTCMilliseconds(), 3) + 'Z'; + }; + } + + //hide all methods not implemented in this mock that the Date prototype exposes + var unimplementedMethods = ['getUTCDay', + 'getYear', 'setDate', 'setFullYear', 'setHours', 'setMilliseconds', + 'setMinutes', 'setMonth', 'setSeconds', 'setTime', 'setUTCDate', 'setUTCFullYear', + 'setUTCHours', 'setUTCMilliseconds', 'setUTCMinutes', 'setUTCMonth', 'setUTCSeconds', + 'setYear', 'toDateString', 'toGMTString', 'toJSON', 'toLocaleFormat', 'toLocaleString', + 'toLocaleTimeString', 'toSource', 'toString', 'toTimeString', 'toUTCString', 'valueOf']; + + angular.forEach(unimplementedMethods, function(methodName) { + self[methodName] = function() { + throw new Error("Method '" + methodName + "' is not implemented in the TzDate mock"); + }; + }); + + return self; +}; + +//make "tzDateInstance instanceof Date" return true +angular.mock.TzDate.prototype = Date.prototype; +/* jshint +W101 */ + +angular.mock.animate = angular.module('mock.animate', ['ng']) + + .config(['$provide', function($provide) { + + $provide.decorator('$animate', function($delegate) { + var animate = { + queue : [], + enabled : $delegate.enabled, + flushNext : function(name) { + var tick = animate.queue.shift(); + + if (!tick) throw new Error('No animation to be flushed'); + if(tick.method !== name) { + throw new Error('The next animation is not "' + name + + '", but is "' + tick.method + '"'); + } + tick.fn(); + return tick; + } + }; + + angular.forEach(['enter','leave','move','addClass','removeClass'], function(method) { + animate[method] = function() { + var params = arguments; + animate.queue.push({ + method : method, + params : params, + element : angular.isElement(params[0]) && params[0], + parent : angular.isElement(params[1]) && params[1], + after : angular.isElement(params[2]) && params[2], + fn : function() { + $delegate[method].apply($delegate, params); + } + }); + }; + }); + + return animate; + }); + + }]); + + +/** + * @ngdoc function + * @name angular.mock.dump + * @description + * + * *NOTE*: this is not an injectable instance, just a globally available function. + * + * Method for serializing common angular objects (scope, elements, etc..) into strings, useful for + * debugging. + * + * This method is also available on window, where it can be used to display objects on debug + * console. + * + * @param {*} object - any object to turn into string. + * @return {string} a serialized string of the argument + */ +angular.mock.dump = function(object) { + return serialize(object); + + function serialize(object) { + var out; + + if (angular.isElement(object)) { + object = angular.element(object); + out = angular.element('
'); + angular.forEach(object, function(element) { + out.append(angular.element(element).clone()); + }); + out = out.html(); + } else if (angular.isArray(object)) { + out = []; + angular.forEach(object, function(o) { + out.push(serialize(o)); + }); + out = '[ ' + out.join(', ') + ' ]'; + } else if (angular.isObject(object)) { + if (angular.isFunction(object.$eval) && angular.isFunction(object.$apply)) { + out = serializeScope(object); + } else if (object instanceof Error) { + out = object.stack || ('' + object.name + ': ' + object.message); + } else { + // TODO(i): this prevents methods being logged, + // we should have a better way to serialize objects + out = angular.toJson(object, true); + } + } else { + out = String(object); + } + + return out; + } + + function serializeScope(scope, offset) { + offset = offset || ' '; + var log = [offset + 'Scope(' + scope.$id + '): {']; + for ( var key in scope ) { + if (Object.prototype.hasOwnProperty.call(scope, key) && !key.match(/^(\$|this)/)) { + log.push(' ' + key + ': ' + angular.toJson(scope[key])); + } + } + var child = scope.$$childHead; + while(child) { + log.push(serializeScope(child, offset + ' ')); + child = child.$$nextSibling; + } + log.push('}'); + return log.join('\n' + offset); + } +}; + +/** + * @ngdoc object + * @name ngMock.$httpBackend + * @description + * Fake HTTP backend implementation suitable for unit testing applications that use the + * {@link ng.$http $http service}. + * + * *Note*: For fake HTTP backend implementation suitable for end-to-end testing or backend-less + * development please see {@link ngMockE2E.$httpBackend e2e $httpBackend mock}. + * + * During unit testing, we want our unit tests to run quickly and have no external dependencies so + * we don’t want to send {@link https://developer.mozilla.org/en/xmlhttprequest XHR} or + * {@link http://en.wikipedia.org/wiki/JSONP JSONP} requests to a real server. All we really need is + * to verify whether a certain request has been sent or not, or alternatively just let the + * application make requests, respond with pre-trained responses and assert that the end result is + * what we expect it to be. + * + * This mock implementation can be used to respond with static or dynamic responses via the + * `expect` and `when` apis and their shortcuts (`expectGET`, `whenPOST`, etc). + * + * When an Angular application needs some data from a server, it calls the $http service, which + * sends the request to a real server using $httpBackend service. With dependency injection, it is + * easy to inject $httpBackend mock (which has the same API as $httpBackend) and use it to verify + * the requests and respond with some testing data without sending a request to real server. + * + * There are two ways to specify what test data should be returned as http responses by the mock + * backend when the code under test makes http requests: + * + * - `$httpBackend.expect` - specifies a request expectation + * - `$httpBackend.when` - specifies a backend definition + * + * + * # Request Expectations vs Backend Definitions + * + * Request expectations provide a way to make assertions about requests made by the application and + * to define responses for those requests. The test will fail if the expected requests are not made + * or they are made in the wrong order. + * + * Backend definitions allow you to define a fake backend for your application which doesn't assert + * if a particular request was made or not, it just returns a trained response if a request is made. + * The test will pass whether or not the request gets made during testing. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Request expectationsBackend definitions
Syntax.expect(...).respond(...).when(...).respond(...)
Typical usagestrict unit testsloose (black-box) unit testing
Fulfills multiple requestsNOYES
Order of requests mattersYESNO
Request requiredYESNO
Response requiredoptional (see below)YES
+ * + * In cases where both backend definitions and request expectations are specified during unit + * testing, the request expectations are evaluated first. + * + * If a request expectation has no response specified, the algorithm will search your backend + * definitions for an appropriate response. + * + * If a request didn't match any expectation or if the expectation doesn't have the response + * defined, the backend definitions are evaluated in sequential order to see if any of them match + * the request. The response from the first matched definition is returned. + * + * + * # Flushing HTTP requests + * + * The $httpBackend used in production, always responds to requests with responses asynchronously. + * If we preserved this behavior in unit testing, we'd have to create async unit tests, which are + * hard to write, follow and maintain. At the same time the testing mock, can't respond + * synchronously because that would change the execution of the code under test. For this reason the + * mock $httpBackend has a `flush()` method, which allows the test to explicitly flush pending + * requests and thus preserving the async api of the backend, while allowing the test to execute + * synchronously. + * + * + * # Unit testing with mock $httpBackend + * The following code shows how to setup and use the mock backend in unit testing a controller. + * First we create the controller under test + * +
+  // The controller code
+  function MyController($scope, $http) {
+    var authToken;
+
+    $http.get('/auth.py').success(function(data, status, headers) {
+      authToken = headers('A-Token');
+      $scope.user = data;
+    });
+
+    $scope.saveMessage = function(message) {
+      var headers = { 'Authorization': authToken };
+      $scope.status = 'Saving...';
+
+      $http.post('/add-msg.py', message, { headers: headers } ).success(function(response) {
+        $scope.status = '';
+      }).error(function() {
+        $scope.status = 'ERROR!';
+      });
+    };
+  }
+  
+ * + * Now we setup the mock backend and create the test specs. + * +
+    // testing controller
+    describe('MyController', function() {
+       var $httpBackend, $rootScope, createController;
+
+       beforeEach(inject(function($injector) {
+         // Set up the mock http service responses
+         $httpBackend = $injector.get('$httpBackend');
+         // backend definition common for all tests
+         $httpBackend.when('GET', '/auth.py').respond({userId: 'userX'}, {'A-Token': 'xxx'});
+
+         // Get hold of a scope (i.e. the root scope)
+         $rootScope = $injector.get('$rootScope');
+         // The $controller service is used to create instances of controllers
+         var $controller = $injector.get('$controller');
+
+         createController = function() {
+           return $controller('MyController', {'$scope' : $rootScope });
+         };
+       }));
+
+
+       afterEach(function() {
+         $httpBackend.verifyNoOutstandingExpectation();
+         $httpBackend.verifyNoOutstandingRequest();
+       });
+
+
+       it('should fetch authentication token', function() {
+         $httpBackend.expectGET('/auth.py');
+         var controller = createController();
+         $httpBackend.flush();
+       });
+
+
+       it('should send msg to server', function() {
+         var controller = createController();
+         $httpBackend.flush();
+
+         // now you don’t care about the authentication, but
+         // the controller will still send the request and
+         // $httpBackend will respond without you having to
+         // specify the expectation and response for this request
+
+         $httpBackend.expectPOST('/add-msg.py', 'message content').respond(201, '');
+         $rootScope.saveMessage('message content');
+         expect($rootScope.status).toBe('Saving...');
+         $httpBackend.flush();
+         expect($rootScope.status).toBe('');
+       });
+
+
+       it('should send auth header', function() {
+         var controller = createController();
+         $httpBackend.flush();
+
+         $httpBackend.expectPOST('/add-msg.py', undefined, function(headers) {
+           // check if the header was send, if it wasn't the expectation won't
+           // match the request and the test will fail
+           return headers['Authorization'] == 'xxx';
+         }).respond(201, '');
+
+         $rootScope.saveMessage('whatever');
+         $httpBackend.flush();
+       });
+    });
+   
+ */ +angular.mock.$HttpBackendProvider = function() { + this.$get = ['$rootScope', createHttpBackendMock]; +}; + +/** + * General factory function for $httpBackend mock. + * Returns instance for unit testing (when no arguments specified): + * - passing through is disabled + * - auto flushing is disabled + * + * Returns instance for e2e testing (when `$delegate` and `$browser` specified): + * - passing through (delegating request to real backend) is enabled + * - auto flushing is enabled + * + * @param {Object=} $delegate Real $httpBackend instance (allow passing through if specified) + * @param {Object=} $browser Auto-flushing enabled if specified + * @return {Object} Instance of $httpBackend mock + */ +function createHttpBackendMock($rootScope, $delegate, $browser) { + var definitions = [], + expectations = [], + responses = [], + responsesPush = angular.bind(responses, responses.push); + + function createResponse(status, data, headers) { + if (angular.isFunction(status)) return status; + + return function() { + return angular.isNumber(status) + ? [status, data, headers] + : [200, status, data]; + }; + } + + // TODO(vojta): change params to: method, url, data, headers, callback + function $httpBackend(method, url, data, callback, headers, timeout, withCredentials) { + var xhr = new MockXhr(), + expectation = expectations[0], + wasExpected = false; + + function prettyPrint(data) { + return (angular.isString(data) || angular.isFunction(data) || data instanceof RegExp) + ? data + : angular.toJson(data); + } + + function wrapResponse(wrapped) { + if (!$browser && timeout && timeout.then) timeout.then(handleTimeout); + + return handleResponse; + + function handleResponse() { + var response = wrapped.response(method, url, data, headers); + xhr.$$respHeaders = response[2]; + callback(response[0], response[1], xhr.getAllResponseHeaders()); + } + + function handleTimeout() { + for (var i = 0, ii = responses.length; i < ii; i++) { + if (responses[i] === handleResponse) { + responses.splice(i, 1); + callback(-1, undefined, ''); + break; + } + } + } + } + + if (expectation && expectation.match(method, url)) { + if (!expectation.matchData(data)) + throw new Error('Expected ' + expectation + ' with different data\n' + + 'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data); + + if (!expectation.matchHeaders(headers)) + throw new Error('Expected ' + expectation + ' with different headers\n' + + 'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' + + prettyPrint(headers)); + + expectations.shift(); + + if (expectation.response) { + responses.push(wrapResponse(expectation)); + return; + } + wasExpected = true; + } + + var i = -1, definition; + while ((definition = definitions[++i])) { + if (definition.match(method, url, data, headers || {})) { + if (definition.response) { + // if $browser specified, we do auto flush all requests + ($browser ? $browser.defer : responsesPush)(wrapResponse(definition)); + } else if (definition.passThrough) { + $delegate(method, url, data, callback, headers, timeout, withCredentials); + } else throw new Error('No response defined !'); + return; + } + } + throw wasExpected ? + new Error('No response defined !') : + new Error('Unexpected request: ' + method + ' ' + url + '\n' + + (expectation ? 'Expected ' + expectation : 'No more request expected')); + } + + /** + * @ngdoc method + * @name ngMock.$httpBackend#when + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @returns {requestHandler} Returns an object with `respond` method that controls how a matched + * request is handled. + * + * - respond – + * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + */ + $httpBackend.when = function(method, url, data, headers) { + var definition = new MockHttpExpectation(method, url, data, headers), + chain = { + respond: function(status, data, headers) { + definition.response = createResponse(status, data, headers); + } + }; + + if ($browser) { + chain.passThrough = function() { + definition.passThrough = true; + }; + } + + definitions.push(definition); + return chain; + }; + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenGET + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenHEAD + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenDELETE + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenPOST + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenPUT + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string))=} data HTTP request body or function that receives + * data string and returns true if the data is as expected. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#whenJSONP + * @methodOf ngMock.$httpBackend + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + createShortMethods('when'); + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expect + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current expectation. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + * + * - respond – + * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + */ + $httpBackend.expect = function(method, url, data, headers) { + var expectation = new MockHttpExpectation(method, url, data, headers); + expectations.push(expectation); + return { + respond: function(status, data, headers) { + expectation.response = createResponse(status, data, headers); + } + }; + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectGET + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for GET requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. See #expect for more info. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectHEAD + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for HEAD requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectDELETE + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for DELETE requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPOST + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for POST requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPUT + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for PUT requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectPATCH + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for PATCH requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp|function(string)|Object)=} data HTTP request body or function that + * receives data string and returns true if the data is as expected, or Object if request body + * is in JSON format. + * @param {Object=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + + /** + * @ngdoc method + * @name ngMock.$httpBackend#expectJSONP + * @methodOf ngMock.$httpBackend + * @description + * Creates a new request expectation for JSONP requests. For more info see `expect()`. + * + * @param {string|RegExp} url HTTP url. + * @returns {requestHandler} Returns an object with `respond` method that control how a matched + * request is handled. + */ + createShortMethods('expect'); + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#flush + * @methodOf ngMock.$httpBackend + * @description + * Flushes all pending requests using the trained responses. + * + * @param {number=} count Number of responses to flush (in the order they arrived). If undefined, + * all pending requests will be flushed. If there are no pending requests when the flush method + * is called an exception is thrown (as this typically a sign of programming error). + */ + $httpBackend.flush = function(count) { + $rootScope.$digest(); + if (!responses.length) throw new Error('No pending request to flush !'); + + if (angular.isDefined(count)) { + while (count--) { + if (!responses.length) throw new Error('No more pending request to flush !'); + responses.shift()(); + } + } else { + while (responses.length) { + responses.shift()(); + } + } + $httpBackend.verifyNoOutstandingExpectation(); + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#verifyNoOutstandingExpectation + * @methodOf ngMock.$httpBackend + * @description + * Verifies that all of the requests defined via the `expect` api were made. If any of the + * requests were not made, verifyNoOutstandingExpectation throws an exception. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + *
+   *   afterEach($httpBackend.verifyNoOutstandingExpectation);
+   * 
+ */ + $httpBackend.verifyNoOutstandingExpectation = function() { + $rootScope.$digest(); + if (expectations.length) { + throw new Error('Unsatisfied requests: ' + expectations.join(', ')); + } + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#verifyNoOutstandingRequest + * @methodOf ngMock.$httpBackend + * @description + * Verifies that there are no outstanding requests that need to be flushed. + * + * Typically, you would call this method following each test case that asserts requests using an + * "afterEach" clause. + * + *
+   *   afterEach($httpBackend.verifyNoOutstandingRequest);
+   * 
+ */ + $httpBackend.verifyNoOutstandingRequest = function() { + if (responses.length) { + throw new Error('Unflushed requests: ' + responses.length); + } + }; + + + /** + * @ngdoc method + * @name ngMock.$httpBackend#resetExpectations + * @methodOf ngMock.$httpBackend + * @description + * Resets all request expectations, but preserves all backend definitions. Typically, you would + * call resetExpectations during a multiple-phase test when you want to reuse the same instance of + * $httpBackend mock. + */ + $httpBackend.resetExpectations = function() { + expectations.length = 0; + responses.length = 0; + }; + + return $httpBackend; + + + function createShortMethods(prefix) { + angular.forEach(['GET', 'DELETE', 'JSONP'], function(method) { + $httpBackend[prefix + method] = function(url, headers) { + return $httpBackend[prefix](method, url, undefined, headers); + }; + }); + + angular.forEach(['PUT', 'POST', 'PATCH'], function(method) { + $httpBackend[prefix + method] = function(url, data, headers) { + return $httpBackend[prefix](method, url, data, headers); + }; + }); + } +} + +function MockHttpExpectation(method, url, data, headers) { + + this.data = data; + this.headers = headers; + + this.match = function(m, u, d, h) { + if (method != m) return false; + if (!this.matchUrl(u)) return false; + if (angular.isDefined(d) && !this.matchData(d)) return false; + if (angular.isDefined(h) && !this.matchHeaders(h)) return false; + return true; + }; + + this.matchUrl = function(u) { + if (!url) return true; + if (angular.isFunction(url.test)) return url.test(u); + return url == u; + }; + + this.matchHeaders = function(h) { + if (angular.isUndefined(headers)) return true; + if (angular.isFunction(headers)) return headers(h); + return angular.equals(headers, h); + }; + + this.matchData = function(d) { + if (angular.isUndefined(data)) return true; + if (data && angular.isFunction(data.test)) return data.test(d); + if (data && angular.isFunction(data)) return data(d); + if (data && !angular.isString(data)) return angular.equals(data, angular.fromJson(d)); + return data == d; + }; + + this.toString = function() { + return method + ' ' + url; + }; +} + +function MockXhr() { + + // hack for testing $http, $httpBackend + MockXhr.$$lastInstance = this; + + this.open = function(method, url, async) { + this.$$method = method; + this.$$url = url; + this.$$async = async; + this.$$reqHeaders = {}; + this.$$respHeaders = {}; + }; + + this.send = function(data) { + this.$$data = data; + }; + + this.setRequestHeader = function(key, value) { + this.$$reqHeaders[key] = value; + }; + + this.getResponseHeader = function(name) { + // the lookup must be case insensitive, + // that's why we try two quick lookups first and full scan last + var header = this.$$respHeaders[name]; + if (header) return header; + + name = angular.lowercase(name); + header = this.$$respHeaders[name]; + if (header) return header; + + header = undefined; + angular.forEach(this.$$respHeaders, function(headerVal, headerName) { + if (!header && angular.lowercase(headerName) == name) header = headerVal; + }); + return header; + }; + + this.getAllResponseHeaders = function() { + var lines = []; + + angular.forEach(this.$$respHeaders, function(value, key) { + lines.push(key + ': ' + value); + }); + return lines.join('\n'); + }; + + this.abort = angular.noop; +} + + +/** + * @ngdoc function + * @name ngMock.$timeout + * @description + * + * This service is just a simple decorator for {@link ng.$timeout $timeout} service + * that adds a "flush" and "verifyNoPendingTasks" methods. + */ + +angular.mock.$TimeoutDecorator = function($delegate, $browser) { + + /** + * @ngdoc method + * @name ngMock.$timeout#flush + * @methodOf ngMock.$timeout + * @description + * + * Flushes the queue of pending tasks. + * + * @param {number=} delay maximum timeout amount to flush up until + */ + $delegate.flush = function(delay) { + $browser.defer.flush(delay); + }; + + /** + * @ngdoc method + * @name ngMock.$timeout#verifyNoPendingTasks + * @methodOf ngMock.$timeout + * @description + * + * Verifies that there are no pending tasks that need to be flushed. + */ + $delegate.verifyNoPendingTasks = function() { + if ($browser.deferredFns.length) { + throw new Error('Deferred tasks to flush (' + $browser.deferredFns.length + '): ' + + formatPendingTasksAsString($browser.deferredFns)); + } + }; + + function formatPendingTasksAsString(tasks) { + var result = []; + angular.forEach(tasks, function(task) { + result.push('{id: ' + task.id + ', ' + 'time: ' + task.time + '}'); + }); + + return result.join(', '); + } + + return $delegate; +}; + +/** + * + */ +angular.mock.$RootElementProvider = function() { + this.$get = function() { + return angular.element('
'); + }; +}; + +/** + * @ngdoc overview + * @name ngMock + * @description + * + * # ngMock + * + * The `ngMock` module providers support to inject and mock Angular services into unit tests. + * In addition, ngMock also extends various core ng services such that they can be + * inspected and controlled in a synchronous manner within test code. + * + * {@installModule mocks} + * + *
+ * + */ +angular.module('ngMock', ['ng']).provider({ + $browser: angular.mock.$BrowserProvider, + $exceptionHandler: angular.mock.$ExceptionHandlerProvider, + $log: angular.mock.$LogProvider, + $interval: angular.mock.$IntervalProvider, + $httpBackend: angular.mock.$HttpBackendProvider, + $rootElement: angular.mock.$RootElementProvider +}).config(['$provide', function($provide) { + $provide.decorator('$timeout', angular.mock.$TimeoutDecorator); +}]); + +/** + * @ngdoc overview + * @name ngMockE2E + * @description + * + * The `ngMockE2E` is an angular module which contains mocks suitable for end-to-end testing. + * Currently there is only one mock present in this module - + * the {@link ngMockE2E.$httpBackend e2e $httpBackend} mock. + */ +angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) { + $provide.decorator('$httpBackend', angular.mock.e2e.$httpBackendDecorator); +}]); + +/** + * @ngdoc object + * @name ngMockE2E.$httpBackend + * @description + * Fake HTTP backend implementation suitable for end-to-end testing or backend-less development of + * applications that use the {@link ng.$http $http service}. + * + * *Note*: For fake http backend implementation suitable for unit testing please see + * {@link ngMock.$httpBackend unit-testing $httpBackend mock}. + * + * This implementation can be used to respond with static or dynamic responses via the `when` api + * and its shortcuts (`whenGET`, `whenPOST`, etc) and optionally pass through requests to the + * real $httpBackend for specific requests (e.g. to interact with certain remote apis or to fetch + * templates from a webserver). + * + * As opposed to unit-testing, in an end-to-end testing scenario or in scenario when an application + * is being developed with the real backend api replaced with a mock, it is often desirable for + * certain category of requests to bypass the mock and issue a real http request (e.g. to fetch + * templates or static files from the webserver). To configure the backend with this behavior + * use the `passThrough` request handler of `when` instead of `respond`. + * + * Additionally, we don't want to manually have to flush mocked out requests like we do during unit + * testing. For this reason the e2e $httpBackend automatically flushes mocked out requests + * automatically, closely simulating the behavior of the XMLHttpRequest object. + * + * To setup the application to run with this http backend, you have to create a module that depends + * on the `ngMockE2E` and your application modules and defines the fake backend: + * + *
+ *   myAppDev = angular.module('myAppDev', ['myApp', 'ngMockE2E']);
+ *   myAppDev.run(function($httpBackend) {
+ *     phones = [{name: 'phone1'}, {name: 'phone2'}];
+ *
+ *     // returns the current list of phones
+ *     $httpBackend.whenGET('/phones').respond(phones);
+ *
+ *     // adds a new phone to the phones array
+ *     $httpBackend.whenPOST('/phones').respond(function(method, url, data) {
+ *       phones.push(angular.fromJson(data));
+ *     });
+ *     $httpBackend.whenGET(/^\/templates\//).passThrough();
+ *     //...
+ *   });
+ * 
+ * + * Afterwards, bootstrap your app with this new module. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#when + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition. + * + * @param {string} method HTTP method. + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers or function that receives http header + * object and returns true if the headers match the current definition. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + * + * - respond – + * `{function([status,] data[, headers])|function(function(method, url, data, headers)}` + * – The respond method takes a set of static data to be returned or a function that can return + * an array containing response status (number), response data (string) and response headers + * (Object). + * - passThrough – `{function()}` – Any request matching a backend definition with `passThrough` + * handler, will be pass through to the real backend (an XHR request will be made to the + * server. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenGET + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for GET requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenHEAD + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for HEAD requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenDELETE + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for DELETE requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPOST + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for POST requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPUT + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for PUT requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenPATCH + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for PATCH requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @param {(string|RegExp)=} data HTTP request body. + * @param {(Object|function(Object))=} headers HTTP headers. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ + +/** + * @ngdoc method + * @name ngMockE2E.$httpBackend#whenJSONP + * @methodOf ngMockE2E.$httpBackend + * @description + * Creates a new backend definition for JSONP requests. For more info see `when()`. + * + * @param {string|RegExp} url HTTP url. + * @returns {requestHandler} Returns an object with `respond` and `passThrough` methods that + * control how a matched request is handled. + */ +angular.mock.e2e = {}; +angular.mock.e2e.$httpBackendDecorator = + ['$rootScope', '$delegate', '$browser', createHttpBackendMock]; + + +angular.mock.clearDataCache = function() { + var key, + cache = angular.element.cache; + + for(key in cache) { + if (Object.prototype.hasOwnProperty.call(cache,key)) { + var handle = cache[key].handle; + + handle && angular.element(handle.elem).off(); + delete cache[key]; + } + } +}; + + + +if(window.jasmine || window.mocha) { + + var currentSpec = null, + isSpecRunning = function() { + return currentSpec && (window.mocha || currentSpec.queue.running); + }; + + + beforeEach(function() { + currentSpec = this; + }); + + afterEach(function() { + var injector = currentSpec.$injector; + + currentSpec.$injector = null; + currentSpec.$modules = null; + currentSpec = null; + + if (injector) { + injector.get('$rootElement').off(); + injector.get('$browser').pollFns.length = 0; + } + + angular.mock.clearDataCache(); + + // clean up jquery's fragment cache + angular.forEach(angular.element.fragments, function(val, key) { + delete angular.element.fragments[key]; + }); + + MockXhr.$$lastInstance = null; + + angular.forEach(angular.callbacks, function(val, key) { + delete angular.callbacks[key]; + }); + angular.callbacks.counter = 0; + }); + + /** + * @ngdoc function + * @name angular.mock.module + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * + * This function registers a module configuration code. It collects the configuration information + * which will be used when the injector is created by {@link angular.mock.inject inject}. + * + * See {@link angular.mock.inject inject} for usage example + * + * @param {...(string|Function|Object)} fns any number of modules which are represented as string + * aliases or as anonymous module initialization functions. The modules are used to + * configure the injector. The 'ng' and 'ngMock' modules are automatically loaded. If an + * object literal is passed they will be register as values in the module, the key being + * the module name and the value being what is returned. + */ + window.module = angular.mock.module = function() { + var moduleFns = Array.prototype.slice.call(arguments, 0); + return isSpecRunning() ? workFn() : workFn; + ///////////////////// + function workFn() { + if (currentSpec.$injector) { + throw new Error('Injector already created, can not register a module!'); + } else { + var modules = currentSpec.$modules || (currentSpec.$modules = []); + angular.forEach(moduleFns, function(module) { + if (angular.isObject(module) && !angular.isArray(module)) { + modules.push(function($provide) { + angular.forEach(module, function(value, key) { + $provide.value(key, value); + }); + }); + } else { + modules.push(module); + } + }); + } + } + }; + + /** + * @ngdoc function + * @name angular.mock.inject + * @description + * + * *NOTE*: This function is also published on window for easy access.
+ * + * The inject function wraps a function into an injectable function. The inject() creates new + * instance of {@link AUTO.$injector $injector} per test, which is then used for + * resolving references. + * + * + * ## Resolving References (Underscore Wrapping) + * Often, we would like to inject a reference once, in a `beforeEach()` block and reuse this + * in multiple `it()` clauses. To be able to do this we must assign the reference to a variable + * that is declared in the scope of the `describe()` block. Since we would, most likely, want + * the variable to have the same name of the reference we have a problem, since the parameter + * to the `inject()` function would hide the outer variable. + * + * To help with this, the injected parameters can, optionally, be enclosed with underscores. + * These are ignored by the injector when the reference name is resolved. + * + * For example, the parameter `_myService_` would be resolved as the reference `myService`. + * Since it is available in the function body as _myService_, we can then assign it to a variable + * defined in an outer scope. + * + * ``` + * // Defined out reference variable outside + * var myService; + * + * // Wrap the parameter in underscores + * beforeEach( inject( function(_myService_){ + * myService = _myService_; + * })); + * + * // Use myService in a series of tests. + * it('makes use of myService', function() { + * myService.doStuff(); + * }); + * + * ``` + * + * See also {@link angular.mock.module angular.mock.module} + * + * ## Example + * Example of what a typical jasmine tests looks like with the inject method. + *
+   *
+   *   angular.module('myApplicationModule', [])
+   *       .value('mode', 'app')
+   *       .value('version', 'v1.0.1');
+   *
+   *
+   *   describe('MyApp', function() {
+   *
+   *     // You need to load modules that you want to test,
+   *     // it loads only the "ng" module by default.
+   *     beforeEach(module('myApplicationModule'));
+   *
+   *
+   *     // inject() is used to inject arguments of all given functions
+   *     it('should provide a version', inject(function(mode, version) {
+   *       expect(version).toEqual('v1.0.1');
+   *       expect(mode).toEqual('app');
+   *     }));
+   *
+   *
+   *     // The inject and module method can also be used inside of the it or beforeEach
+   *     it('should override a version and test the new version is injected', function() {
+   *       // module() takes functions or strings (module aliases)
+   *       module(function($provide) {
+   *         $provide.value('version', 'overridden'); // override version here
+   *       });
+   *
+   *       inject(function(version) {
+   *         expect(version).toEqual('overridden');
+   *       });
+   *     });
+   *   });
+   *
+   * 
+ * + * @param {...Function} fns any number of functions which will be injected using the injector. + */ + window.inject = angular.mock.inject = function() { + var blockFns = Array.prototype.slice.call(arguments, 0); + var errorForStack = new Error('Declaration Location'); + return isSpecRunning() ? workFn() : workFn; + ///////////////////// + function workFn() { + var modules = currentSpec.$modules || []; + + modules.unshift('ngMock'); + modules.unshift('ng'); + var injector = currentSpec.$injector; + if (!injector) { + injector = currentSpec.$injector = angular.injector(modules); + } + for(var i = 0, ii = blockFns.length; i < ii; i++) { + try { + /* jshint -W040 *//* Jasmine explicitly provides a `this` object when calling functions */ + injector.invoke(blockFns[i] || angular.noop, this); + /* jshint +W040 */ + } catch (e) { + if(e.stack && errorForStack) e.stack += '\n' + errorForStack.stack; + throw e; + } finally { + errorForStack = null; + } + } + } + }; +} + + +})(window, window.angular); diff --git a/tests/elastic_models.js b/tests/elastic_models.js new file mode 100644 index 00000000..06a9767c --- /dev/null +++ b/tests/elastic_models.js @@ -0,0 +1,73 @@ +// isDefined tests +test("cluster creating", function() { + // 1.0.0 BASED + var state = JSON.parse('{"cluster_name":"kopf_cluster","master_node":"JvYCtd57Tne-fxlK1GVHRw","blocks":{},"nodes":{"JvYCtd57Tne-fxlK1GVHRw":{"name":"Black Dragon","transport_address":"inet[/192.168.2.104:9300]","attributes":{}}},"metadata":{"templates":{},"indices":{"kopf_test":{"state":"open","settings":{"index":{"uuid":"7yDVKjHeS7uKbQuQbs3VNw","analysis":{"analyzer":{"kopf_analyzer_1":{"type":"custom","tokenizer":"whitespace"},"kopf_analyzer_2":{"type":"custom","tokenizer":"whitespace"}}},"number_of_replicas":"1","number_of_shards":"5","version":{"created":"1000051"}}},"mappings":{"kopf":{"properties":{"id":{"type":"long"},"content_2":{"type":"string","fields":{"content_2_raw":{"type":"string"}}},"content_1":{"path":"just_name","type":"string","fields":{"content_1_raw":{"type":"string"}}}}}},"aliases":["kopf_alias_2","kopf_alias_1"]}}},"routing_table":{"indices":{"kopf_test":{"shards":{"2":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":2,"index":"kopf_test"}],"0":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":0,"index":"kopf_test"}],"3":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":3,"index":"kopf_test"}],"1":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":1,"index":"kopf_test"}],"4":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":4,"index":"kopf_test"}]}}}},"routing_nodes":{"unassigned":[{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":2,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":4,"index":"kopf_test"}],"nodes":{"JvYCtd57Tne-fxlK1GVHRw":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"}]}},"allocations":[]}'); + var status = JSON.parse('{"_shards":{"total":10,"successful":5,"failed":0},"indices":{"kopf_test":{"index":{"primary_size_in_bytes":16887,"size_in_bytes":16887},"translog":{"operations":8},"docs":{"num_docs":6,"max_doc":6,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":13,"total_time_in_millis":125},"flush":{"total":0,"total_time_in_millis":0},"shards":{"0":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},"state":"STARTED","index":{"size_in_bytes":2831},"translog":{"id":1390857853127,"operations":1},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":2,"total_time_in_millis":15},"flush":{"total":0,"total_time_in_millis":0}}],"1":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},"state":"STARTED","index":{"size_in_bytes":2831},"translog":{"id":1390857853155,"operations":1},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":2,"total_time_in_millis":16},"flush":{"total":0,"total_time_in_millis":0}}],"2":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"},"state":"STARTED","index":{"size_in_bytes":5563},"translog":{"id":1390857853170,"operations":2},"docs":{"num_docs":2,"max_doc":2,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":3,"total_time_in_millis":43},"flush":{"total":0,"total_time_in_millis":0}}],"3":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},"state":"STARTED","index":{"size_in_bytes":2831},"translog":{"id":1390857853142,"operations":3},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":4,"total_time_in_millis":35},"flush":{"total":0,"total_time_in_millis":0}}],"4":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"},"state":"STARTED","index":{"size_in_bytes":2831},"translog":{"id":1390857853204,"operations":1},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":2,"total_time_in_millis":16},"flush":{"total":0,"total_time_in_millis":0}}]}}}}'); + var stats = JSON.parse('{"cluster_name":"kopf_cluster","nodes":{"JvYCtd57Tne-fxlK1GVHRw":{"timestamp":1390858085598,"name":"Black Dragon","transport_address":"inet[/192.168.2.104:9300]","host":"leo.local","ip":["inet[/192.168.2.104:9300]","NONE"],"indices":{"docs":{"count":6,"deleted":0},"store":{"size_in_bytes":16887,"throttle_time_in_millis":0},"indexing":{"index_total":11,"index_time_in_millis":194,"index_current":0,"delete_total":2,"delete_time_in_millis":2,"delete_current":0},"get":{"total":0,"time_in_millis":0,"exists_total":0,"exists_time_in_millis":0,"missing_total":0,"missing_time_in_millis":0,"current":0},"search":{"open_contexts":0,"query_total":20,"query_time_in_millis":31,"query_current":0,"fetch_total":0,"fetch_time_in_millis":0,"fetch_current":0},"merges":{"current":0,"current_docs":0,"current_size_in_bytes":0,"total":0,"total_time_in_millis":0,"total_docs":0,"total_size_in_bytes":0},"refresh":{"total":65,"total_time_in_millis":295},"flush":{"total":2,"total_time_in_millis":16},"warmer":{"current":0,"total":12,"total_time_in_millis":4},"filter_cache":{"memory_size_in_bytes":0,"evictions":0},"id_cache":{"memory_size_in_bytes":0},"fielddata":{"memory_size_in_bytes":0,"evictions":0},"percolate":{"total":0,"time_in_millis":0,"current":0,"memory_size_in_bytes":0,"memory_size":"0b","queries":0},"completion":{"size_in_bytes":0},"segments":{"count":6,"memory_in_bytes":0},"translog":{"operations":8,"size_in_bytes":113}},"os":{"timestamp":1390858084866,"uptime_in_millis":693508,"load_average":[1.42822265625,1.63525390625,1.88037109375],"cpu":{"sys":11,"user":18,"idle":69,"usage":29,"stolen":0},"mem":{"free_in_bytes":495689728,"used_in_bytes":8094244864,"free_percent":41,"used_percent":58,"actual_free_in_bytes":3530981376,"actual_used_in_bytes":5058953216},"swap":{"used_in_bytes":0,"free_in_bytes":1073741824}},"process":{"timestamp":1390858084866,"open_file_descriptors":148,"cpu":{"percent":0,"sys_in_millis":63331,"user_in_millis":113294,"total_in_millis":176625},"mem":{"resident_in_bytes":324427776,"share_in_bytes":-1,"total_virtual_in_bytes":3883094016}},"jvm":{"timestamp":1390858084867,"uptime_in_millis":182268477,"mem":{"heap_used_in_bytes":102054072,"heap_used_percent":9,"heap_committed_in_bytes":251002880,"heap_max_in_bytes":1056309248,"non_heap_used_in_bytes":38337328,"non_heap_committed_in_bytes":38666240,"pools":{"young":{"used_in_bytes":83487440,"max_in_bytes":139591680,"peak_used_in_bytes":139591680,"peak_max_in_bytes":139591680},"survivor":{"used_in_bytes":2591784,"max_in_bytes":17432576,"peak_used_in_bytes":17432576,"peak_max_in_bytes":17432576},"old":{"used_in_bytes":15974848,"max_in_bytes":899284992,"peak_used_in_bytes":15974848,"peak_max_in_bytes":899284992}}},"threads":{"count":49,"peak_count":50},"gc":{"collectors":{"young":{"collection_count":9,"collection_time_in_millis":299},"old":{"collection_count":0,"collection_time_in_millis":0}}},"buffer_pools":{"direct":{"count":37,"used_in_bytes":9392421,"total_capacity_in_bytes":9392421},"mapped":{"count":0,"used_in_bytes":0,"total_capacity_in_bytes":0}}},"thread_pool":{"generic":{"threads":4,"queue":0,"active":0,"rejected":0,"largest":5,"completed":17733},"index":{"threads":2,"queue":0,"active":0,"rejected":0,"largest":2,"completed":13},"get":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"snapshot":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":13},"merge":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":13},"suggest":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"bulk":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"optimize":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"warmer":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":86},"flush":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":2},"search":{"threads":6,"queue":0,"active":0,"rejected":0,"largest":6,"completed":20},"percolate":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"management":{"threads":5,"queue":0,"active":2,"rejected":0,"largest":5,"completed":19932},"refresh":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":13}},"network":{"tcp":{"active_opens":26842,"passive_opens":1470,"curr_estab":53,"in_segs":1812321,"out_segs":1561574,"retrans_segs":2190,"estab_resets":1395,"attempt_fails":4109,"in_errs":16,"out_rsts":-1}},"fs":{"timestamp":1390858084868,"total":{"total_in_bytes":255200755712,"free_in_bytes":82445713408,"available_in_bytes":82183569408,"disk_reads":578277,"disk_writes":854240,"disk_io_op":1432517,"disk_read_size_in_bytes":9500736512,"disk_write_size_in_bytes":15133154304,"disk_io_size_in_bytes":24633890816},"data":[{"path":"/Users/leo/Downloads/elasticsearch-1.0.0.RC1/data/kopf_cluster/nodes/0","mount":"/","dev":"/dev/disk0s2","total_in_bytes":255200755712,"free_in_bytes":82445713408,"available_in_bytes":82183569408,"disk_reads":578277,"disk_writes":854240,"disk_io_op":1432517,"disk_read_size_in_bytes":9500736512,"disk_write_size_in_bytes":15133154304,"disk_io_size_in_bytes":24633890816}]},"transport":{"server_open":13,"rx_count":27,"rx_size_in_bytes":2753,"tx_count":27,"tx_size_in_bytes":3409},"http":{"current_open":6,"total_opened":139},"fielddata_breaker":{"maximum_size_in_bytes":845047398,"maximum_size":"805.8mb","estimated_size_in_bytes":0,"estimated_size":"0b","overhead":1.03}}}}'); + var settings = JSON.parse('{"persistent":{},"transient":{"cluster":{"routing":{"allocation":{"disable_allocation":"false"}}}}}'); + var cluster = new Cluster(state, status, stats, settings); + + // 0.90.0 BASED + var state_90 = JSON.parse('{"cluster_name":"kopf_cluster","master_node":"JvYCtd57Tne-fxlK1GVHRw","blocks":{},"nodes":{"JvYCtd57Tne-fxlK1GVHRw":{"name":"Black Dragon","transport_address":"inet[/192.168.2.104:9300]","attributes":{}}},"metadata":{"templates":{},"indices":{"kopf_test":{"state":"open","settings":{"index.analysis.analyzer.kopf_analyzer_1.tokenizer":"whitespace","index.analysis.analyzer.kopf_analyzer_1.type":"custom","index.analysis.analyzer.kopf_analyzer_2.type":"custom","index.version.created":"1000051","index.number_of_replicas":"1","index.uuid":"7yDVKjHeS7uKbQuQbs3VNw","index.number_of_shards":"5","index.analysis.analyzer.kopf_analyzer_2.tokenizer":"whitespace"},"mappings":{"kopf":{"properties":{"id":{"type":"long"},"content_2":{"type":"multi_field","fields":{"content_2":{"include_in_all":false,"type":"string"},"content_2_raw":{"include_in_all":false,"type":"string"}}},"content_1":{"path":"just_name","type":"multi_field","fields":{"content_1":{"type":"string"},"content_1_raw":{"include_in_all":false,"type":"string"}}}}}},"aliases":["kopf_alias_2","kopf_alias_1"]}}},"routing_table":{"indices":{"kopf_test":{"shards":{"4":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":4,"index":"kopf_test"}],"0":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":0,"index":"kopf_test"}],"3":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":3,"index":"kopf_test"}],"1":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":1,"index":"kopf_test"}],"2":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":2,"index":"kopf_test"}]}}}},"routing_nodes":{"unassigned":[{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":4,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"UNASSIGNED","primary":false,"node":null,"relocating_node":null,"shard":2,"index":"kopf_test"}],"nodes":{"JvYCtd57Tne-fxlK1GVHRw":[{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"}]}},"allocations":[]}'); + var status_90 = JSON.parse('{"ok":true,"_shards":{"total":10,"successful":5,"failed":0},"indices":{"kopf_test":{"index":{"primary_size":"16.9kb","primary_size_in_bytes":16887,"size":"16.9kb","size_in_bytes":16887},"translog":{"operations":8},"docs":{"num_docs":6,"max_doc":6,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":5,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0},"shards":{"0":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":0,"index":"kopf_test"},"state":"STARTED","index":{"size":"2.8kb","size_in_bytes":2831},"translog":{"id":1390857853128,"operations":0},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":1,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0}}],"1":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":1,"index":"kopf_test"},"state":"STARTED","index":{"size":"2.8kb","size_in_bytes":2831},"translog":{"id":1390857853156,"operations":0},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":1,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0}}],"2":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":2,"index":"kopf_test"},"state":"STARTED","index":{"size":"5.5kb","size_in_bytes":5721},"translog":{"id":1390857853171,"operations":0},"docs":{"num_docs":2,"max_doc":2,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":1,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0}}],"3":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":3,"index":"kopf_test"},"state":"STARTED","index":{"size":"2.8kb","size_in_bytes":2909},"translog":{"id":1390857853143,"operations":0},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":1,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0}}],"4":[{"routing":{"state":"STARTED","primary":true,"node":"JvYCtd57Tne-fxlK1GVHRw","relocating_node":null,"shard":4,"index":"kopf_test"},"state":"STARTED","index":{"size":"2.8kb","size_in_bytes":2909},"translog":{"id":1390857853205,"operations":0},"docs":{"num_docs":1,"max_doc":1,"deleted_docs":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":1,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0}}]}}}}'); + var stats_90 = JSON.parse('{"cluster_name":"kopf_cluster","nodes":{"JvYCtd57Tne-fxlK1GVHRw":{"timestamp":1390928019968,"name":"Black Dragon","transport_address":"inet[/192.168.2.104:9300]","hostname":"leo.local","indices":{"docs":{"count":6,"deleted":0},"store":{"size":"16.9kb","size_in_bytes":17358,"throttle_time":"0s","throttle_time_in_millis":0},"indexing":{"index_total":0,"index_time":"0s","index_time_in_millis":0,"index_current":0,"delete_total":0,"delete_time":"0s","delete_time_in_millis":0,"delete_current":0},"get":{"total":0,"get_time":"0s","time_in_millis":0,"exists_total":0,"exists_time":"0s","exists_time_in_millis":0,"missing_total":0,"missing_time":"0s","missing_time_in_millis":0,"current":0},"search":{"open_contexts":0,"query_total":0,"query_time":"0s","query_time_in_millis":0,"query_current":0,"fetch_total":0,"fetch_time":"0s","fetch_time_in_millis":0,"fetch_current":0},"merges":{"current":0,"current_docs":0,"current_size":"0b","current_size_in_bytes":0,"total":0,"total_time":"0s","total_time_in_millis":0,"total_docs":0,"total_size":"0b","total_size_in_bytes":0},"refresh":{"total":5,"total_time":"0s","total_time_in_millis":0},"flush":{"total":0,"total_time":"0s","total_time_in_millis":0},"warmer":{"current":0,"total":5,"total_time":"58ms","total_time_in_millis":58},"filter_cache":{"memory_size":"0b","memory_size_in_bytes":0,"evictions":0},"id_cache":{"memory_size":"0b","memory_size_in_bytes":0},"fielddata":{"memory_size":"0b","memory_size_in_bytes":0,"evictions":0},"completion":{"size":"0b","size_in_bytes":0},"segments":{"count":6,"memory":"0b","memory_in_bytes":0}},"os":{"timestamp":1390928019969,"uptime":"12.7m","uptime_in_millis":763467,"load_average":[5.7880859375,30.56884765625,23.6787109375],"cpu":{"sys":8,"user":12,"idle":78,"usage":20,"stolen":0},"mem":{"free":"579.8mb","free_in_bytes":608063488,"used":"7.4gb","used_in_bytes":7981871104,"free_percent":36,"used_percent":63,"actual_free":"2.9gb","actual_free_in_bytes":3153018880,"actual_used":"5gb","actual_used_in_bytes":5436915712},"swap":{"used":"0b","used_in_bytes":0,"free":"1gb","free_in_bytes":1073741824}},"process":{"timestamp":1390928019969,"open_file_descriptors":148,"cpu":{"percent":0,"sys":"645ms","sys_in_millis":645,"user":"5.8s","user_in_millis":5804,"total":"6.4s","total_in_millis":6449},"mem":{"resident":"215.7mb","resident_in_bytes":226185216,"share":"-1b","share_in_bytes":-1,"total_virtual":"3.5gb","total_virtual_in_bytes":3832135680}},"jvm":{"timestamp":1390928019969,"uptime":"36.6s","uptime_in_millis":36681,"mem":{"heap_used":"104.5mb","heap_used_in_bytes":102054072,"heap_used_percent":10,"heap_committed":"239.3mb","heap_committed_in_bytes":251002880,"heap_max":"1007.3mb","heap_max_in_bytes":1056309248,"non_heap_used":"30.1mb","non_heap_used_in_bytes":31588520,"non_heap_committed":"31.6mb","non_heap_committed_in_bytes":33161216,"pools":{"Code Cache":{"used":"1016.4kb","used_in_bytes":1040832,"max":"48mb","max_in_bytes":50331648,"peak_used":"1mb","peak_used_in_bytes":1061376,"peak_max":"48mb","peak_max_in_bytes":50331648},"Par Eden Space":{"used":"84mb","used_in_bytes":88143432,"max":"133.1mb","max_in_bytes":139591680,"peak_used":"133.1mb","peak_used_in_bytes":139591680,"peak_max":"133.1mb","peak_max_in_bytes":139591680},"Par Survivor Space":{"used":"16.6mb","used_in_bytes":17432568,"max":"16.6mb","max_in_bytes":17432576,"peak_used":"16.6mb","peak_used_in_bytes":17432568,"peak_max":"16.6mb","peak_max_in_bytes":17432576},"CMS Old Gen":{"used":"3.8mb","used_in_bytes":4040200,"max":"857.6mb","max_in_bytes":899284992,"peak_used":"3.8mb","peak_used_in_bytes":4040200,"peak_max":"857.6mb","peak_max_in_bytes":899284992},"CMS Perm Gen":{"used":"29.1mb","used_in_bytes":30547688,"max":"82mb","max_in_bytes":85983232,"peak_used":"29.1mb","peak_used_in_bytes":30547688,"peak_max":"82mb","peak_max_in_bytes":85983232}}},"threads":{"count":38,"peak_count":38},"gc":{"collection_count":1,"collection_time":"59ms","collection_time_in_millis":59,"collectors":{"ParNew":{"collection_count":1,"collection_time":"59ms","collection_time_in_millis":59},"ConcurrentMarkSweep":{"collection_count":0,"collection_time":"0s","collection_time_in_millis":0}}},"buffer_pools":{"direct":{"count":25,"used":"3.4mb","used_in_bytes":3666966,"total_capacity":"3.4mb","total_capacity_in_bytes":3666966},"mapped":{"count":0,"used":"0b","used_in_bytes":0,"total_capacity":"0b","total_capacity_in_bytes":0}}},"thread_pool":{"generic":{"threads":5,"queue":0,"active":0,"rejected":0,"largest":5,"completed":35},"index":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"get":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"snapshot":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"merge":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"suggest":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"bulk":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"optimize":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"warmer":{"threads":1,"queue":0,"active":0,"rejected":0,"largest":1,"completed":5},"flush":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"search":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"percolate":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0},"management":{"threads":5,"queue":0,"active":1,"rejected":0,"largest":5,"completed":19},"refresh":{"threads":0,"queue":0,"active":0,"rejected":0,"largest":0,"completed":0}},"network":{"tcp":{"active_opens":27355,"passive_opens":1514,"curr_estab":53,"in_segs":1841779,"out_segs":1588047,"retrans_segs":2344,"estab_resets":1470,"attempt_fails":4266,"in_errs":16,"out_rsts":-1}},"fs":{"timestamp":1390928019971,"total":{"total":"237.6gb","total_in_bytes":255200755712,"free":"76.7gb","free_in_bytes":82420838400,"available":"76.5gb","available_in_bytes":82158694400,"disk_reads":588547,"disk_writes":878132,"disk_io_op":1466679,"disk_read_size":"8.9gb","disk_read_size_in_bytes":9578772480,"disk_write_size":"14.4gb","disk_write_size_in_bytes":15483418624,"disk_io_size":"23.3gb","disk_io_size_in_bytes":25062191104},"data":[{"path":"/Users/leo/Downloads/elasticsearch-1.0.0.RC1/data/kopf_cluster/nodes/0","mount":"/","dev":"/dev/disk0s2","total":"237.6gb","total_in_bytes":255200755712,"free":"76.7gb","free_in_bytes":82420838400,"available":"76.5gb","available_in_bytes":82158694400,"disk_reads":588547,"disk_writes":878132,"disk_io_op":1466679,"disk_read_size":"8.9gb","disk_read_size_in_bytes":9578772480,"disk_write_size":"14.4gb","disk_write_size_in_bytes":15483418624,"disk_io_size":"23.3gb","disk_io_size_in_bytes":25062191104}]},"transport":{"server_open":13,"rx_count":0,"rx_size":"0b","rx_size_in_bytes":0,"tx_count":0,"tx_size":"0b","tx_size_in_bytes":0},"http":{"current_open":6,"total_opened":6}}}}'); + var settings_90 = JSON.parse('{"persistent":{},"transient":{}}'); + var cluster_90 = new Cluster(state_90, status_90, stats_90, settings_90); + + var clusters = []; + clusters.push(cluster); + clusters.push(cluster_90); + clusters.forEach(function(current_cluster) { + // check current_clustercluster properties + ok(current_cluster.num_docs == 6, "Checking num docs"); + ok(current_cluster.total_size == '16.49KB', "Checking total size"); + ok(current_cluster.unassigned_shards == 5, "Checking unassigned shards"); + ok(current_cluster.shards == 10, "Checking total shards"); + ok(current_cluster.failed_shards == 0, "Checking failed shards"); + ok(current_cluster.total_indices == 1, "Checking number of indices"); + ok(current_cluster.master_node == 'JvYCtd57Tne-fxlK1GVHRw', "Checking master node"); + + // index info + ok(current_cluster.indices.length == 1, "Checking indices list"); + var kopf_index = current_cluster.indices[0]; + ok(kopf_index.name == 'kopf_test', "Checking index name"); + ok(kopf_index.num_of_shards == 5, "Checking number of shards"); + ok(kopf_index.num_of_replicas == 1, "Checking number of replicas"); + ok(kopf_index.state == 'open', "Checking index state"); + ok(kopf_index.size == 16887, "Checking primary size"); + ok(kopf_index.total_size == 16887, "Checking total size"); + ok(kopf_index.size_in_bytes == '16.49KB', "Checking primary size"); + ok(kopf_index.total_size_in_bytes == '16.49KB', "Checking total size"); + ok(kopf_index.num_docs == 6, "Checking num docs"); + ok(kopf_index.max_doc == 6, "Checking max doc"); + ok(kopf_index.deleted_docs == 0, "Checking deleted docs"); + + // check aliases + ok(kopf_index.getTypes().length == 1, "Checking types length"); + ok(kopf_index.getTypes()[0] == 'kopf', "Checking type name"); + ok(kopf_index.getFields('kopf').length == 4, "Checking fields length"); + ok(kopf_index.getFields('kopf')[0] == 'content_1', "Checking 1st field"); + ok(kopf_index.getFields('kopf')[1] == 'content_1_raw', "Checking 2nd multi field just name"); + ok(kopf_index.getFields('kopf')[2] == 'content_2', "Checking 3nd field"); + ok(kopf_index.getFields('kopf')[3] == 'content_2.content_2_raw', "Checking 4th multi field full path"); + + // check analyzers + ok(kopf_index.getAnalyzers().length == 2, "Checking analyzers length"); + ok(kopf_index.getAnalyzers()[0] == 'kopf_analyzer_1', "Checking 1st analyzer"); + ok(kopf_index.getAnalyzers()[1] == 'kopf_analyzer_2', "Checking 2nd analyzer"); + + // check nodes + ok(current_cluster.getNodes('', true, true, true).length == 1, "Checking nodes length"); + ok(current_cluster.getNodes('', false, false, false).length == 0, "Checking nodes length"); + ok(current_cluster.getNodes('lack Drago', true, true, true).length == 1, "Checking nodes length"); + ok(current_cluster.getNodes('xxxxxxxxx', true, true, true).length == 0, "Checking nodes length"); + var node = current_cluster.getNodes('', true, true, true)[0]; + ok(node.name == 'Black Dragon', "Checking node name"); + ok(node.id == 'JvYCtd57Tne-fxlK1GVHRw', "Checking node name"); + ok(node.heap_used == '97.33MB', "Checking node heap used " + node.heap_used); + ok(node.heap_committed == '239.38MB', "Checking node heap commited " + node.heap_committed); + ok(node.transport_address == 'inet[/192.168.2.104:9300]', "Checking node transport address"); + ok(node.current_master, "Checking node is current master"); + }); + +}); diff --git a/tests/jasmine/aliases.tests.js b/tests/jasmine/aliases.tests.js new file mode 100644 index 00000000..9ae6b440 --- /dev/null +++ b/tests/jasmine/aliases.tests.js @@ -0,0 +1,212 @@ +'use strict'; + +describe('AliasesController', function(){ + var scope, createController; + + beforeEach(angular.mock.module('kopf')); + + beforeEach(angular.mock.inject(function($rootScope, $controller, $injector){ + //create an empty scope + this.scope = $rootScope.$new(); + this.scope.client = {} //set fake client + var $timeout = $injector.get('$timeout'); + var $location = $injector.get('$location'); + this.AlertService = $injector.get('AlertService'); + + this.createController = function() { + return $controller('AliasesController', {$scope: this.scope}, $location, $timeout, this.AlertService); + }; + + this._controller = this.createController(); + })); + + //TESTS + it('init : values are set', function(){ + expect(this.scope.aliases).toEqual(null); + expect(this.scope.new_index).toEqual({}); + expect(this.scope.pagination).not.toEqual(undefined); + expect(this.scope.editor).toEqual(undefined); + }); + + it('on : makes calls loadAliases and initEditor', function() { + spyOn(this.scope, 'loadAliases').andReturn(true); + spyOn(this.scope, 'initEditor').andReturn(true); + this.scope.$emit("loadAliasesEvent"); + expect(this.scope.loadAliases).toHaveBeenCalled(); + expect(this.scope.initEditor).toHaveBeenCalled(); + }); + + it('loadAliases : assigns a value to new_alias finds existing', function() { + this.scope.new_alias = {}; + this.scope.client.fetchAliases = function(){}; + this.scope.updateModel = function(){}; + spyOn(this.scope, "_parseAliases"); + spyOn(this.scope.client, "fetchAliases").andReturn(true); + this.scope.loadAliases(); + expect(this.scope.client.fetchAliases).toHaveBeenCalledWith(jasmine.any(Function),jasmine.any(Function)); + expect(this.scope.new_alias).not.toEqual({}); + expect(this.scope.new_alias instanceof Alias).toEqual(true); + }); + + it('_parseAliases : sets originalAliases, extends aliases, setResults on pagination', function() { + this.scope.originalAliases = "fake"; + this.scope.aliases = "blank"; + this.scope.pagination = {}; + this.scope.pagination.setResults = function(){}; + spyOn(this.scope.pagination, "setResults"); + var param_aliases = new Aliases({}); + + this.scope._parseAliases(param_aliases); + + expect(this.scope.originalAliases).toEqual(param_aliases); + expect(this.scope.aliases).toEqual(param_aliases) + expect(this.scope.pagination.setResults).toHaveBeenCalledWith(param_aliases.info) + }); + + it('viewDetails : set details on scope', function() { + this.scope.viewDetails("pizza"); + expect(this.scope.details).toEqual("pizza"); + }); + + it('addAlias : sets new_alias filter', function () { + this.scope.editor = {}; + this.scope.new_alias = {}; + this.scope.editor.format = function(){return 'tacos';}; + + this.scope.addAlias(); + expect(this.scope.new_alias.filter).toEqual('tacos'); + }); + + it('addAlias : new alias : gets added to aliases map, calls pagination', function () { + this.scope.aliases = {info:{}}; + this.scope.pagination = {setResults:function(){}}; + + var fake_editor = { + error : undefined, + format : function(){return {'filter':'fromeditor'}; } + }; + + var fake_alias = new Alias('myalias', 'myindex', { 'filter' : 'fromeditor' }); + + this.scope.editor = fake_editor; + this.scope.new_alias = fake_alias; + var expected = + { + "myalias": [fake_alias] + } + spyOn(this.scope.pagination, "setResults"); + + this.scope.addAlias(); + expect(this.scope.pagination.setResults).toHaveBeenCalled( ); + expect(this.scope.pagination.setResults).toHaveBeenCalledWith( expected ); + }); + + it('addAlias : existing alias throws alert, does not change state, does not call pagination', function () { + this.scope.aliases = {info:{ + 'myalias':[new Alias('myalias', 'myindex')] + }}; + this.scope.pagination = {setResults:function(){}}; + + var fake_editor = { + error : undefined, + format : function(){return {'filter':'fromeditor'}; } + }; + var fake_alias = new Alias('myalias', 'myindex', { 'filter' : 'fromeditor' }); + + this.scope.editor = fake_editor; + this.scope.new_alias = fake_alias; + + spyOn(this.scope.pagination, "setResults"); + spyOn(this.AlertService, "error"); + + this.scope.addAlias(); + + expect(this.AlertService.error).toHaveBeenCalled(); + expect(this.scope.pagination.setResults).not.toHaveBeenCalled( ); + }); + + it('removeAlias : changes alias list, calls pagination setresults with new info', function () { + this.scope.pagination = {setResults:function(){}}; + this.scope.aliases = {info:{ + 'remove_me':[new Alias('myalias', 'myindex')] + }}; + var blank_aliases = {info:{}}; + + spyOn(this.scope.pagination, "setResults"); + + this.scope.removeAlias("remove_me"); + expect(this.scope.aliases).toEqual(blank_aliases); + expect(this.scope.pagination.setResults).toHaveBeenCalledWith(blank_aliases.info); + }); + + it('removeAliasFromIndex : ', function () { + this.scope.pagination = {setResults:function(){}}; + this.scope.aliases = {info:{ + 'remove_me':[new Alias('remove_me', 'myindex'), new Alias('remove_me', 'myindex2')] + }}; + var expected = {info:{ + 'remove_me':[new Alias('remove_me', 'myindex2')] + }}; + + spyOn(this.AlertService, "success"); + + expect(this.scope.aliases.info['remove_me'].length).toEqual(2) + this.scope.removeAliasFromIndex('myindex', 'remove_me'); + expect(this.AlertService.success).toHaveBeenCalled(); + expect(this.scope.aliases.info['remove_me'].length).toEqual(1); + + }); + + it('mergeAliases : calls client updateAliases', function () { + this.scope.client.updateAliases = function(){}; + this.scope.aliases = {info:{}}; + this.scope.originalAliases = {info:{}}; + spyOn(this.scope.client, "updateAliases"); + + this.scope.mergeAliases(); + expect(this.scope.client.updateAliases).toHaveBeenCalled() + }); + + it('mergeAliases : adds new', function () { + this.scope.client.updateAliases = function(){}; + var newalias = new Alias('new'); + this.scope.aliases = {info:{'new':[newalias]}}; + this.scope.originalAliases = {info:{}}; + spyOn(this.scope.client, "updateAliases"); + + var expected = [newalias]; + this.scope.mergeAliases(); + expect(this.scope.client.updateAliases).toHaveBeenCalledWith(expected,[], + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('mergeAliases : does not add existing', function () { + this.scope.client.updateAliases = function(){}; + var newalias = new Alias('new'); + this.scope.aliases = {info:{'new':[newalias]}}; + this.scope.originalAliases = {info:{'new':[newalias]}}; + spyOn(this.scope.client, "updateAliases"); + + var expected = []; + this.scope.mergeAliases(); + expect(this.scope.client.updateAliases).toHaveBeenCalledWith(expected,[], + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('mergeAliases : does not add existing', function () { + this.scope.client.updateAliases = function(){}; + var existingalias = new Alias('new'); + this.scope.aliases = {info:{}}; + this.scope.originalAliases = {info:{'new':[existingalias]}}; + spyOn(this.scope.client, "updateAliases"); + + var expected = [existingalias]; + this.scope.mergeAliases(); + expect(this.scope.client.updateAliases).toHaveBeenCalledWith([],expected, + jasmine.any(Function), + jasmine.any(Function)); + }); + +}); \ No newline at end of file diff --git a/tests/jasmine/repository.tests.js b/tests/jasmine/repository.tests.js new file mode 100644 index 00000000..ddf1af26 --- /dev/null +++ b/tests/jasmine/repository.tests.js @@ -0,0 +1,358 @@ +'use strict'; + +describe('RepositoryController', function(){ + var scope, createController, _q, _rootScope; + + beforeEach(angular.mock.module('kopf')); + + beforeEach(angular.mock.inject(function($rootScope, $controller, $injector){ + //create an empty scope + this.scope = $rootScope.$new(); + this.scope.client = {} //set fake client + var $q = $injector.get('$q'); + this._q = $q + this._rootScope = $rootScope; + var $timeout = $injector.get('$timeout'); + var $location = $injector.get('$location'); + this.ConfirmDialogService = $injector.get('ConfirmDialogService'); + var AlertService = $injector.get('AlertService'); + this.AlertService = AlertService; + var AceEditorService = $injector.get('AceEditorService'); + this.AceEditorService = AceEditorService; + + this.createController = function() { + return $controller('RepositoryController', {$scope: this.scope}, $location, $timeout, this.ConfirmDialogService, AlertService, AceEditorService); + }; + + this._controller = this.createController(); + })); + + //TESTS + it('init : values are set', function(){ + expect(this.scope.dialog_service).not.toBe(null); + expect(this.scope.repositories).toEqual([]); + expect(this.scope.repositories_names).toEqual([]); + expect(this.scope.snapshots).toEqual([]); + expect(this.scope.indices).toEqual([]); + expect(this.scope.new_repo).toEqual({}); + expect(this.scope.editor).toEqual(undefined); + expect(this.scope.new_snap).toEqual({}); + }); + + it('on : makes calls reload and initEditor', function() { + spyOn(this.scope, 'reload').andReturn(true); + spyOn(this.scope, 'initEditor').andCallThrough(); + spyOn(this.AceEditorService, 'init').andReturn("fakeaceeditor"); + this.scope.$emit("loadRepositoryEvent"); + expect(this.scope.reload).toHaveBeenCalled(); + expect(this.scope.initEditor).toHaveBeenCalled(); + expect(this.AceEditorService.init).toHaveBeenCalledWith('repository-settings-editor'); + expect(this.scope.editor).toEqual("fakeaceeditor"); + }); + + it('loadIndices : assigns a value to indices from cluster.indices', function() { + this.scope.cluster = {}; + this.scope.cluster.indices = ["chicken", "kale", "potatoes"]; + this.scope.loadIndices(); + expect(this.scope.indices).toEqual(["chicken", "kale", "potatoes"]); + }); + + it('reload : calls loadRepositories, allSnapshots, and loadIndices', function() { + var deferred = this._q.defer(); + deferred.resolve(true); + spyOn(this.scope, 'loadRepositories').andReturn(deferred.promise); + spyOn(this.scope, 'allSnapshots').andReturn(true); + spyOn(this.scope, 'loadIndices').andReturn(true); + this.scope.reload(); + this._rootScope.$apply(); //force cycle so promise gets resolved + expect(this.scope.loadRepositories).toHaveBeenCalled(); + expect(this.scope.allSnapshots).toHaveBeenCalled(); + expect(this.scope.loadIndices).toHaveBeenCalled(); + }); + + it('reload : calls loadRepositories and loadIndices (failed promise)', function() { + var deferred = this._q.defer(); + deferred.reject(true); + spyOn(this.scope, 'loadRepositories').andReturn(deferred.promise); + spyOn(this.scope, 'allSnapshots').andReturn(true); + spyOn(this.scope, 'loadIndices').andReturn(true); + this.scope.reload(); + this._rootScope.$apply(); //force cycle so promise gets resolved + expect(this.scope.loadRepositories).toHaveBeenCalled(); + expect(this.scope.allSnapshots).not.toHaveBeenCalled(); + expect(this.scope.loadIndices).toHaveBeenCalled(); + }); + + it('optionalParam : sets param on body', function() { + var answer = this.scope.optionalParam({"chicken":"no"}, {"chicken":"yes"}, "chicken"); + expect(answer).toEqual({"chicken":"yes"}); + + answer = this.scope.optionalParam({"chicken":"no"}, {"chicken":"yes"}, "pork"); + expect(answer).toEqual({"chicken":"no"}); + + answer = this.scope.optionalParam({"chicken":"no"}, {}, "pork"); + expect(answer).toEqual({"chicken":"no"}); + }); + + it('deleteRepository : calls dialog_service open', function() { + spyOn(this.ConfirmDialogService, "open").andReturn(true); + this.scope.deleteRepository("name", "value"); + expect(this.ConfirmDialogService.open).toHaveBeenCalled(); + }); + + it('restoreSnapshot : calls client.restorSnapshot : missing optionals', function () { + this.scope.client.restoreSnapshot = function(){}; + this.scope.restore_snap = { + "snapshot": { + "snapshot":"my_snap", + "repository":"my_repo" + } + }; + var expected = {}; + spyOn(this.scope.client, "restoreSnapshot").andReturn(true); + + this.scope.restoreSnapshot(); + expect(this.scope.client.restoreSnapshot).toHaveBeenCalledWith("my_repo", + "my_snap", + JSON.stringify(expected), + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('restoreSnapshot : calls client.restorSnapshot : all params', function () { + this.scope.client.restoreSnapshot = function(){}; + this.scope.restore_snap = { + "indices":["idx-20140107","idx-20140108"], + "ignore_unavailable": false, + "include_global_state":false, + "rename_replacement": "-chicken-", + "rename_pattern":"-", + "snapshot": { + "snapshot":"my_snap", + "repository":"my_repo" + } + }; + + var expected = { + "indices":"idx-20140107,idx-20140108", + "include_global_state":false, + "ignore_unavailable":false, + "rename_replacement":"-chicken-", + "rename_pattern":"-" + }; + spyOn(this.scope.client, "restoreSnapshot").andReturn(true); + + this.scope.restoreSnapshot(); + expect(this.scope.client.restoreSnapshot).toHaveBeenCalledWith("my_repo", + "my_snap", + JSON.stringify(expected), + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('createRepository : calls client.createRepository', function(){ + this.scope.client.createRepository = function(){}; + spyOn(this.scope.client, "createRepository").andReturn(true); + + this.scope.new_repo = { + name:"my_repo", + type:"o positive", + }; + this.scope.editor = { + error: null, + format:function(){return "{\"settings_key\":\"settings_value\"}";} + }; + var expected = {type: "o positive", settings: {"settings_key":"settings_value"}}; + this.scope.createRepository(); + expect(this.scope.client.createRepository).toHaveBeenCalledWith("my_repo", + JSON.stringify(expected), + jasmine.any(Function), + jasmine.any(Function)); + + }); + + it('createRepository : does NOT call client.createRepository if editor error', function(){ + this.scope.client.createRepository = function(){}; + spyOn(this.scope.client, "createRepository").andReturn(true); + this.scope.editor = { + error: "yep this is an error", + format:function(){return "{\"settings_key\":\"settings_value\"}";} + }; + this.scope.createRepository(); + expect(this.scope.client.createRepository).not.toHaveBeenCalled(); + }); + + it('_parseRepositories : calls updateModel and deferred.resolve', function() { + var fakedeferred = {resolve : function(){}} + this.scope.updateModel = function(){}; + spyOn(this.scope, "updateModel"); + spyOn(fakedeferred, "resolve"); + this.scope._parseRepositories("chicken", fakedeferred); + expect(this.scope.updateModel).toHaveBeenCalled(); + expect(fakedeferred.resolve).toHaveBeenCalled(); + }); + + it('_parseRepositories : sets repository names for dropdown', function() { + var fakedeferred = {resolve : function(){}} + + // fake version of updateModel, to test logic in the function we will pass it + this.scope.updateModel=function(action) { + action(); + }; + + var repositories = {"repo1":{"r1k1":"r1v1"}} + spyOn(this.scope, "updateModel").andCallThrough(); + spyOn(fakedeferred, "resolve"); + + this.scope._parseRepositories(repositories, fakedeferred); + expect(this.scope.updateModel).toHaveBeenCalled(); + expect(fakedeferred.resolve).toHaveBeenCalled(); + expect(this.scope.repositories_names).toEqual([{"name":"repo1", "value":"repo1"}]); + expect(this.scope.repositories).toEqual(repositories); + }); + + it('loadRepositories : calls client.getRepositories', function() { + this.scope.client.getRepositories = function(){}; + spyOn(this.scope.client, "getRepositories").andReturn(true); + + this.scope.loadRepositories(); + expect(this.scope.client.getRepositories).toHaveBeenCalledWith(jasmine.any(Function), jasmine.any(Function)); + }); + + it('createSnapshot : repository is required', function() { + this.scope.client.createSnapshot = function(){}; + spyOn(this.AlertService, "warn"); + this.scope.new_snap = {}; + + this.scope.createSnapshot(); + expect(this.AlertService.warn).toHaveBeenCalledWith("Repository is required"); + }); + + it('createSnapshot : snapshot name is required', function() { + this.scope.client.createSnapshot = function(){}; + spyOn(this.AlertService, "warn"); + this.scope.new_snap = {"repository":"yep"}; + + this.scope.createSnapshot(); + expect(this.AlertService.warn).toHaveBeenCalledWith("Snapshot name is required"); + }); + + it('createSnapshot : calls client.createSnapshot', function() { + this.scope.client.createSnapshot = function(){}; + spyOn(this.scope.client, "createSnapshot").andReturn(true); + this.scope.new_snap = {"repository":"my_repo", + "name":"my_snap" + }; + + this.scope.createSnapshot(); + expect(this.scope.client.createSnapshot).toHaveBeenCalledWith("my_repo", + "my_snap", + JSON.stringify({}), + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('createSnapshot : calls client.createSnapshot - sets optional', function() { + this.scope.client.createSnapshot = function(){}; + spyOn(this.scope.client, "createSnapshot").andReturn(true); + this.scope.new_snap = {"repository":"my_repo", + "name":"my_snap", + "indices":["one","two"], + "include_global_state":"true", + "ignore_unavailable":true + }; + + var expected = { + "indices":"one,two", + "include_global_state":true, + "ignore_unavailable":true + }; + + this.scope.createSnapshot(); + expect(this.scope.client.createSnapshot).toHaveBeenCalledWith("my_repo", + "my_snap", + JSON.stringify(expected), + jasmine.any(Function), + jasmine.any(Function)); + }); + + it('deleteSnapshot : calls dialog_service open', function() { + spyOn(this.ConfirmDialogService, "open").andReturn(true); + this.scope.deleteSnapshot("name", "value"); + expect(this.ConfirmDialogService.open).toHaveBeenCalled(); + }); + + it('allSnapshots : creates list of all snapshots', function() { + var repositories = { + "repo_1": {"fake_key":"fake_value"} + }; + + var snapshots = [{"snapshot": "my_snapshot_1"}, {"snapshot": "my_snapshot_2"}]; + var deferred = this._q.defer(); + + deferred.resolve(snapshots); + + spyOn(this.scope, 'loadRepositories').andReturn(); + + this.scope.snapshots = [{"snapshot":"existing"}]; + spyOn(this.scope, "fetchSnapshots").andReturn(deferred.promise); + this.scope.allSnapshots(repositories); + this._rootScope.$apply(); + expect(this.scope.snapshots).toEqual([{"snapshot": "my_snapshot_1"}, + {"snapshot": "my_snapshot_2"}]); + }); + + it('allSnapshots : creates list of all snapshots - fail', function() { + var repositories = { + "repo_1": {"fake_key":"fake_value"} + }; + var deferred = this._q.defer(); + deferred.reject(true); + + spyOn(this.scope, 'loadRepositories').andReturn(); + + this.scope.snapshots = [{"snapshot":"existing"}]; + spyOn(this.scope, "fetchSnapshots").andReturn(deferred.promise); + this.scope.allSnapshots(repositories); + this._rootScope.$apply(); + expect(this.scope.snapshots).toEqual([]); + }); + + it('_parseSnapshots : adds repo name to each snapshot', function() { + var fakedeferred = {resolve : function(){} }; + var response = { + "snapshots":[{"s1":{"name":"tylerdyrden"}}] + }; + + var expected = [{"s1":{"name":"tylerdyrden"}, "repository":"chicken"}]; + spyOn(fakedeferred, "resolve"); + + this.scope._parseSnapshots("chicken", response, fakedeferred); + expect(fakedeferred.resolve).toHaveBeenCalledWith(expected); + }); + + it('_parseSnapshots : adds nothing if snapshots not array', function() { + var fakedeferred = {resolve : function(){} }; + var response = { + "snapshots":{"s1":{"name":"tylerdyrden"}} + }; + + var expected = {"s1":{"name":"tylerdyrden"}}; + spyOn(fakedeferred, "resolve"); + + this.scope._parseSnapshots("chicken", response, fakedeferred); + expect(fakedeferred.resolve).toHaveBeenCalledWith(expected); + }); + + it('fetchSnapshots : calls getSnapshots', function() { + this.scope.client.getSnapshots = function(){}; + spyOn(this.scope.client, "getSnapshots").andReturn(true); + this.scope.fetchSnapshots("chicken"); + expect(this.scope.client.getSnapshots).toHaveBeenCalledWith( + "chicken", + jasmine.any(Function), + jasmine.any(Function)); + + }); + +}); \ No newline at end of file diff --git a/tests/karma.config.js b/tests/karma.config.js new file mode 100644 index 00000000..e3bc549a --- /dev/null +++ b/tests/karma.config.js @@ -0,0 +1,65 @@ +// Karma configuration +// Generated on Sat Feb 01 2014 16:02:02 GMT-0500 (EST) + +module.exports = function(config) { + config.set({ + + // base path, that will be used to resolve files and exclude + basePath: '', + + // frameworks to use + frameworks: ['jasmine'], + + // list of files / patterns to load in the browser + files: [ + '../src/lib/angularjs/angular.min.js', + '../src/lib/jquery/jquery-1.10.2.min.js', + '../src/lib/ace/*.js', + '../src/lib/jsontree/*.js', + '../src/kopf/*.js', + '../src/kopf/**/*.js', + 'angular-mocks.js', + 'jasmine/*.tests.js' + ], + + // list of files to exclude + exclude: [ + + ], + + // test results reporter to use + // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage' + reporters: ['progress'], + + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_DEBUG, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: true, + + // Start these browsers, currently available: + // - Chrome + // - ChromeCanary + // - Firefox + // - Opera (has to be installed with `npm install karma-opera-launcher`) + // - Safari (only Mac; has to be installed with `npm install karma-safari-launcher`) + // - PhantomJS + // - IE (only Windows; has to be installed with `npm install karma-ie-launcher`) + browsers: ['PhantomJS'], + + // If browser does not capture in given timeout [ms], kill it + captureTimeout: 60000, + + // Continuous Integration mode + // if true, it capture browsers, run tests and exit + singleRun: false + }); +}; diff --git a/tests/tests.js b/tests/tests.js deleted file mode 100644 index 0b16e4db..00000000 --- a/tests/tests.js +++ /dev/null @@ -1,3 +0,0 @@ -test("empty test", function() { - ok( 1 == "1", "Passed!" ); -}); \ No newline at end of file diff --git a/tests/util_tests.js b/tests/util_tests.js index f3393d37..1d76d214 100644 --- a/tests/util_tests.js +++ b/tests/util_tests.js @@ -61,4 +61,12 @@ test("getTimeString method", function() { ok(getTimeString(new Date(79,5,24,11,33,0)) == "11:33:00", "Date with time 11:33:00"); ok(getTimeString(new Date(79,5,24,0,0,0)) == "00:00:00", "Date with time 00:00:00"); ok(getTimeString(new Date(79,5,24,12,5,5)) == "12:05:05", "Date with time 12:05:05"); +}); + +// prettyPrintObject tests +test("getTimeString method", function() { + ok(prettyPrintObject({"foo":"bar"}) == "{\n \"foo\": \"bar\"\n}", "Single property"); + ok(prettyPrintObject({"foo.bar":"bar"}) == "{\n \"foo\": {\n \"bar\": \"bar\"\n }\n}", "Single nested property"); + ok(prettyPrintObject({"foo": ["bar","foobar"]}) == "{\n \"foo\": [\n \"bar\",\n \"foobar\"\n ]\n}", "Array property"); + ok(prettyPrintObject({"foo": [ {"foo":"bar"},{"bar":"foo"}]}) == "{\n \"foo\": [\n {\n \"foo\": \"bar\"\n },\n {\n \"bar\": \"foo\"\n }\n ]\n}", "Array of objects"); }); \ No newline at end of file