From 86c09b32462595553edaa7385312f687a97bd205 Mon Sep 17 00:00:00 2001 From: Julius Volz Date: Fri, 7 Nov 2014 01:10:00 +0100 Subject: [PATCH] Support for uploading dashboard definitions. This enables PUT-ing and GET-ing of full dashboard definitions through /.json. In this API, the 'dashboards_json' database field is converted from a JSON string to a native JSON suboject of the Rails Dashboard model JSON. The full externally exposed JSON looks like this: { "name": "Test Dashboard", "created_at": "2014-11-07T00:25:57.000Z", "updated_at": "2014-11-07T00:38:23.000Z", "dashboard_json": { "globalConfig": { // ...global configuration... }, "widgets": [ // ...widget definitions... ] } } Upon PUT, the 'dashboards_json' field is validated against the JSON-schema definition in the file 'dashboard_schema.json' and then saved again as a string in a single database field. Since Rails converts JSON empty arrays to "nil" values by default, this also upgrades PromDash to Rails 4.1.1, which supports an option to turn off this automatic conversion. See the following issues about this regarding security implications (which for now we shouldn't need to worry about in PromDash): https://github.com/rails/rails/issues/8832 https://github.com/rails/rails/issues/13420 Further, as we're exposing the dashboard_json as an external API for the first time, this also includes a lot of naming cleanups in the JSON fields and the related code. --- Gemfile | 9 +- Gemfile.lock | 127 +++---- .../angular/controllers/dashboard_ctrl.js | 34 +- .../angular/controllers/embed_ctrl.js | 2 +- .../angular/controllers/frame_ctrl.js | 8 +- .../angular/controllers/graph_ctrl.js | 20 +- .../angular/controllers/pie_ctrl.js | 2 +- .../angular/controllers/single_widget_ctrl.js | 6 +- .../angular/directives/expression.js.erb | 10 +- .../angular/directives/graph_chart.js | 38 +- .../angular/directives/pie_chart.js | 6 +- .../resources/shared_graph_behavior.js | 6 +- .../angular/services/graph_refresher.js | 6 +- .../angular/services/metric_names_querier.js | 8 +- .../services/rickshaw_data_transformer.js | 4 +- .../angular/services/servers_by_id_object.js | 2 +- .../angular/services/shared_widget_setup.js | 14 +- .../angular/services/url_config.js | 12 +- app/assets/javascripts/controllers.js | 1 - app/assets/javascripts/dashboards.js.coffee | 3 - app/assets/javascripts/servers.js.coffee | 3 - app/assets/templates/expression_template.html | 8 +- app/assets/templates/frame_template.html | 2 +- app/assets/templates/graph_template.html.erb | 6 +- app/assets/templates/pie_template.html.erb | 6 +- app/controllers/dashboards_controller.rb | 16 +- app/models/dashboard.rb | 13 + app/views/dashboards/show.html.erb | 6 +- app/views/dashboards/show.json.jbuilder | 1 + app/views/servers/index.html.erb | 2 +- app/views/servers/show.html.erb | 2 +- config/application.rb | 4 + dashboard_schema.json | 351 ++++++++++++++++++ ...13708_update_dashboard_json_for_api.rb.swp | Bin 0 -> 12288 bytes ...107013708_update_dashboard_json_for_api.rb | 28 ++ db/schema.rb | 2 +- .../controllers/dashboards_controller_spec.rb | 15 +- .../services/servers_by_id_object_spec.js | 2 +- .../sample_json/1_expression_dashboard_json | 9 +- .../sample_json/2_expression_dashboard_json | 19 +- .../sample_json/pie_chart_dashboard_json | 2 +- 41 files changed, 602 insertions(+), 213 deletions(-) delete mode 100644 app/assets/javascripts/dashboards.js.coffee delete mode 100644 app/assets/javascripts/servers.js.coffee create mode 100644 dashboard_schema.json create mode 100644 db/migrate/.20141107013708_update_dashboard_json_for_api.rb.swp create mode 100644 db/migrate/20141107013708_update_dashboard_json_for_api.rb diff --git a/Gemfile b/Gemfile index bb2a5e88..c7c480c9 100644 --- a/Gemfile +++ b/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' # Bundle edge Rails instead: gem 'rails', github: 'rails/rails' -gem 'rails', '4.0.0' +gem 'rails', '4.1.7' gem 'mysql2' @@ -29,14 +29,11 @@ group :test, :development do end # Use SCSS for stylesheets -gem 'sass-rails', '~> 4.0.0' +gem 'sass-rails', '~> 4.0.4' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' -# Use CoffeeScript for .js.coffee assets and views -gem 'coffee-rails', '~> 4.0.0' - # See https://github.com/sstephenson/execjs#readme for more supported runtimes gem 'therubyracer', platforms: :ruby @@ -68,3 +65,5 @@ end #gem 'bootstrap-sass', github: 'thomas-mcdonald/bootstrap-sass', branch: 'master' gem 'entypo-rails' + +gem 'json-schema' diff --git a/Gemfile.lock b/Gemfile.lock index 6c3f8800..22ea63f8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,36 +1,37 @@ GEM remote: https://rubygems.org/ specs: - actionmailer (4.0.0) - actionpack (= 4.0.0) - mail (~> 2.5.3) - actionpack (4.0.0) - activesupport (= 4.0.0) - builder (~> 3.1.0) - erubis (~> 2.7.0) + actionmailer (4.1.7) + actionpack (= 4.1.7) + actionview (= 4.1.7) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.7) + actionview (= 4.1.7) + activesupport (= 4.1.7) rack (~> 1.5.2) rack-test (~> 0.6.2) + actionview (4.1.7) + activesupport (= 4.1.7) + builder (~> 3.1) + erubis (~> 2.7.0) active_model_serializers (0.8.1) activemodel (>= 3.0) - activemodel (4.0.0) - activesupport (= 4.0.0) - builder (~> 3.1.0) - activerecord (4.0.0) - activemodel (= 4.0.0) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.0) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.3) - activesupport (4.0.0) - i18n (~> 0.6, >= 0.6.4) - minitest (~> 4.2) - multi_json (~> 1.3) + activemodel (4.1.7) + activesupport (= 4.1.7) + builder (~> 3.1) + activerecord (4.1.7) + activemodel (= 4.1.7) + activesupport (= 4.1.7) + arel (~> 5.0.0) + activesupport (4.1.7) + i18n (~> 0.6, >= 0.6.9) + json (~> 1.7, >= 1.7.7) + minitest (~> 5.1) thread_safe (~> 0.1) - tzinfo (~> 0.3.37) + tzinfo (~> 1.1) addressable (2.3.5) - arel (4.0.0) - atomic (1.1.14) - builder (3.1.4) + arel (5.0.1.20140414130214) + builder (3.2.2) capybara (2.3.0) mime-types (>= 1.16) nokogiri (>= 1.3.3) @@ -40,13 +41,6 @@ GEM childprocess (0.5.3) ffi (~> 1.0, >= 1.0.11) coderay (1.0.9) - coffee-rails (4.0.0) - coffee-script (>= 2.2.0) - railties (>= 4.0.0.beta, < 5.0) - coffee-script (2.2.0) - coffee-script-source - execjs - coffee-script-source (1.6.3) daemons (1.1.9) database_cleaner (0.9.1) diff-lcs (1.2.5) @@ -62,7 +56,7 @@ GEM railties (>= 3.0.0) ffi (1.9.5) hike (1.2.3) - i18n (0.6.5) + i18n (0.6.11) jasmine-core (2.0.0) jasmine-rails (0.6.0) jasmine-core (>= 1.3, < 3.0) @@ -76,22 +70,21 @@ GEM railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) json (1.8.0) + json-schema (2.4.1) launchy (2.4.2) addressable (~> 2.3) libv8 (3.16.14.3) - mail (2.5.4) - mime-types (~> 1.16) - treetop (~> 1.4.8) + mail (2.6.3) + mime-types (>= 1.16, < 3) method_source (0.8.2) - mime-types (1.25.1) + mime-types (2.4.3) mini_portile (0.6.0) - minitest (4.7.5) + minitest (5.4.2) multi_json (1.10.1) mysql2 (0.3.13) nokogiri (1.6.3.1) mini_portile (= 0.6.0) phantomjs (1.9.7.0) - polyglot (0.3.3) pry (0.9.12.2) coderay (~> 1.0.5) method_source (~> 0.8) @@ -101,20 +94,22 @@ GEM rack rack-test (0.6.2) rack (>= 1.0) - rails (4.0.0) - actionmailer (= 4.0.0) - actionpack (= 4.0.0) - activerecord (= 4.0.0) - activesupport (= 4.0.0) + rails (4.1.7) + actionmailer (= 4.1.7) + actionpack (= 4.1.7) + actionview (= 4.1.7) + activemodel (= 4.1.7) + activerecord (= 4.1.7) + activesupport (= 4.1.7) bundler (>= 1.3.0, < 2.0) - railties (= 4.0.0) - sprockets-rails (~> 2.0.0) - railties (4.0.0) - actionpack (= 4.0.0) - activesupport (= 4.0.0) + railties (= 4.1.7) + sprockets-rails (~> 2.0) + railties (4.1.7) + actionpack (= 4.1.7) + activesupport (= 4.1.7) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.0) + rake (10.3.2) rdoc (3.12.2) json (~> 1.4) ref (1.0.5) @@ -139,11 +134,12 @@ GEM rspec-support (= 3.0.0.beta2) rspec-support (3.0.0.beta2) rubyzip (1.1.6) - sass (3.2.12) - sass-rails (4.0.0) - railties (>= 4.0.0.beta, < 5.0) - sass (>= 3.1.10) - sprockets-rails (~> 2.0.0) + sass (3.2.19) + sass-rails (4.0.4) + railties (>= 4.0.0, < 5.0) + sass (~> 3.2.2) + sprockets (~> 2.8, < 2.12) + sprockets-rails (~> 2.0) sdoc (0.3.20) json (>= 1.1.3) rdoc (~> 3.10) @@ -157,15 +153,15 @@ GEM rack-protection (~> 1.4) tilt (~> 1.3, >= 1.3.4) slop (3.4.6) - sprockets (2.10.0) + sprockets (2.11.3) hike (~> 1.2) multi_json (~> 1.0) rack (~> 1.0) tilt (~> 1.1, != 1.3.0) - sprockets-rails (2.0.0) + sprockets-rails (2.2.0) actionpack (>= 3.0) activesupport (>= 3.0) - sprockets (~> 2.8) + sprockets (>= 2.8, < 4.0) sqlite3 (1.3.8) therubyracer (0.12.0) libv8 (~> 3.16.14.0) @@ -174,14 +170,11 @@ GEM daemons (>= 1.0.9) eventmachine (>= 1.0.0) rack (>= 1.5.0) - thor (0.18.1) - thread_safe (0.1.3) - atomic + thor (0.19.1) + thread_safe (0.3.4) tilt (1.4.1) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - tzinfo (0.3.38) + tzinfo (1.2.2) + thread_safe (~> 0.1) uglifier (2.2.1) execjs (>= 0.3.0) multi_json (~> 1.0, >= 1.0.2) @@ -195,19 +188,19 @@ PLATFORMS DEPENDENCIES active_model_serializers capybara (~> 2.3.0) - coffee-rails (~> 4.0.0) database_cleaner entypo-rails factory_girl_rails jasmine-rails jbuilder (~> 1.2) jquery-rails + json-schema launchy mysql2 pry - rails (= 4.0.0) + rails (= 4.1.7) rspec-rails (~> 3.0.0.beta2) - sass-rails (~> 4.0.0) + sass-rails (~> 4.0.4) sdoc selenium-webdriver sinatra diff --git a/app/assets/javascripts/angular/controllers/dashboard_ctrl.js b/app/assets/javascripts/angular/controllers/dashboard_ctrl.js index a904defb..116980cf 100644 --- a/app/assets/javascripts/angular/controllers/dashboard_ctrl.js +++ b/app/assets/javascripts/angular/controllers/dashboard_ctrl.js @@ -6,8 +6,8 @@ angular.module("Prometheus.controllers") "$document", "$location", "WidgetHeightCalculator", - "UrlConfigEncoder", - "UrlVariablesDecoder", + "URLConfigEncoder", + "URLVariablesDecoder", "SharedGraphBehavior", "InputHighlighter", "ModalService", @@ -19,8 +19,8 @@ angular.module("Prometheus.controllers") $document, $location, WidgetHeightCalculator, - UrlConfigEncoder, - UrlVariablesDecoder, + URLConfigEncoder, + URLVariablesDecoder, SharedGraphBehavior, InputHighlighter, ModalService, @@ -62,10 +62,10 @@ angular.module("Prometheus.controllers") $scope.saving = true; $http.put(window.location.pathname + '.json', { 'dashboard': { - 'dashboard_json': angular.toJson({ - 'globalConfig': $scope.globalConfig, - 'widgets': $scope.widgets - }) + 'dashboard_json': { + 'globalConfig': angular.copy($scope.globalConfig), + 'widgets': angular.copy($scope.widgets) + } } }).error(function(data, status) { alert("Error saving dashboard."); @@ -155,9 +155,9 @@ angular.module("Prometheus.controllers") title: "Title", expression: { id: 0, - server_id: 1, + serverID: 1, expression: "", - legend_id: 1 + legendID: 1 }, type: "pie" }; @@ -175,22 +175,22 @@ angular.module("Prometheus.controllers") $scope.globalConfig.vars = vars; }, true); - $scope.syncEntireUrlEncode = function() { - if (!$scope.globalConfig.keepUrlUpdated) { - $scope.globalConfig.encodeEntireUrl = false; + $scope.syncEntireURLEncode = function() { + if (!$scope.globalConfig.keepURLUpdated) { + $scope.globalConfig.encodeEntireURL = false; } }; $scope.$watch('globalConfig', function() { - if ($scope.globalConfig.keepUrlUpdated) { + if ($scope.globalConfig.keepURLUpdated) { if ($scope.globalConfig.range) { $location.search("range", $scope.globalConfig.range) } if ($scope.globalConfig.endTime) { $location.search("until", (new Date($scope.globalConfig.endTime)).toISOString()) } - if ($scope.globalConfig.encodeEntireUrl) { - UrlConfigEncoder({globalConfig: $scope.globalConfig}); + if ($scope.globalConfig.encodeEntireURL) { + URLConfigEncoder({globalConfig: $scope.globalConfig}); } } }, true); @@ -242,7 +242,7 @@ angular.module("Prometheus.controllers") $scope.widgets.push(angular.copy($scope.widgetToClone)); }; - var searchVars = UrlVariablesDecoder() + var searchVars = URLVariablesDecoder() if (searchVars.fullscreen) { $scope.enableFullscreen(); } diff --git a/app/assets/javascripts/angular/controllers/embed_ctrl.js b/app/assets/javascripts/angular/controllers/embed_ctrl.js index 1067d3ce..bb4f8e7e 100644 --- a/app/assets/javascripts/angular/controllers/embed_ctrl.js +++ b/app/assets/javascripts/angular/controllers/embed_ctrl.js @@ -1,4 +1,4 @@ -angular.module("Prometheus.controllers").controller('EmbedCtrl',["$scope", "$window", "$timeout", "WidgetHeightCalculator", "UrlConfigEncoder", "FullScreenAspectRatio", "SharedGraphBehavior", function($scope, $window, $timeout, WidgetHeightCalculator, UrlConfigEncoder, FullScreenAspectRatio, SharedGraphBehavior) { +angular.module("Prometheus.controllers").controller('EmbedCtrl',["$scope", "$window", "$timeout", "WidgetHeightCalculator", "URLConfigEncoder", "FullScreenAspectRatio", "SharedGraphBehavior", function($scope, $window, $timeout, WidgetHeightCalculator, URLConfigEncoder, FullScreenAspectRatio, SharedGraphBehavior) { $window.onresize = function() { $scope.$apply(function() { // Need to $apply to propagate aspectRatio change, diff --git a/app/assets/javascripts/angular/controllers/frame_ctrl.js b/app/assets/javascripts/angular/controllers/frame_ctrl.js index 69250510..b73a533f 100644 --- a/app/assets/javascripts/angular/controllers/frame_ctrl.js +++ b/app/assets/javascripts/angular/controllers/frame_ctrl.js @@ -1,7 +1,7 @@ angular.module("Prometheus.controllers").controller('FrameCtrl', ["$scope", "$sce", "$timeout", "VariableInterpolator", - "UrlHashEncoder", + "URLHashEncoder", "InputHighlighter", "WidgetLinkHelper", "GraphiteTimeConverter", @@ -10,7 +10,7 @@ angular.module("Prometheus.controllers").controller('FrameCtrl', ["$scope", function($scope, $sce, $timeout, VariableInterpolator, - UrlHashEncoder, + URLHashEncoder, InputHighlighter, WidgetLinkHelper, GraphiteTimeConverter, @@ -29,7 +29,7 @@ angular.module("Prometheus.controllers").controller('FrameCtrl', ["$scope", graphBlob.globalConfig = dashboardData.globalConfig; WidgetLinkHelper .createLink({ - encoded_url: UrlHashEncoder(graphBlob), + encoded_url: URLHashEncoder(graphBlob), graph_title: $scope.getTitle(), dashboard_name: dashboardName }, event) @@ -92,7 +92,7 @@ angular.module("Prometheus.controllers").controller('FrameCtrl', ["$scope", return $sce.trustAsResourceUrl(buildFrameURL(url)); }; - $scope.updateUrl = function() { + $scope.updateURL = function() { $scope.frame.url = $scope.urlInput; }; diff --git a/app/assets/javascripts/angular/controllers/graph_ctrl.js b/app/assets/javascripts/angular/controllers/graph_ctrl.js index 16c8e856..57f3a576 100644 --- a/app/assets/javascripts/angular/controllers/graph_ctrl.js +++ b/app/assets/javascripts/angular/controllers/graph_ctrl.js @@ -30,23 +30,23 @@ angular.module("Prometheus.controllers").controller('GraphCtrl', $scope.palettes = Palettes; $scope.addExpression = function() { - var serverId = 0; - var axisId = 0; + var serverID = 0; + var axisID = 0; var id = 0; if ($scope.graph.expressions.length != 0) { var prev = $scope.graph.expressions[$scope.graph.expressions.length-1]; id = prev['id'] + 1; - serverId = prev['server_id']; - axisId = prev['axis_id']; + serverID = prev['serverID']; + axisID = prev['axisID']; } else if ($scope.servers.length != 0) { - serverId = $scope.servers[0]['id']; - axisId = $scope.graph.axes[0]['id']; + serverID = $scope.servers[0]['id']; + axisID = $scope.graph.axes[0]['id']; } var exp = { 'id': id, - 'server_id': serverId, - 'axis_id': axisId, + 'serverID': serverID, + 'axisID': axisID, 'expression': '' }; $scope.graph.expressions.push(exp); @@ -66,8 +66,8 @@ angular.module("Prometheus.controllers").controller('GraphCtrl', var len = axes.length; $scope.graph.expressions.forEach(function(expr) { - if (expr.axis_id === axes[idx].id) { - expr.axis_id = axes[0].id + if (expr.axisID === axes[idx].id) { + expr.axisID = axes[0].id } }); diff --git a/app/assets/javascripts/angular/controllers/pie_ctrl.js b/app/assets/javascripts/angular/controllers/pie_ctrl.js index 28da4cc8..f3d47cdc 100644 --- a/app/assets/javascripts/angular/controllers/pie_ctrl.js +++ b/app/assets/javascripts/angular/controllers/pie_ctrl.js @@ -10,7 +10,7 @@ angular.module("Prometheus.controllers").controller('PieCtrl', // Query for the data. $scope.refreshGraph = function() { var exp = $scope.graph.expression; - var server = $scope.serversById[exp['server_id'] || 1]; + var server = $scope.serversById[exp['serverID'] || 1]; $scope.requestInFlight = true; $http.get(server.url + "api/query", { params: { diff --git a/app/assets/javascripts/angular/controllers/single_widget_ctrl.js b/app/assets/javascripts/angular/controllers/single_widget_ctrl.js index 48203cc2..24f7dcc8 100644 --- a/app/assets/javascripts/angular/controllers/single_widget_ctrl.js +++ b/app/assets/javascripts/angular/controllers/single_widget_ctrl.js @@ -1,8 +1,8 @@ -angular.module("Prometheus.controllers").controller('SingleWidgetCtrl', ["$window", "$timeout", "$scope", "$http", "UrlConfigDecoder", "VariableInterpolator", "GraphRefresher", "WidgetHeightCalculator", "ServersByIdObject", "FullScreenAspectRatio", "ThemeManager", function($window, $timeout, $scope, $http, UrlConfigDecoder, VariableInterpolator, GraphRefresher, WidgetHeightCalculator, ServersByIdObject, FullScreenAspectRatio, ThemeManager) { - var graphBlob = UrlConfigDecoder(blob); +angular.module("Prometheus.controllers").controller('SingleWidgetCtrl', ["$window", "$timeout", "$scope", "$http", "URLConfigDecoder", "VariableInterpolator", "GraphRefresher", "WidgetHeightCalculator", "ServersByIDObject", "FullScreenAspectRatio", "ThemeManager", function($window, $timeout, $scope, $http, URLConfigDecoder, VariableInterpolator, GraphRefresher, WidgetHeightCalculator, ServersByIDObject, FullScreenAspectRatio, ThemeManager) { + var graphBlob = URLConfigDecoder(blob); $scope.widget = graphBlob.widget; $scope.servers = servers; - $scope.serversById = ServersByIdObject($scope.servers); + $scope.serversById = ServersByIDObject($scope.servers); $scope.globalConfig = graphBlob.globalConfig; $scope.globalConfig.aspectRatio = FullScreenAspectRatio(); ThemeManager.setTheme($scope.globalConfig.theme); diff --git a/app/assets/javascripts/angular/directives/expression.js.erb b/app/assets/javascripts/angular/directives/expression.js.erb index 6e75adfd..fa1defa3 100644 --- a/app/assets/javascripts/angular/directives/expression.js.erb +++ b/app/assets/javascripts/angular/directives/expression.js.erb @@ -15,20 +15,20 @@ angular.module("Prometheus.directives").directive('expression', ["$timeout", "Me }, link: function(scope, element, attrs) { var el = element[0].querySelector("[ng-model='expr.expression']"); - var server = scope.serversById[scope.expr.server_id]; + var server = scope.serversById[scope.expr.serverID]; scope.MetricNamesQuerier = MetricNamesQuerier; - MetricNamesQuerier(scope.expr.server_id, server.url, scope); + MetricNamesQuerier(scope.expr.serverID, server.url, scope); scope.linkToPrometheus = function() { - if (!scope.expr.axis_id && scope.type !== "pie") { + if (!scope.expr.axisID && scope.type !== "pie") { return } - var server = scope.serversById[scope.expr.server_id]['url']; + var server = scope.serversById[scope.expr.serverID]['url']; var stacked = ""; if (scope.type !== "pie") { scope.axes.forEach(function(a) { - if (a.id === scope.expr.axis_id && a.renderer === "stack") { + if (a.id === scope.expr.axisID && a.renderer === "stack") { stacked = "true"; } }); diff --git a/app/assets/javascripts/angular/directives/graph_chart.js b/app/assets/javascripts/angular/directives/graph_chart.js index a93bc1fa..1ce747ea 100644 --- a/app/assets/javascripts/angular/directives/graph_chart.js +++ b/app/assets/javascripts/angular/directives/graph_chart.js @@ -18,7 +18,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W series.forEach(function(s) { if (s.exp_id === exp.id) { var lst = scope.graphSettings.legendFormatStrings.filter(function(lst) { - return lst.id === exp.legend_id; + return lst.id === exp.legendID; })[0]; if (!(lst || {}).name) { return; @@ -60,12 +60,12 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W } var graphEl = $el.find('.graph_chart').get(0); - var axisIdByExprId = {}; + var axisIDByExprID = {}; scope.graphSettings.expressions.forEach(function(expr) { - axisIdByExprId[expr.id] = expr.axis_id; + axisIDByExprID[expr.id] = expr.axisID; }); - var series = RickshawDataTransformer(graphData, axisIdByExprId); + var series = RickshawDataTransformer(graphData, axisIDByExprID); var seriesYLimitFn = calculateBound(series); var yMinForLog = seriesYLimitFn(Math.min); @@ -85,7 +85,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W var axesBounds = {}; var a1series = series.filter(function(s) { - return s.axis_id === 1; + return s.axisID === 1; }); var a1LimitFn = calculateBound(a1series); axesBounds[1] = { @@ -94,7 +94,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W }; var a2series = series.filter(function(s) { - return s.axis_id === 2; + return s.axisID === 2; }); var a2LimitFn = calculateBound(a2series); axesBounds[2] = { @@ -104,12 +104,12 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W var palette = new Rickshaw.Color.Palette({scheme: scope.graphSettings.palette}); var yScales = {}; - var scaleId; + var scaleID; var graphMax; series.forEach(function(s) { var axes = scope.graphSettings.axes; var matchingAxis = axes.filter(function(a) { - return a.id === s.axis_id; + return a.id === s.axisID; })[0] || axes[0]; s.color = palette.color(); @@ -133,7 +133,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W bound.max = maybeYMax; graphMax = maybeYMax; } - scaleId = s.axis_id; + scaleID = s.axisID; break; case "<": if (axesBounds[matchingAxis.id].max < maybeYMax) { @@ -143,7 +143,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W bound.max = maybeYMax; graphMax = maybeYMax; } - scaleId = s.axis_id; + scaleID = s.axisID; break; default: // Do nothing. @@ -151,7 +151,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W } else if (!isNaN(maybeYMax)) { bound.max = maybeYMax; graphMax = maybeYMax; - scaleId = s.axis_id; + scaleID = s.axisID; } if (!isNaN(enteredYMin)) { // min is used for the linear scale; all numbers are acceptable. @@ -171,8 +171,8 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W YAxisUtilities.setLinearScale(min, bound.max); s.scale = YAxisUtilities.getScale(matchingAxis.scale); - yScales[s.axis_id] = s.scale; - delete s.axis_id; + yScales[s.axisID] = s.scale; + delete s.axisID; if (matchingAxis.renderer) { s.renderer = matchingAxis.renderer; @@ -222,8 +222,8 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W series: series }); - if (scaleId) { - rsGraph.max = yScales[scaleId](graphMax); + if (scaleID) { + rsGraph.max = yScales[scaleID](graphMax); } else { rsGraph.max = rsGraph.renderer.domain().y[1]; } @@ -341,8 +341,8 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W function setLegendPresence(series) { $(element[0]).find(".legend").show(); - if (scope.graphSettings.legendSetting === "never" || - (scope.graphSettings.legendSetting === "sometimes" && series.length > 5)) { + if (scope.graphSettings.showLegend === "never" || + (scope.graphSettings.showLegend === "sometimes" && series.length > 5)) { $(element[0]).find(".legend").hide(); series.forEach(function(s) { s.noLegend = true; @@ -376,7 +376,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W scope.$watch(function(scope) { return scope.graphSettings.expressions.map(function(expr) { - return "" + expr.legend_id + expr.axis_id; + return "" + expr.legendID + expr.axisID; }); }, redrawGraph, true); scope.$watch('graphSettings.legendFormatStrings', redrawGraph, true); @@ -384,7 +384,7 @@ angular.module("Prometheus.directives").directive('graphChart', ["$location", "W scope.$watch('graphSettings.stacked', redrawGraph); scope.$watch('graphSettings.palette', redrawGraph); scope.$watch('graphSettings.interpolationMethod', redrawGraph); - scope.$watch('graphSettings.legendSetting', redrawGraph); + scope.$watch('graphSettings.showLegend', redrawGraph); scope.$watch('graphSettings.axes', redrawGraph, true); scope.$watch('graphData', redrawGraph, true); scope.$on('annotateGraph', function(e, data) { diff --git a/app/assets/javascripts/angular/directives/pie_chart.js b/app/assets/javascripts/angular/directives/pie_chart.js index 948be9d9..c513ebec 100644 --- a/app/assets/javascripts/angular/directives/pie_chart.js +++ b/app/assets/javascripts/angular/directives/pie_chart.js @@ -56,8 +56,8 @@ angular.module("Prometheus.directives").directive('pieChart', ["$location", "Wid }; var showLegend = !( - scope.graphSettings.legendSetting === "never" || - (scope.graphSettings.legendSetting === "sometimes" && pieData.length > 5) + scope.graphSettings.showLegend === "never" || + (scope.graphSettings.showLegend === "sometimes" && pieData.length > 5) ); if (showLegend) { pieGraph.addLegend(10, 15, graphWidth - 20, graphHeight, "left"); @@ -76,7 +76,7 @@ angular.module("Prometheus.directives").directive('pieChart', ["$location", "Wid return tooltipText; } - scope.$watch('graphSettings.legendSetting', redrawGraph); + scope.$watch('graphSettings.showLegend', redrawGraph); scope.$watch('graphSettings.legendFormatString', redrawGraph); scope.$watch('data', redrawGraph, true); scope.$on('redrawGraphs', function(e, data) { diff --git a/app/assets/javascripts/angular/resources/shared_graph_behavior.js b/app/assets/javascripts/angular/resources/shared_graph_behavior.js index a8d4adb6..2f00b374 100644 --- a/app/assets/javascripts/angular/resources/shared_graph_behavior.js +++ b/app/assets/javascripts/angular/resources/shared_graph_behavior.js @@ -1,4 +1,4 @@ -angular.module("Prometheus.services").factory("SharedGraphBehavior", ["$http", "$timeout", "UrlConfigDecoder", "UrlVariablesDecoder", "ThemeManager", function($http, $timeout, UrlConfigDecoder, UrlVariablesDecoder, ThemeManager) { +angular.module("Prometheus.services").factory("SharedGraphBehavior", ["$http", "$timeout", "URLConfigDecoder", "URLVariablesDecoder", "ThemeManager", function($http, $timeout, URLConfigDecoder, URLVariablesDecoder, ThemeManager) { function commonSetup($scope) { $scope.globalConfig = dashboardData.globalConfig || { numColumns: 2, @@ -14,7 +14,7 @@ angular.module("Prometheus.services").factory("SharedGraphBehavior", ["$http", " $scope.themeChange(); // If settings were passed in via the URL hash, merge them into globalConfig. - var urlConfig = UrlConfigDecoder(); + var urlConfig = URLConfigDecoder(); if (urlConfig.globalConfig) { for (var o in urlConfig.globalConfig) { $scope.globalConfig[o] = urlConfig.globalConfig[o]; @@ -23,7 +23,7 @@ angular.module("Prometheus.services").factory("SharedGraphBehavior", ["$http", " // If we have manual variable overrides in the hashbang search part of the // URL (http://docs.angularjs.org/img/guide/hashbang_vs_regular_url.jpg), // merge them into the globalConfig's template vars. - var urlVars = UrlVariablesDecoder(); + var urlVars = URLVariablesDecoder(); var templateVarRe = /^var\.(.*)$/; for (var o in urlVars) { var matches = o.match(templateVarRe) diff --git a/app/assets/javascripts/angular/services/graph_refresher.js b/app/assets/javascripts/angular/services/graph_refresher.js index 489b33ea..67092cd0 100644 --- a/app/assets/javascripts/angular/services/graph_refresher.js +++ b/app/assets/javascripts/angular/services/graph_refresher.js @@ -6,7 +6,7 @@ angular.module("Prometheus.services").factory('GraphRefresher', $q, VariableInterpolator) { return function($scope) { - function loadGraphData(idx, expression, server, expressionId, allData) { + function loadGraphData(idx, expression, server, expressionID, allData) { var rangeSeconds = Prometheus.Graph.parseDuration($scope.graph.range); return $http.get(server.url + 'api/query_range', { params: { @@ -24,7 +24,7 @@ angular.module("Prometheus.services").factory('GraphRefresher', break; case 'matrix': allData[idx] = { - 'exp_id': expressionId, + 'exp_id': expressionID, 'data': data }; break; @@ -45,7 +45,7 @@ angular.module("Prometheus.services").factory('GraphRefresher', $scope.errorMessages = []; for (var i = 0; i < $scope.graph.expressions.length; i++) { var exp = $scope.graph.expressions[i]; - var server = $scope.serversById[exp['server_id']]; + var server = $scope.serversById[exp['serverID']]; if (server == undefined) { console.log('No server selected for expression, skipping.'); continue; diff --git a/app/assets/javascripts/angular/services/metric_names_querier.js b/app/assets/javascripts/angular/services/metric_names_querier.js index a16928e2..63535ce2 100644 --- a/app/assets/javascripts/angular/services/metric_names_querier.js +++ b/app/assets/javascripts/angular/services/metric_names_querier.js @@ -1,14 +1,14 @@ angular.module("Prometheus.services").factory('MetricNamesQuerier', ["$http", function($http) { var metricNamesCache = {}; - return function(server_id, url, scope) { - if (metricNamesCache[server_id]) { - scope.metricNames = metricNamesCache[server_id]; + return function(serverID, url, scope) { + if (metricNamesCache[serverID]) { + scope.metricNames = metricNamesCache[serverID]; return; } $http.get(url + 'api/metrics').success(function(metricNames) { - metricNamesCache[server_id] = metricNames; + metricNamesCache[serverID] = metricNames; scope.metricNames = metricNames; return; }).error(function() { diff --git a/app/assets/javascripts/angular/services/rickshaw_data_transformer.js b/app/assets/javascripts/angular/services/rickshaw_data_transformer.js index 07257368..401065b0 100644 --- a/app/assets/javascripts/angular/services/rickshaw_data_transformer.js +++ b/app/assets/javascripts/angular/services/rickshaw_data_transformer.js @@ -19,7 +19,7 @@ angular.module("Prometheus.services").factory('RickshawDataTransformer', [functi return tsName; } - return function(data, axisIdByExprId) { + return function(data, axisIDByExprID) { var series = []; for (var i = 0; i < data.length; i++) { if (!data[i]) { @@ -29,7 +29,7 @@ angular.module("Prometheus.services").factory('RickshawDataTransformer', [functi series = series.concat(data[i]['data'].Value.map(function(ts) { return { name: metricToTsName(ts.Metric), - axis_id: axisIdByExprId[data[i].exp_id], + axisID: axisIDByExprID[data[i].exp_id], exp_id: data[i].exp_id, labels: ts.Metric, data: ts.Values.map(function(value) { diff --git a/app/assets/javascripts/angular/services/servers_by_id_object.js b/app/assets/javascripts/angular/services/servers_by_id_object.js index 44a76d37..cba1e5b0 100644 --- a/app/assets/javascripts/angular/services/servers_by_id_object.js +++ b/app/assets/javascripts/angular/services/servers_by_id_object.js @@ -1,4 +1,4 @@ -angular.module("Prometheus.services").factory('ServersByIdObject', function() { +angular.module("Prometheus.services").factory('ServersByIDObject', function() { return function(servers) { var serversById = {}; for (var i = 0; i < servers.length; i++) { diff --git a/app/assets/javascripts/angular/services/shared_widget_setup.js b/app/assets/javascripts/angular/services/shared_widget_setup.js index 2f8b1ba4..ee340875 100644 --- a/app/assets/javascripts/angular/services/shared_widget_setup.js +++ b/app/assets/javascripts/angular/services/shared_widget_setup.js @@ -1,18 +1,18 @@ angular.module("Prometheus.services").factory('SharedWidgetSetup', ["$timeout", "VariableInterpolator", - "ServersByIdObject", + "ServersByIDObject", "ModalService", "WidgetLinkHelper", - "UrlHashEncoder", + "URLHashEncoder", "CheckWidgetMenuAlignment", "WidgetTabService", function($timeout, VariableInterpolator, - ServersByIdObject, + ServersByIDObject, ModalService, WidgetLinkHelper, - UrlHashEncoder, + URLHashEncoder, CheckWidgetMenuAlignment, WidgetTabService) { @@ -26,15 +26,15 @@ return function($scope) { graphBlob.globalConfig = dashboardData.globalConfig; WidgetLinkHelper .createLink({ - encoded_url: UrlHashEncoder(graphBlob), + encoded_url: URLHashEncoder(graphBlob), graph_title: $scope.graph.title, dashboard_name: dashboardName }, event) .setLink($scope) .highlightInput(event); }; - $scope.serversById = ServersByIdObject($scope.servers); - $scope.graph.legendSetting = $scope.graph.legendSetting || "sometimes"; + $scope.serversById = ServersByIDObject($scope.servers); + $scope.graph.showLegend = $scope.graph.showLegend || "sometimes"; $scope.$on('refreshDashboard', function(ev) { $scope.refreshGraph(); diff --git a/app/assets/javascripts/angular/services/url_config.js b/app/assets/javascripts/angular/services/url_config.js index 69f9c92c..2aab352a 100644 --- a/app/assets/javascripts/angular/services/url_config.js +++ b/app/assets/javascripts/angular/services/url_config.js @@ -1,4 +1,4 @@ -angular.module("Prometheus.services").factory('UrlConfigDecoder', function($location) { +angular.module("Prometheus.services").factory('URLConfigDecoder', function($location) { return function(defaultHash) { var hash = $location.hash() || defaultHash; if (!hash) { @@ -12,9 +12,9 @@ angular.module("Prometheus.services").factory('UrlConfigDecoder', function($loca }; }); -angular.module("Prometheus.services").factory('UrlHashEncoder', ["UrlConfigDecoder", function(UrlConfigDecoder) { +angular.module("Prometheus.services").factory('URLHashEncoder', ["URLConfigDecoder", function(URLConfigDecoder) { return function(config) { - var urlConfig = UrlConfigDecoder(); + var urlConfig = URLConfigDecoder(); for (var o in config) { urlConfig[o] = config[o]; } @@ -25,13 +25,13 @@ angular.module("Prometheus.services").factory('UrlHashEncoder', ["UrlConfigDecod }; }]); -angular.module("Prometheus.services").factory('UrlConfigEncoder', ["$location", "UrlHashEncoder", function($location, UrlHashEncoder) { +angular.module("Prometheus.services").factory('URLConfigEncoder', ["$location", "URLHashEncoder", function($location, URLHashEncoder) { return function(config) { - $location.hash(UrlHashEncoder(config)); + $location.hash(URLHashEncoder(config)); }; }]); -angular.module("Prometheus.services").factory('UrlVariablesDecoder', function($location) { +angular.module("Prometheus.services").factory('URLVariablesDecoder', function($location) { return function() { return $location.search(); }; diff --git a/app/assets/javascripts/controllers.js b/app/assets/javascripts/controllers.js index fecda888..b514391a 100644 --- a/app/assets/javascripts/controllers.js +++ b/app/assets/javascripts/controllers.js @@ -73,7 +73,6 @@ Prometheus.Graph = { getGraphDefaults: function() { return { title: "Title", - stacked: false, range: "1h", endTime: null, expressions: [], diff --git a/app/assets/javascripts/dashboards.js.coffee b/app/assets/javascripts/dashboards.js.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/dashboards.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/javascripts/servers.js.coffee b/app/assets/javascripts/servers.js.coffee deleted file mode 100644 index 24f83d18..00000000 --- a/app/assets/javascripts/servers.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://coffeescript.org/ diff --git a/app/assets/templates/expression_template.html b/app/assets/templates/expression_template.html index e56a208f..d1c0af55 100644 --- a/app/assets/templates/expression_template.html +++ b/app/assets/templates/expression_template.html @@ -2,22 +2,22 @@
-
diff --git a/app/assets/templates/frame_template.html b/app/assets/templates/frame_template.html index e8b8b1e7..1276e8f6 100644 --- a/app/assets/templates/frame_template.html +++ b/app/assets/templates/frame_template.html @@ -18,7 +18,7 @@
Frame Source
- +
diff --git a/app/assets/templates/graph_template.html.erb b/app/assets/templates/graph_template.html.erb index 04f4af5f..df229ab5 100644 --- a/app/assets/templates/graph_template.html.erb +++ b/app/assets/templates/graph_template.html.erb @@ -167,17 +167,17 @@
diff --git a/app/assets/templates/pie_template.html.erb b/app/assets/templates/pie_template.html.erb index fdfa804c..7230f5b7 100644 --- a/app/assets/templates/pie_template.html.erb +++ b/app/assets/templates/pie_template.html.erb @@ -60,17 +60,17 @@
diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb index c9aad3b8..2687bb35 100644 --- a/app/controllers/dashboards_controller.rb +++ b/app/controllers/dashboards_controller.rb @@ -1,14 +1,20 @@ require 'open-uri' class DashboardsController < ApplicationController + protect_from_forgery with: :exception, except: :update before_action :set_dashboard, only: [:edit, :destroy, :widgets, :clone] - before_action :set_dashboard_via_slug, only: [:show, :update] + before_action :set_dashboard_via_slug, only: [:show, :content, :update] before_action :set_directories, only: [:edit, :new, :clone] # GET /dashboards/1 # GET /dashboards/1.json def show - @directoryName = @dashboard.directory.name if @dashboard.directory - @servers = Server.order("lower(name)") + respond_to do |format| + format.html do + @directoryName = @dashboard.directory.name if @dashboard.directory + @servers = Server.order("lower(name)") + end + format.json + end end # GET /dashboards/new @@ -97,6 +103,10 @@ def set_directories # Never trust parameters from the scary internet, only allow the white list through. def dashboard_params + # We receive the "dashboard_json" column as an object, but need to turn it into JSON. + if params[:dashboard] && params[:dashboard][:dashboard_json] + params[:dashboard][:dashboard_json] = JSON.generate(params[:dashboard][:dashboard_json]) + end params.require(:dashboard).permit(:name, :dashboard_json, :slug, :directory_id) end end diff --git a/app/models/dashboard.rb b/app/models/dashboard.rb index 55ca1f47..9044d648 100644 --- a/app/models/dashboard.rb +++ b/app/models/dashboard.rb @@ -1,5 +1,17 @@ require 'slug_maker' +class DashboardJSONValidator < ActiveModel::Validator + def validate(record) + return unless record.dashboard_json + errors = JSON::Validator.fully_validate('dashboard_schema.json', record.dashboard_json, :validate_schema => true) + if !errors.empty? + errors.each do |e| + record.errors[:dashboard_json] << e + end + end + end +end + class Dashboard < ActiveRecord::Base belongs_to :directory has_many :shortened_urls @@ -12,6 +24,7 @@ class Dashboard < ActiveRecord::Base with: /\A[a-z0-9\-]+\z/, message: "Only alphanumeric characters connected by hyphens are allowed." } + validates_with DashboardJSONValidator scope :alphabetical, -> { order("lower(name)") } scope :cloneable, -> { where("dashboard_json is not null").select :id, :name } diff --git a/app/views/dashboards/show.html.erb b/app/views/dashboards/show.html.erb index 1b0a080d..4e68e7ea 100644 --- a/app/views/dashboards/show.html.erb +++ b/app/views/dashboards/show.html.erb @@ -99,12 +99,12 @@
-
+
diff --git a/app/views/dashboards/show.json.jbuilder b/app/views/dashboards/show.json.jbuilder index adb2184b..93de0ad1 100644 --- a/app/views/dashboards/show.json.jbuilder +++ b/app/views/dashboards/show.json.jbuilder @@ -1 +1,2 @@ json.extract! @dashboard, :name, :created_at, :updated_at +json.dashboard_json JSON.parse(@dashboard.dashboard_json) diff --git a/app/views/servers/index.html.erb b/app/views/servers/index.html.erb index 3b302fa8..a211ca3d 100644 --- a/app/views/servers/index.html.erb +++ b/app/views/servers/index.html.erb @@ -10,7 +10,7 @@ Name - Url + URL Actions diff --git a/app/views/servers/show.html.erb b/app/views/servers/show.html.erb index 2117b3c4..58224b85 100644 --- a/app/views/servers/show.html.erb +++ b/app/views/servers/show.html.erb @@ -5,7 +5,7 @@

- Url: + URL: <%= @server.url %>

diff --git a/config/application.rb b/config/application.rb index 31adb861..7cdad731 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,5 +21,9 @@ class Application < Rails::Application # config.i18n.default_locale = :de config.assets.precompile << /\.(?:svg|eot|woff|ttf)$/ + + # Required for working dashboard JSON PUTs. + # See http://stackoverflow.com/a/25428800/915941. + config.action_dispatch.perform_deep_munge = false end end diff --git a/dashboard_schema.json b/dashboard_schema.json new file mode 100644 index 00000000..5a1331fc --- /dev/null +++ b/dashboard_schema.json @@ -0,0 +1,351 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "additionalProperties": false, + "required": [ + "globalConfig", + "widgets" + ], + "properties": { + "globalConfig": { + "type": "object", + "additionalProperties": false, + "required": [ + //"aspectRatio", + "endTime", + //"keepURLUpdated", + "numColumns", + //"palette", + //"range", + //"refresh", + //"showTags", + //"showVars", + //"tags", + "theme", + "vars" + ], + "properties": { + "aspectRatio": { + "type": "number" + }, + "encodeEntireURL": { + "type": "boolean" + }, + "endTime": { + "type": [ + "integer", + "null" + ] + }, + "keepURLUpdated": { + "type": "boolean" + }, + "numColumns": { + "type": "integer" + }, + "palette": { + "type": "string" + }, + "range": { + "type": "string" + }, + "refresh": { + "type": "string" + }, + "showTags": { + "type": "boolean" + }, + "showVars": { + "type": "boolean" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "theme": { + "type": "string" + }, + "vars": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "widgets": { + "type": "array", + "items": { + "anyOf": [ + // "graph" widget type. + { + "type": "object", + "additionalProperties": false, + "required": [ + "axes", + "endTime", + "expressions", + //"interpolationMethod", + //"legendFormatStrings", + "showLegend", + //"palette", + "range", + //"tags", + "title", + "type" + ], + "properties": { + "axes": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + "format", + "id", + "orientation", + //"renderer", + "scale" + //"yMax", + //"yMin" + ], + "properties": { + "format": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "orientation": { + "type": "string" + }, + "renderer": { + "type": "string" + }, + "scale": { + "type": "string" + }, + "yMax": { + "type": "string" + }, + "yMin": { + "type": "string" + } + } + } + }, + "endTime": { + "type": [ + "integer", + "null" + ] + }, + "expressions": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": [ + // TODO: Make all underscore names be camelcase. + "axisID", + "expression", + "id", + "serverID" + ], + "properties": { + "axisID": { + "type": "integer", + "minimum": 1, + "maximum": 2 + }, + "expression": { + "type": "string" + }, + "id": { + "type": "integer", + "minimum": 1 + }, + "id": { + "type": "integer", + "minimum": 0 + }, + "legendID": { + "type": [ + // TODO: Check if we should standardize on either integers + // or strings for legend IDs. + "integer", + "string", + "null" + ], + "minimum": 1 + }, + "serverID": { + "type": "integer", + "minimum": 1 + } + } + } + }, + "interpolationMethod": { + "type": "string" + }, + "legendFormatStrings": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": [ + // TODO: Check if we should standardize on either integers + // or strings for legend IDs. + "integer", + "string" + ] + } + } + } + }, + "showLegend": { + "type": "string" + }, + "palette": { + "type": "string" + }, + "range": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "title": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["graph"] + } + } + }, + // "pie" widget type. + { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + //"legendFormatString", + "showLegend", + "title", + "type" + ], + "properties": { + "expression": { + "type": "object", + "additionalProperties": false, + "required": [ + "expression", + "id", + "legendID", + "serverID" + ], + "properties": { + "expression": { + "type": "string" + }, + // TODO: Can we get rid of this for the pie chart? + "id": { + "type": "integer", + "enum": [0] + }, + // TODO: Can we get rid of this for the pie chart? + "legendID": { + "type": "integer", + "enum": [1] + }, + "serverID": { + "type": "integer", + "minimum": 1 + } + } + }, + "legendFormatString": { + "type": "string" + }, + "showLegend": { + "type": "string", + "enum": [ + "sometimes", + "always", + "never" + ] + }, + "title": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["pie"] + } + } + }, + // "frame" widget type. + { + "type": "object", + "additionalProperties": false, + "required": [ + //"endTime", + //"graphite", + //"range", + "title", + "type", + "url" + ], + "properties": { + "endTime": { + "type": [ + "integer", + "null" + ] + }, + "graphite": { + "type": "boolean" + }, + "range": { + "type": "string" + }, + "title": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["frame"] + }, + "url": { + "type": [ + "null", + "string" + ] + } + } + } + ] + } + } + } +} diff --git a/db/migrate/.20141107013708_update_dashboard_json_for_api.rb.swp b/db/migrate/.20141107013708_update_dashboard_json_for_api.rb.swp new file mode 100644 index 0000000000000000000000000000000000000000..ad9bf0966b80ec3c2acf2db0e66f5a13d88c76e4 GIT binary patch literal 12288 zcmeI2zi-n(6vwa36e^gI7%m0LEoz;lMQT;ip|l{SYN0@5OQJ*w)>-DW$;7 z%*Ow~#KOXg#K3?AVrSvs;GLZxGzx_k^_}#M?R($zyZhW_i1NJ3g{T^GFPe2&Q<2?St_x&Ze3m?l&tUTB(66SRTpXKD(^2dp)e6R zX9P0Sn4TYHvoqC-UHOgi62E$7=bS;H>Y5g-CYfCvzQ^G(2{BkUz2xX{6H zy>lHIxON`tLIj8a5g-CYfCvx)B0vO)01+SpM1TnVhXjOVZ0RCnpHcSz|L*VqkCz#H zk9vc8hB`)VqAI8{)Xz(deL{Uey+gf5wNT5bYp5TijJ-uYN7YfAs2XY+br&^*`i?oj zbaH>Q`Kb{BB0vO)01+SpM1Tko0U|&I{*eGcEE(s;C+A9p8u*fL!b8{sKT&dFp{5iZ zg3_TE!Fpa=947G;A72)+V|Wq6F&j0$y8ZA0_ahK-mhz$s8Lg#pOdz4bHAD?pCheI0 zma&@|Ev^=aP{NpP(mh?M%on+>S7v!IzjXthd8G*_f&gE#4aS6V;7oBxH1@900-Mqr@FS*hUO~fRuA(u#Y1gns8fSN z$Q@_#@CT#-Ou$xwPbA)ccR_vD{T(lOwp|Gkpxe<#_y-_+bF}e( zv{=IZM74z3Hp&9;zMS*0!P~ozd76T=#}^m5gJtMx)9Gvh&STeUhcW=8ojn(X-{dmk zC+(BIp*#6hC=C;XM_|&P8`}m0JW)J`Bg1TPrCZ [ - {"title"=>"Graph 1"}, - {"title"=>"Title"} - ]}.to_json + @dashboard.dashboard_json = @sample_dashboard_json @dashboard.save! end @@ -26,7 +24,7 @@ end it "clones the dashboard given a source_id" do - d = Dashboard.new_with_slug(name: "example dash", dashboard_json: {some: "key"}.to_json) + d = Dashboard.new_with_slug(name: "example dash", dashboard_json: @sample_dashboard_json) d.save! post :create, dashboard: { name: "dashboard clone"}, source_id: d.id expect(Dashboard.last.dashboard_json).to eq(d.dashboard_json) @@ -34,7 +32,7 @@ it "clones a dashboard and associates it with a directory" do directory = FactoryGirl.create :directory - d = Dashboard.new_with_slug(name: "example dash", dashboard_json: {some: "key"}.to_json) + d = Dashboard.new_with_slug(name: "example dash", dashboard_json: @sample_dashboard_json) d.save! post :create, dashboard: { name: "dashboard clone", directory_id: directory.id }, source_id: d.id expect(Dashboard.last.directory_id).to eq(directory.id) @@ -67,8 +65,9 @@ context "getting widgets json" do it "dashboard_json exists" do get :widgets, id: @dashboard - widgets = { widgets: [{title: "Graph 1"}, {title: "Title"}]} - expect(response.body).to eq(widgets.to_json) + widgets_json = JSON.parse(@sample_dashboard_json) + widgets_json.delete('globalConfig') + expect(JSON.parse(response.body)).to eq(widgets_json) end it "dashboard_json is nil" do diff --git a/spec/javascripts/angular/services/servers_by_id_object_spec.js b/spec/javascripts/angular/services/servers_by_id_object_spec.js index 332908da..7246511e 100644 --- a/spec/javascripts/angular/services/servers_by_id_object_spec.js +++ b/spec/javascripts/angular/services/servers_by_id_object_spec.js @@ -1,5 +1,5 @@ //= require spec_helper -var serversById = getService('ServersByIdObject'); +var serversById = getService('ServersByIDObject'); describe('ThemeManager', function() { it('returns a server object keyed by id', function() { var server1 = {name:'server1', id:1} diff --git a/spec/support/sample_json/1_expression_dashboard_json b/spec/support/sample_json/1_expression_dashboard_json index 94090b30..26200f0f 100644 --- a/spec/support/sample_json/1_expression_dashboard_json +++ b/spec/support/sample_json/1_expression_dashboard_json @@ -6,18 +6,17 @@ "vars":{}}, "widgets":[ {"title":"Title", - "stacked":false, "range":"1h", "endTime":null, "expressions":[ - {"server_id":1, + {"serverID":1, "id":0, - "legend_id":1, - "axis_id":1, + "legendID":1, + "axisID":1, "expression":"prometheus_targetpool_duration_ms"} ], "type":"graph", - "legendSetting":"sometimes", + "showLegend":"sometimes", "interpolationMethod":"cardinal", "axes":[ {"orientation":"left", diff --git a/spec/support/sample_json/2_expression_dashboard_json b/spec/support/sample_json/2_expression_dashboard_json index 3cde7d13..5ee3f338 100644 --- a/spec/support/sample_json/2_expression_dashboard_json +++ b/spec/support/sample_json/2_expression_dashboard_json @@ -6,27 +6,26 @@ "vars":{}}, "widgets":[ {"title":"Title", - "stacked":false, "range":"1h", "endTime":null, "legendFormatStrings": [ - {"id": 1, "name": "string 1", "hidden": false }, - {"id": 2, "name": "string 2", "hidden": false } + {"id": 1, "name": "string 1" }, + {"id": 2, "name": "string 2" } ], "expressions":[ - {"server_id":1, + {"serverID":1, "id":0, - "legend_id":1, - "axis_id":1, + "legendID":1, + "axisID":1, "expression":"prometheus_targetpool_duration_ms"}, - {"server_id":1, + {"serverID":1, "id":1, - "legend_id":2, - "axis_id":1, + "legendID":2, + "axisID":1, "expression":"prometheus_targetpool_duration_ms"} ], "type":"graph", - "legendSetting":"always", + "showLegend":"always", "interpolationMethod":"cardinal", "axes":[ {"orientation":"left", diff --git a/spec/support/sample_json/pie_chart_dashboard_json b/spec/support/sample_json/pie_chart_dashboard_json index d9e0006d..b1354e7e 100644 --- a/spec/support/sample_json/pie_chart_dashboard_json +++ b/spec/support/sample_json/pie_chart_dashboard_json @@ -1 +1 @@ -{"globalConfig":{"numColumns":1,"aspectRatio":0.4166666666666667,"theme":"dark_theme","endTime":null,"vars":{}},"widgets":[{"title":"My awesome pie-chart","expression":{"id":0,"server_id":1,"expression":"cpu_idle"},"type":"pie","legendSetting":"always","legendFormatStrings":[{"id":1,"name":""}],"interpolationMethod":"cardinal","dimensions":["cluster","instance","job","name","owner"],"chosenDimension":"instance","legendFormatString":""}]} \ No newline at end of file +{"globalConfig":{"numColumns":1,"aspectRatio":0.75,"theme":"dark_theme","endTime":null,"vars":{},"tags":[],"palette":"colorwheel"},"widgets":[{"title":"My awesome pie-chart","expression":{"id":0,"serverID":1,"expression":"cpu_idle","legendID":1},"type":"pie","showLegend":"always"}]}