From d5564b476f1a89f346692448db3e6206576c7514 Mon Sep 17 00:00:00 2001 From: Eric Heydrick Date: Thu, 12 Mar 2015 15:03:59 -0700 Subject: [PATCH 01/18] correct spelling --- docs/sources/guides/changes_in_v2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/guides/changes_in_v2.md b/docs/sources/guides/changes_in_v2.md index f5bf9d02617..c9d08f046bd 100644 --- a/docs/sources/guides/changes_in_v2.md +++ b/docs/sources/guides/changes_in_v2.md @@ -99,8 +99,8 @@ Here you can update your user details, UI Theme and change password. ## PNG rendering In the panel share dialog you now have access to a link that will render the panel to a PNG image. -The panel is rendered on the backend via phantomjs (headless browser). This requires that you metric -data source is accessable from your Grafana server host machine. +The panel is rendered on the backend via phantomjs (headless browser). This requires that your metric +data source is accessible from your Grafana server host machine. ![](/img/v2/share_dialog_image_highlight.jpg) From 068ccf1c0fb6d386e9417dc762f65d72fb977fd2 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 17 Mar 2015 23:07:08 +0800 Subject: [PATCH 02/18] fixes 1606 emit OrgCreated event when new org created. --- pkg/services/sqlstore/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 81d4067bf5e..4dc289aa04d 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -55,7 +55,11 @@ func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) { if _, err := sess.Insert(&org); err != nil { return 0, err } - + sess.publishAfterCommit(&events.OrgCreated{ + Timestamp: org.Created, + Id: org.Id, + Name: org.Name, + }) return org.Id, nil } From 6a09a7c3982f5ddab398eeefc3d14c5b5396d305 Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 17 Mar 2015 23:07:08 +0800 Subject: [PATCH 03/18] fixes #1606 emit OrgCreated event when new org created. --- pkg/services/sqlstore/user.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 81d4067bf5e..4dc289aa04d 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -55,7 +55,11 @@ func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) { if _, err := sess.Insert(&org); err != nil { return 0, err } - + sess.publishAfterCommit(&events.OrgCreated{ + Timestamp: org.Created, + Id: org.Id, + Name: org.Name, + }) return org.Id, nil } From bce62c49d04ca6e3e48b1100a993186e6033130d Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 17 Mar 2015 23:10:49 +0800 Subject: [PATCH 04/18] fixes #1606 --- pkg/services/sqlstore/user.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 4dc289aa04d..de4ad70164f 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -55,11 +55,13 @@ func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) { if _, err := sess.Insert(&org); err != nil { return 0, err } + sess.publishAfterCommit(&events.OrgCreated{ Timestamp: org.Created, Id: org.Id, Name: org.Name, }) + return org.Id, nil } From fc5839d8f13dbd88065c939ae636c5c92c8b3c0e Mon Sep 17 00:00:00 2001 From: Anthony Woods Date: Tue, 17 Mar 2015 23:42:16 +0800 Subject: [PATCH 05/18] gofmt. remove extra whitespace --- pkg/services/sqlstore/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index de4ad70164f..61798d724ef 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -61,7 +61,7 @@ func getOrgIdForNewUser(userEmail string, sess *session) (int64, error) { Id: org.Id, Name: org.Name, }) - + return org.Id, nil } From bb5eeee82e9d2e940bde7f1d991df573624b12aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 17 Mar 2015 17:33:31 -0400 Subject: [PATCH 06/18] fixed small issue with share modal --- src/app/directives/tip.js | 5 +++-- .../dashboard/partials/shareModal.html | 6 +++--- src/app/features/dashboard/sharePanelCtrl.js | 18 ++++++++---------- src/test/specs/sharePanelCtrl-specs.js | 6 +++--- 4 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/app/directives/tip.js b/src/app/directives/tip.js index f86668461a4..edf57d47828 100644 --- a/src/app/directives/tip.js +++ b/src/app/directives/tip.js @@ -57,14 +57,15 @@ function (angular, kbn) { angular .module('grafana.directives') - .directive('editorCheckbox', function($compile) { + .directive('editorCheckbox', function($compile, $interpolate) { return { restrict: 'E', link: function(scope, elem, attrs) { + var text = $interpolate(attrs.text)(scope); var ngchange = attrs.change ? (' ng-change="' + attrs.change + '"') : ''; var tip = attrs.tip ? (' ' + attrs.tip + '') : ''; var label = ''; + text + tip + ''; var template = '
- +
- +
- +
diff --git a/src/app/features/dashboard/sharePanelCtrl.js b/src/app/features/dashboard/sharePanelCtrl.js index 42a0e26fbee..eca10678806 100644 --- a/src/app/features/dashboard/sharePanelCtrl.js +++ b/src/app/features/dashboard/sharePanelCtrl.js @@ -13,15 +13,13 @@ function (angular, _, require, config) { $scope.init = function() { $scope.editor = { index: 0 }; - $scope.forCurrent = true; + $scope.options = { + forCurrent: true, + toPanel: $scope.panel ? true : false, + includeTemplateVars: true + }; - if ($scope.panel) { - $scope.toPanel = true; - } - - $scope.includeTemplateVars = true; $scope.buildUrl(); - }; $scope.buildUrl = function() { @@ -38,7 +36,7 @@ function (angular, _, require, config) { params.from = range.from; params.to = range.to; - if ($scope.includeTemplateVars) { + if ($scope.options.includeTemplateVars) { _.each(templateSrv.variables, function(variable) { params['var-' + variable.name] = variable.current.text; }); @@ -49,12 +47,12 @@ function (angular, _, require, config) { }); } - if (!$scope.forCurrent) { + if (!$scope.options.forCurrent) { delete params.from; delete params.to; } - if ($scope.toPanel) { + if ($scope.options.toPanel) { params.panelId = $scope.panel.id; params.fullscreen = true; } else { diff --git a/src/test/specs/sharePanelCtrl-specs.js b/src/test/specs/sharePanelCtrl-specs.js index acfc849db25..bdbc3cfe5c3 100644 --- a/src/test/specs/sharePanelCtrl-specs.js +++ b/src/test/specs/sharePanelCtrl-specs.js @@ -42,7 +42,7 @@ define([ it('should remove panel id when toPanel is false', function() { ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; - ctx.scope.toPanel = false; + ctx.scope.options = { toPanel: false, forCurrent: true }; setTime({ from: 'now-1h', to: 'now' }); ctx.scope.buildUrl(); @@ -52,8 +52,8 @@ define([ it('should include template variables in url', function() { ctx.$location.path('/test'); ctx.scope.panel = { id: 22 }; - ctx.scope.includeTemplateVars = true; - ctx.scope.toPanel = false; + ctx.scope.options = { includeTemplateVars: true, toPanel: false, forCurrent: true }; + ctx.templateSrv.variables = [{ name: 'app', current: {text: 'mupp' }}, {name: 'server', current: {text: 'srv-01'}}]; setTime({ from: 'now-1h', to: 'now' }); From d08144e73091a96296387f927a2d69f52adede5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 11:15:21 -0400 Subject: [PATCH 07/18] Began work on template multi select feature --- src/app/controllers/search.js | 18 --------- src/app/directives/all.js | 1 + src/app/directives/giveFocus.js | 26 ++++++++++++ src/app/directives/templateParamSelector.js | 40 +++++++++++++++++++ .../partials/variableValueSelect.html | 23 +++++++++++ src/app/partials/search.html | 2 +- src/app/partials/submenu.html | 24 +++++++---- src/css/less/submenu.less | 29 +++++++++++++- 8 files changed, 136 insertions(+), 27 deletions(-) create mode 100644 src/app/directives/giveFocus.js create mode 100644 src/app/features/dashboard/partials/variableValueSelect.html diff --git a/src/app/controllers/search.js b/src/app/controllers/search.js index e031559f3a8..00ad972e60a 100644 --- a/src/app/controllers/search.js +++ b/src/app/controllers/search.js @@ -136,24 +136,6 @@ function (angular, _, config) { }); - module.directive('xngFocus', function() { - return function(scope, element, attrs) { - element.click(function(e) { - e.stopPropagation(); - }); - - scope.$watch(attrs.xngFocus,function (newValue) { - if (!newValue) { - return; - } - setTimeout(function() { - element.focus(); - var pos = element.val().length * 2; - element[0].setSelectionRange(pos, pos); - }, 200); - },true); - }; - }); module.directive('tagColorFromName', function() { diff --git a/src/app/directives/all.js b/src/app/directives/all.js index 6190ef89099..3ef0b669d6c 100644 --- a/src/app/directives/all.js +++ b/src/app/directives/all.js @@ -16,4 +16,5 @@ define([ './grafanaVersionCheck', './dropdown.typeahead', './topnav', + './giveFocus', ], function () {}); diff --git a/src/app/directives/giveFocus.js b/src/app/directives/giveFocus.js new file mode 100644 index 00000000000..ac4a80e6197 --- /dev/null +++ b/src/app/directives/giveFocus.js @@ -0,0 +1,26 @@ +define([ + 'angular', +], +function (angular) { + 'use strict'; + + var module = angular.module('grafana.directives'); + + module.directive('giveFocus', function() { + return function(scope, element, attrs) { + element.click(function(e) { + e.stopPropagation(); + }); + + scope.$watch(attrs.giveFocus, function (newValue) { + if (!newValue) { return; } + + setTimeout(function() { + element.focus(); + var pos = element.val().length * 2; + element[0].setSelectionRange(pos, pos); + }, 200); + },true); + }; + }); +}); diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index d9ad33512b5..a7ae67e363c 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -84,4 +84,44 @@ function (angular, app, _, $) { } }; }); + + angular + .module('grafana.directives') + .directive('variableValueSelect', function($compile, $window, $timeout) { + return { + scope: { + variable: "=", + }, + templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', + link: function(scope, elem) { + var bodyEl = angular.element($window.document.body); + + scope.show = function() { + scope.selectorOpen = true; + scope.giveFocus = 1; + + $timeout(function() { + bodyEl.on('click', scope.bodyOnClick); + }, 0, false); + }; + + scope.hide = function() { + scope.selectorOpen = false; + bodyEl.off('click', scope.bodyOnClick); + }; + + + scope.bodyOnClick = function(e) { + var dropdown = elem.find('.variable-value-dropdown'); + if (dropdown.has(e.target).length === 0) { + scope.$apply(scope.hide); + } + }; + + scope.$on('$destroy', function() { + }); + }, + }; + }); + }); diff --git a/src/app/features/dashboard/partials/variableValueSelect.html b/src/app/features/dashboard/partials/variableValueSelect.html new file mode 100644 index 00000000000..2a923f14ab6 --- /dev/null +++ b/src/app/features/dashboard/partials/variableValueSelect.html @@ -0,0 +1,23 @@ + + {{variable.name}}: {{variable.current.text}} + + +
+
+ + + +
+ +
+
+ + + +
+
+
+ + diff --git a/src/app/partials/search.html b/src/app/partials/search.html index 76400cd3eba..1a28ddc74a2 100644 --- a/src/app/partials/search.html +++ b/src/app/partials/search.html @@ -2,7 +2,7 @@
-
diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index 416fb47558b..c5e2762985e 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -3,17 +3,27 @@
    -
  • - VARIABLES: -
  • -
  • - - ${{variable.name}}: - + +
  • + + + + + + + + + + + + +
  • diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 74a2664d3ce..32820b9f051 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -5,7 +5,7 @@ } .submenu-controls { - margin: 10px 10px 0 10px; + margin: 20px 0px 15px 20px; } .annotation-disabled, .annotation-disabled a { @@ -18,3 +18,30 @@ } } +.variable-value-link { + font-size: 16px; + margin-right: 20px; +} + +.variable-value-dropdown { + position: absolute; + top: 30px; + width: 300px; + height: 400px; + background: @grafanaPanelBackground; + box-shadow: 0px 0px 55px 0px black; + border: 1px solid @grafanaTargetFuncBackground; + z-index: 1000; + padding: 10px; + + .variable-options-container { + height: 350px; + overflow: auto; + display: block; + line-height: 28px; + } + + .variable-option { + display: block; + } +} From 080847ec4d26d353b7769fd6f83629ee80497f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 12:47:45 -0400 Subject: [PATCH 08/18] Change: shared tooltip is now enabled by default in graph panel --- src/app/panels/graph/module.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index d14d30fd300..1a554f600c9 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -95,7 +95,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { // tooltip options tooltip : { value_type: 'cumulative', - shared: false, + shared: true, }, // time overrides timeFrom: null, From eb8b9c4ac3c79c3ebe8ce65b3fbfd2a5034a318f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 13:20:15 -0400 Subject: [PATCH 09/18] Increase api_key.key varchar to 255 length --- pkg/services/sqlstore/migrations/apikey_mig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/apikey_mig.go b/pkg/services/sqlstore/migrations/apikey_mig.go index e25b57c77ef..a02e759f551 100644 --- a/pkg/services/sqlstore/migrations/apikey_mig.go +++ b/pkg/services/sqlstore/migrations/apikey_mig.go @@ -42,7 +42,7 @@ func addApiKeyMigrations(mg *Migrator) { {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "org_id", Type: DB_BigInt, Nullable: false}, {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, - {Name: "key", Type: DB_Varchar, Length: 64, Nullable: false}, + {Name: "key", Type: DB_Varchar, Length: 255, Nullable: false}, {Name: "role", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, From d81d0c8c44d1424c9df568d432335bac25dc46fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 18:51:29 -0400 Subject: [PATCH 10/18] Graph: Adds logarithmic scale option (log base 10), Closes #452 --- CHANGELOG.md | 1 + src/app/panels/graph/axisEditor.html | 12 +++ src/app/panels/graph/graph.js | 130 +++++++++++++++++--------- src/app/panels/graph/graph.tooltip.js | 2 +- src/app/panels/graph/module.js | 4 + 5 files changed, 106 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23cc05810eb..0ade6b78d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [Issue #171](https://github.com/grafana/grafana/issues/171). Panel: Different time periods, panels can override dashboard relative time and/or add a time shift - [Issue #1488](https://github.com/grafana/grafana/issues/1488). Dashboard: Clone dashboard / Save as - [Issue #1458](https://github.com/grafana/grafana/issues/1458). User: persisted user option for dark or light theme (no longer an option on a dashboard) +- [Issue #452](https://github.com/grafana/grafana/issues/452). Graph: Adds logarithmic scale option (log base 10) **Enhancements** - [Issue #1366](https://github.com/grafana/grafana/issues/1366). Graph & Singlestat: Support for additional units, Fahrenheit (°F) and Celsius (°C), Humidity (%H), kW, watt-hour (Wh), kilowatt-hour (kWh), velocities (m/s, km/h, mpg, knot) diff --git a/src/app/panels/graph/axisEditor.html b/src/app/panels/graph/axisEditor.html index edf61a04cd3..40391995fc4 100644 --- a/src/app/panels/graph/axisEditor.html +++ b/src/app/panels/graph/axisEditor.html @@ -30,6 +30,12 @@ empty-to-null ng-model="panel.grid.leftMin" ng-change="render()" ng-model-onblur> +
  • + Scale type +
  • +
  • + +
  • Label
  • @@ -69,6 +75,12 @@ empty-to-null ng-model="panel.grid.rightMin" ng-change="render()" ng-model-onblur> +
  • + Scale type +
  • +
  • + +
  • Label
  • diff --git a/src/app/panels/graph/graph.js b/src/app/panels/graph/graph.js index 967991e5da8..5e2066466ca 100755 --- a/src/app/panels/graph/graph.js +++ b/src/app/panels/graph/graph.js @@ -27,6 +27,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { var dashboard = scope.dashboard; var data, annotations; var sortedSeries; + var graphHeight; var legendSideLastValue = null; scope.crosshairEmiter = false; @@ -64,19 +65,19 @@ function (angular, $, kbn, moment, _, GraphTooltip) { function setElementHeight() { try { - var height = scope.height || scope.panel.height || scope.row.height; - if (_.isString(height)) { - height = parseInt(height.replace('px', ''), 10); + graphHeight = scope.height || scope.panel.height || scope.row.height; + if (_.isString(graphHeight)) { + graphHeight = parseInt(graphHeight.replace('px', ''), 10); } - height -= 5; // padding - height -= scope.panel.title ? 24 : 9; // subtract panel title bar + graphHeight -= 5; // padding + graphHeight -= scope.panel.title ? 24 : 9; // subtract panel title bar if (scope.panel.legend.show && !scope.panel.legend.rightSide) { - height = height - 26; // subtract one line legend + graphHeight = graphHeight - 26; // subtract one line legend } - elem.css('height', height + 'px'); + elem.css('height', graphHeight + 'px'); return true; } catch(e) { // IE throws errors sometimes @@ -349,6 +350,8 @@ function (angular, $, kbn, moment, _, GraphTooltip) { position: 'left', show: scope.panel['y-axis'], min: scope.panel.grid.leftMin, + index: 1, + scale: scope.panel.grid.leftScale, max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax, }; @@ -356,16 +359,59 @@ function (angular, $, kbn, moment, _, GraphTooltip) { if (_.findWhere(data, {yaxis: 2})) { var secondY = _.clone(defaults); + secondY.index = 2, + secondY.scale = scope.panel.grid.rightScale; secondY.position = 'right'; secondY.min = scope.panel.grid.rightMin; secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax; options.yaxes.push(secondY); + + applyLogScale(options.yaxes[1], data); configureAxisMode(options.yaxes[1], scope.panel.y_formats[1]); } + applyLogScale(options.yaxes[0], data); configureAxisMode(options.yaxes[0], scope.panel.y_formats[0]); } + function applyLogScale(axis, data) { + if (axis.scale !== 2) { + return; + } + + var series, i; + var max = axis.max; + + if (max === null) { + for (i = 0; i < data.length; i++) { + series = data[i]; + if (series.yaxis === axis.index) { + if (max < series.stats.max) { + max = series.stats.max; + } + } + } + + if (max === null) { + max = 10000000000; + } + } + + axis.ticks = [0, 1]; + var tick = 1; + + while (true) { + tick = tick*10; + axis.ticks.push(tick); + if (tick > max) { + break; + } + } + + axis.transform = function(v) { return Math.log(v+0.1); }; + axis.inverseTransform = function (v) { return Math.pow(10,v); }; + } + function configureAxisMode(axis, format) { axis.tickFormatter = function(val, axis) { return kbn.valueFormats[format](val, axis.tickDecimals, axis.scaledDecimals); @@ -411,44 +457,44 @@ function (angular, $, kbn, moment, _, GraphTooltip) { url += scope.panel['y-axis'] ? '' : '&hideYAxis=true'; switch(scope.panel.y_formats[0]) { - case 'bytes': - url += '&yUnitSystem=binary'; - break; - case 'bits': - url += '&yUnitSystem=binary'; - break; - case 'bps': - url += '&yUnitSystem=si'; - break; - case 'Bps': - url += '&yUnitSystem=si'; - break; - case 'short': - url += '&yUnitSystem=si'; - break; - case 'joule': - url += '&yUnitSystem=si'; - break; - case 'watt': - url += '&yUnitSystem=si'; - break; - case 'ev': - url += '&yUnitSystem=si'; - break; - case 'none': - url += '&yUnitSystem=none'; - break; + case 'bytes': + url += '&yUnitSystem=binary'; + break; + case 'bits': + url += '&yUnitSystem=binary'; + break; + case 'bps': + url += '&yUnitSystem=si'; + break; + case 'Bps': + url += '&yUnitSystem=si'; + break; + case 'short': + url += '&yUnitSystem=si'; + break; + case 'joule': + url += '&yUnitSystem=si'; + break; + case 'watt': + url += '&yUnitSystem=si'; + break; + case 'ev': + url += '&yUnitSystem=si'; + break; + case 'none': + url += '&yUnitSystem=none'; + break; } switch(scope.panel.nullPointMode) { - case 'connected': - url += '&lineMode=connected'; - break; - case 'null': - break; // graphite default lineMode - case 'null as zero': - url += "&drawNullAsZero=true"; - break; + case 'connected': + url += '&lineMode=connected'; + break; + case 'null': + break; // graphite default lineMode + case 'null as zero': + url += "&drawNullAsZero=true"; + break; } url += scope.panel.steppedLine ? '&lineMode=staircase' : ''; diff --git a/src/app/panels/graph/graph.tooltip.js b/src/app/panels/graph/graph.tooltip.js index d4690bea96d..724588c4fd5 100644 --- a/src/app/panels/graph/graph.tooltip.js +++ b/src/app/panels/graph/graph.tooltip.js @@ -99,7 +99,7 @@ function ($) { var group, value, timestamp, hoverInfo, i, series, seriesHtml; if(dashboard.sharedCrosshair){ - scope.appEvent('setCrosshair', { pos: pos, scope: scope }); + scope.appEvent('setCrosshair', { pos: pos, scope: scope }); } if (seriesList.length === 0) { diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 1a554f600c9..485e7593b73 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -53,10 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { y_formats : ['short', 'short'], // grid options grid : { + leftScale: 1, leftMax: null, rightMax: null, leftMin: null, rightMin: null, + rightScale: 1, threshold1: null, threshold2: null, threshold1Color: 'rgba(216, 200, 27, 0.27)', @@ -114,6 +116,8 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { _.defaults($scope.panel.grid, _d.grid); _.defaults($scope.panel.legend, _d.legend); + $scope.scaleTypes = {'linear': 1, 'log (base 10)': 2}; + $scope.hiddenSeries = {}; $scope.seriesList = []; $scope.unitFormats = kbn.getUnitFormats(); From 94415e2b6066b8c3ea9c2b4c8ff21d61156cad4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 19:42:18 -0400 Subject: [PATCH 11/18] Fix for panel height in fullscreen mode --- src/app/features/panel/panelDirective.js | 4 ++-- src/app/partials/dasheditor.html | 1 - src/css/less/variables.dark.less | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/app/features/panel/panelDirective.js b/src/app/features/panel/panelDirective.js index f8048d53637..7330bb627de 100644 --- a/src/app/features/panel/panelDirective.js +++ b/src/app/features/panel/panelDirective.js @@ -30,8 +30,8 @@ function (angular, $, config) { link: function(scope, elem) { var panelContainer = elem.find('.panel-container'); - scope.$watchGroup(['fullscreen', 'panel.height', 'row.height'], function() { - panelContainer.css({ minHeight: scope.panel.height || scope.row.height, display: 'block' }); + scope.$watchGroup(['fullscreen', 'height', 'panel.height', 'row.height'], function() { + panelContainer.css({ minHeight: scope.height || scope.panel.height || scope.row.height, display: 'block' }); elem.toggleClass('panel-fullscreen', scope.fullscreen ? true : false); }); } diff --git a/src/app/partials/dasheditor.html b/src/app/partials/dasheditor.html index a9de9af2acb..e6ab6070f9f 100644 --- a/src/app/partials/dasheditor.html +++ b/src/app/partials/dasheditor.html @@ -31,7 +31,6 @@
-
diff --git a/src/css/less/variables.dark.less b/src/css/less/variables.dark.less index 6aab758ad82..9a7613ff311 100644 --- a/src/css/less/variables.dark.less +++ b/src/css/less/variables.dark.less @@ -28,7 +28,7 @@ // grafana Variables // ------------------------- @grafanaPanelBackground: @grayDarker; -@grafanaPanelBorder: solid 1px @grayDark; +@grafanaPanelBorder: solid 1px @grayDark; @grafanaTriggerBorder: solid 1px #555; // Graphite Target Editor From c1d6fcd18d2392ebf3807e68236861470bac5f1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 18 Mar 2015 19:57:54 -0400 Subject: [PATCH 12/18] Fixed full screen edit css issue where part of the dashboard became visible --- src/css/less/grafana.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/css/less/grafana.less b/src/css/less/grafana.less index b6c127c5319..579415f17f8 100644 --- a/src/css/less/grafana.less +++ b/src/css/less/grafana.less @@ -62,6 +62,7 @@ .main-view-container { overflow: hidden; height: 0; + padding: 0; .row-control-inner { display: none; } From 30047e6a9f1ba85e4dc6eab87d742b5d785d6329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Mar 2015 10:17:16 -0400 Subject: [PATCH 13/18] Updated macaron and ini package --- Godeps/Godeps.json | 6 +- .../src/github.com/Unknwon/macaron/README.md | 11 +- .../Unknwon/macaron/bpool/README.md | 37 ------ .../Unknwon/macaron/bpool/bufferpool.go | 45 -------- .../src/github.com/Unknwon/macaron/context.go | 81 +++++++++++-- .../Unknwon/macaron/context_test.go | 106 +++++++++++++----- .../Unknwon/macaron/inject/inject_test.go | 2 +- .../github.com/Unknwon/macaron/logger_test.go | 2 +- .../src/github.com/Unknwon/macaron/macaron.go | 4 +- .../src/github.com/Unknwon/macaron/render.go | 42 ++++++- .../Unknwon/macaron/return_handler.go | 46 ++++---- .../Unknwon/macaron/return_handler_test.go | 2 +- .../_workspace/src/gopkg.in/ini.v1/.gitignore | 4 +- .../_workspace/src/gopkg.in/ini.v1/README.md | 36 +++++- .../src/gopkg.in/ini.v1/README_ZH.md | 34 +++++- Godeps/_workspace/src/gopkg.in/ini.v1/ini.go | 106 ++++++++++++++++-- .../src/gopkg.in/ini.v1/ini_test.go | 51 ++++++++- .../_workspace/src/gopkg.in/ini.v1/struct.go | 18 ++- .../src/gopkg.in/ini.v1/struct_test.go | 44 ++++---- pkg/cmd/web.go | 4 + 20 files changed, 475 insertions(+), 206 deletions(-) delete mode 100644 Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md delete mode 100644 Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index 323c3acfa79..79b49db2f0a 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -11,7 +11,7 @@ }, { "ImportPath": "github.com/Unknwon/macaron", - "Rev": "da7cbddc50b9d33e076fb1eabff13b55c3b85fc5" + "Rev": "93de4f3fad97bf246b838f828e2348f46f21f20a" }, { "ImportPath": "github.com/codegangsta/cli", @@ -72,8 +72,8 @@ }, { "ImportPath": "gopkg.in/ini.v1", - "Comment": "v0-10-g28ad8c4", - "Rev": "28ad8c408ba20e5c86b06d64cd2cc9248f640a83" + "Comment": "v0-16-g1772191", + "Rev": "177219109c97e7920c933e21c9b25f874357b237" } ] } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md b/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md index 551790a7f6a..8b201624f61 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/README.md @@ -1,11 +1,11 @@ Macaron [![Build Status](https://drone.io/github.com/Unknwon/macaron/status.png)](https://drone.io/github.com/Unknwon/macaron/latest) [![](http://gocover.io/_badge/github.com/Unknwon/macaron)](http://gocover.io/github.com/Unknwon/macaron) ======================= -![Macaron Logo](macaronlogo.png) +![Macaron Logo](https://raw.githubusercontent.com/Unknwon/macaron/master/macaronlogo.png) Package macaron is a high productive and modular design web framework in Go. -##### Current version: 0.5.0 +##### Current version: 0.5.4 ## Getting Started @@ -41,7 +41,7 @@ func main() { - Handy dependency injection powered by [inject](https://github.com/codegangsta/inject). - Better router layer and less reflection make faster speed. -## Middlewares +## Middlewares Middlewares allow you easily plugin/unplugin features for your Macaron applications. @@ -70,15 +70,18 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp - [Gogs](https://github.com/gogits/gogs): Go Git Service - [Gogs Web](https://github.com/gogits/gogsweb): Gogs official website +- [Go Walker](https://gowalker.org): Go online API documentation - [Switch](https://github.com/gpmgo/switch): Gopm registry - [YouGam](http://yougam.com): Online Forum - [Car Girl](http://qcnl.gzsy.com/): Online campaign +- [Critical Stack Intel](https://intel.criticalstack.com/): A 100% free intel marketplace from Critical Stack, Inc. ## Getting Help - [API Reference](https://gowalker.org/github.com/Unknwon/macaron) - [Documentation](http://macaron.gogs.io) - [FAQs](http://macaron.gogs.io/docs/faqs) +- [![Join the chat at https://gitter.im/Unknwon/macaron](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Unknwon/macaron?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Credits @@ -88,4 +91,4 @@ There are already many [middlewares](https://github.com/macaron-contrib) to simp ## License -This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. \ No newline at end of file +This project is under Apache v2 License. See the [LICENSE](LICENSE) file for the full license text. diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md b/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md deleted file mode 100644 index b51601c90b2..00000000000 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# bpool [![GoDoc](https://godoc.org/github.com/oxtoacart/bpool?status.png)](https://godoc.org/github.com/oxtoacart/bpool) - -Package bpool implements leaky pools of byte arrays and Buffers as bounded channels. It is based on the leaky buffer example from the Effective Go documentation: http://golang.org/doc/effective_go.html#leaky_buffer - -## Install - -`go get github.com/oxtoacart/bpool` - -## Documentation - -See [godoc.org](http://godoc.org/github.com/oxtoacart/bpool) or use `godoc github.com/oxtoacart/bpool` - -## Example - -```go - -var bufpool *bpol.BufferPool - -func main() { - - bufpool = bpool.NewBufferPool(48) - -} - -func someFunction() error { - - // Get a buffer from the pool - buf := bufpool.Get() - ... - ... - ... - // Return the buffer to the pool - bufpool.Put(buf) - - return nil -} -``` diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go deleted file mode 100644 index 93981b1907d..00000000000 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/bpool/bufferpool.go +++ /dev/null @@ -1,45 +0,0 @@ -package bpool - -import ( - "bytes" -) - -/* -BufferPool implements a pool of bytes.Buffers in the form of a bounded -channel. -*/ -type BufferPool struct { - c chan *bytes.Buffer -} - -/* -NewBufferPool creates a new BufferPool bounded to the given size. -*/ -func NewBufferPool(size int) (bp *BufferPool) { - return &BufferPool{ - c: make(chan *bytes.Buffer, size), - } -} - -/* -Get gets a Buffer from the BufferPool, or creates a new one if none are available -in the pool. -*/ -func (bp *BufferPool) Get() (b *bytes.Buffer) { - select { - case b = <-bp.c: - // reuse existing buffer - default: - // create new buffer - b = bytes.NewBuffer([]byte{}) - } - return -} - -/* -Put returns the given Buffer to the BufferPool. -*/ -func (bp *BufferPool) Put(b *bytes.Buffer) { - b.Reset() - bp.c <- b -} diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go index fdaaf9d6775..abf7ac90257 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/context.go @@ -23,9 +23,11 @@ import ( "mime/multipart" "net/http" "net/url" + "os" "path" "path/filepath" "reflect" + "strconv" "strings" "time" @@ -182,6 +184,11 @@ func (ctx *Context) Query(name string) string { return ctx.Req.Form.Get(name) } +// QueryTrim querys and trims spaces form parameter. +func (ctx *Context) QueryTrim(name string) string { + return strings.TrimSpace(ctx.Query(name)) +} + // QueryStrings returns a list of results by given query name. func (ctx *Context) QueryStrings(name string) []string { if ctx.Req.Form == nil { @@ -210,9 +217,21 @@ func (ctx *Context) QueryInt64(name string) int64 { return com.StrTo(ctx.Query(name)).MustInt64() } +// QueryFloat64 returns query result in float64 type. +func (ctx *Context) QueryFloat64(name string) float64 { + v, _ := strconv.ParseFloat(ctx.Query(name), 64) + return v +} + // Params returns value of given param name. -// e.g. ctx.Params(":uid") +// e.g. ctx.Params(":uid") or ctx.Params("uid") func (ctx *Context) Params(name string) string { + if len(name) == 0 { + return "" + } + if name[0] != '*' && name[0] != ':' { + name = ":" + name + } return ctx.params[name] } @@ -242,12 +261,20 @@ func (ctx *Context) ParamsInt64(name string) int64 { return com.StrTo(ctx.Params(name)).MustInt64() } +// ParamsFloat64 returns params result in int64 type. +// e.g. ctx.ParamsFloat64(":uid") +func (ctx *Context) ParamsFloat64(name string) float64 { + v, _ := strconv.ParseFloat(ctx.Params(name), 64) + return v +} + // GetFile returns information about user upload file by given form field name. func (ctx *Context) GetFile(name string) (multipart.File, *multipart.FileHeader, error) { return ctx.Req.FormFile(name) } // SetCookie sets given cookie value to response header. +// FIXME: IE support? http://golanghome.com/post/620#reply2 func (ctx *Context) SetCookie(name string, value string, others ...interface{}) { cookie := http.Cookie{} cookie.Name = name @@ -264,23 +291,19 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{}) } } - // default "/" + cookie.Path = "/" if len(others) > 1 { if v, ok := others[1].(string); ok && len(v) > 0 { cookie.Path = v } - } else { - cookie.Path = "/" } - // default empty if len(others) > 2 { if v, ok := others[2].(string); ok && len(v) > 0 { cookie.Domain = v } } - // default empty if len(others) > 3 { switch v := others[3].(type) { case bool: @@ -292,7 +315,6 @@ func (ctx *Context) SetCookie(name string, value string, others ...interface{}) } } - // default false. for session cookie default true if len(others) > 4 { if v, ok := others[4].(bool); ok && v { cookie.HttpOnly = true @@ -322,6 +344,12 @@ func (ctx *Context) GetCookieInt64(name string) int64 { return com.StrTo(ctx.GetCookie(name)).MustInt64() } +// GetCookieFloat64 returns cookie result in float64 type. +func (ctx *Context) GetCookieFloat64(name string) float64 { + v, _ := strconv.ParseFloat(ctx.GetCookie(name), 64) + return v +} + var defaultCookieSecret string // SetDefaultCookieSecret sets global default secure cookie secret. @@ -368,6 +396,14 @@ func (ctx *Context) GetSuperSecureCookie(secret, key string) (string, bool) { return string(text), err == nil } +func (ctx *Context) setRawContentHeader() { + ctx.Resp.Header().Set("Content-Description", "Raw content") + ctx.Resp.Header().Set("Content-Type", "text/plain") + ctx.Resp.Header().Set("Expires", "0") + ctx.Resp.Header().Set("Cache-Control", "must-revalidate") + ctx.Resp.Header().Set("Pragma", "public") +} + // ServeContent serves given content to response. func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interface{}) { modtime := time.Now() @@ -377,14 +413,35 @@ func (ctx *Context) ServeContent(name string, r io.ReadSeeker, params ...interfa modtime = v } } - ctx.Resp.Header().Set("Content-Description", "Raw content") - ctx.Resp.Header().Set("Content-Type", "text/plain") - ctx.Resp.Header().Set("Expires", "0") - ctx.Resp.Header().Set("Cache-Control", "must-revalidate") - ctx.Resp.Header().Set("Pragma", "public") + + ctx.setRawContentHeader() http.ServeContent(ctx.Resp, ctx.Req.Request, name, modtime, r) } +// ServeFileContent serves given file as content to response. +func (ctx *Context) ServeFileContent(file string, names ...string) { + var name string + if len(names) > 0 { + name = names[0] + } else { + name = path.Base(file) + } + + f, err := os.Open(file) + if err != nil { + if Env == PROD { + http.Error(ctx.Resp, "Internal Server Error", 500) + } else { + http.Error(ctx.Resp, err.Error(), 500) + } + return + } + defer f.Close() + + ctx.setRawContentHeader() + http.ServeContent(ctx.Resp, ctx.Req.Request, name, time.Now(), f) +} + // ServeFile serves given file to response. func (ctx *Context) ServeFile(file string, names ...string) { var name string diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go index e5d8dd2dc02..c4b4752e12f 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/context_test.go @@ -76,35 +76,53 @@ func Test_Context(t *testing.T) { }) Convey("Render HTML", func() { - m.Get("/html", func(ctx *Context) { - ctx.HTML(304, "hello", "Unknwon") // 304 for logger test. + + Convey("Normal HTML", func() { + m.Get("/html", func(ctx *Context) { + ctx.HTML(304, "hello", "Unknwon") // 304 for logger test. + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/html", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") }) - resp := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/html", nil) - So(err, ShouldBeNil) - m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") + Convey("HTML template set", func() { + m.Get("/html2", func(ctx *Context) { + ctx.Data["Name"] = "Unknwon" + ctx.HTMLSet(200, "basic2", "hello2") + }) - m.Get("/html2", func(ctx *Context) { - ctx.Data["Name"] = "Unknwon" - ctx.HTMLSet(200, "basic2", "hello2") + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/html2", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") }) - resp = httptest.NewRecorder() - req, err = http.NewRequest("GET", "/html2", nil) - So(err, ShouldBeNil) - m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "

Hello Unknwon

") + Convey("With layout", func() { + m.Get("/layout", func(ctx *Context) { + ctx.HTML(200, "hello", "Unknwon", HTMLOptions{"layout"}) + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/layout", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "head

Hello Unknwon

foot") + }) }) Convey("Parse from and query", func() { m.Get("/query", func(ctx *Context) string { var buf bytes.Buffer - buf.WriteString(ctx.Query("name") + " ") + buf.WriteString(ctx.QueryTrim("name") + " ") buf.WriteString(ctx.QueryEscape("name") + " ") buf.WriteString(com.ToStr(ctx.QueryInt("int")) + " ") buf.WriteString(com.ToStr(ctx.QueryInt64("int64")) + " ") + buf.WriteString(com.ToStr(ctx.QueryFloat64("float64")) + " ") return buf.String() }) m.Get("/query2", func(ctx *Context) string { @@ -115,10 +133,10 @@ func Test_Context(t *testing.T) { }) resp := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123", nil) + req, err := http.NewRequest("GET", "/query?name=Unknwon&int=12&int64=123&float64=1.25", nil) So(err, ShouldBeNil) m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 ") + So(resp.Body.String(), ShouldEqual, "Unknwon Unknwon 12 123 1.25 ") resp = httptest.NewRecorder() req, err = http.NewRequest("GET", "/query2?list=item1&list=item2", nil) @@ -128,21 +146,23 @@ func Test_Context(t *testing.T) { }) Convey("URL parameter", func() { - m.Get("/:name/:int/:int64", func(ctx *Context) string { + m.Get("/:name/:int/:int64/:float64", func(ctx *Context) string { var buf bytes.Buffer - ctx.SetParams(":name", ctx.Params(":name")) + ctx.SetParams("name", ctx.Params("name")) + buf.WriteString(ctx.Params("")) buf.WriteString(ctx.Params(":name") + " ") buf.WriteString(ctx.ParamsEscape(":name") + " ") buf.WriteString(com.ToStr(ctx.ParamsInt(":int")) + " ") buf.WriteString(com.ToStr(ctx.ParamsInt64(":int64")) + " ") + buf.WriteString(com.ToStr(ctx.ParamsFloat64(":float64")) + " ") return buf.String() }) resp := httptest.NewRecorder() - req, err := http.NewRequest("GET", "/user/1/13", nil) + req, err := http.NewRequest("GET", "/user/1/13/1.24", nil) So(err, ShouldBeNil) m.ServeHTTP(resp, req) - So(resp.Body.String(), ShouldEqual, "user user 1 13 ") + So(resp.Body.String(), ShouldEqual, "user user 1 13 1.24 ") }) Convey("Get file", func() { @@ -158,26 +178,29 @@ func Test_Context(t *testing.T) { Convey("Set and get cookie", func() { m.Get("/set", func(ctx *Context) { - ctx.SetCookie("user", "Unknwon", 1) + ctx.SetCookie("user", "Unknwon", 1, "/", "localhost", true, true) + ctx.SetCookie("user", "Unknwon", int32(1), "/", "localhost", 1) + ctx.SetCookie("user", "Unknwon", int64(1)) }) resp := httptest.NewRecorder() req, err := http.NewRequest("GET", "/set", nil) So(err, ShouldBeNil) m.ServeHTTP(resp, req) - So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Max-Age=1") + So(resp.Header().Get("Set-Cookie"), ShouldEqual, "user=Unknwon; Path=/; Domain=localhost; Max-Age=1; HttpOnly; Secure") m.Get("/get", func(ctx *Context) string { ctx.GetCookie("404") So(ctx.GetCookieInt("uid"), ShouldEqual, 1) So(ctx.GetCookieInt64("uid"), ShouldEqual, 1) + So(ctx.GetCookieFloat64("balance"), ShouldEqual, 1.25) return ctx.GetCookie("user") }) resp = httptest.NewRecorder() req, err = http.NewRequest("GET", "/get", nil) So(err, ShouldBeNil) - req.Header.Set("Cookie", "user=Unknwon; uid=1") + req.Header.Set("Cookie", "user=Unknwon; uid=1; balance=1.25") m.ServeHTTP(resp, req) So(resp.Body.String(), ShouldEqual, "Unknwon") }) @@ -231,6 +254,39 @@ func Test_Context(t *testing.T) { So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}") }) + Convey("Serve file content", func() { + m.Get("/file", func(ctx *Context) { + ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl") + }) + + resp := httptest.NewRecorder() + req, err := http.NewRequest("GET", "/file", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}") + + m.Get("/file2", func(ctx *Context) { + ctx.ServeFileContent("fixtures/custom_funcs/index.tmpl", "ok.tmpl") + }) + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/file2", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "{{ myCustomFunc }}") + + m.Get("/file3", func(ctx *Context) { + ctx.ServeFileContent("404.tmpl") + }) + + resp = httptest.NewRecorder() + req, err = http.NewRequest("GET", "/file3", nil) + So(err, ShouldBeNil) + m.ServeHTTP(resp, req) + So(resp.Body.String(), ShouldEqual, "open 404.tmpl: no such file or directory\n") + So(resp.Code, ShouldEqual, 500) + }) + Convey("Serve content", func() { m.Get("/content", func(ctx *Context) { ctx.ServeContent("content1", bytes.NewReader([]byte("Hello world!"))) diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go index a6d85ef5483..748572ac2bf 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/inject/inject_test.go @@ -1,5 +1,5 @@ // Copyright 2013 Martini Authors -// Copyright 2014 Unknown +// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go index e4951dc28cc..c81f70237f9 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/logger_test.go @@ -46,7 +46,7 @@ func Test_Logger(t *testing.T) { So(len(buf.String()), ShouldBeGreaterThan, 0) }) - if !isWindows { + if ColorLog { Convey("Color console output", t, func() { m := Classic() m.Get("/:code:int", func(ctx *Context) (int, string) { diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go index 414e8e80b65..adbe9e3692d 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/macaron.go @@ -29,7 +29,7 @@ import ( "github.com/Unknwon/macaron/inject" ) -const _VERSION = "0.5.0.0116" +const _VERSION = "0.5.4.0318" func Version() string { return _VERSION @@ -267,7 +267,7 @@ func SetConfig(source interface{}, others ...interface{}) (_ *ini.File, err erro // It returns an empty object if there is no one available. func Config() *ini.File { if cfg == nil { - return &ini.File{} + return ini.Empty() } return cfg } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go index bc13888dcc6..b0558c95742 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/render.go @@ -1,4 +1,5 @@ // Copyright 2013 Martini Authors +// Copyright 2013 oxtoacart // Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may @@ -32,10 +33,39 @@ import ( "time" "github.com/Unknwon/com" - - "github.com/Unknwon/macaron/bpool" ) +// BufferPool implements a pool of bytes.Buffers in the form of a bounded channel. +type BufferPool struct { + c chan *bytes.Buffer +} + +// NewBufferPool creates a new BufferPool bounded to the given size. +func NewBufferPool(size int) (bp *BufferPool) { + return &BufferPool{ + c: make(chan *bytes.Buffer, size), + } +} + +// Get gets a Buffer from the BufferPool, or creates a new one if none are available +// in the pool. +func (bp *BufferPool) Get() (b *bytes.Buffer) { + select { + case b = <-bp.c: + // reuse existing buffer + default: + // create new buffer + b = bytes.NewBuffer([]byte{}) + } + return +} + +// Put returns the given Buffer to the BufferPool. +func (bp *BufferPool) Put(b *bytes.Buffer) { + b.Reset() + bp.c <- b +} + const ( ContentType = "Content-Type" ContentLength = "Content-Length" @@ -50,7 +80,7 @@ const ( var ( // Provides a temporary buffer to execute templates into and catch errors. - bufpool = bpool.NewBufferPool(64) + bufpool = NewBufferPool(64) // Included helper functions for use when rendering html helperFuncs = template.FuncMap{ @@ -392,8 +422,10 @@ func (r *TplRender) RW() http.ResponseWriter { } func (r *TplRender) JSON(status int, v interface{}) { - var result []byte - var err error + var ( + result []byte + err error + ) if r.Opt.IndentJSON { result, err = json.MarshalIndent(v, "", " ") } else { diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go index 8a2c5063e2d..ea1e0447646 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler.go @@ -1,5 +1,5 @@ // Copyright 2013 Martini Authors -// Copyright 2014 Unknown +// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain @@ -28,32 +28,32 @@ import ( // that are passed into this function. type ReturnHandler func(*Context, []reflect.Value) -func defaultReturnHandler() ReturnHandler { - return func(ctx *Context, vals []reflect.Value) { - rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) - res := rv.Interface().(http.ResponseWriter) - var responseVal reflect.Value - if len(vals) > 1 && vals[0].Kind() == reflect.Int { - res.WriteHeader(int(vals[0].Int())) - responseVal = vals[1] - } else if len(vals) > 0 { - responseVal = vals[0] - } - if canDeref(responseVal) { - responseVal = responseVal.Elem() - } - if isByteSlice(responseVal) { - res.Write(responseVal.Bytes()) - } else { - res.Write([]byte(responseVal.String())) - } - } +func canDeref(val reflect.Value) bool { + return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr } func isByteSlice(val reflect.Value) bool { return val.Kind() == reflect.Slice && val.Type().Elem().Kind() == reflect.Uint8 } -func canDeref(val reflect.Value) bool { - return val.Kind() == reflect.Interface || val.Kind() == reflect.Ptr +func defaultReturnHandler() ReturnHandler { + return func(ctx *Context, vals []reflect.Value) { + rv := ctx.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) + res := rv.Interface().(http.ResponseWriter) + var respVal reflect.Value + if len(vals) > 1 && vals[0].Kind() == reflect.Int { + res.WriteHeader(int(vals[0].Int())) + respVal = vals[1] + } else if len(vals) > 0 { + respVal = vals[0] + } + if canDeref(respVal) { + respVal = respVal.Elem() + } + if isByteSlice(respVal) { + res.Write(respVal.Bytes()) + } else { + res.Write([]byte(respVal.String())) + } + } } diff --git a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go index 27cc9d953f0..02325b218b0 100644 --- a/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go +++ b/Godeps/_workspace/src/github.com/Unknwon/macaron/return_handler_test.go @@ -1,4 +1,4 @@ -// Copyright 2014 Unknown +// Copyright 2014 Unknwon // // Licensed under the Apache License, Version 2.0 (the "License"): you may // not use this file except in compliance with the License. You may obtain diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore index 610ecf3242c..53b64215a3b 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/.gitignore @@ -1 +1,3 @@ -testdata/conf_out.ini \ No newline at end of file +testdata/conf_out.ini +ini.sublime-project +ini.sublime-workspace diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README.md index 626a6ae5c53..6d771819656 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README.md +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/README.md @@ -32,6 +32,12 @@ A **Data Source** is either raw data in type `[]byte` or a file name with type ` cfg, err := ini.Load([]byte("raw data"), "filename") ``` +Or start with an empty object: + +```go +cfg := ini.Empty() +``` + When you cannot decide how many data sources to load at the beginning, you still able to **Append()** them later. ```go @@ -58,7 +64,7 @@ When you're pretty sure the section exists, following code could make your life section := cfg.Section("") ``` -What happens when the section somehow does not exists? Won't panic, it returns an empty section object. +What happens when the section somehow does not exist? Don't panic, it automatically creates and returns a new section to you. To create a new section: @@ -117,6 +123,9 @@ val := cfg.Section("").Key("key name").String() To get value with types: ```go +// For boolean values: +// true when value is: 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On +// false when value is: 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() @@ -176,11 +185,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) -v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) +v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) +v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` Default value will be presented if value of key is not in candidates you given, and default value does not need be one of candidates. +To validate value in a given range: + +```go +vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) +vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) +vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) +vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) +vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 +``` + To auto-split value into slice: ```go @@ -295,6 +315,18 @@ func main() { } ``` +Can I have default value for field? Absolutely. + +Assign it before you map to struct. It will keep the value as it is if the key is not presented or got wrong type. + +```go +// ... +p := &Person{ + Name: "Joe", +} +// ... +``` + #### Name Mapper To save your time and make your code cleaner, this library supports [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) between struct field and actual secion and key name. diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md index 2dc38df4a42..c455cb67297 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/README_ZH.md @@ -27,6 +27,12 @@ cfg, err := ini.Load([]byte("raw data"), "filename") ``` +或者从一个空白的文件开始: + +```go +cfg := ini.Empty() +``` + 当您在一开始无法决定需要加载哪些数据源时,仍可以使用 **Append()** 在需要的时候加载它们。 ```go @@ -53,7 +59,7 @@ section, err := cfg.GetSection("") section := cfg.Section("") ``` -如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会返回一个空的分区对象。 +如果不小心判断错了,要获取的分区其实是不存在的,那会发生什么呢?没事的,它会自动创建并返回一个对应的分区对象给您。 创建一个分区: @@ -112,6 +118,9 @@ val := cfg.Section("").Key("key name").String() 获取其它类型的值: ```go +// 布尔值的规则: +// true 当值为:1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On +// false 当值为:0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off v, err = cfg.Section("").Key("BOOL").Bool() v, err = cfg.Section("").Key("FLOAT64").Float64() v, err = cfg.Section("").Key("INT").Int() @@ -171,11 +180,22 @@ v = cfg.Section("").Key("STRING").In("default", []string{"str", "arr", "types"}) v = cfg.Section("").Key("FLOAT64").InFloat64(1.1, []float64{1.25, 2.5, 3.75}) v = cfg.Section("").Key("INT").InInt(5, []int{10, 20, 30}) v = cfg.Section("").Key("INT64").InInt64(10, []int64{10, 20, 30}) -v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) +v = cfg.Section("").Key("TIME").InTimeFormat(time.RFC3339, time.Now(), []time.Time{time1, time2, time3}) +v = cfg.Section("").Key("TIME").InTime(time.Now(), []time.Time{time1, time2, time3}) // RFC3339 ``` 如果获取到的值不是候选值的任意一个,则会返回默认值,而默认值不需要是候选值中的一员。 +验证获取的值是否在指定范围内: + +```go +vals = cfg.Section("").Key("FLOAT64").RangeFloat64(0.0, 1.1, 2.2) +vals = cfg.Section("").Key("INT").RangeInt(0, 10, 20) +vals = cfg.Section("").Key("INT64").RangeInt64(0, 10, 20) +vals = cfg.Section("").Key("TIME").RangeTimeFormat(time.RFC3339, time.Now(), minTime, maxTime) +vals = cfg.Section("").Key("TIME").RangeTime(time.Now(), minTime, maxTime) // RFC3339 +``` + 自动分割键值为切片(slice): ```go @@ -290,6 +310,16 @@ func main() { } ``` +结构的字段怎么设置默认值呢?很简单,只要在映射之前对指定字段进行赋值就可以了。如果键未找到或者类型错误,该值不会发生改变。 + +```go +// ... +p := &Person{ + Name: "Joe", +} +// ... +``` + #### 名称映射器(Name Mapper) 为了节省您的时间并简化代码,本库支持类型为 [`NameMapper`](https://gowalker.org/gopkg.in/ini.v1#NameMapper) 的名称映射器,该映射器负责结构字段名与分区名和键名之间的映射。 diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go index be45f25e4ac..6674baf0b00 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/ini.go @@ -35,7 +35,7 @@ const ( // Maximum allowed depth when recursively substituing variable names. _DEPTH_VALUES = 99 - _VERSION = "1.0.1" + _VERSION = "1.2.6" ) func Version() string { @@ -144,9 +144,24 @@ func (k *Key) String() string { return val } +// parseBool returns the boolean value represented by the string. +// +// It accepts 1, t, T, TRUE, true, True, YES, yes, Yes, ON, on, On, +// 0, f, F, FALSE, false, False, NO, no, No, OFF, off, Off. +// Any other value returns an error. +func parseBool(str string) (value bool, err error) { + switch str { + case "1", "t", "T", "true", "TRUE", "True", "YES", "yes", "Yes", "ON", "on", "On": + return true, nil + case "0", "f", "F", "false", "FALSE", "False", "NO", "no", "No", "OFF", "off", "Off": + return false, nil + } + return false, fmt.Errorf("parsing \"%s\": invalid syntax", str) +} + // Bool returns bool type value. func (k *Key) Bool() (bool, error) { - return strconv.ParseBool(k.String()) + return parseBool(k.String()) } // Float64 returns float64 type value. @@ -305,6 +320,52 @@ func (k *Key) InTime(defaultVal time.Time, candidates []time.Time) time.Time { return k.InTimeFormat(time.RFC3339, defaultVal, candidates) } +// RangeFloat64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeFloat64(defaultVal, min, max float64) float64 { + val := k.MustFloat64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt(defaultVal, min, max int) int { + val := k.MustInt() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeInt64 checks if value is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeInt64(defaultVal, min, max int64) int64 { + val := k.MustInt64() + if val < min || val > max { + return defaultVal + } + return val +} + +// RangeTimeFormat checks if value with given format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTimeFormat(format string, defaultVal, min, max time.Time) time.Time { + val := k.MustTimeFormat(format) + if val.Unix() < min.Unix() || val.Unix() > max.Unix() { + return defaultVal + } + return val +} + +// RangeTime checks if value with RFC3339 format is in given range inclusively, +// and returns default value if it's not. +func (k *Key) RangeTime(defaultVal, min, max time.Time) time.Time { + return k.RangeTimeFormat(time.RFC3339, defaultVal, min, max) +} + // Strings returns list of string devide by given delimiter. func (k *Key) Strings(delim string) []string { str := k.String() @@ -440,7 +501,10 @@ func (s *Section) GetKey(name string) (*Key, error) { func (s *Section) Key(name string) *Key { key, err := s.GetKey(name) if err != nil { - return &Key{} + // It's OK here because the only possible error is empty key name, + // but if it's empty, this piece of code won't be executed. + key, _ = s.NewKey(name, "") + return key } return key } @@ -555,6 +619,13 @@ func Load(source interface{}, others ...interface{}) (_ *File, err error) { return f, f.Reload() } +// Empty returns an empty file object. +func Empty() *File { + // Ignore error here, we sure our data is good. + f, _ := Load([]byte("")) + return f +} + // NewSection creates a new section. func (f *File) NewSection(name string) (*Section, error) { if len(name) == 0 { @@ -575,6 +646,16 @@ func (f *File) NewSection(name string) (*Section, error) { return f.sections[name], nil } +// NewSections creates a list of sections. +func (f *File) NewSections(names ...string) (err error) { + for _, name := range names { + if _, err = f.NewSection(name); err != nil { + return err + } + } + return nil +} + // GetSection returns section by given name. func (f *File) GetSection(name string) (*Section, error) { if len(name) == 0 { @@ -597,7 +678,10 @@ func (f *File) GetSection(name string) (*Section, error) { func (f *File) Section(name string) *Section { sec, err := f.GetSection(name) if err != nil { - return newSection(f, name) + // It's OK here because the only possible error is empty section name, + // but if it's empty, this piece of code won't be executed. + sec, _ = f.NewSection(name) + return sec } return sec } @@ -638,6 +722,14 @@ func (f *File) DeleteSection(name string) { } } +func cutComment(str string) string { + i := strings.Index(str, "#") + if i == -1 { + return str + } + return str[:i] +} + // parse parses data through an io.Reader. func (f *File) parse(reader io.Reader) error { buf := bufio.NewReader(reader) @@ -776,7 +868,6 @@ func (f *File) parse(reader io.Reader) error { val = lineRight[qLen:] + "\n" for { next, err := buf.ReadString('\n') - val += next if err != nil { if err != io.EOF { return err @@ -785,9 +876,10 @@ func (f *File) parse(reader io.Reader) error { } pos = strings.LastIndex(next, valQuote) if pos > -1 { - val = val[:len(val)-len(valQuote)-1] + val += next[:pos] break } + val += next if isEnd { return fmt.Errorf("error parsing line: missing closing key quote from '%s' to '%s'", line, next) } @@ -796,7 +888,7 @@ func (f *File) parse(reader io.Reader) error { val = lineRight[qLen : pos+qLen] } } else { - val = strings.TrimSpace(lineRight[0:]) + val = strings.TrimSpace(cutComment(lineRight[0:])) } k, err := section.NewKey(kname, val) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go index aafe91826e5..c6daf81ca52 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/ini_test.go @@ -40,13 +40,13 @@ IMPORT_PATH = gopkg.in/%(NAME)s.%(VERSION)s # Information about package author # Bio can be written in multiple lines. [author] -NAME = Unknwon +NAME = Unknwon # Succeeding comment E-MAIL = fake@localhost GITHUB = https://github.com/%(NAME)s BIO = """Gopher. Coding addict. Good man. -""" +""" # Succeeding comment [package] CLONE_URL = https://%(IMPORT_PATH)s @@ -62,6 +62,7 @@ UNUSED_KEY = should be deleted [types] STRING = str BOOL = true +BOOL_FALSE = false FLOAT64 = 1.25 INT = 10 TIME = 2015-01-01T20:17:05Z @@ -88,9 +89,7 @@ func Test_Load(t *testing.T) { Convey("Load from data sources", t, func() { Convey("Load with empty data", func() { - cfg, err := Load([]byte("")) - So(err, ShouldBeNil) - So(cfg, ShouldNotBeNil) + So(Empty(), ShouldNotBeNil) }) Convey("Load with multiple data sources", func() { @@ -203,6 +202,10 @@ func Test_Values(t *testing.T) { So(err, ShouldBeNil) So(v1, ShouldBeTrue) + v1, err = sec.Key("BOOL_FALSE").Bool() + So(err, ShouldBeNil) + So(v1, ShouldBeFalse) + v2, err := sec.Key("FLOAT64").Float64() So(err, ShouldBeNil) So(v2, ShouldEqual, 1.25) @@ -265,6 +268,30 @@ func Test_Values(t *testing.T) { }) }) + Convey("Get values in range", func() { + sec := cfg.Section("types") + So(sec.Key("FLOAT64").RangeFloat64(0, 1, 2), ShouldEqual, 1.25) + So(sec.Key("INT").RangeInt(0, 10, 20), ShouldEqual, 10) + So(sec.Key("INT").RangeInt64(0, 10, 20), ShouldEqual, 10) + + minT, err := time.Parse(time.RFC3339, "0001-01-01T01:00:00Z") + So(err, ShouldBeNil) + midT, err := time.Parse(time.RFC3339, "2013-01-01T01:00:00Z") + So(err, ShouldBeNil) + maxT, err := time.Parse(time.RFC3339, "9999-01-01T01:00:00Z") + So(err, ShouldBeNil) + t, err := time.Parse(time.RFC3339, "2015-01-01T20:17:05Z") + So(err, ShouldBeNil) + So(sec.Key("TIME").RangeTime(t, minT, maxT).String(), ShouldEqual, t.String()) + + Convey("Get value in range with default value", func() { + So(sec.Key("FLOAT64").RangeFloat64(5, 0, 1), ShouldEqual, 5) + So(sec.Key("INT").RangeInt(7, 0, 5), ShouldEqual, 7) + So(sec.Key("INT").RangeInt64(7, 0, 5), ShouldEqual, 7) + So(sec.Key("TIME").RangeTime(t, minT, midT).String(), ShouldEqual, t.String()) + }) + }) + Convey("Get values into slice", func() { sec := cfg.Section("array") So(strings.Join(sec.Key("STRINGS").Strings(","), ","), ShouldEqual, "en,zh,de") @@ -304,7 +331,7 @@ func Test_Values(t *testing.T) { }) Convey("Get key strings", func() { - So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,FLOAT64,INT,TIME") + So(strings.Join(cfg.Section("types").KeyStrings(), ","), ShouldEqual, "STRING,BOOL,BOOL_FALSE,FLOAT64,INT,TIME") }) Convey("Delete a key", func() { @@ -321,6 +348,14 @@ func Test_Values(t *testing.T) { cfg.DeleteSection("") So(cfg.SectionStrings()[0], ShouldNotEqual, DEFAULT_SECTION) }) + + Convey("Create new sections", func() { + cfg.NewSections("test", "test2") + _, err := cfg.GetSection("test") + So(err, ShouldBeNil) + _, err = cfg.GetSection("test2") + So(err, ShouldBeNil) + }) }) Convey("Test getting and setting bad values", t, func() { @@ -340,6 +375,10 @@ func Test_Values(t *testing.T) { So(s, ShouldBeNil) }) + Convey("Create new sections with empty name", func() { + So(cfg.NewSections(""), ShouldNotBeNil) + }) + Convey("Get section that not exists", func() { s, err := cfg.GetSection("404") So(err, ShouldNotBeNil) diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go index 1875faf7695..09ea816442b 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/struct.go @@ -29,7 +29,7 @@ type NameMapper func(string) string var ( // AllCapsUnderscore converts to format ALL_CAPS_UNDERSCORE. AllCapsUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, 10) + newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { @@ -42,7 +42,7 @@ var ( } // TitleUnderscore converts to format title_underscore. TitleUnderscore NameMapper = func(raw string) string { - newstr := make([]rune, 0, 10) + newstr := make([]rune, 0, len(raw)) for i, chr := range raw { if isUpper := 'A' <= chr && chr <= 'Z'; isUpper { if i > 0 { @@ -75,32 +75,38 @@ func parseDelim(actual string) string { var reflectTime = reflect.TypeOf(time.Now()).Kind() +// setWithProperType sets proper value to field based on its type, +// but it does not return error for failing parsing, +// because we want to use default value that is already assigned to strcut. func setWithProperType(kind reflect.Kind, key *Key, field reflect.Value, delim string) error { switch kind { case reflect.String: + if len(key.String()) == 0 { + return nil + } field.SetString(key.String()) case reflect.Bool: boolVal, err := key.Bool() if err != nil { - return err + return nil } field.SetBool(boolVal) case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: intVal, err := key.Int64() if err != nil { - return err + return nil } field.SetInt(intVal) case reflect.Float64: floatVal, err := key.Float64() if err != nil { - return err + return nil } field.SetFloat(floatVal) case reflectTime: timeVal, err := key.Time() if err != nil { - return err + return nil } field.Set(reflect.ValueOf(timeVal)) case reflect.Slice: diff --git a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go b/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go index 97cf7452705..f6fad196ae0 100644 --- a/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go +++ b/Godeps/_workspace/src/gopkg.in/ini.v1/struct_test.go @@ -78,27 +78,17 @@ type unsupport4 struct { *unsupport3 `ini:"Others"` } -type invalidInt struct { - Age int -} - -type invalidBool struct { - Male bool -} - -type invalidFloat struct { - Money float64 -} - -type invalidTime struct { - Born time.Time -} - -type emptySlice struct { +type defaultValue struct { + Name string + Age int + Male bool + Money float64 + Born time.Time Cities []string } const _INVALID_DATA_CONF_STRUCT = ` +Name = Age = age Male = 123 Money = money @@ -154,12 +144,20 @@ func Test_Struct(t *testing.T) { So(MapTo(&testStruct{}, "hi"), ShouldNotBeNil) }) - Convey("Map to wrong types", t, func() { - So(MapTo(&invalidInt{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil) - So(MapTo(&invalidBool{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil) - So(MapTo(&invalidFloat{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil) - So(MapTo(&invalidTime{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldNotBeNil) - So(MapTo(&emptySlice{}, []byte(_INVALID_DATA_CONF_STRUCT)), ShouldBeNil) + Convey("Map to wrong types and gain default values", t, func() { + cfg, err := Load([]byte(_INVALID_DATA_CONF_STRUCT)) + So(err, ShouldBeNil) + + t, err := time.Parse(time.RFC3339, "1993-10-07T20:17:05Z") + So(err, ShouldBeNil) + dv := &defaultValue{"Joe", 10, true, 1.25, t, []string{"HangZhou", "Boston"}} + So(cfg.MapTo(dv), ShouldBeNil) + So(dv.Name, ShouldEqual, "Joe") + So(dv.Age, ShouldEqual, 10) + So(dv.Male, ShouldBeTrue) + So(dv.Money, ShouldEqual, 1.25) + So(dv.Born.String(), ShouldEqual, t.String()) + So(strings.Join(dv.Cities, ","), ShouldEqual, "HangZhou,Boston") }) } diff --git a/pkg/cmd/web.go b/pkg/cmd/web.go index d8f8295d874..6619e5b1e0e 100644 --- a/pkg/cmd/web.go +++ b/pkg/cmd/web.go @@ -11,6 +11,7 @@ import ( "path" "path/filepath" "strconv" + "time" "github.com/Unknwon/macaron" "github.com/codegangsta/cli" @@ -68,6 +69,9 @@ func mapStatic(m *macaron.Macaron, dir string, prefix string) { macaron.StaticOptions{ SkipLogging: true, Prefix: prefix, + Expires: func() string { + return time.Now().UTC().Format(http.TimeFormat) + }, }, )) } From 94c3a07115e035fb1d2cd7196e99021a13285db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Mar 2015 10:32:47 -0400 Subject: [PATCH 14/18] Fixed small issue when using 'Save As', dashboard version was not reset --- pkg/models/dashboards.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 4043063278e..7f39dbc74c7 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -14,6 +14,7 @@ var ( ErrDashboardVersionMismatch = errors.New("The dashboard has been changed by someone else") ) +// Dashboard model type Dashboard struct { Id int64 Slug string @@ -27,6 +28,7 @@ type Dashboard struct { Data map[string]interface{} } +// NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} dash.Data = make(map[string]interface{}) @@ -36,6 +38,7 @@ func NewDashboard(title string) *Dashboard { return dash } +// GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { jsonTags := dash.Data["tags"] if jsonTags == nil { @@ -50,6 +53,7 @@ func (dash *Dashboard) GetTags() []string { return b } +// GetDashboardModel turns the command into the savable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := &Dashboard{} dash.Data = cmd.Dashboard @@ -63,15 +67,19 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { if dash.Data["version"] != nil { dash.Version = int(dash.Data["version"].(float64)) } + } else { + dash.Data["version"] = 0 } return dash } +// GetString a func (dash *Dashboard) GetString(prop string) string { return dash.Data[prop].(string) } +// UpdateSlug updates the slug func (dash *Dashboard) UpdateSlug() { title := strings.ToLower(dash.Data["title"].(string)) re := regexp.MustCompile("[^\\w ]+") From a8363f02b8440267ed19bdb3471fe42944962b91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Mar 2015 15:16:47 -0400 Subject: [PATCH 15/18] Small changes to log scale handling, #452 --- src/app/panels/graph/axisEditor.html | 4 ++-- src/app/panels/graph/graph.js | 15 ++++++++------- src/app/panels/graph/module.js | 6 +++--- src/css/less/tightform.less | 4 ---- 4 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/app/panels/graph/axisEditor.html b/src/app/panels/graph/axisEditor.html index 40391995fc4..1d8e459be3b 100644 --- a/src/app/panels/graph/axisEditor.html +++ b/src/app/panels/graph/axisEditor.html @@ -34,7 +34,7 @@ Scale type
  • - +
  • Label @@ -79,7 +79,7 @@ Scale type
  • - +
  • Label diff --git a/src/app/panels/graph/graph.js b/src/app/panels/graph/graph.js index 5e2066466ca..e7cdba21dd2 100755 --- a/src/app/panels/graph/graph.js +++ b/src/app/panels/graph/graph.js @@ -351,7 +351,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { show: scope.panel['y-axis'], min: scope.panel.grid.leftMin, index: 1, - scale: scope.panel.grid.leftScale, + logBase: scope.panel.grid.leftLogBase, max: scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.leftMax, }; @@ -360,7 +360,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { if (_.findWhere(data, {yaxis: 2})) { var secondY = _.clone(defaults); secondY.index = 2, - secondY.scale = scope.panel.grid.rightScale; + secondY.logBase = scope.panel.grid.rightLogBase; secondY.position = 'right'; secondY.min = scope.panel.grid.rightMin; secondY.max = scope.panel.percentage && scope.panel.stack ? 100 : scope.panel.grid.rightMax; @@ -375,7 +375,7 @@ function (angular, $, kbn, moment, _, GraphTooltip) { } function applyLogScale(axis, data) { - if (axis.scale !== 2) { + if (axis.logBase !== 10) { return; } @@ -393,22 +393,23 @@ function (angular, $, kbn, moment, _, GraphTooltip) { } if (max === null) { - max = 10000000000; + max = Number.MAX_VALUE; } } - axis.ticks = [0, 1]; + axis.min = axis.min !== null ? axis.min : 1; + axis.ticks = [1]; var tick = 1; while (true) { - tick = tick*10; + tick = tick * axis.logBase; axis.ticks.push(tick); if (tick > max) { break; } } - axis.transform = function(v) { return Math.log(v+0.1); }; + axis.transform = function(v) { return Math.log(v+0.001); }; axis.inverseTransform = function (v) { return Math.pow(10,v); }; } diff --git a/src/app/panels/graph/module.js b/src/app/panels/graph/module.js index 485e7593b73..98e5baf1e55 100644 --- a/src/app/panels/graph/module.js +++ b/src/app/panels/graph/module.js @@ -53,12 +53,12 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { y_formats : ['short', 'short'], // grid options grid : { - leftScale: 1, + leftLogBase: 1, leftMax: null, rightMax: null, leftMin: null, rightMin: null, - rightScale: 1, + rightLogBase: 1, threshold1: null, threshold2: null, threshold1Color: 'rgba(216, 200, 27, 0.27)', @@ -116,7 +116,7 @@ function (angular, app, $, _, kbn, moment, TimeSeries, PanelMeta) { _.defaults($scope.panel.grid, _d.grid); _.defaults($scope.panel.legend, _d.legend); - $scope.scaleTypes = {'linear': 1, 'log (base 10)': 2}; + $scope.logScales = {'linear': 1, 'log (base 10)': 10}; $scope.hiddenSeries = {}; $scope.seriesList = []; diff --git a/src/css/less/tightform.less b/src/css/less/tightform.less index 3618ddd020b..799d6119279 100644 --- a/src/css/less/tightform.less +++ b/src/css/less/tightform.less @@ -5,10 +5,6 @@ background: @grafanaTargetBackground; width: 100%; - .dropdown { - padding: 0; margin: 0; - } - &:last-child, &.last { border-bottom: 1px solid @grafanaTargetBorder; } From 35888c814c94e82f27a2f239c4f890ee108bca01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 19 Mar 2015 23:09:50 -0400 Subject: [PATCH 16/18] more work on multi select --- src/app/directives/templateParamSelector.js | 77 +++++++- .../partials/variableValueSelect.html | 7 +- src/app/features/dashboard/submenuCtrl.js | 6 +- src/app/features/templating/editorCtrl.js | 4 +- src/app/features/templating/templateSrv.js | 14 +- .../features/templating/templateValuesSrv.js | 5 + src/app/partials/submenu.html | 3 +- src/app/partials/templating_editor.html | 176 ++++++++++-------- src/css/less/forms.less | 5 + src/css/less/submenu.less | 6 +- src/test/specs/templateSrv-specs.js | 28 +++ 11 files changed, 238 insertions(+), 93 deletions(-) diff --git a/src/app/directives/templateParamSelector.js b/src/app/directives/templateParamSelector.js index a7ae67e363c..8a21f75ef54 100644 --- a/src/app/directives/templateParamSelector.js +++ b/src/app/directives/templateParamSelector.js @@ -91,25 +91,84 @@ function (angular, app, _, $) { return { scope: { variable: "=", + onUpdated: "&" }, templateUrl: 'app/features/dashboard/partials/variableValueSelect.html', link: function(scope, elem) { var bodyEl = angular.element($window.document.body); + var variable = scope.variable; scope.show = function() { scope.selectorOpen = true; scope.giveFocus = 1; + scope.oldCurrentText = variable.current.text; + + var currentValues = variable.current.value; + + if (_.isString(currentValues)) { + currentValues = [currentValues]; + } + + scope.options = _.map(variable.options, function(option) { + var op = {text: option.text, value: option.value}; + if (_.indexOf(currentValues, option.value) >= 0) { + op.selected = true; + } + return op; + }); $timeout(function() { bodyEl.on('click', scope.bodyOnClick); }, 0, false); }; - scope.hide = function() { - scope.selectorOpen = false; - bodyEl.off('click', scope.bodyOnClick); + scope.optionSelected = function(option) { + if (!variable.multi) { + _.each(scope.options, function(other) { + if (option !== other) { + other.selected = false; + } + }); + } + + var selected = _.filter(scope.options, {selected: true}); + + if (selected.length === 0) { + // encode the first selected if no option is selected + scope.options[0].selected = true; + $timeout(function() { + scope.optionSelected(scope.options[0]); + }); + return; + } + + if (selected.length > 1) { + if (selected[0].text === 'All') { + selected = selected.slice(1, selected.length); + } + } + + variable.current = { + text: _.pluck(selected, 'text').join(', '), + value: _.pluck(selected, 'value'), + }; + + // only single value + if (variable.current.value.length === 1) { + variable.current.value = selected[0].value; + } + + scope.updateLinkText(); }; + scope.hide = function() { + scope.selectorOpen = false; + if (scope.oldCurrentText !== variable.current.text) { + scope.onUpdated(); + } + + bodyEl.off('click', scope.bodyOnClick); + }; scope.bodyOnClick = function(e) { var dropdown = elem.find('.variable-value-dropdown'); @@ -118,7 +177,17 @@ function (angular, app, _, $) { } }; - scope.$on('$destroy', function() { + scope.updateLinkText = function() { + scope.linkText = ""; + if (!variable.hideLabel) { + scope.linkText = (variable.label || variable.name) + ': '; + } + + scope.linkText += variable.current.text; + }; + + scope.$watchGroup(['variable.hideLabel', 'variable.name', 'variable.label'], function() { + scope.updateLinkText(); }); }, }; diff --git a/src/app/features/dashboard/partials/variableValueSelect.html b/src/app/features/dashboard/partials/variableValueSelect.html index 2a923f14ab6..cc9d3b08f1b 100644 --- a/src/app/features/dashboard/partials/variableValueSelect.html +++ b/src/app/features/dashboard/partials/variableValueSelect.html @@ -1,5 +1,6 @@ - {{variable.name}}: {{variable.current.text}} + {{linkText}} +
    @@ -11,9 +12,9 @@
    -
    - +
    diff --git a/src/app/features/dashboard/submenuCtrl.js b/src/app/features/dashboard/submenuCtrl.js index 456cc1b76c1..5099147fd53 100644 --- a/src/app/features/dashboard/submenuCtrl.js +++ b/src/app/features/dashboard/submenuCtrl.js @@ -26,8 +26,10 @@ function (angular, _) { $rootScope.$broadcast('refresh'); }; - $scope.setVariableValue = function(param, option) { - templateValuesSrv.setVariableValue(param, option); + $scope.variableUpdated = function(variable) { + templateValuesSrv.variableUpdated(variable).then(function() { + $rootScope.$broadcast('refresh'); + }); }; $scope.init(); diff --git a/src/app/features/templating/editorCtrl.js b/src/app/features/templating/editorCtrl.js index f17c383155d..f48452e4569 100644 --- a/src/app/features/templating/editorCtrl.js +++ b/src/app/features/templating/editorCtrl.js @@ -17,6 +17,8 @@ function (angular, _) { options: [], includeAll: false, allFormat: 'glob', + multi: false, + multiFormat: 'glob', }; $scope.init = function() { @@ -75,7 +77,7 @@ function (angular, _) { if ($scope.current.datasource === void 0) { $scope.current.datasource = null; $scope.current.type = 'query'; - $scope.current.allFormat = 'Glob'; + $scope.current.allFormat = 'glob'; } }; diff --git a/src/app/features/templating/templateSrv.js b/src/app/features/templating/templateSrv.js index 8fc45097944..008d0653cd5 100644 --- a/src/app/features/templating/templateSrv.js +++ b/src/app/features/templating/templateSrv.js @@ -29,11 +29,23 @@ function (angular, _) { _.each(this.variables, function(variable) { if (!variable.current || !variable.current.value) { return; } - this._values[variable.name] = variable.current.value; + this._values[variable.name] = this.renderVariableValue(variable); this._texts[variable.name] = variable.current.text; }, this); }; + this.renderVariableValue = function(variable) { + var value = variable.current.value; + if (_.isString(value)) { + return value; + } else { + if (variable.multiFormat === 'regex values') { + return '(' + value.join('|') + ')'; + } + return '{' + value.join(',') + '}'; + } + }; + this.setGrafanaVariable = function (name, value) { this._grafanaVariables[name] = value; }; diff --git a/src/app/features/templating/templateValuesSrv.js b/src/app/features/templating/templateValuesSrv.js index 73bcfb662bd..7d4830d960d 100644 --- a/src/app/features/templating/templateValuesSrv.js +++ b/src/app/features/templating/templateValuesSrv.js @@ -73,6 +73,11 @@ function (angular, _, kbn) { }); }; + this.variableUpdated = function(variable) { + templateSrv.updateTemplateData(); + return this.updateOptionsInChildVariables(variable); + }; + this.updateOptionsInChildVariables = function(updatedVariable) { var promises = _.map(self.variables, function(otherVariable) { if (otherVariable === updatedVariable) { diff --git a/src/app/partials/submenu.html b/src/app/partials/submenu.html index c5e2762985e..bdd75cb50b6 100644 --- a/src/app/partials/submenu.html +++ b/src/app/partials/submenu.html @@ -1,11 +1,10 @@
    diff --git a/src/css/less/submenu.less b/src/css/less/submenu.less index 3fe5cfffd42..f49d56876f2 100644 --- a/src/css/less/submenu.less +++ b/src/css/less/submenu.less @@ -43,5 +43,21 @@ .variable-option { display: block; + .fa { + font-size: 130%; + position: relative; + top: 2px; + padding-right: 6px; + } + .fa-check-square-o { display: none; } + + &.selected { + .fa-square-o { + display: none; + } + .fa-check-square-o { + display: inline-block; + } + } } }