From 87fc83678ce0baef4daa2fcf5b388cd87e40c5ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 11 Feb 2016 21:02:01 +0100 Subject: [PATCH 001/113] feat(imports): work on datasource dashboards --- public/app/features/dashboard/all.js | 1 + .../dashboard/import_list/import_list.ts | 50 +++++++++++++++++++ public/app/features/datasources/edit_ctrl.js | 2 - .../features/datasources/partials/edit.html | 2 + .../graphite/dashboards/carbon_stats.json | 21 ++++++++ 5 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard/import_list/import_list.ts create mode 100644 public/app/plugins/datasource/graphite/dashboards/carbon_stats.json diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index 073ca2ae1d9..65338c3d0f1 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -13,6 +13,7 @@ define([ './timeSrv', './unsavedChangesSrv', './timepicker/timepicker', + './import_list/import_list', './graphiteImportCtrl', './dynamicDashboardSrv', './importCtrl', diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/dashboard/import_list/import_list.ts new file mode 100644 index 00000000000..f229e56d425 --- /dev/null +++ b/public/app/features/dashboard/import_list/import_list.ts @@ -0,0 +1,50 @@ +/// + +import angular from 'angular'; +import coreModule from 'app/core/core_module'; + +class DashboardScriptLoader { + +} + +export class DashImportListCtrl { + constructor(private $http) { + console.log('importList', this); + } + + load(json) { + var model = angular.fromJson(json); + console.log(model); + } + + import() { + var url = 'public/app/plugins/datasource/graphite/dashboards/carbon_stats.json'; + this.$http.get(url).then(res => { + this.load(res.data); + }); + } +} + +var template = ` + diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json new file mode 100644 index 00000000000..642f32b6e6a --- /dev/null +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json @@ -0,0 +1,21 @@ +{ + "__inputs": { + "graphite": { + "type": "datasource", + "description": "Graphite datasource" + } + }, + + "title": "Carbon stats", + "version": 1, + "rows": [ + { + "panels": [ + { + "type": "graph", + "datasource": "__$graphite" + } + ] + } + ] +} From 08a45e9a3b8d83f77e4b975f252d9822032657e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 Mar 2016 08:52:16 +0100 Subject: [PATCH 002/113] docs(): docs update --- docs/mkdocs.yml | 5 ++- docs/sources/index.md | 60 +++++++++++++++--------------- docs/sources/installation/index.md | 1 + 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index a87f6d7d2dd..b527fa8f046 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -25,9 +25,10 @@ google_analytics: ['UA-47280256-1', 'grafana.org'] pages: # Introduction: -- ['index.md', 'Project', 'About Grafana'] -- ['project/cla.md', 'Project', 'Contributor License Agreement'] +# - ['index.md', 'Project', 'About Grafana'] +# - ['project/cla.md', 'Project', 'Contributor License Agreement'] +- ['index.md', '**HIDDEN**'] - ['installation/index.md', 'Installation', 'Installation'] - ['installation/debian.md', 'Installation', 'Installing on Debian / Ubuntu'] - ['installation/rpm.md', 'Installation', 'Installing on RPM-based Linux'] diff --git a/docs/sources/index.md b/docs/sources/index.md index f595c81ef28..83b7f44dd5b 100644 --- a/docs/sources/index.md +++ b/docs/sources/index.md @@ -1,39 +1,37 @@ -page_title: About Grafana -page_description: Introduction to Grafana. -page_keywords: grafana, introduction, documentation, about +--- +page_title: Grafana Installation +page_description: Install guide for Grafana. +page_keywords: grafana, installation, documentation +--- -# About Grafana +# Installation -Grafana is a leading open source application for visualizing large-scale measurement data. +Grafana is easily installed via a Debian/Ubuntu package (.deb), via +Redhat/Centos package (.rpm) or manually via a tarball that contains all +required files and binaries. If you can't find a package or binary for +your platform, you might be able to build one yourself. Read the [build +from source](../project/building_from_source) instructions for more +information. -It provides a powerful and elegant way to create, share, and explore data and dashboards from your disparate metric databases, either with your team or the world. +## Platforms +- [Installing on Debian / Ubuntu](installation/debian.md) +- [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](installation/rpm.md) +- [Installing on Mac OS X](installation/mac.md) +- [Installing on Windows](installation/windows.md) +- [Installing on Docker](installation/docker.md) +- [Installing using Provisioning (Chef, Puppet, Salt, Ansible, etc)](installation/provisioning.md) +- [Nightly Builds](http://grafana.org/download/builds.html) -Grafana is most commonly used for Internet infrastructure and application analytics, but many use it in other domains including industrial sensors, home automation, weather, and process control. +## Configuration -Grafana features pluggable panels and data sources allowing easy extensibility. There is currently rich support for [Graphite](http://graphite.readthedocs.org/en/latest/), [InfluxDB](http://influxdb.org) and [OpenTSDB](http://opentsdb.net). There is also experimental support for [KairosDB](https://github.com/kairosdb/kairosdb), [Prometheus](http://prometheus.io/), and SQL is on the roadmap. Grafana has a variety of panels, including a fully featured graph panel with rich visualization options. +The back-end web server has a number of configuration options. Go the +[Configuration](/installation/configuration) page for details on all +those options. -Version 2.0 was released in April 2015: Grafana now ships with its own backend server that brings [many changes and features](../guides/whats-new-in-v2/). -Version 2.1 was released in July 2015 and added [even more features and enhancements](../guides/whats-new-in-v2-1/). +## Data sources guides -## Community Resources, Feedback, and Support +- [Graphite](datasources/graphite.md) +- [Elasticsearch](datasources/elasticsearch.md) +- [InfluxDB](datasources/influxdb.md) +- [OpenTSDB](datasources/opentsdb.md) -Thousands of organizations large and small rely on Grafana, and we have a vibrant and active community that constantly inspires us. - -Please don't hesitate to [open a new issue on Github](https://github.com/grafana/grafana/issues) with your suggestions, ideas, and bug reports. - -Most of the new features and improvements that go into Grafana come from our users. We greatly value your feedback and suggestions; we consider them paramount to making the product better! - -If you have any trouble with Grafana, whether you can't get it set up or you just want clarification on a feature, there are a number of ways to get help: - -- [Troubleshooting guide](/installation/troubleshooting/) -- \#grafana IRC channel on the freenode network (chat.freenode.net) -- Search closed and open [issues on GitHub](https://github.com/grafana/grafana/issues) -- [Mailing list](https://groups.io/org/groupsio/grafana) - -## Commercial Support - -[raintank](http://www.raintank.io), the company behind Grafana, will be launching a SaaS Grafana-based platform later this year that will also include commercial support for all your existing Grafana installations. Please sign up for [early access at raintank](http://www.raintank.io) for more information. - -## License - -By utilizing this software, you agree to the terms of the included license. Grafana is licensed under the Apache 2.0 agreement. See [LICENSE](https://github.com/grafana/grafana/blob/master/LICENSE.md) for the full license terms. diff --git a/docs/sources/installation/index.md b/docs/sources/installation/index.md index 21c8c127b46..395c101a8e8 100644 --- a/docs/sources/installation/index.md +++ b/docs/sources/installation/index.md @@ -13,6 +13,7 @@ your platform, you might be able to build one yourself. Read the [build from source](../project/building_from_source) instructions for more information. +## Platforms - [Installing on Debian / Ubuntu](debian.md) - [Installing on RPM-based Linux (CentOS, Fedora, OpenSuse, RedHat)](rpm.md) - [Installing on Mac OS X](mac.md) From dc2738afadfdbda55a299965dc1112ffbc7f4b6a Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 08:59:37 +0100 Subject: [PATCH 003/113] docs(example): move datasource example to seperate repo --- examples/README.md | 3 + .../.jscs.json | 13 ---- .../.jshintrc | 36 --------- .../Gruntfile.js | 54 ------------- .../README.md | 12 --- .../package.json | 37 --------- .../plugin.json | 25 ------- .../src/css/query-editor.css | 3 - .../src/datasource.js | 75 ------------------- .../src/module.js | 19 ----- .../src/partials/annotations.editor.html | 9 --- .../src/partials/config.html | 2 - .../src/partials/query.editor.html | 8 -- .../src/partials/query.options.html | 4 - .../src/query_ctrl.js | 26 ------- 15 files changed, 3 insertions(+), 323 deletions(-) create mode 100644 examples/README.md delete mode 100644 examples/datasource-plugin-genericdatasource/.jscs.json delete mode 100644 examples/datasource-plugin-genericdatasource/.jshintrc delete mode 100644 examples/datasource-plugin-genericdatasource/Gruntfile.js delete mode 100644 examples/datasource-plugin-genericdatasource/README.md delete mode 100644 examples/datasource-plugin-genericdatasource/package.json delete mode 100644 examples/datasource-plugin-genericdatasource/plugin.json delete mode 100644 examples/datasource-plugin-genericdatasource/src/css/query-editor.css delete mode 100644 examples/datasource-plugin-genericdatasource/src/datasource.js delete mode 100644 examples/datasource-plugin-genericdatasource/src/module.js delete mode 100644 examples/datasource-plugin-genericdatasource/src/partials/annotations.editor.html delete mode 100644 examples/datasource-plugin-genericdatasource/src/partials/config.html delete mode 100644 examples/datasource-plugin-genericdatasource/src/partials/query.editor.html delete mode 100644 examples/datasource-plugin-genericdatasource/src/partials/query.options.html delete mode 100644 examples/datasource-plugin-genericdatasource/src/query_ctrl.js diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000000..2e8a46f2aa3 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,3 @@ +## Example plugin implementations + +[datasource-plugin-genericdatsource](https://github.com/grafana/datasource-plugin-genericdatasource/tree/3.0) \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/.jscs.json b/examples/datasource-plugin-genericdatasource/.jscs.json deleted file mode 100644 index dcf694dcc63..00000000000 --- a/examples/datasource-plugin-genericdatasource/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/.jshintrc b/examples/datasource-plugin-genericdatasource/.jshintrc deleted file mode 100644 index 3725af83afc..00000000000 --- a/examples/datasource-plugin-genericdatasource/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "browser": true, - "esnext": true, - - "bitwise":false, - "curly": true, - "eqnull": true, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/examples/datasource-plugin-genericdatasource/Gruntfile.js b/examples/datasource-plugin-genericdatasource/Gruntfile.js deleted file mode 100644 index d36a4716f31..00000000000 --- a/examples/datasource-plugin-genericdatasource/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.loadNpmTasks('grunt-execute'); - grunt.loadNpmTasks('grunt-contrib-clean'); - - grunt.initConfig({ - - clean: ["dist"], - - copy: { - src_to_dist: { - cwd: 'src', - expand: true, - src: ['**/*', '!**/*.js', '!**/*.scss'], - dest: 'dist' - }, - pluginDef: { - expand: true, - src: 'plugin.json', - dest: 'dist', - } - }, - - watch: { - rebuild_all: { - files: ['src/**/*', 'plugin.json'], - tasks: ['default'], - options: {spawn: false} - }, - }, - - babel: { - options: { - sourceMap: true, - presets: ["es2015"], - plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], - }, - dist: { - files: [{ - cwd: 'src', - expand: true, - src: ['**/*.js'], - dest: 'dist', - ext:'.js' - }] - }, - }, - - }); - - grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']); -}; diff --git a/examples/datasource-plugin-genericdatasource/README.md b/examples/datasource-plugin-genericdatasource/README.md deleted file mode 100644 index 903b0fbf2f0..00000000000 --- a/examples/datasource-plugin-genericdatasource/README.md +++ /dev/null @@ -1,12 +0,0 @@ -#Generic backend datasource# - -More documentation about datasource plugins can be found in the [Docs](https://github.com/grafana/grafana/blob/master/docs/sources/plugins/datasources.md) - -Your backend need implement 4 urls - * "/" Should return 200 ok. Used for "Test connection" on the datasource config page. - * "/search" Used by the find metric options on the query tab in panels - * "/query" Should return metrics based on input - * "/annotations" should return annotations - -## Example backend implementation ## -https://gist.github.com/bergquist/bc4aa5baface3cffa109 \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/package.json b/examples/datasource-plugin-genericdatasource/package.json deleted file mode 100644 index 91c53734ec8..00000000000 --- a/examples/datasource-plugin-genericdatasource/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "kentik-app", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/raintank/kentik-app-poc.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/raintank/kentik-app-poc/issues" - }, - "devDependencies": { - "grunt": "~0.4.5", - "babel": "~6.5.1", - "grunt-babel": "~6.0.0", - "grunt-contrib-copy": "~0.8.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-uglify": "~0.11.0", - "grunt-systemjs-builder": "^0.2.5", - "load-grunt-tasks": "~3.2.0", - "grunt-execute": "~0.2.2", - "grunt-contrib-clean": "~0.6.0" - }, - "dependencies": { - "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", - "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0" - }, - "homepage": "https://github.com/raintank/kentik-app-poc#readme" -} diff --git a/examples/datasource-plugin-genericdatasource/plugin.json b/examples/datasource-plugin-genericdatasource/plugin.json deleted file mode 100644 index 08985726fa3..00000000000 --- a/examples/datasource-plugin-genericdatasource/plugin.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "GenericDatasource", - "id": "datasource-plugin-genericdatasource", - "type": "datasource", - - "staticRoot": ".", - - "metrics": true, - "annotations": true, - - "info": { - "description": "generic datsource plugin", - "author": { - "name": "Raintank Inc.", - "url": "http://raintank.io" - }, - "version": "0.9.0", - "updated": "2016-02-10" - }, - - "dependencies": { - "grafanaVersion": "2.6.x", - "plugins": [ ] - } -} diff --git a/examples/datasource-plugin-genericdatasource/src/css/query-editor.css b/examples/datasource-plugin-genericdatasource/src/css/query-editor.css deleted file mode 100644 index 3b678b9f368..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/css/query-editor.css +++ /dev/null @@ -1,3 +0,0 @@ -.generic-datasource-query-row .query-keyword { - width: 75px; -} \ No newline at end of file diff --git a/examples/datasource-plugin-genericdatasource/src/datasource.js b/examples/datasource-plugin-genericdatasource/src/datasource.js deleted file mode 100644 index a59362f8268..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/datasource.js +++ /dev/null @@ -1,75 +0,0 @@ -export class GenericDatasource { - - constructor(instanceSettings, $q, backendSrv) { - this.type = instanceSettings.type; - this.url = instanceSettings.url; - this.name = instanceSettings.name; - this.q = $q; - this.backendSrv = backendSrv; - } - - // Called once per panel (graph) - query(options) { - var query = this.buildQueryParameters(options); - - if (query.targets.length <= 0) { - return this.q.when([]); - } - - return this.backendSrv.datasourceRequest({ - url: this.url + '/query', - data: query, - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }); - } - - // Required - // Used for testing datasource in datasource configuration pange - testDatasource() { - return this.backendSrv.datasourceRequest({ - url: this.url + '/', - method: 'GET' - }).then(response => { - if (response.status === 200) { - return { status: "success", message: "Data source is working", title: "Success" }; - } - }); - } - - annotationQuery(options) { - return this.backendSrv.datasourceRequest({ - url: this.url + '/annotations', - method: 'POST', - data: options - }).then(result => { - return result.data; - }); - } - - // Optional - // Required for templating - metricFindQuery(options) { - return this.backendSrv.datasourceRequest({ - url: this.url + '/search', - data: options, - method: 'POST', - headers: { 'Content-Type': 'application/json' } - }).then(this.mapToTextValue); - } - - mapToTextValue(result) { - return _.map(result.data, (d, i) => { - return { text: d, value: i}; - }); - } - - buildQueryParameters(options) { - //remove placeholder targets - options.targets = _.filter(options.targets, target => { - return target.target !== 'select metric'; - }); - - return options; - } -} diff --git a/examples/datasource-plugin-genericdatasource/src/module.js b/examples/datasource-plugin-genericdatasource/src/module.js deleted file mode 100644 index 8ab46f4edec..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/module.js +++ /dev/null @@ -1,19 +0,0 @@ -import {GenericDatasource} from './datasource'; -import {GenericDatasourceQueryCtrl} from './query_ctrl'; - -class GenericConfigCtrl {} -GenericConfigCtrl.templateUrl = 'partials/config.html'; - -class GenericQueryOptionsCtrl {} -GenericQueryOptionsCtrl.templateUrl = 'partials/query.options.html'; - -class GenericAnnotationsQueryCtrl {} -GenericAnnotationsQueryCtrl.templateUrl = 'partials/annotations.editor.html' - -export { - GenericDatasource as Datasource, - GenericDatasourceQueryCtrl as QueryCtrl, - GenericConfigCtrl as ConfigCtrl, - GenericQueryOptionsCtrl as QueryOptionsCtrl, - GenericAnnotationsQueryCtrl as AnnotationsQueryCtrl -}; diff --git a/examples/datasource-plugin-genericdatasource/src/partials/annotations.editor.html b/examples/datasource-plugin-genericdatasource/src/partials/annotations.editor.html deleted file mode 100644 index f4422caa4fc..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/annotations.editor.html +++ /dev/null @@ -1,9 +0,0 @@ - -
Query
-
-
- -
-
- - diff --git a/examples/datasource-plugin-genericdatasource/src/partials/config.html b/examples/datasource-plugin-genericdatasource/src/partials/config.html deleted file mode 100644 index 30470039554..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/config.html +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html b/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html deleted file mode 100644 index b24cb0cb884..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/query.editor.html +++ /dev/null @@ -1,8 +0,0 @@ - -
    -
  • Query
  • -
  • - -
  • -
-
diff --git a/examples/datasource-plugin-genericdatasource/src/partials/query.options.html b/examples/datasource-plugin-genericdatasource/src/partials/query.options.html deleted file mode 100644 index b4600118683..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/partials/query.options.html +++ /dev/null @@ -1,4 +0,0 @@ -
-
-
-
diff --git a/examples/datasource-plugin-genericdatasource/src/query_ctrl.js b/examples/datasource-plugin-genericdatasource/src/query_ctrl.js deleted file mode 100644 index 30c572b6892..00000000000 --- a/examples/datasource-plugin-genericdatasource/src/query_ctrl.js +++ /dev/null @@ -1,26 +0,0 @@ -import {QueryCtrl} from 'app/plugins/sdk'; -import './css/query-editor.css!' - -export class GenericDatasourceQueryCtrl extends QueryCtrl { - - constructor($scope, $injector, uiSegmentSrv) { - super($scope, $injector); - - this.scope = $scope; - this.uiSegmentSrv = uiSegmentSrv; - this.target.target = this.target.target || 'select metric'; - } - - getOptions() { - return this.datasource.metricFindQuery(this.target) - .then(this.uiSegmentSrv.transformToSegments(false)); - // Options have to be transformed by uiSegmentSrv to be usable by metric-segment-model directive - } - - onChangeInternal() { - this.panelCtrl.refresh(); // Asks the panel to refresh data. - } -} - -GenericDatasourceQueryCtrl.templateUrl = 'partials/query.editor.html'; - From 435d0f22f5df07a2cc4a52533a43ed3310e11e27 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 11:04:02 +0100 Subject: [PATCH 004/113] fix(dashlist): better handling of invalid dashboard ids --- pkg/services/sqlstore/dashboard.go | 3 ++- public/app/features/dashboard/impression_store.ts | 11 +++++++++-- public/app/plugins/panel/dashlist/module.ts | 3 ++- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index f99265754d5..a33ef10d725 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -150,7 +150,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { sql.WriteString(" AND (") for i, dashboardId := range query.DashboardIds { if i != 0 { - sql.WriteString("OR") + sql.WriteString(" OR") } sql.WriteString(" dashboard.id = ?") @@ -167,6 +167,7 @@ func SearchDashboards(query *search.FindPersistedDashboardsQuery) error { sql.WriteString(fmt.Sprintf(" ORDER BY dashboard.title ASC LIMIT 1000")) var res []DashboardSearchProjection + err := x.Sql(sql.String(), params...).Find(&res) if err != nil { return err diff --git a/public/app/features/dashboard/impression_store.ts b/public/app/features/dashboard/impression_store.ts index 6afe763ceac..4bd979a6b34 100644 --- a/public/app/features/dashboard/impression_store.ts +++ b/public/app/features/dashboard/impression_store.ts @@ -28,8 +28,15 @@ export class ImpressionsStore { } getDashboardOpened() { - var impressions = store.get("dashboard_impressions"); - return JSON.parse(impressions || "[]"); + var impressions = store.get("dashboard_impressions") || "[]"; + + impressions = JSON.parse(impressions); + + impressions = _.filter(impressions, el => { + return _.isNumber(el); + }); + + return impressions; } } diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 707c5367f55..c0e34183548 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -49,11 +49,12 @@ class DashListCtrl extends PanelCtrl { dashboardIds: impressions.getDashboardOpened(), limit: this.panel.limit }).then(result => { - this.dashList = dashboardIds.map(orderId => { return _.find(result, dashboard => { return dashboard.id === orderId; }); + }).filter(el => { + return el !== undefined; }); this.renderingCompleted(); From e16f45e4949fb892f403af81f6ae3e52aaa49014 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 11:10:10 +0100 Subject: [PATCH 005/113] fix(playlist): fixes failed refactoring --- public/app/features/playlist/partials/playlist.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/playlist/partials/playlist.html b/public/app/features/playlist/partials/playlist.html index c06bd5b8e0e..08c31522b7f 100644 --- a/public/app/features/playlist/partials/playlist.html +++ b/public/app/features/playlist/partials/playlist.html @@ -62,7 +62,7 @@ - From bcb7eff6fa5c0e4a835f2823bfa7c23a16c39250 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 12:16:52 +0100 Subject: [PATCH 006/113] docs(plugins): basic overview text --- docs/sources/plugins/overview.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/sources/plugins/overview.md b/docs/sources/plugins/overview.md index 4863b4ee3f4..bb14b906e1a 100644 --- a/docs/sources/plugins/overview.md +++ b/docs/sources/plugins/overview.md @@ -6,5 +6,7 @@ page_keywords: grafana, plugins, documentation # Plugins -TODO +From Grafana 3.0 not only datsource plugins is supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favortie dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together. + +Grafana allready have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. From aef8dab4d91f75e18ca89fd19d708a3cb267b009 Mon Sep 17 00:00:00 2001 From: Utkarsh Bhatnagar Date: Mon, 7 Mar 2016 03:49:49 -0800 Subject: [PATCH 007/113] Fixing language plus typos --- docs/sources/plugins/overview.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/plugins/overview.md b/docs/sources/plugins/overview.md index bb14b906e1a..6d0864ac8f5 100644 --- a/docs/sources/plugins/overview.md +++ b/docs/sources/plugins/overview.md @@ -6,7 +6,7 @@ page_keywords: grafana, plugins, documentation # Plugins -From Grafana 3.0 not only datsource plugins is supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favortie dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together. +From Grafana 3.0 not only datasource plugins are supported but also panel plugins and apps. Having panels as plugins make it easy to create and add any kind of panel, to show your data or improve your favorite dashboards. Apps is something new in Grafana that enables bundling of datasources, panels that belongs together. -Grafana allready have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. +Grafana already have a strong community of contributors and plugin developers. By making it easier to develop and install plugins we hope that the community can grow even stronger and develop new plugins that we would never think about. From f6c5242a93d233a02ece73aaa35f79efe36fadd5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 13:27:51 +0100 Subject: [PATCH 008/113] feat(cli): make repo url a parameter this is a quick hack to support repo url as parameter. Will refactor later --- pkg/cmd/grafana-cli/commands/install_command.go | 8 ++++---- pkg/cmd/grafana-cli/commands/listremote_command.go | 2 +- pkg/cmd/grafana-cli/commands/upgrade_all_command.go | 4 ++-- pkg/cmd/grafana-cli/commands/upgrade_command.go | 4 ++-- pkg/cmd/grafana-cli/main.go | 5 +++++ pkg/cmd/grafana-cli/services/services.go | 12 ++++++------ 6 files changed, 20 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 1d085cad89e..181b899554b 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -45,11 +45,11 @@ func installCommand(c CommandLine) error { log.Infof("version: %v\n", version) - return InstallPlugin(pluginToInstall, pluginFolder, version) + return InstallPlugin(pluginToInstall, pluginFolder, version, c.GlobalString("repo")) } -func InstallPlugin(pluginName, pluginFolder, version string) error { - plugin, err := s.GetPlugin(pluginName) +func InstallPlugin(pluginName, pluginFolder, version, repoUrl string) error { + plugin, err := s.GetPlugin(pluginName, repoUrl) if err != nil { return err } @@ -77,7 +77,7 @@ func InstallPlugin(pluginName, pluginFolder, version string) error { res, _ := s.ReadPlugin(pluginFolder, pluginName) for _, v := range res.Dependency.Plugins { - InstallPlugin(v.Id, pluginFolder, "") + InstallPlugin(v.Id, pluginFolder, "", repoUrl) log.Infof("Installed Dependency: %v ✔\n", v.Id) } diff --git a/pkg/cmd/grafana-cli/commands/listremote_command.go b/pkg/cmd/grafana-cli/commands/listremote_command.go index 7cb3d650c55..0f0c3077ab9 100644 --- a/pkg/cmd/grafana-cli/commands/listremote_command.go +++ b/pkg/cmd/grafana-cli/commands/listremote_command.go @@ -6,7 +6,7 @@ import ( ) func listremoteCommand(c CommandLine) error { - plugin, err := s.ListAllPlugins() + plugin, err := s.ListAllPlugins(c.GlobalString("repo")) if err != nil { return err diff --git a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go index 6c1e0182f51..0bb89866fcd 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_all_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_all_command.go @@ -32,7 +32,7 @@ func upgradeAllCommand(c CommandLine) error { localPlugins := s.GetLocalPlugins(pluginDir) - remotePlugins, err := s.ListAllPlugins() + remotePlugins, err := s.ListAllPlugins(c.GlobalString("repo")) if err != nil { return err @@ -54,7 +54,7 @@ func upgradeAllCommand(c CommandLine) error { log.Infof("Upgrading %v \n", p.Id) s.RemoveInstalledPlugin(pluginDir, p.Id) - InstallPlugin(p.Id, pluginDir, "") + InstallPlugin(p.Id, pluginDir, "", c.GlobalString("repo")) } return nil diff --git a/pkg/cmd/grafana-cli/commands/upgrade_command.go b/pkg/cmd/grafana-cli/commands/upgrade_command.go index 5a4fe477c9b..c3d2cf06aea 100644 --- a/pkg/cmd/grafana-cli/commands/upgrade_command.go +++ b/pkg/cmd/grafana-cli/commands/upgrade_command.go @@ -14,7 +14,7 @@ func upgradeCommand(c CommandLine) error { return err } - remotePlugins, err2 := s.ListAllPlugins() + remotePlugins, err2 := s.ListAllPlugins(c.GlobalString("repo")) if err2 != nil { return err2 @@ -24,7 +24,7 @@ func upgradeCommand(c CommandLine) error { if localPlugin.Id == v.Id { if ShouldUpgrade(localPlugin.Info.Version, v) { s.RemoveInstalledPlugin(pluginDir, pluginName) - return InstallPlugin(localPlugin.Id, pluginDir, "") + return InstallPlugin(localPlugin.Id, pluginDir, "", c.GlobalString("repo")) } } } diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 938cfcc284a..8c2b0b9c53d 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -36,6 +36,11 @@ func main() { Usage: "path to the grafana installation", Value: getGrafanaPluginPath(), }, + cli.StringFlag{ + Name: "repo", + Usage: "url to the plugin repository", + Value: "https://raw.githubusercontent.com/grafana/grafana-plugin-repository/master/repo.json", + }, cli.BoolFlag{ Name: "debug, d", Usage: "enable debug logging", diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index d5051a623a3..c23c716b62f 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -11,8 +11,8 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} -func ListAllPlugins() (m.PluginRepo, error) { - res, _ := goreq.Request{Uri: "https://raw.githubusercontent.com/grafana/grafana-plugin-repository/master/repo.json"}.Do() +func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { + res, _ := goreq.Request{Uri: repoUrl}.Do() var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) @@ -59,16 +59,16 @@ func RemoveInstalledPlugin(pluginPath, id string) error { return IoHelper.RemoveAll(path.Join(pluginPath, id)) } -func GetPlugin(id string) (m.Plugin, error) { - resp, err := ListAllPlugins() +func GetPlugin(pluginId, repoUrl string) (m.Plugin, error) { + resp, err := ListAllPlugins(repoUrl) if err != nil { } for _, i := range resp.Plugins { - if i.Id == id { + if i.Id == pluginId { return i, nil } } - return m.Plugin{}, errors.New("could not find plugin named \"" + id + "\"") + return m.Plugin{}, errors.New("could not find plugin named \"" + pluginId + "\"") } From 1ff428087ea6373085e983fa0ee9c08d80c1c042 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 7 Mar 2016 14:20:51 +0100 Subject: [PATCH 009/113] feat(cli): add suppot for plugindir as environment variable --- pkg/cmd/grafana-cli/main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 8c2b0b9c53d..12bebd46898 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -12,7 +12,9 @@ import ( var version = "master" func getGrafanaPluginPath() string { - //TODO: try to get path from os:env GF_PLUGIN_FOLDER + if os.Getenv("GF_PLUGIN_DIR") != "" { + return os.Getenv("GF_PLUGIN_DIR") + } os := runtime.GOOS if os == "windows" { From 135679096be4db01b793e4c8fe4acbf97fae06cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 7 Mar 2016 14:31:02 +0100 Subject: [PATCH 010/113] feat(plugins): progress on plugin details page, # 4275 --- examples/nginx-app/plugin.json | 2 +- pkg/api/dtos/plugins.go | 23 +++++++------ pkg/api/plugin_setting.go | 9 ++--- pkg/cmd/grafana-server/main.go | 2 +- pkg/plugins/frontend_plugin.go | 8 ++--- pkg/plugins/models.go | 21 +++++++++--- .../features/dashboard/partials/settings.html | 1 - public/app/features/plugins/edit_ctrl.ts | 16 ++++++++- .../app/features/plugins/partials/edit.html | 32 +++++++++++++++--- public/app/partials/unsaved-changes.html | 2 +- .../elasticsearch/img/logo_large.png | Bin 0 -> 40972 bytes .../datasource/elasticsearch/plugin.json | 18 +++++++++- public/sass/layout/_page.scss | 2 -- public/sass/pages/_plugins.scss | 8 ++++- 14 files changed, 107 insertions(+), 37 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/img/logo_large.png diff --git a/examples/nginx-app/plugin.json b/examples/nginx-app/plugin.json index 7e3cdd7ad08..4e0c4a114fa 100644 --- a/examples/nginx-app/plugin.json +++ b/examples/nginx-app/plugin.json @@ -35,7 +35,7 @@ }, "dependencies": { - "grafanaVersion": "2.6.x", + "grafanaVersion": "3.x.x", "plugins": [ {"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"}, {"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"} diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index af96202222f..20b976af122 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -3,17 +3,18 @@ package dtos import "github.com/grafana/grafana/pkg/plugins" type PluginSetting struct { - Name string `json:"name"` - Type string `json:"type"` - PluginId string `json:"pluginId"` - Enabled bool `json:"enabled"` - Pinned bool `json:"pinned"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - Info *plugins.PluginInfo `json:"info"` - Pages []*plugins.AppPluginPage `json:"pages"` - Includes []*plugins.AppIncludeInfo `json:"includes"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name"` + Type string `json:"type"` + PluginId string `json:"pluginId"` + Enabled bool `json:"enabled"` + Pinned bool `json:"pinned"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` + Info *plugins.PluginInfo `json:"info"` + Pages []*plugins.AppPluginPage `json:"pages"` + Includes []*plugins.AppIncludeInfo `json:"includes"` + Dependencies *plugins.PluginDependencies `json:"dependencies"` + JsonData map[string]interface{} `json:"jsonData"` } type PluginListItem struct { diff --git a/pkg/api/plugin_setting.go b/pkg/api/plugin_setting.go index 08fc5296434..4509cc00c35 100644 --- a/pkg/api/plugin_setting.go +++ b/pkg/api/plugin_setting.go @@ -42,10 +42,11 @@ func GetPluginSettingById(c *middleware.Context) Response { return ApiError(404, "Plugin not found, no installed plugin with that id", nil) } else { dto := &dtos.PluginSetting{ - Type: def.Type, - PluginId: def.Id, - Name: def.Name, - Info: &def.Info, + Type: def.Type, + PluginId: def.Id, + Name: def.Name, + Info: &def.Info, + Dependencies: &def.Dependencies, } if app, exists := plugins.Apps[pluginId]; exists { diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index d78b8f700ff..dad3f437390 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -24,7 +24,7 @@ import ( "github.com/grafana/grafana/pkg/social" ) -var version = "master" +var version = "3.0.0-pre1" var commit = "NA" var buildstamp string var build_date string diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index 5acb9966495..d09342c84bb 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -28,8 +28,8 @@ func (fp *FrontendPluginBase) initFrontendPlugin() { fp.handleModuleDefaults() - fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.Id) - fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.Id) + fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.BaseUrl) + fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.BaseUrl) for i := 0; i < len(fp.Info.Screenshots); i++ { fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.Id) @@ -55,7 +55,7 @@ func (fp *FrontendPluginBase) handleModuleDefaults() { fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id) } -func evalRelativePluginUrlPath(pathStr string, pluginId string) string { +func evalRelativePluginUrlPath(pathStr string, baseUrl string) string { if pathStr == "" { return "" } @@ -64,5 +64,5 @@ func evalRelativePluginUrlPath(pathStr string, pluginId string) string { if u.IsAbs() { return pathStr } - return path.Join("public/plugins", pluginId, pathStr) + return path.Join(baseUrl, pathStr) } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 83f43c1f544..26620a45fea 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -14,10 +14,11 @@ type PluginLoader interface { } type PluginBase struct { - Type string `json:"type"` - Name string `json:"name"` - Id string `json:"id"` - Info PluginInfo `json:"info"` + Type string `json:"type"` + Name string `json:"name"` + Id string `json:"id"` + Info PluginInfo `json:"info"` + Dependencies PluginDependencies `json:"dependencies"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` @@ -37,6 +38,18 @@ func (pb *PluginBase) registerPlugin(pluginDir string) error { return nil } +type PluginDependencies struct { + GrafanaVersion string `json:"grafanaVersion"` + Plugins []PluginDependencyItem `json:"plugins"` +} + +type PluginDependencyItem struct { + Type string `json:"type"` + Id string `json:"id"` + Name string `json:"name"` + Version string `json:"version"` +} + type PluginInfo struct { Author PluginInfoLink `json:"author"` Description string `json:"description"` diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 8fd2b34ae5a..deccd22305b 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -19,7 +19,6 @@
-
Dashboard Detail
diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/edit_ctrl.ts index 6d693c19a4e..5a0e565f3db 100644 --- a/public/app/features/plugins/edit_ctrl.ts +++ b/public/app/features/plugins/edit_ctrl.ts @@ -5,6 +5,7 @@ import _ from 'lodash'; export class PluginEditCtrl { model: any; + pluginIcon: string; pluginId: any; includedPanels: any; includedDatasources: any; @@ -22,9 +23,22 @@ export class PluginEditCtrl { this.model = result; this.includedPanels = _.where(result.includes, {type: 'panel'}); this.includedDatasources = _.where(result.includes, {type: 'datasource'}); + this.pluginIcon = this.getPluginIcon(this.model.type); + + this.model.dependencies.plugins.forEach(plug => { + plug.icon = this.getPluginIcon(plug.type); + }); }); } + getPluginIcon(type) { + switch (type) { + case 'datasource': return 'icon-gf icon-gf-datasources'; + case 'panel': return 'icon-gf icon-gf-panel'; + case 'app': return 'icon-gf icon-gf-apps'; + } + } + update() { var chain = Promise.resolve(); var self = this; @@ -53,7 +67,7 @@ export class PluginEditCtrl { // if set, performt he postUpdate hook. If a promise is returned it will block // the final step of the update procedure (reloading the page) until the promise - // resolves. If the promise is rejected the page will not be reloaded. + // resolves. If the promise is rejected the page will not be reloaded. if (this.postUpdateHook != null) { chain = chain.then(function() { return Promise.resolve(this.postUpdateHook()); diff --git a/public/app/features/plugins/partials/edit.html b/public/app/features/plugins/partials/edit.html index 815e165cc85..70bc3af4a93 100644 --- a/public/app/features/plugins/partials/edit.html +++ b/public/app/features/plugins/partials/edit.html @@ -1,7 +1,6 @@ - - Apps + Plugin details @@ -15,7 +14,7 @@
By {{ctrl.model.info.author.name}}
- {{ctrl.model.type}} + {{ctrl.model.type}}
@@ -59,7 +58,7 @@
`; export function dashboardImportList() { diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js index cbb3cfdd4c5..062cec6e5e9 100644 --- a/public/app/features/datasources/edit_ctrl.js +++ b/public/app/features/datasources/edit_ctrl.js @@ -40,7 +40,7 @@ function (angular, _, config) { return $q.when(null); } - return backendSrv.get('/api/datasources/plugins').then(function(plugins) { + return backendSrv.get('/api/org/plugins', {enabled: 1, type: 'datasource'}).then(function(plugins) { datasourceTypes = plugins; $scope.types = plugins; }); @@ -55,7 +55,9 @@ function (angular, _, config) { }; $scope.typeChanged = function() { - $scope.datasourceMeta = $scope.types[$scope.current.type]; + return backendSrv.get('/api/org/plugins/' + $scope.current.type + '/settings').then(function(pluginInfo) { + $scope.datasourceMeta = pluginInfo; + }); }; $scope.updateFrontendSettings = function() { diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index facddc498c9..005efab49d3 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -27,34 +27,36 @@ icon="icon-gf icon-gf-datasources">
Type
- +
+
- - - - - + + + + -
-
Testing....
-
Test results
-
-
{{testing.title}}
-
-
+
+
Testing....
+
Test results
+
+
{{testing.title}}
+
+
-
- - - - Cancel -
+ + +
+ + + + Cancel +
diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/edit_ctrl.ts index 2c060c230de..f8e7c96792c 100644 --- a/public/app/features/plugins/edit_ctrl.ts +++ b/public/app/features/plugins/edit_ctrl.ts @@ -72,8 +72,6 @@ export class PluginEditCtrl { // Perform the core update procedure chain = chain.then(function() { var updateCmd = _.extend({ - pluginId: self.model.pluginId, - orgId: self.model.orgId, enabled: self.model.enabled, pinned: self.model.pinned, jsonData: self.model.jsonData, diff --git a/public/app/features/plugins/partials/edit.html b/public/app/features/plugins/partials/edit.html index 14e75cea15f..9bb9ebfdbaf 100644 --- a/public/app/features/plugins/partials/edit.html +++ b/public/app/features/plugins/partials/edit.html @@ -48,7 +48,7 @@
-
+
@@ -70,7 +70,7 @@
  • - {{page.name}} + {{page.name}}
  • diff --git a/public/app/features/plugins/partials/list.html b/public/app/features/plugins/partials/list.html index 2b2f9741be0..b00c280c43f 100644 --- a/public/app/features/plugins/partials/list.html +++ b/public/app/features/plugins/partials/list.html @@ -18,7 +18,7 @@ - + {{plugin.name}} @@ -30,7 +30,7 @@ Pinned - + Edit diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index d2836b2a107..b079ec320f2 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -3,7 +3,10 @@ "type": "datasource", "id": "graphite", - "defaultMatchFormat": "glob", + "includes": [ + {"type": "dashboard", "name": "Carbon Stats", "path": "dashboards/carbon_stats.json"} + ], + "metrics": true, "annotations": true } From 091726b374c0d17b0ecbebafbfd9820b3f80040f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 8 Mar 2016 20:27:40 +0100 Subject: [PATCH 047/113] feat(plugins): commented out dashboard import list --- public/app/features/datasources/partials/edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index 005efab49d3..39639206c23 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -47,7 +47,7 @@ icon="icon-gf icon-gf-datasources">
    - +
    From 3191678b14625e2edbe4207fd0ac2e037cb0ac44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 8 Mar 2016 20:46:25 +0100 Subject: [PATCH 048/113] feat(plugins): work on plugins that include dashboards --- .../dashboard/import_list/import_list.ts | 26 +++++++++++++------ public/app/features/datasources/edit_ctrl.js | 8 +++++- .../features/datasources/partials/edit.html | 4 +-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/dashboard/import_list/import_list.ts index 1d3aa42611b..d8333bec4fa 100644 --- a/public/app/features/dashboard/import_list/import_list.ts +++ b/public/app/features/dashboard/import_list/import_list.ts @@ -8,8 +8,13 @@ class DashboardScriptLoader { } export class DashImportListCtrl { + dashboards: any[]; + plugin: any; + constructor(private $http) { - console.log('importList', this); + + this.dashboards = this.plugin.includes.filter(val => val.type === 'dashboard'); + } load(json) { @@ -18,17 +23,22 @@ export class DashImportListCtrl { } import() { - var url = 'public/app/plugins/datasource/graphite/dashboards/carbon_stats.json'; - this.$http.get(url).then(res => { - this.load(res.data); - }); + // this.$http.get(url).then(res => { + // this.load(res.data); + // }); } } var template = ` -

    Dashboards

    -
    - +
    +

    Dashboards

    +
    + + + +
    `; diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js index 062cec6e5e9..4e99dc211ff 100644 --- a/public/app/features/datasources/edit_ctrl.js +++ b/public/app/features/datasources/edit_ctrl.js @@ -18,7 +18,13 @@ function (angular, _, config) { module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) { - var defaults = {name: '', type: 'graphite', url: '', access: 'proxy', jsonData: {}}; + var defaults = { + name: '', + type: 'graphite', + url: '', + access: 'proxy', + jsonData: {} + }; $scope.init = function() { $scope.isNew = true; diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index 39639206c23..9176ff4f4ac 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -36,6 +36,8 @@ icon="icon-gf icon-gf-datasources"> + +
    @@ -47,8 +49,6 @@ icon="icon-gf icon-gf-datasources">
    - -
    From 2c396d4922a42260f81018b44f03377a0e98b130 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 8 Mar 2016 21:50:02 +0100 Subject: [PATCH 049/113] example(plugin.json): align example app with latests version --- examples/nginx-app/plugin.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/examples/nginx-app/plugin.json b/examples/nginx-app/plugin.json index 2cd4a9486d4..0c252186b4e 100644 --- a/examples/nginx-app/plugin.json +++ b/examples/nginx-app/plugin.json @@ -34,13 +34,10 @@ "updated": "2015-02-10" }, - "includes": { - "dashboards": [], - "plugins": [ + "includes": [ {"type": "panel", "name": "Nginx Panel"}, {"type": "datasource", "name": "Nginx Datasource"} - ] - }, + ], "dependencies": { "grafanaVersion": "3.x.x", From 59a02ce76a98adc2ddef573dc6dc9442f7081ec1 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 8 Mar 2016 22:13:40 +0100 Subject: [PATCH 050/113] example(plugin.json): add example dashboard file --- examples/nginx-app/plugin.json | 5 +++-- .../nginx-app/src/dashboards/nginx_connection_stats.json | 0 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 examples/nginx-app/src/dashboards/nginx_connection_stats.json diff --git a/examples/nginx-app/plugin.json b/examples/nginx-app/plugin.json index 0c252186b4e..65f01ad62fc 100644 --- a/examples/nginx-app/plugin.json +++ b/examples/nginx-app/plugin.json @@ -35,8 +35,9 @@ }, "includes": [ - {"type": "panel", "name": "Nginx Panel"}, - {"type": "datasource", "name": "Nginx Datasource"} + {"type": "dashboard", "name": "Nginx Connection stats", "path": "dashboards/nginx_connection_stats.json"}, + {"type": "panel", "name": "Nginx Panel"}, + {"type": "datasource", "name": "Nginx Datasource"} ], "dependencies": { diff --git a/examples/nginx-app/src/dashboards/nginx_connection_stats.json b/examples/nginx-app/src/dashboards/nginx_connection_stats.json new file mode 100644 index 00000000000..e69de29bb2d From de41536046cf0fe4f86dec461afef9db4e0418a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Mar 2016 10:26:00 +0100 Subject: [PATCH 051/113] fix(share): fixed checkboxes in share modal, fixes #4301 --- public/app/features/dashboard/partials/shareModal.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/partials/shareModal.html b/public/app/features/dashboard/partials/shareModal.html index a6d439baadf..41c08fede0b 100644 --- a/public/app/features/dashboard/partials/shareModal.html +++ b/public/app/features/dashboard/partials/shareModal.html @@ -54,11 +54,11 @@
    Include - +
    Include - +
    Theme From c0b92eb5d056847b6488c998d85403fd7faaf710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Mar 2016 10:31:43 +0100 Subject: [PATCH 052/113] fix(jshint): fixed jshint warning --- public/app/features/dashboard/shareModalCtrl.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.js index 5218297b2c1..2944cc79e42 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.js @@ -1,5 +1,4 @@ -define([ - 'angular', +define(['angular', 'lodash', 'require', 'app/core/config', From ca7c3cc27d86b5085f983c83db5ade61082e75c7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 9 Mar 2016 11:34:59 +0100 Subject: [PATCH 053/113] docs(changelog): add opentsdb annotations to changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd9b6c0ec6d..40a0421d45a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ * **graph**: Template variables can now be used in TimeShift and TimeFrom, closes[#1960](https://github.com/grafana/grafana/issues/1960) * **Tooltip**: Optionally add milliseconds to timestamp in tool tip, closes[#2248](https://github.com/grafana/grafana/issues/2248) * **Opentsdb**: Support milliseconds when using openTSDB datasource, closes [#2865](https://github.com/grafana/grafana/issues/2865) +* **Opentsdb**: Add support for annotations, closes[#664](https://github.com/grafana/grafana/issues/664) ### Bug fixes * **Playlist**: Fix for memory leak when running a playlist, closes [#3794](https://github.com/grafana/grafana/pull/3794) From f0d1853a668c8d0db38f865b0fcf215eaa1ab053 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 9 Mar 2016 11:56:52 +0100 Subject: [PATCH 054/113] style(templating): tigth-form -> gf-form --- public/app/features/templating/partials/editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index 3028bfa8a36..bbdabe92fb8 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -171,7 +171,7 @@
    Refresh - + When to update the values of this template, will slow down dashboard load / time change
    From e5ffb2bfd2013c9efaa387d03a2e76289c455c7e Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 9 Mar 2016 12:08:09 +0100 Subject: [PATCH 055/113] github(issue_template): minor improvement --- .github/ISSUE_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 19a05fc78d7..61f039fe615 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -4,7 +4,7 @@ To help us respond to your issues faster, please make sure to add as much inform If this issue is about a plugin, please open the issue in that repository. -Tag your issues title with [Feature Request] / [Bug] / [Question] or no tag if your unsure. +Start your issues title with [Feature Request] / [Bug] / [Question] or no tag if your unsure. Ex * What grafana version are you using? From bb1e7f62a0f127b21c744bc9970d343b202dc8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Mar 2016 12:38:17 +0100 Subject: [PATCH 056/113] fix(build): updated moment.js to fix systemjs build issue, fixes #4306 --- public/vendor/moment.js | 5394 +++++++++++++++++++++++---------------- 1 file changed, 3137 insertions(+), 2257 deletions(-) diff --git a/public/vendor/moment.js b/public/vendor/moment.js index 03a2460105d..a1335de9166 100644 --- a/public/vendor/moment.js +++ b/public/vendor/moment.js @@ -1,468 +1,165 @@ //! moment.js -//! version : 2.8.1 +//! version : 2.12.0 //! authors : Tim Wood, Iskren Chernev, Moment.js contributors //! license : MIT //! momentjs.com -(function (undefined) { - /************************************ - Constants - ************************************/ +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + global.moment = factory() +}(this, function () { 'use strict'; - var moment, - VERSION = '2.8.1', - // the global-scope this is NOT the global object in Node.js - globalScope = typeof global !== 'undefined' ? global : this, - oldGlobalMoment, - round = Math.round, - i, + var hookCallback; - YEAR = 0, - MONTH = 1, - DATE = 2, - HOUR = 3, - MINUTE = 4, - SECOND = 5, - MILLISECOND = 6, + function utils_hooks__hooks () { + return hookCallback.apply(null, arguments); + } - // internal storage for locale config files - locales = {}, + // This is done to register the method called with moment() + // without creating circular dependencies. + function setHookCallback (callback) { + hookCallback = callback; + } - // extra moment internal properties (plugins register props here) - momentProperties = [], + function isArray(input) { + return input instanceof Array || Object.prototype.toString.call(input) === '[object Array]'; + } - // check for nodeJS - hasModule = (typeof module !== 'undefined' && module.exports), + function isDate(input) { + return input instanceof Date || Object.prototype.toString.call(input) === '[object Date]'; + } - // ASP.NET json date format regex - aspNetJsonRegex = /^\/?Date\((\-?\d+)/i, - aspNetTimeSpanJsonRegex = /(\-)?(?:(\d*)\.)?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?)?/, - - // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html - // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere - isoDurationRegex = /^(-)?P(?:(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?|([0-9,.]*)W)$/, - - // format tokens - formattingTokens = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Q|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,4}|X|zz?|ZZ?|.)/g, - localFormattingTokens = /(\[[^\[]*\])|(\\)?(LT|LL?L?L?|l{1,4})/g, - - // parsing token regexes - parseTokenOneOrTwoDigits = /\d\d?/, // 0 - 99 - parseTokenOneToThreeDigits = /\d{1,3}/, // 0 - 999 - parseTokenOneToFourDigits = /\d{1,4}/, // 0 - 9999 - parseTokenOneToSixDigits = /[+\-]?\d{1,6}/, // -999,999 - 999,999 - parseTokenDigits = /\d+/, // nonzero number of digits - parseTokenWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i, // any word (or two) characters or numbers including two/three word month in arabic. - parseTokenTimezone = /Z|[\+\-]\d\d:?\d\d/gi, // +00:00 -00:00 +0000 -0000 or Z - parseTokenT = /T/i, // T (ISO separator) - parseTokenTimestampMs = /[\+\-]?\d+(\.\d{1,3})?/, // 123456789 123456789.123 - parseTokenOrdinal = /\d{1,2}/, - - //strict parsing regexes - parseTokenOneDigit = /\d/, // 0 - 9 - parseTokenTwoDigits = /\d\d/, // 00 - 99 - parseTokenThreeDigits = /\d{3}/, // 000 - 999 - parseTokenFourDigits = /\d{4}/, // 0000 - 9999 - parseTokenSixDigits = /[+-]?\d{6}/, // -999,999 - 999,999 - parseTokenSignedNumber = /[+-]?\d+/, // -inf - inf - - // iso 8601 regex - // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) - isoRegex = /^\s*(?:[+-]\d{6}|\d{4})-(?:(\d\d-\d\d)|(W\d\d$)|(W\d\d-\d)|(\d\d\d))((T| )(\d\d(:\d\d(:\d\d(\.\d+)?)?)?)?([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/, - - isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', - - isoDates = [ - ['YYYYYY-MM-DD', /[+-]\d{6}-\d{2}-\d{2}/], - ['YYYY-MM-DD', /\d{4}-\d{2}-\d{2}/], - ['GGGG-[W]WW-E', /\d{4}-W\d{2}-\d/], - ['GGGG-[W]WW', /\d{4}-W\d{2}/], - ['YYYY-DDD', /\d{4}-\d{3}/] - ], - - // iso time formats and regexes - isoTimes = [ - ['HH:mm:ss.SSSS', /(T| )\d\d:\d\d:\d\d\.\d+/], - ['HH:mm:ss', /(T| )\d\d:\d\d:\d\d/], - ['HH:mm', /(T| )\d\d:\d\d/], - ['HH', /(T| )\d\d/] - ], - - // timezone chunker "+10:00" > ["10", "00"] or "-1530" > ["-15", "30"] - parseTimezoneChunker = /([\+\-]|\d\d)/gi, - - // getter and setter names - proxyGettersAndSetters = 'Date|Hours|Minutes|Seconds|Milliseconds'.split('|'), - unitMillisecondFactors = { - 'Milliseconds' : 1, - 'Seconds' : 1e3, - 'Minutes' : 6e4, - 'Hours' : 36e5, - 'Days' : 864e5, - 'Months' : 2592e6, - 'Years' : 31536e6 - }, - - unitAliases = { - ms : 'millisecond', - s : 'second', - m : 'minute', - h : 'hour', - d : 'day', - D : 'date', - w : 'week', - W : 'isoWeek', - M : 'month', - Q : 'quarter', - y : 'year', - DDD : 'dayOfYear', - e : 'weekday', - E : 'isoWeekday', - gg: 'weekYear', - GG: 'isoWeekYear' - }, - - camelFunctions = { - dayofyear : 'dayOfYear', - isoweekday : 'isoWeekday', - isoweek : 'isoWeek', - weekyear : 'weekYear', - isoweekyear : 'isoWeekYear' - }, - - // format function strings - formatFunctions = {}, - - // default relative time thresholds - relativeTimeThresholds = { - s: 45, // seconds to minute - m: 45, // minutes to hour - h: 22, // hours to day - d: 26, // days to month - M: 11 // months to year - }, - - // tokens to ordinalize and pad - ordinalizeTokens = 'DDD w W M D d'.split(' '), - paddedTokens = 'M D H h m s w W'.split(' '), - - formatTokenFunctions = { - M : function () { - return this.month() + 1; - }, - MMM : function (format) { - return this.localeData().monthsShort(this, format); - }, - MMMM : function (format) { - return this.localeData().months(this, format); - }, - D : function () { - return this.date(); - }, - DDD : function () { - return this.dayOfYear(); - }, - d : function () { - return this.day(); - }, - dd : function (format) { - return this.localeData().weekdaysMin(this, format); - }, - ddd : function (format) { - return this.localeData().weekdaysShort(this, format); - }, - dddd : function (format) { - return this.localeData().weekdays(this, format); - }, - w : function () { - return this.week(); - }, - W : function () { - return this.isoWeek(); - }, - YY : function () { - return leftZeroFill(this.year() % 100, 2); - }, - YYYY : function () { - return leftZeroFill(this.year(), 4); - }, - YYYYY : function () { - return leftZeroFill(this.year(), 5); - }, - YYYYYY : function () { - var y = this.year(), sign = y >= 0 ? '+' : '-'; - return sign + leftZeroFill(Math.abs(y), 6); - }, - gg : function () { - return leftZeroFill(this.weekYear() % 100, 2); - }, - gggg : function () { - return leftZeroFill(this.weekYear(), 4); - }, - ggggg : function () { - return leftZeroFill(this.weekYear(), 5); - }, - GG : function () { - return leftZeroFill(this.isoWeekYear() % 100, 2); - }, - GGGG : function () { - return leftZeroFill(this.isoWeekYear(), 4); - }, - GGGGG : function () { - return leftZeroFill(this.isoWeekYear(), 5); - }, - e : function () { - return this.weekday(); - }, - E : function () { - return this.isoWeekday(); - }, - a : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), true); - }, - A : function () { - return this.localeData().meridiem(this.hours(), this.minutes(), false); - }, - H : function () { - return this.hours(); - }, - h : function () { - return this.hours() % 12 || 12; - }, - m : function () { - return this.minutes(); - }, - s : function () { - return this.seconds(); - }, - S : function () { - return toInt(this.milliseconds() / 100); - }, - SS : function () { - return leftZeroFill(toInt(this.milliseconds() / 10), 2); - }, - SSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - SSSS : function () { - return leftZeroFill(this.milliseconds(), 3); - }, - Z : function () { - var a = -this.zone(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + ':' + leftZeroFill(toInt(a) % 60, 2); - }, - ZZ : function () { - var a = -this.zone(), - b = '+'; - if (a < 0) { - a = -a; - b = '-'; - } - return b + leftZeroFill(toInt(a / 60), 2) + leftZeroFill(toInt(a) % 60, 2); - }, - z : function () { - return this.zoneAbbr(); - }, - zz : function () { - return this.zoneName(); - }, - X : function () { - return this.unix(); - }, - Q : function () { - return this.quarter(); - } - }, - - deprecations = {}, - - lists = ['months', 'monthsShort', 'weekdays', 'weekdaysShort', 'weekdaysMin']; - - // Pick the first defined of two or three arguments. dfl comes from - // default. - function dfl(a, b, c) { - switch (arguments.length) { - case 2: return a != null ? a : b; - case 3: return a != null ? a : b != null ? b : c; - default: throw new Error('Implement me'); + function map(arr, fn) { + var res = [], i; + for (i = 0; i < arr.length; ++i) { + res.push(fn(arr[i], i)); } + return res; } - function defaultParsingFlags() { - // We need to deep clone this object, and es5 standard is not very - // helpful. - return { - empty : false, - unusedTokens : [], - unusedInput : [], - overflow : -2, - charsLeftOver : 0, - nullInput : false, - invalidMonth : null, - invalidFormat : false, - userInvalidated : false, - iso: false - }; + function hasOwnProp(a, b) { + return Object.prototype.hasOwnProperty.call(a, b); } - function printMsg(msg) { - if (moment.suppressDeprecationWarnings === false && - typeof console !== 'undefined' && console.warn) { - console.warn("Deprecation warning: " + msg); - } - } - - function deprecate(msg, fn) { - var firstTime = true; - return extend(function () { - if (firstTime) { - printMsg(msg); - firstTime = false; - } - return fn.apply(this, arguments); - }, fn); - } - - function deprecateSimple(name, msg) { - if (!deprecations[name]) { - printMsg(msg); - deprecations[name] = true; - } - } - - function padToken(func, count) { - return function (a) { - return leftZeroFill(func.call(this, a), count); - }; - } - function ordinalizeToken(func, period) { - return function (a) { - return this.localeData().ordinal(func.call(this, a), period); - }; - } - - while (ordinalizeTokens.length) { - i = ordinalizeTokens.pop(); - formatTokenFunctions[i + 'o'] = ordinalizeToken(formatTokenFunctions[i], i); - } - while (paddedTokens.length) { - i = paddedTokens.pop(); - formatTokenFunctions[i + i] = padToken(formatTokenFunctions[i], 2); - } - formatTokenFunctions.DDDD = padToken(formatTokenFunctions.DDD, 3); - - - /************************************ - Constructors - ************************************/ - - function Locale() { - } - - // Moment prototype object - function Moment(config, skipOverflow) { - if (skipOverflow !== false) { - checkOverflow(config); - } - copyConfig(this, config); - this._d = new Date(+config._d); - } - - // Duration Constructor - function Duration(duration) { - var normalizedInput = normalizeObjectUnits(duration), - years = normalizedInput.year || 0, - quarters = normalizedInput.quarter || 0, - months = normalizedInput.month || 0, - weeks = normalizedInput.week || 0, - days = normalizedInput.day || 0, - hours = normalizedInput.hour || 0, - minutes = normalizedInput.minute || 0, - seconds = normalizedInput.second || 0, - milliseconds = normalizedInput.millisecond || 0; - - // representation for dateAddRemove - this._milliseconds = +milliseconds + - seconds * 1e3 + // 1000 - minutes * 6e4 + // 1000 * 60 - hours * 36e5; // 1000 * 60 * 60 - // Because of dateAddRemove treats 24 hours as different from a - // day when working around DST, we need to store them separately - this._days = +days + - weeks * 7; - // It is impossible translate months into days without knowing - // which months you are are talking about, so we have to store - // it separately. - this._months = +months + - quarters * 3 + - years * 12; - - this._data = {}; - - this._locale = moment.localeData(); - - this._bubble(); - } - - /************************************ - Helpers - ************************************/ - - function extend(a, b) { for (var i in b) { - if (b.hasOwnProperty(i)) { + if (hasOwnProp(b, i)) { a[i] = b[i]; } } - if (b.hasOwnProperty('toString')) { + if (hasOwnProp(b, 'toString')) { a.toString = b.toString; } - if (b.hasOwnProperty('valueOf')) { + if (hasOwnProp(b, 'valueOf')) { a.valueOf = b.valueOf; } return a; } + function create_utc__createUTC (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, true).utc(); + } + + function defaultParsingFlags() { + // We need to deep clone this object. + return { + empty : false, + unusedTokens : [], + unusedInput : [], + overflow : -2, + charsLeftOver : 0, + nullInput : false, + invalidMonth : null, + invalidFormat : false, + userInvalidated : false, + iso : false + }; + } + + function getParsingFlags(m) { + if (m._pf == null) { + m._pf = defaultParsingFlags(); + } + return m._pf; + } + + function valid__isValid(m) { + if (m._isValid == null) { + var flags = getParsingFlags(m); + m._isValid = !isNaN(m._d.getTime()) && + flags.overflow < 0 && + !flags.empty && + !flags.invalidMonth && + !flags.invalidWeekday && + !flags.nullInput && + !flags.invalidFormat && + !flags.userInvalidated; + + if (m._strict) { + m._isValid = m._isValid && + flags.charsLeftOver === 0 && + flags.unusedTokens.length === 0 && + flags.bigHour === undefined; + } + } + return m._isValid; + } + + function valid__createInvalid (flags) { + var m = create_utc__createUTC(NaN); + if (flags != null) { + extend(getParsingFlags(m), flags); + } + else { + getParsingFlags(m).userInvalidated = true; + } + + return m; + } + + function isUndefined(input) { + return input === void 0; + } + + // Plugins that add properties should also add the key here (null value), + // so we can properly clone ourselves. + var momentProperties = utils_hooks__hooks.momentProperties = []; + function copyConfig(to, from) { var i, prop, val; - if (typeof from._isAMomentObject !== 'undefined') { + if (!isUndefined(from._isAMomentObject)) { to._isAMomentObject = from._isAMomentObject; } - if (typeof from._i !== 'undefined') { + if (!isUndefined(from._i)) { to._i = from._i; } - if (typeof from._f !== 'undefined') { + if (!isUndefined(from._f)) { to._f = from._f; } - if (typeof from._l !== 'undefined') { + if (!isUndefined(from._l)) { to._l = from._l; } - if (typeof from._strict !== 'undefined') { + if (!isUndefined(from._strict)) { to._strict = from._strict; } - if (typeof from._tzm !== 'undefined') { + if (!isUndefined(from._tzm)) { to._tzm = from._tzm; } - if (typeof from._isUTC !== 'undefined') { + if (!isUndefined(from._isUTC)) { to._isUTC = from._isUTC; } - if (typeof from._offset !== 'undefined') { + if (!isUndefined(from._offset)) { to._offset = from._offset; } - if (typeof from._pf !== 'undefined') { - to._pf = from._pf; + if (!isUndefined(from._pf)) { + to._pf = getParsingFlags(from); } - if (typeof from._locale !== 'undefined') { + if (!isUndefined(from._locale)) { to._locale = from._locale; } @@ -470,7 +167,7 @@ for (i in momentProperties) { prop = momentProperties[i]; val = from[prop]; - if (typeof val !== 'undefined') { + if (!isUndefined(val)) { to[prop] = val; } } @@ -479,7 +176,26 @@ return to; } - function absRound(number) { + var updateInProgress = false; + + // Moment prototype object + function Moment(config) { + copyConfig(this, config); + this._d = new Date(config._d != null ? config._d.getTime() : NaN); + // Prevent infinite loop in case updateOffset creates new moment + // objects. + if (updateInProgress === false) { + updateInProgress = true; + utils_hooks__hooks.updateOffset(this); + updateInProgress = false; + } + } + + function isMoment (obj) { + return obj instanceof Moment || (obj != null && obj._isAMomentObject != null); + } + + function absFloor (number) { if (number < 0) { return Math.ceil(number); } else { @@ -487,91 +203,15 @@ } } - // left zero fill a number - // see http://jsperf.com/left-zero-filling for performance comparison - function leftZeroFill(number, targetLength, forceSign) { - var output = '' + Math.abs(number), - sign = number >= 0; + function toInt(argumentForCoercion) { + var coercedNumber = +argumentForCoercion, + value = 0; - while (output.length < targetLength) { - output = '0' + output; - } - return (sign ? (forceSign ? '+' : '') : '-') + output; - } - - function positiveMomentsDifference(base, other) { - var res = {milliseconds: 0, months: 0}; - - res.months = other.month() - base.month() + - (other.year() - base.year()) * 12; - if (base.clone().add(res.months, 'M').isAfter(other)) { - --res.months; + if (coercedNumber !== 0 && isFinite(coercedNumber)) { + value = absFloor(coercedNumber); } - res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - - return res; - } - - function momentsDifference(base, other) { - var res; - other = makeAs(other, base); - if (base.isBefore(other)) { - res = positiveMomentsDifference(base, other); - } else { - res = positiveMomentsDifference(other, base); - res.milliseconds = -res.milliseconds; - res.months = -res.months; - } - - return res; - } - - // TODO: remove 'name' arg after deprecation is removed - function createAdder(direction, name) { - return function (val, period) { - var dur, tmp; - //invert the arguments, but complain about it - if (period !== null && !isNaN(+period)) { - deprecateSimple(name, "moment()." + name + "(period, number) is deprecated. Please use moment()." + name + "(number, period)."); - tmp = val; val = period; period = tmp; - } - - val = typeof val === 'string' ? +val : val; - dur = moment.duration(val, period); - addOrSubtractDurationFromMoment(this, dur, direction); - return this; - }; - } - - function addOrSubtractDurationFromMoment(mom, duration, isAdding, updateOffset) { - var milliseconds = duration._milliseconds, - days = duration._days, - months = duration._months; - updateOffset = updateOffset == null ? true : updateOffset; - - if (milliseconds) { - mom._d.setTime(+mom._d + milliseconds * isAdding); - } - if (days) { - rawSetter(mom, 'Date', rawGetter(mom, 'Date') + days * isAdding); - } - if (months) { - rawMonthSetter(mom, rawGetter(mom, 'Month') + months * isAdding); - } - if (updateOffset) { - moment.updateOffset(mom, days || months); - } - } - - // check if is an array - function isArray(input) { - return Object.prototype.toString.call(input) === '[object Array]'; - } - - function isDate(input) { - return Object.prototype.toString.call(input) === '[object Date]' || - input instanceof Date; + return value; } // compare two arrays, return the number of differences @@ -589,142 +229,87 @@ return diffs + lengthDiff; } - function normalizeUnits(units) { - if (units) { - var lowered = units.toLowerCase().replace(/(.)s$/, '$1'); - units = unitAliases[units] || camelFunctions[lowered] || lowered; + function warn(msg) { + if (utils_hooks__hooks.suppressDeprecationWarnings === false && + (typeof console !== 'undefined') && console.warn) { + console.warn('Deprecation warning: ' + msg); } - return units; } - function normalizeObjectUnits(inputObject) { - var normalizedInput = {}, - normalizedProp, - prop; + function deprecate(msg, fn) { + var firstTime = true; - for (prop in inputObject) { - if (inputObject.hasOwnProperty(prop)) { - normalizedProp = normalizeUnits(prop); - if (normalizedProp) { - normalizedInput[normalizedProp] = inputObject[prop]; - } + return extend(function () { + if (firstTime) { + warn(msg + '\nArguments: ' + Array.prototype.slice.call(arguments).join(', ') + '\n' + (new Error()).stack); + firstTime = false; } - } - - return normalizedInput; + return fn.apply(this, arguments); + }, fn); } - function makeList(field) { - var count, setter; + var deprecations = {}; - if (field.indexOf('week') === 0) { - count = 7; - setter = 'day'; + function deprecateSimple(name, msg) { + if (!deprecations[name]) { + warn(msg); + deprecations[name] = true; } - else if (field.indexOf('month') === 0) { - count = 12; - setter = 'month'; - } - else { - return; - } - - moment[field] = function (format, index) { - var i, getter, - method = moment._locale[field], - results = []; - - if (typeof format === 'number') { - index = format; - format = undefined; - } - - getter = function (i) { - var m = moment().utc().set(setter, i); - return method.call(moment._locale, m, format || ''); - }; - - if (index != null) { - return getter(index); - } - else { - for (i = 0; i < count; i++) { - results.push(getter(i)); - } - return results; - } - }; } - function toInt(argumentForCoercion) { - var coercedNumber = +argumentForCoercion, - value = 0; + utils_hooks__hooks.suppressDeprecationWarnings = false; - if (coercedNumber !== 0 && isFinite(coercedNumber)) { - if (coercedNumber >= 0) { - value = Math.floor(coercedNumber); + function isFunction(input) { + return input instanceof Function || Object.prototype.toString.call(input) === '[object Function]'; + } + + function isObject(input) { + return Object.prototype.toString.call(input) === '[object Object]'; + } + + function locale_set__set (config) { + var prop, i; + for (i in config) { + prop = config[i]; + if (isFunction(prop)) { + this[i] = prop; } else { - value = Math.ceil(coercedNumber); + this['_' + i] = prop; } } - - return value; + this._config = config; + // Lenient ordinal parsing accepts just a number in addition to + // number + (possibly) stuff coming from _ordinalParseLenient. + this._ordinalParseLenient = new RegExp(this._ordinalParse.source + '|' + (/\d{1,2}/).source); } - function daysInMonth(year, month) { - return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); - } - - function weeksInYear(year, dow, doy) { - return weekOfYear(moment([year, 11, 31 + dow - doy]), dow, doy).week; - } - - function daysInYear(year) { - return isLeapYear(year) ? 366 : 365; - } - - function isLeapYear(year) { - return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; - } - - function checkOverflow(m) { - var overflow; - if (m._a && m._pf.overflow === -2) { - overflow = - m._a[MONTH] < 0 || m._a[MONTH] > 11 ? MONTH : - m._a[DATE] < 1 || m._a[DATE] > daysInMonth(m._a[YEAR], m._a[MONTH]) ? DATE : - m._a[HOUR] < 0 || m._a[HOUR] > 23 ? HOUR : - m._a[MINUTE] < 0 || m._a[MINUTE] > 59 ? MINUTE : - m._a[SECOND] < 0 || m._a[SECOND] > 59 ? SECOND : - m._a[MILLISECOND] < 0 || m._a[MILLISECOND] > 999 ? MILLISECOND : - -1; - - if (m._pf._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { - overflow = DATE; + function mergeConfigs(parentConfig, childConfig) { + var res = extend({}, parentConfig), prop; + for (prop in childConfig) { + if (hasOwnProp(childConfig, prop)) { + if (isObject(parentConfig[prop]) && isObject(childConfig[prop])) { + res[prop] = {}; + extend(res[prop], parentConfig[prop]); + extend(res[prop], childConfig[prop]); + } else if (childConfig[prop] != null) { + res[prop] = childConfig[prop]; + } else { + delete res[prop]; + } } + } + return res; + } - m._pf.overflow = overflow; + function Locale(config) { + if (config != null) { + this.set(config); } } - function isValid(m) { - if (m._isValid == null) { - m._isValid = !isNaN(m._d.getTime()) && - m._pf.overflow < 0 && - !m._pf.empty && - !m._pf.invalidMonth && - !m._pf.nullInput && - !m._pf.invalidFormat && - !m._pf.userInvalidated; - - if (m._strict) { - m._isValid = m._isValid && - m._pf.charsLeftOver === 0 && - m._pf.unusedTokens.length === 0; - } - } - return m._isValid; - } + // internal storage for locale config files + var locales = {}; + var globalLocale; function normalizeLocale(key) { return key ? key.toLowerCase().replace('_', '-') : key; @@ -759,215 +344,235 @@ function loadLocale(name) { var oldLocale = null; - if (!locales[name] && hasModule) { + // TODO: Find a better way to register and load all the locales in Node + if (!locales[name] && (typeof module !== 'undefined') && + module && module.exports) { try { - oldLocale = moment.locale(); + oldLocale = globalLocale._abbr; require('./locale/' + name); - // because defineLocale currently also sets the global locale, we want to undo that for lazy loaded locales - moment.locale(oldLocale); + // because defineLocale currently also sets the global locale, we + // want to undo that for lazy loaded locales + locale_locales__getSetGlobalLocale(oldLocale); } catch (e) { } } return locales[name]; } - // Return a moment from input, that is local/utc/zone equivalent to model. - function makeAs(input, model) { - return model._isUTC ? moment(input).zone(model._offset || 0) : - moment(input).local(); + // This function will load locale and then set the global locale. If + // no arguments are passed in, it will simply return the current global + // locale key. + function locale_locales__getSetGlobalLocale (key, values) { + var data; + if (key) { + if (isUndefined(values)) { + data = locale_locales__getLocale(key); + } + else { + data = defineLocale(key, values); + } + + if (data) { + // moment.duration._locale = moment._locale = data; + globalLocale = data; + } + } + + return globalLocale._abbr; } - /************************************ - Locale - ************************************/ - - - extend(Locale.prototype, { - - set : function (config) { - var prop, i; - for (i in config) { - prop = config[i]; - if (typeof prop === 'function') { - this[i] = prop; + function defineLocale (name, config) { + if (config !== null) { + config.abbr = name; + if (locales[name] != null) { + deprecateSimple('defineLocaleOverride', + 'use moment.updateLocale(localeName, config) to change ' + + 'an existing locale. moment.defineLocale(localeName, ' + + 'config) should only be used for creating a new locale'); + config = mergeConfigs(locales[name]._config, config); + } else if (config.parentLocale != null) { + if (locales[config.parentLocale] != null) { + config = mergeConfigs(locales[config.parentLocale]._config, config); } else { - this['_' + i] = prop; + // treat as if there is no base config + deprecateSimple('parentLocaleUndefined', + 'specified parentLocale is not defined yet'); } } - }, + locales[name] = new Locale(config); - _months : 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'), - months : function (m) { - return this._months[m.month()]; - }, + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); - _monthsShort : 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'), - monthsShort : function (m) { - return this._monthsShort[m.month()]; - }, - - monthsParse : function (monthName) { - var i, mom, regex; - - if (!this._monthsParse) { - this._monthsParse = []; - } - - for (i = 0; i < 12; i++) { - // make the regex if we don't have it already - if (!this._monthsParse[i]) { - mom = moment.utc([2000, i]); - regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); - this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._monthsParse[i].test(monthName)) { - return i; - } - } - }, - - _weekdays : 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'), - weekdays : function (m) { - return this._weekdays[m.day()]; - }, - - _weekdaysShort : 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'), - weekdaysShort : function (m) { - return this._weekdaysShort[m.day()]; - }, - - _weekdaysMin : 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'), - weekdaysMin : function (m) { - return this._weekdaysMin[m.day()]; - }, - - weekdaysParse : function (weekdayName) { - var i, mom, regex; - - if (!this._weekdaysParse) { - this._weekdaysParse = []; - } - - for (i = 0; i < 7; i++) { - // make the regex if we don't have it already - if (!this._weekdaysParse[i]) { - mom = moment([2000, 1]).day(i); - regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); - this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); - } - // test the regex - if (this._weekdaysParse[i].test(weekdayName)) { - return i; - } - } - }, - - _longDateFormat : { - LT : 'h:mm A', - L : 'MM/DD/YYYY', - LL : 'MMMM D, YYYY', - LLL : 'MMMM D, YYYY LT', - LLLL : 'dddd, MMMM D, YYYY LT' - }, - longDateFormat : function (key) { - var output = this._longDateFormat[key]; - if (!output && this._longDateFormat[key.toUpperCase()]) { - output = this._longDateFormat[key.toUpperCase()].replace(/MMMM|MM|DD|dddd/g, function (val) { - return val.slice(1); - }); - this._longDateFormat[key] = output; - } - return output; - }, - - isPM : function (input) { - // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays - // Using charAt should be more compatible. - return ((input + '').toLowerCase().charAt(0) === 'p'); - }, - - _meridiemParse : /[ap]\.?m?\.?/i, - meridiem : function (hours, minutes, isLower) { - if (hours > 11) { - return isLower ? 'pm' : 'PM'; - } else { - return isLower ? 'am' : 'AM'; - } - }, - - _calendar : { - sameDay : '[Today at] LT', - nextDay : '[Tomorrow at] LT', - nextWeek : 'dddd [at] LT', - lastDay : '[Yesterday at] LT', - lastWeek : '[Last] dddd [at] LT', - sameElse : 'L' - }, - calendar : function (key, mom) { - var output = this._calendar[key]; - return typeof output === 'function' ? output.apply(mom) : output; - }, - - _relativeTime : { - future : 'in %s', - past : '%s ago', - s : 'a few seconds', - m : 'a minute', - mm : '%d minutes', - h : 'an hour', - hh : '%d hours', - d : 'a day', - dd : '%d days', - M : 'a month', - MM : '%d months', - y : 'a year', - yy : '%d years' - }, - - relativeTime : function (number, withoutSuffix, string, isFuture) { - var output = this._relativeTime[string]; - return (typeof output === 'function') ? - output(number, withoutSuffix, string, isFuture) : - output.replace(/%d/i, number); - }, - - pastFuture : function (diff, output) { - var format = this._relativeTime[diff > 0 ? 'future' : 'past']; - return typeof format === 'function' ? format(output) : format.replace(/%s/i, output); - }, - - ordinal : function (number) { - return this._ordinal.replace('%d', number); - }, - _ordinal : '%d', - - preparse : function (string) { - return string; - }, - - postformat : function (string) { - return string; - }, - - week : function (mom) { - return weekOfYear(mom, this._week.dow, this._week.doy).week; - }, - - _week : { - dow : 0, // Sunday is the first day of the week. - doy : 6 // The week that contains Jan 1st is the first week of the year. - }, - - _invalidDate: 'Invalid date', - invalidDate: function () { - return this._invalidDate; + return locales[name]; + } else { + // useful for testing + delete locales[name]; + return null; } - }); + } - /************************************ - Formatting - ************************************/ + function updateLocale(name, config) { + if (config != null) { + var locale; + if (locales[name] != null) { + config = mergeConfigs(locales[name]._config, config); + } + locale = new Locale(config); + locale.parentLocale = locales[name]; + locales[name] = locale; + // backwards compat for now: also set the locale + locale_locales__getSetGlobalLocale(name); + } else { + // pass null for config to unupdate, useful for tests + if (locales[name] != null) { + if (locales[name].parentLocale != null) { + locales[name] = locales[name].parentLocale; + } else if (locales[name] != null) { + delete locales[name]; + } + } + } + return locales[name]; + } + + // returns locale data + function locale_locales__getLocale (key) { + var locale; + + if (key && key._locale && key._locale._abbr) { + key = key._locale._abbr; + } + + if (!key) { + return globalLocale; + } + + if (!isArray(key)) { + //short-circuit everything else + locale = loadLocale(key); + if (locale) { + return locale; + } + key = [key]; + } + + return chooseLocale(key); + } + + function locale_locales__listLocales() { + return Object.keys(locales); + } + + var aliases = {}; + + function addUnitAlias (unit, shorthand) { + var lowerCase = unit.toLowerCase(); + aliases[lowerCase] = aliases[lowerCase + 's'] = aliases[shorthand] = unit; + } + + function normalizeUnits(units) { + return typeof units === 'string' ? aliases[units] || aliases[units.toLowerCase()] : undefined; + } + + function normalizeObjectUnits(inputObject) { + var normalizedInput = {}, + normalizedProp, + prop; + + for (prop in inputObject) { + if (hasOwnProp(inputObject, prop)) { + normalizedProp = normalizeUnits(prop); + if (normalizedProp) { + normalizedInput[normalizedProp] = inputObject[prop]; + } + } + } + + return normalizedInput; + } + + function makeGetSet (unit, keepTime) { + return function (value) { + if (value != null) { + get_set__set(this, unit, value); + utils_hooks__hooks.updateOffset(this, keepTime); + return this; + } else { + return get_set__get(this, unit); + } + }; + } + + function get_set__get (mom, unit) { + return mom.isValid() ? + mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit]() : NaN; + } + + function get_set__set (mom, unit, value) { + if (mom.isValid()) { + mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + } + } + + // MOMENTS + + function getSet (units, value) { + var unit; + if (typeof units === 'object') { + for (unit in units) { + this.set(unit, units[unit]); + } + } else { + units = normalizeUnits(units); + if (isFunction(this[units])) { + return this[units](value); + } + } + return this; + } + + function zeroFill(number, targetLength, forceSign) { + var absNumber = '' + Math.abs(number), + zerosToFill = targetLength - absNumber.length, + sign = number >= 0; + return (sign ? (forceSign ? '+' : '') : '-') + + Math.pow(10, Math.max(0, zerosToFill)).toString().substr(1) + absNumber; + } + + var formattingTokens = /(\[[^\[]*\])|(\\)?([Hh]mm(ss)?|Mo|MM?M?M?|Do|DDDo|DD?D?D?|ddd?d?|do?|w[o|w]?|W[o|W]?|Qo?|YYYYYY|YYYYY|YYYY|YY|gg(ggg?)?|GG(GGG?)?|e|E|a|A|hh?|HH?|mm?|ss?|S{1,9}|x|X|zz?|ZZ?|.)/g; + + var localFormattingTokens = /(\[[^\[]*\])|(\\)?(LTS|LT|LL?L?L?|l{1,4})/g; + + var formatFunctions = {}; + + var formatTokenFunctions = {}; + + // token: 'M' + // padded: ['MM', 2] + // ordinal: 'Mo' + // callback: function () { this.month() + 1 } + function addFormatToken (token, padded, ordinal, callback) { + var func = callback; + if (typeof callback === 'string') { + func = function () { + return this[callback](); + }; + } + if (token) { + formatTokenFunctions[token] = func; + } + if (padded) { + formatTokenFunctions[padded[0]] = function () { + return zeroFill(func.apply(this, arguments), padded[1], padded[2]); + }; + } + if (ordinal) { + formatTokenFunctions[ordinal] = function () { + return this.localeData().ordinal(func.apply(this, arguments), token); + }; + } + } function removeFormattingTokens(input) { if (input.match(/\[[\s\S]/)) { @@ -1003,10 +608,7 @@ } format = expandFormat(format, m.localeData()); - - if (!formatFunctions[format]) { - formatFunctions[format] = makeFormatFunction(format); - } + formatFunctions[format] = formatFunctions[format] || makeFormatFunction(format); return formatFunctions[format](m); } @@ -1028,292 +630,615 @@ return format; } + var match1 = /\d/; // 0 - 9 + var match2 = /\d\d/; // 00 - 99 + var match3 = /\d{3}/; // 000 - 999 + var match4 = /\d{4}/; // 0000 - 9999 + var match6 = /[+-]?\d{6}/; // -999999 - 999999 + var match1to2 = /\d\d?/; // 0 - 99 + var match3to4 = /\d\d\d\d?/; // 999 - 9999 + var match5to6 = /\d\d\d\d\d\d?/; // 99999 - 999999 + var match1to3 = /\d{1,3}/; // 0 - 999 + var match1to4 = /\d{1,4}/; // 0 - 9999 + var match1to6 = /[+-]?\d{1,6}/; // -999999 - 999999 - /************************************ - Parsing - ************************************/ + var matchUnsigned = /\d+/; // 0 - inf + var matchSigned = /[+-]?\d+/; // -inf - inf + + var matchOffset = /Z|[+-]\d\d:?\d\d/gi; // +00:00 -00:00 +0000 -0000 or Z + var matchShortOffset = /Z|[+-]\d\d(?::?\d\d)?/gi; // +00 -00 +00:00 -00:00 +0000 -0000 or Z + + var matchTimestamp = /[+-]?\d+(\.\d{1,3})?/; // 123456789 123456789.123 + + // any word (or two) characters or numbers including two/three word month in arabic. + // includes scottish gaelic two word and hyphenated months + var matchWord = /[0-9]*['a-z\u00A0-\u05FF\u0700-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]+|[\u0600-\u06FF\/]+(\s*?[\u0600-\u06FF]+){1,2}/i; - // get the regex to find the next token - function getParseRegexForToken(token, config) { - var a, strict = config._strict; - switch (token) { - case 'Q': - return parseTokenOneDigit; - case 'DDDD': - return parseTokenThreeDigits; - case 'YYYY': - case 'GGGG': - case 'gggg': - return strict ? parseTokenFourDigits : parseTokenOneToFourDigits; - case 'Y': - case 'G': - case 'g': - return parseTokenSignedNumber; - case 'YYYYYY': - case 'YYYYY': - case 'GGGGG': - case 'ggggg': - return strict ? parseTokenSixDigits : parseTokenOneToSixDigits; - case 'S': - if (strict) { - return parseTokenOneDigit; + var regexes = {}; + + function addRegexToken (token, regex, strictRegex) { + regexes[token] = isFunction(regex) ? regex : function (isStrict, localeData) { + return (isStrict && strictRegex) ? strictRegex : regex; + }; + } + + function getParseRegexForToken (token, config) { + if (!hasOwnProp(regexes, token)) { + return new RegExp(unescapeFormat(token)); + } + + return regexes[token](config._strict, config._locale); + } + + // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript + function unescapeFormat(s) { + return regexEscape(s.replace('\\', '').replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { + return p1 || p2 || p3 || p4; + })); + } + + function regexEscape(s) { + return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + } + + var tokens = {}; + + function addParseToken (token, callback) { + var i, func = callback; + if (typeof token === 'string') { + token = [token]; + } + if (typeof callback === 'number') { + func = function (input, array) { + array[callback] = toInt(input); + }; + } + for (i = 0; i < token.length; i++) { + tokens[token[i]] = func; + } + } + + function addWeekParseToken (token, callback) { + addParseToken(token, function (input, array, config, token) { + config._w = config._w || {}; + callback(input, config._w, config, token); + }); + } + + function addTimeToArrayFromToken(token, input, config) { + if (input != null && hasOwnProp(tokens, token)) { + tokens[token](input, config._a, config, token); + } + } + + var YEAR = 0; + var MONTH = 1; + var DATE = 2; + var HOUR = 3; + var MINUTE = 4; + var SECOND = 5; + var MILLISECOND = 6; + var WEEK = 7; + var WEEKDAY = 8; + + function daysInMonth(year, month) { + return new Date(Date.UTC(year, month + 1, 0)).getUTCDate(); + } + + // FORMATTING + + addFormatToken('M', ['MM', 2], 'Mo', function () { + return this.month() + 1; + }); + + addFormatToken('MMM', 0, 0, function (format) { + return this.localeData().monthsShort(this, format); + }); + + addFormatToken('MMMM', 0, 0, function (format) { + return this.localeData().months(this, format); + }); + + // ALIASES + + addUnitAlias('month', 'M'); + + // PARSING + + addRegexToken('M', match1to2); + addRegexToken('MM', match1to2, match2); + addRegexToken('MMM', function (isStrict, locale) { + return locale.monthsShortRegex(isStrict); + }); + addRegexToken('MMMM', function (isStrict, locale) { + return locale.monthsRegex(isStrict); + }); + + addParseToken(['M', 'MM'], function (input, array) { + array[MONTH] = toInt(input) - 1; + }); + + addParseToken(['MMM', 'MMMM'], function (input, array, config, token) { + var month = config._locale.monthsParse(input, token, config._strict); + // if we didn't find a month name, mark the date as invalid. + if (month != null) { + array[MONTH] = month; + } else { + getParsingFlags(config).invalidMonth = input; + } + }); + + // LOCALES + + var MONTHS_IN_FORMAT = /D[oD]?(\[[^\[\]]*\]|\s+)+MMMM?/; + var defaultLocaleMonths = 'January_February_March_April_May_June_July_August_September_October_November_December'.split('_'); + function localeMonths (m, format) { + return isArray(this._months) ? this._months[m.month()] : + this._months[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + } + + var defaultLocaleMonthsShort = 'Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec'.split('_'); + function localeMonthsShort (m, format) { + return isArray(this._monthsShort) ? this._monthsShort[m.month()] : + this._monthsShort[MONTHS_IN_FORMAT.test(format) ? 'format' : 'standalone'][m.month()]; + } + + function localeMonthsParse (monthName, format, strict) { + var i, mom, regex; + + if (!this._monthsParse) { + this._monthsParse = []; + this._longMonthsParse = []; + this._shortMonthsParse = []; + } + + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + if (strict && !this._longMonthsParse[i]) { + this._longMonthsParse[i] = new RegExp('^' + this.months(mom, '').replace('.', '') + '$', 'i'); + this._shortMonthsParse[i] = new RegExp('^' + this.monthsShort(mom, '').replace('.', '') + '$', 'i'); } - /* falls through */ - case 'SS': - if (strict) { - return parseTokenTwoDigits; + if (!strict && !this._monthsParse[i]) { + regex = '^' + this.months(mom, '') + '|^' + this.monthsShort(mom, ''); + this._monthsParse[i] = new RegExp(regex.replace('.', ''), 'i'); } - /* falls through */ - case 'SSS': - if (strict) { - return parseTokenThreeDigits; + // test the regex + if (strict && format === 'MMMM' && this._longMonthsParse[i].test(monthName)) { + return i; + } else if (strict && format === 'MMM' && this._shortMonthsParse[i].test(monthName)) { + return i; + } else if (!strict && this._monthsParse[i].test(monthName)) { + return i; } - /* falls through */ - case 'DDD': - return parseTokenOneToThreeDigits; - case 'MMM': - case 'MMMM': - case 'dd': - case 'ddd': - case 'dddd': - return parseTokenWord; - case 'a': - case 'A': - return config._locale._meridiemParse; - case 'X': - return parseTokenTimestampMs; - case 'Z': - case 'ZZ': - return parseTokenTimezone; - case 'T': - return parseTokenT; - case 'SSSS': - return parseTokenDigits; - case 'MM': - case 'DD': - case 'YY': - case 'GG': - case 'gg': - case 'HH': - case 'hh': - case 'mm': - case 'ss': - case 'ww': - case 'WW': - return strict ? parseTokenTwoDigits : parseTokenOneOrTwoDigits; - case 'M': - case 'D': - case 'd': - case 'H': - case 'h': - case 'm': - case 's': - case 'w': - case 'W': - case 'e': - case 'E': - return parseTokenOneOrTwoDigits; - case 'Do': - return parseTokenOrdinal; - default : - a = new RegExp(regexpEscape(unescapeFormat(token.replace('\\', '')), 'i')); + } + } + + // MOMENTS + + function setMonth (mom, value) { + var dayOfMonth; + + if (!mom.isValid()) { + // No op + return mom; + } + + if (typeof value === 'string') { + if (/^\d+$/.test(value)) { + value = toInt(value); + } else { + value = mom.localeData().monthsParse(value); + // TODO: Another silent failure? + if (typeof value !== 'number') { + return mom; + } + } + } + + dayOfMonth = Math.min(mom.date(), daysInMonth(mom.year(), value)); + mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); + return mom; + } + + function getSetMonth (value) { + if (value != null) { + setMonth(this, value); + utils_hooks__hooks.updateOffset(this, true); + return this; + } else { + return get_set__get(this, 'Month'); + } + } + + function getDaysInMonth () { + return daysInMonth(this.year(), this.month()); + } + + var defaultMonthsShortRegex = matchWord; + function monthsShortRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsShortStrictRegex; + } else { + return this._monthsShortRegex; + } + } else { + return this._monthsShortStrictRegex && isStrict ? + this._monthsShortStrictRegex : this._monthsShortRegex; + } + } + + var defaultMonthsRegex = matchWord; + function monthsRegex (isStrict) { + if (this._monthsParseExact) { + if (!hasOwnProp(this, '_monthsRegex')) { + computeMonthsParse.call(this); + } + if (isStrict) { + return this._monthsStrictRegex; + } else { + return this._monthsRegex; + } + } else { + return this._monthsStrictRegex && isStrict ? + this._monthsStrictRegex : this._monthsRegex; + } + } + + function computeMonthsParse () { + function cmpLenRev(a, b) { + return b.length - a.length; + } + + var shortPieces = [], longPieces = [], mixedPieces = [], + i, mom; + for (i = 0; i < 12; i++) { + // make the regex if we don't have it already + mom = create_utc__createUTC([2000, i]); + shortPieces.push(this.monthsShort(mom, '')); + longPieces.push(this.months(mom, '')); + mixedPieces.push(this.months(mom, '')); + mixedPieces.push(this.monthsShort(mom, '')); + } + // Sorting makes sure if one month (or abbr) is a prefix of another it + // will match the longer piece. + shortPieces.sort(cmpLenRev); + longPieces.sort(cmpLenRev); + mixedPieces.sort(cmpLenRev); + for (i = 0; i < 12; i++) { + shortPieces[i] = regexEscape(shortPieces[i]); + longPieces[i] = regexEscape(longPieces[i]); + mixedPieces[i] = regexEscape(mixedPieces[i]); + } + + this._monthsRegex = new RegExp('^(' + mixedPieces.join('|') + ')', 'i'); + this._monthsShortRegex = this._monthsRegex; + this._monthsStrictRegex = new RegExp('^(' + longPieces.join('|') + ')$', 'i'); + this._monthsShortStrictRegex = new RegExp('^(' + shortPieces.join('|') + ')$', 'i'); + } + + function checkOverflow (m) { + var overflow; + var a = m._a; + + if (a && getParsingFlags(m).overflow === -2) { + overflow = + a[MONTH] < 0 || a[MONTH] > 11 ? MONTH : + a[DATE] < 1 || a[DATE] > daysInMonth(a[YEAR], a[MONTH]) ? DATE : + a[HOUR] < 0 || a[HOUR] > 24 || (a[HOUR] === 24 && (a[MINUTE] !== 0 || a[SECOND] !== 0 || a[MILLISECOND] !== 0)) ? HOUR : + a[MINUTE] < 0 || a[MINUTE] > 59 ? MINUTE : + a[SECOND] < 0 || a[SECOND] > 59 ? SECOND : + a[MILLISECOND] < 0 || a[MILLISECOND] > 999 ? MILLISECOND : + -1; + + if (getParsingFlags(m)._overflowDayOfYear && (overflow < YEAR || overflow > DATE)) { + overflow = DATE; + } + if (getParsingFlags(m)._overflowWeeks && overflow === -1) { + overflow = WEEK; + } + if (getParsingFlags(m)._overflowWeekday && overflow === -1) { + overflow = WEEKDAY; + } + + getParsingFlags(m).overflow = overflow; + } + + return m; + } + + // iso 8601 regex + // 0000-00-00 0000-W00 or 0000-W00-0 + T + 00 or 00:00 or 00:00:00 or 00:00:00.000 + +00:00 or +0000 or +00) + var extendedIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/; + var basicIsoRegex = /^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?/; + + var tzRegex = /Z|[+-]\d\d(?::?\d\d)?/; + + var isoDates = [ + ['YYYYYY-MM-DD', /[+-]\d{6}-\d\d-\d\d/], + ['YYYY-MM-DD', /\d{4}-\d\d-\d\d/], + ['GGGG-[W]WW-E', /\d{4}-W\d\d-\d/], + ['GGGG-[W]WW', /\d{4}-W\d\d/, false], + ['YYYY-DDD', /\d{4}-\d{3}/], + ['YYYY-MM', /\d{4}-\d\d/, false], + ['YYYYYYMMDD', /[+-]\d{10}/], + ['YYYYMMDD', /\d{8}/], + // YYYYMM is NOT allowed by the standard + ['GGGG[W]WWE', /\d{4}W\d{3}/], + ['GGGG[W]WW', /\d{4}W\d{2}/, false], + ['YYYYDDD', /\d{7}/] + ]; + + // iso time formats and regexes + var isoTimes = [ + ['HH:mm:ss.SSSS', /\d\d:\d\d:\d\d\.\d+/], + ['HH:mm:ss,SSSS', /\d\d:\d\d:\d\d,\d+/], + ['HH:mm:ss', /\d\d:\d\d:\d\d/], + ['HH:mm', /\d\d:\d\d/], + ['HHmmss.SSSS', /\d\d\d\d\d\d\.\d+/], + ['HHmmss,SSSS', /\d\d\d\d\d\d,\d+/], + ['HHmmss', /\d\d\d\d\d\d/], + ['HHmm', /\d\d\d\d/], + ['HH', /\d\d/] + ]; + + var aspNetJsonRegex = /^\/?Date\((\-?\d+)/i; + + // date from iso format + function configFromISO(config) { + var i, l, + string = config._i, + match = extendedIsoRegex.exec(string) || basicIsoRegex.exec(string), + allowTime, dateFormat, timeFormat, tzFormat; + + if (match) { + getParsingFlags(config).iso = true; + + for (i = 0, l = isoDates.length; i < l; i++) { + if (isoDates[i][1].exec(match[1])) { + dateFormat = isoDates[i][0]; + allowTime = isoDates[i][2] !== false; + break; + } + } + if (dateFormat == null) { + config._isValid = false; + return; + } + if (match[3]) { + for (i = 0, l = isoTimes.length; i < l; i++) { + if (isoTimes[i][1].exec(match[3])) { + // match[2] should be 'T' or space + timeFormat = (match[2] || ' ') + isoTimes[i][0]; + break; + } + } + if (timeFormat == null) { + config._isValid = false; + return; + } + } + if (!allowTime && timeFormat != null) { + config._isValid = false; + return; + } + if (match[4]) { + if (tzRegex.exec(match[4])) { + tzFormat = 'Z'; + } else { + config._isValid = false; + return; + } + } + config._f = dateFormat + (timeFormat || '') + (tzFormat || ''); + configFromStringAndFormat(config); + } else { + config._isValid = false; + } + } + + // date from iso format or fallback + function configFromString(config) { + var matched = aspNetJsonRegex.exec(config._i); + + if (matched !== null) { + config._d = new Date(+matched[1]); + return; + } + + configFromISO(config); + if (config._isValid === false) { + delete config._isValid; + utils_hooks__hooks.createFromInputFallback(config); + } + } + + utils_hooks__hooks.createFromInputFallback = deprecate( + 'moment construction falls back to js Date. This is ' + + 'discouraged and will be removed in upcoming major ' + + 'release. Please refer to ' + + 'https://github.com/moment/moment/issues/1407 for more info.', + function (config) { + config._d = new Date(config._i + (config._useUTC ? ' UTC' : '')); + } + ); + + function createDate (y, m, d, h, M, s, ms) { + //can't just apply() to create a date: + //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply + var date = new Date(y, m, d, h, M, s, ms); + + //the date constructor remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getFullYear())) { + date.setFullYear(y); + } + return date; + } + + function createUTCDate (y) { + var date = new Date(Date.UTC.apply(null, arguments)); + + //the Date.UTC function remaps years 0-99 to 1900-1999 + if (y < 100 && y >= 0 && isFinite(date.getUTCFullYear())) { + date.setUTCFullYear(y); + } + return date; + } + + // FORMATTING + + addFormatToken('Y', 0, 0, function () { + var y = this.year(); + return y <= 9999 ? '' + y : '+' + y; + }); + + addFormatToken(0, ['YY', 2], 0, function () { + return this.year() % 100; + }); + + addFormatToken(0, ['YYYY', 4], 0, 'year'); + addFormatToken(0, ['YYYYY', 5], 0, 'year'); + addFormatToken(0, ['YYYYYY', 6, true], 0, 'year'); + + // ALIASES + + addUnitAlias('year', 'y'); + + // PARSING + + addRegexToken('Y', matchSigned); + addRegexToken('YY', match1to2, match2); + addRegexToken('YYYY', match1to4, match4); + addRegexToken('YYYYY', match1to6, match6); + addRegexToken('YYYYYY', match1to6, match6); + + addParseToken(['YYYYY', 'YYYYYY'], YEAR); + addParseToken('YYYY', function (input, array) { + array[YEAR] = input.length === 2 ? utils_hooks__hooks.parseTwoDigitYear(input) : toInt(input); + }); + addParseToken('YY', function (input, array) { + array[YEAR] = utils_hooks__hooks.parseTwoDigitYear(input); + }); + addParseToken('Y', function (input, array) { + array[YEAR] = parseInt(input, 10); + }); + + // HELPERS + + function daysInYear(year) { + return isLeapYear(year) ? 366 : 365; + } + + function isLeapYear(year) { + return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; + } + + // HOOKS + + utils_hooks__hooks.parseTwoDigitYear = function (input) { + return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); + }; + + // MOMENTS + + var getSetYear = makeGetSet('FullYear', false); + + function getIsLeapYear () { + return isLeapYear(this.year()); + } + + // start-of-first-week - start-of-year + function firstWeekOffset(year, dow, doy) { + var // first-week day -- which january is always in the first week (4 for iso, 1 for other) + fwd = 7 + dow - doy, + // first-week day local weekday -- which local weekday is fwd + fwdlw = (7 + createUTCDate(year, 0, fwd).getUTCDay() - dow) % 7; + + return -fwdlw + fwd - 1; + } + + //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday + function dayOfYearFromWeeks(year, week, weekday, dow, doy) { + var localWeekday = (7 + weekday - dow) % 7, + weekOffset = firstWeekOffset(year, dow, doy), + dayOfYear = 1 + 7 * (week - 1) + localWeekday + weekOffset, + resYear, resDayOfYear; + + if (dayOfYear <= 0) { + resYear = year - 1; + resDayOfYear = daysInYear(resYear) + dayOfYear; + } else if (dayOfYear > daysInYear(year)) { + resYear = year + 1; + resDayOfYear = dayOfYear - daysInYear(year); + } else { + resYear = year; + resDayOfYear = dayOfYear; + } + + return { + year: resYear, + dayOfYear: resDayOfYear + }; + } + + function weekOfYear(mom, dow, doy) { + var weekOffset = firstWeekOffset(mom.year(), dow, doy), + week = Math.floor((mom.dayOfYear() - weekOffset - 1) / 7) + 1, + resWeek, resYear; + + if (week < 1) { + resYear = mom.year() - 1; + resWeek = week + weeksInYear(resYear, dow, doy); + } else if (week > weeksInYear(mom.year(), dow, doy)) { + resWeek = week - weeksInYear(mom.year(), dow, doy); + resYear = mom.year() + 1; + } else { + resYear = mom.year(); + resWeek = week; + } + + return { + week: resWeek, + year: resYear + }; + } + + function weeksInYear(year, dow, doy) { + var weekOffset = firstWeekOffset(year, dow, doy), + weekOffsetNext = firstWeekOffset(year + 1, dow, doy); + return (daysInYear(year) - weekOffset + weekOffsetNext) / 7; + } + + // Pick the first defined of two or three arguments. + function defaults(a, b, c) { + if (a != null) { return a; } - } - - function timezoneMinutesFromString(string) { - string = string || ''; - var possibleTzMatches = (string.match(parseTokenTimezone) || []), - tzChunk = possibleTzMatches[possibleTzMatches.length - 1] || [], - parts = (tzChunk + '').match(parseTimezoneChunker) || ['-', 0, 0], - minutes = +(parts[1] * 60) + toInt(parts[2]); - - return parts[0] === '+' ? -minutes : minutes; - } - - // function to convert string input to date - function addTimeToArrayFromToken(token, input, config) { - var a, datePartArray = config._a; - - switch (token) { - // QUARTER - case 'Q': - if (input != null) { - datePartArray[MONTH] = (toInt(input) - 1) * 3; - } - break; - // MONTH - case 'M' : // fall through to MM - case 'MM' : - if (input != null) { - datePartArray[MONTH] = toInt(input) - 1; - } - break; - case 'MMM' : // fall through to MMMM - case 'MMMM' : - a = config._locale.monthsParse(input); - // if we didn't find a month name, mark the date as invalid. - if (a != null) { - datePartArray[MONTH] = a; - } else { - config._pf.invalidMonth = input; - } - break; - // DAY OF MONTH - case 'D' : // fall through to DD - case 'DD' : - if (input != null) { - datePartArray[DATE] = toInt(input); - } - break; - case 'Do' : - if (input != null) { - datePartArray[DATE] = toInt(parseInt(input, 10)); - } - break; - // DAY OF YEAR - case 'DDD' : // fall through to DDDD - case 'DDDD' : - if (input != null) { - config._dayOfYear = toInt(input); - } - - break; - // YEAR - case 'YY' : - datePartArray[YEAR] = moment.parseTwoDigitYear(input); - break; - case 'YYYY' : - case 'YYYYY' : - case 'YYYYYY' : - datePartArray[YEAR] = toInt(input); - break; - // AM / PM - case 'a' : // fall through to A - case 'A' : - config._isPm = config._locale.isPM(input); - break; - // 24 HOUR - case 'H' : // fall through to hh - case 'HH' : // fall through to hh - case 'h' : // fall through to hh - case 'hh' : - datePartArray[HOUR] = toInt(input); - break; - // MINUTE - case 'm' : // fall through to mm - case 'mm' : - datePartArray[MINUTE] = toInt(input); - break; - // SECOND - case 's' : // fall through to ss - case 'ss' : - datePartArray[SECOND] = toInt(input); - break; - // MILLISECOND - case 'S' : - case 'SS' : - case 'SSS' : - case 'SSSS' : - datePartArray[MILLISECOND] = toInt(('0.' + input) * 1000); - break; - // UNIX TIMESTAMP WITH MS - case 'X': - config._d = new Date(parseFloat(input) * 1000); - break; - // TIMEZONE - case 'Z' : // fall through to ZZ - case 'ZZ' : - config._useUTC = true; - config._tzm = timezoneMinutesFromString(input); - break; - // WEEKDAY - human - case 'dd': - case 'ddd': - case 'dddd': - a = config._locale.weekdaysParse(input); - // if we didn't get a weekday name, mark the date as invalid - if (a != null) { - config._w = config._w || {}; - config._w['d'] = a; - } else { - config._pf.invalidWeekday = input; - } - break; - // WEEK, WEEK DAY - numeric - case 'w': - case 'ww': - case 'W': - case 'WW': - case 'd': - case 'e': - case 'E': - token = token.substr(0, 1); - /* falls through */ - case 'gggg': - case 'GGGG': - case 'GGGGG': - token = token.substr(0, 2); - if (input) { - config._w = config._w || {}; - config._w[token] = toInt(input); - } - break; - case 'gg': - case 'GG': - config._w = config._w || {}; - config._w[token] = moment.parseTwoDigitYear(input); + if (b != null) { + return b; } + return c; } - function dayOfYearFromWeekInfo(config) { - var w, weekYear, week, weekday, dow, doy, temp; - - w = config._w; - if (w.GG != null || w.W != null || w.E != null) { - dow = 1; - doy = 4; - - // TODO: We need to take the current isoWeekYear, but that depends on - // how we interpret now (local, utc, fixed offset). So create - // a now version of current config (take local/utc/offset flags, and - // create now). - weekYear = dfl(w.GG, config._a[YEAR], weekOfYear(moment(), 1, 4).year); - week = dfl(w.W, 1); - weekday = dfl(w.E, 1); - } else { - dow = config._locale._week.dow; - doy = config._locale._week.doy; - - weekYear = dfl(w.gg, config._a[YEAR], weekOfYear(moment(), dow, doy).year); - week = dfl(w.w, 1); - - if (w.d != null) { - // weekday -- low day numbers are considered next week - weekday = w.d; - if (weekday < dow) { - ++week; - } - } else if (w.e != null) { - // local weekday -- counting starts from begining of week - weekday = w.e + dow; - } else { - // default to begining of week - weekday = dow; - } + function currentDateArray(config) { + // hooks is actually the exported moment object + var nowValue = new Date(utils_hooks__hooks.now()); + if (config._useUTC) { + return [nowValue.getUTCFullYear(), nowValue.getUTCMonth(), nowValue.getUTCDate()]; } - temp = dayOfYearFromWeeks(weekYear, week, weekday, doy, dow); - - config._a[YEAR] = temp.year; - config._dayOfYear = temp.dayOfYear; + return [nowValue.getFullYear(), nowValue.getMonth(), nowValue.getDate()]; } // convert an array to a date. // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] - function dateFromConfig(config) { + function configFromArray (config) { var i, date, input = [], currentDate, yearToUse; if (config._d) { @@ -1329,13 +1254,13 @@ //if the day of the year is set, figure out what it is if (config._dayOfYear) { - yearToUse = dfl(config._a[YEAR], currentDate[YEAR]); + yearToUse = defaults(config._a[YEAR], currentDate[YEAR]); if (config._dayOfYear > daysInYear(yearToUse)) { - config._pf._overflowDayOfYear = true; + getParsingFlags(config)._overflowDayOfYear = true; } - date = makeUTCDate(yearToUse, 0, config._dayOfYear); + date = createUTCDate(yearToUse, 0, config._dayOfYear); config._a[MONTH] = date.getUTCMonth(); config._a[DATE] = date.getUTCDate(); } @@ -1354,57 +1279,93 @@ config._a[i] = input[i] = (config._a[i] == null) ? (i === 2 ? 1 : 0) : config._a[i]; } - config._d = (config._useUTC ? makeUTCDate : makeDate).apply(null, input); - // Apply timezone offset from input. The actual zone can be changed + // Check for 24:00:00.000 + if (config._a[HOUR] === 24 && + config._a[MINUTE] === 0 && + config._a[SECOND] === 0 && + config._a[MILLISECOND] === 0) { + config._nextDay = true; + config._a[HOUR] = 0; + } + + config._d = (config._useUTC ? createUTCDate : createDate).apply(null, input); + // Apply timezone offset from input. The actual utcOffset can be changed // with parseZone. if (config._tzm != null) { - config._d.setUTCMinutes(config._d.getUTCMinutes() + config._tzm); + config._d.setUTCMinutes(config._d.getUTCMinutes() - config._tzm); + } + + if (config._nextDay) { + config._a[HOUR] = 24; } } - function dateFromObject(config) { - var normalizedInput; + function dayOfYearFromWeekInfo(config) { + var w, weekYear, week, weekday, dow, doy, temp, weekdayOverflow; - if (config._d) { - return; - } + w = config._w; + if (w.GG != null || w.W != null || w.E != null) { + dow = 1; + doy = 4; - normalizedInput = normalizeObjectUnits(config._i); - config._a = [ - normalizedInput.year, - normalizedInput.month, - normalizedInput.day, - normalizedInput.hour, - normalizedInput.minute, - normalizedInput.second, - normalizedInput.millisecond - ]; - - dateFromConfig(config); - } - - function currentDateArray(config) { - var now = new Date(); - if (config._useUTC) { - return [ - now.getUTCFullYear(), - now.getUTCMonth(), - now.getUTCDate() - ]; + // TODO: We need to take the current isoWeekYear, but that depends on + // how we interpret now (local, utc, fixed offset). So create + // a now version of current config (take local/utc/offset flags, and + // create now). + weekYear = defaults(w.GG, config._a[YEAR], weekOfYear(local__createLocal(), 1, 4).year); + week = defaults(w.W, 1); + weekday = defaults(w.E, 1); + if (weekday < 1 || weekday > 7) { + weekdayOverflow = true; + } } else { - return [now.getFullYear(), now.getMonth(), now.getDate()]; + dow = config._locale._week.dow; + doy = config._locale._week.doy; + + weekYear = defaults(w.gg, config._a[YEAR], weekOfYear(local__createLocal(), dow, doy).year); + week = defaults(w.w, 1); + + if (w.d != null) { + // weekday -- low day numbers are considered next week + weekday = w.d; + if (weekday < 0 || weekday > 6) { + weekdayOverflow = true; + } + } else if (w.e != null) { + // local weekday -- counting starts from begining of week + weekday = w.e + dow; + if (w.e < 0 || w.e > 6) { + weekdayOverflow = true; + } + } else { + // default to begining of week + weekday = dow; + } + } + if (week < 1 || week > weeksInYear(weekYear, dow, doy)) { + getParsingFlags(config)._overflowWeeks = true; + } else if (weekdayOverflow != null) { + getParsingFlags(config)._overflowWeekday = true; + } else { + temp = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy); + config._a[YEAR] = temp.year; + config._dayOfYear = temp.dayOfYear; } } + // constant that refers to the ISO standard + utils_hooks__hooks.ISO_8601 = function () {}; + // date from string and format string - function makeDateFromStringAndFormat(config) { - if (config._f === moment.ISO_8601) { - parseISO(config); + function configFromStringAndFormat(config) { + // TODO: Move this to another part of the creation flow to prevent circular deps + if (config._f === utils_hooks__hooks.ISO_8601) { + configFromISO(config); return; } config._a = []; - config._pf.empty = true; + getParsingFlags(config).empty = true; // This array is used to make a Date, either with `new Date` or `Date.UTC` var string = '' + config._i, @@ -1417,10 +1378,12 @@ for (i = 0; i < tokens.length; i++) { token = tokens[i]; parsedInput = (string.match(getParseRegexForToken(token, config)) || [])[0]; + // console.log('token', token, 'parsedInput', parsedInput, + // 'regex', getParseRegexForToken(token, config)); if (parsedInput) { skipped = string.substr(0, string.indexOf(parsedInput)); if (skipped.length > 0) { - config._pf.unusedInput.push(skipped); + getParsingFlags(config).unusedInput.push(skipped); } string = string.slice(string.indexOf(parsedInput) + parsedInput.length); totalParsedInputLength += parsedInput.length; @@ -1428,50 +1391,65 @@ // don't parse if it's not a known token if (formatTokenFunctions[token]) { if (parsedInput) { - config._pf.empty = false; + getParsingFlags(config).empty = false; } else { - config._pf.unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(token); } addTimeToArrayFromToken(token, parsedInput, config); } else if (config._strict && !parsedInput) { - config._pf.unusedTokens.push(token); + getParsingFlags(config).unusedTokens.push(token); } } // add remaining unparsed input length to the string - config._pf.charsLeftOver = stringLength - totalParsedInputLength; + getParsingFlags(config).charsLeftOver = stringLength - totalParsedInputLength; if (string.length > 0) { - config._pf.unusedInput.push(string); + getParsingFlags(config).unusedInput.push(string); } - // handle am pm - if (config._isPm && config._a[HOUR] < 12) { - config._a[HOUR] += 12; - } - // if is 12 am, change hours to 0 - if (config._isPm === false && config._a[HOUR] === 12) { - config._a[HOUR] = 0; + // clear _12h flag if hour is <= 12 + if (getParsingFlags(config).bigHour === true && + config._a[HOUR] <= 12 && + config._a[HOUR] > 0) { + getParsingFlags(config).bigHour = undefined; } + // handle meridiem + config._a[HOUR] = meridiemFixWrap(config._locale, config._a[HOUR], config._meridiem); - dateFromConfig(config); + configFromArray(config); checkOverflow(config); } - function unescapeFormat(s) { - return s.replace(/\\(\[)|\\(\])|\[([^\]\[]*)\]|\\(.)/g, function (matched, p1, p2, p3, p4) { - return p1 || p2 || p3 || p4; - }); - } - // Code from http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript - function regexpEscape(s) { - return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); + function meridiemFixWrap (locale, hour, meridiem) { + var isPm; + + if (meridiem == null) { + // nothing to do + return hour; + } + if (locale.meridiemHour != null) { + return locale.meridiemHour(hour, meridiem); + } else if (locale.isPM != null) { + // Fallback + isPm = locale.isPM(meridiem); + if (isPm && hour < 12) { + hour += 12; + } + if (!isPm && hour === 12) { + hour = 0; + } + return hour; + } else { + // this is not supposed to happen + return hour; + } } // date from string and array of format strings - function makeDateFromStringAndArray(config) { + function configFromStringAndArray(config) { var tempConfig, bestMoment, @@ -1480,7 +1458,7 @@ currentScore; if (config._f.length === 0) { - config._pf.invalidFormat = true; + getParsingFlags(config).invalidFormat = true; config._d = new Date(NaN); return; } @@ -1488,21 +1466,23 @@ for (i = 0; i < config._f.length; i++) { currentScore = 0; tempConfig = copyConfig({}, config); - tempConfig._pf = defaultParsingFlags(); + if (config._useUTC != null) { + tempConfig._useUTC = config._useUTC; + } tempConfig._f = config._f[i]; - makeDateFromStringAndFormat(tempConfig); + configFromStringAndFormat(tempConfig); - if (!isValid(tempConfig)) { + if (!valid__isValid(tempConfig)) { continue; } // if there is any input that was not parsed add a penalty for that format - currentScore += tempConfig._pf.charsLeftOver; + currentScore += getParsingFlags(tempConfig).charsLeftOver; //or tokens - currentScore += tempConfig._pf.unusedTokens.length * 10; + currentScore += getParsingFlags(tempConfig).unusedTokens.length * 10; - tempConfig._pf.score = currentScore; + getParsingFlags(tempConfig).score = currentScore; if (scoreToBeat == null || currentScore < scoreToBeat) { scoreToBeat = currentScore; @@ -1513,251 +1493,130 @@ extend(config, bestMoment || tempConfig); } - // date from iso format - function parseISO(config) { - var i, l, - string = config._i, - match = isoRegex.exec(string); - - if (match) { - config._pf.iso = true; - for (i = 0, l = isoDates.length; i < l; i++) { - if (isoDates[i][1].exec(string)) { - // match[5] should be "T" or undefined - config._f = isoDates[i][0] + (match[6] || ' '); - break; - } - } - for (i = 0, l = isoTimes.length; i < l; i++) { - if (isoTimes[i][1].exec(string)) { - config._f += isoTimes[i][0]; - break; - } - } - if (string.match(parseTokenTimezone)) { - config._f += 'Z'; - } - makeDateFromStringAndFormat(config); - } else { - config._isValid = false; - } - } - - // date from iso format or fallback - function makeDateFromString(config) { - parseISO(config); - if (config._isValid === false) { - delete config._isValid; - moment.createFromInputFallback(config); - } - } - - function makeDateFromInput(config) { - var input = config._i, matched; - if (input === undefined) { - config._d = new Date(); - } else if (isDate(input)) { - config._d = new Date(+input); - } else if ((matched = aspNetJsonRegex.exec(input)) !== null) { - config._d = new Date(+matched[1]); - } else if (typeof input === 'string') { - makeDateFromString(config); - } else if (isArray(input)) { - config._a = input.slice(0); - dateFromConfig(config); - } else if (typeof(input) === 'object') { - dateFromObject(config); - } else if (typeof(input) === 'number') { - // from milliseconds - config._d = new Date(input); - } else { - moment.createFromInputFallback(config); - } - } - - function makeDate(y, m, d, h, M, s, ms) { - //can't just apply() to create a date: - //http://stackoverflow.com/questions/181348/instantiating-a-javascript-object-by-calling-prototype-constructor-apply - var date = new Date(y, m, d, h, M, s, ms); - - //the date constructor doesn't accept years < 1970 - if (y < 1970) { - date.setFullYear(y); - } - return date; - } - - function makeUTCDate(y) { - var date = new Date(Date.UTC.apply(null, arguments)); - if (y < 1970) { - date.setUTCFullYear(y); - } - return date; - } - - function parseWeekday(input, locale) { - if (typeof input === 'string') { - if (!isNaN(input)) { - input = parseInt(input, 10); - } - else { - input = locale.weekdaysParse(input); - if (typeof input !== 'number') { - return null; - } - } - } - return input; - } - - /************************************ - Relative Time - ************************************/ - - - // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize - function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { - return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); - } - - function relativeTime(posNegDuration, withoutSuffix, locale) { - var duration = moment.duration(posNegDuration).abs(), - seconds = round(duration.as('s')), - minutes = round(duration.as('m')), - hours = round(duration.as('h')), - days = round(duration.as('d')), - months = round(duration.as('M')), - years = round(duration.as('y')), - - args = seconds < relativeTimeThresholds.s && ['s', seconds] || - minutes === 1 && ['m'] || - minutes < relativeTimeThresholds.m && ['mm', minutes] || - hours === 1 && ['h'] || - hours < relativeTimeThresholds.h && ['hh', hours] || - days === 1 && ['d'] || - days < relativeTimeThresholds.d && ['dd', days] || - months === 1 && ['M'] || - months < relativeTimeThresholds.M && ['MM', months] || - years === 1 && ['y'] || ['yy', years]; - - args[2] = withoutSuffix; - args[3] = +posNegDuration > 0; - args[4] = locale; - return substituteTimeAgo.apply({}, args); - } - - - /************************************ - Week of Year - ************************************/ - - - // firstDayOfWeek 0 = sun, 6 = sat - // the day of the week that starts the week - // (usually sunday or monday) - // firstDayOfWeekOfYear 0 = sun, 6 = sat - // the first week is the week that contains the first - // of this day of the week - // (eg. ISO weeks use thursday (4)) - function weekOfYear(mom, firstDayOfWeek, firstDayOfWeekOfYear) { - var end = firstDayOfWeekOfYear - firstDayOfWeek, - daysToDayOfWeek = firstDayOfWeekOfYear - mom.day(), - adjustedMoment; - - - if (daysToDayOfWeek > end) { - daysToDayOfWeek -= 7; + function configFromObject(config) { + if (config._d) { + return; } - if (daysToDayOfWeek < end - 7) { - daysToDayOfWeek += 7; + var i = normalizeObjectUnits(config._i); + config._a = map([i.year, i.month, i.day || i.date, i.hour, i.minute, i.second, i.millisecond], function (obj) { + return obj && parseInt(obj, 10); + }); + + configFromArray(config); + } + + function createFromConfig (config) { + var res = new Moment(checkOverflow(prepareConfig(config))); + if (res._nextDay) { + // Adding is smart enough around DST + res.add(1, 'd'); + res._nextDay = undefined; } - adjustedMoment = moment(mom).add(daysToDayOfWeek, 'd'); - return { - week: Math.ceil(adjustedMoment.dayOfYear() / 7), - year: adjustedMoment.year() - }; + return res; } - //http://en.wikipedia.org/wiki/ISO_week_date#Calculating_a_date_given_the_year.2C_week_number_and_weekday - function dayOfYearFromWeeks(year, week, weekday, firstDayOfWeekOfYear, firstDayOfWeek) { - var d = makeUTCDate(year, 0, 1).getUTCDay(), daysToAdd, dayOfYear; - - d = d === 0 ? 7 : d; - weekday = weekday != null ? weekday : firstDayOfWeek; - daysToAdd = firstDayOfWeek - d + (d > firstDayOfWeekOfYear ? 7 : 0) - (d < firstDayOfWeek ? 7 : 0); - dayOfYear = 7 * (week - 1) + (weekday - firstDayOfWeek) + daysToAdd + 1; - - return { - year: dayOfYear > 0 ? year : year - 1, - dayOfYear: dayOfYear > 0 ? dayOfYear : daysInYear(year - 1) + dayOfYear - }; - } - - /************************************ - Top Level Functions - ************************************/ - - function makeMoment(config) { + function prepareConfig (config) { var input = config._i, format = config._f; - config._locale = config._locale || moment.localeData(config._l); + config._locale = config._locale || locale_locales__getLocale(config._l); if (input === null || (format === undefined && input === '')) { - return moment.invalid({nullInput: true}); + return valid__createInvalid({nullInput: true}); } if (typeof input === 'string') { config._i = input = config._locale.preparse(input); } - if (moment.isMoment(input)) { - return new Moment(input, true); + if (isMoment(input)) { + return new Moment(checkOverflow(input)); + } else if (isArray(format)) { + configFromStringAndArray(config); } else if (format) { - if (isArray(format)) { - makeDateFromStringAndArray(config); - } else { - makeDateFromStringAndFormat(config); - } + configFromStringAndFormat(config); + } else if (isDate(input)) { + config._d = input; } else { - makeDateFromInput(config); + configFromInput(config); } - return new Moment(config); + if (!valid__isValid(config)) { + config._d = null; + } + + return config; } - moment = function (input, format, locale, strict) { - var c; + function configFromInput(config) { + var input = config._i; + if (input === undefined) { + config._d = new Date(utils_hooks__hooks.now()); + } else if (isDate(input)) { + config._d = new Date(+input); + } else if (typeof input === 'string') { + configFromString(config); + } else if (isArray(input)) { + config._a = map(input.slice(0), function (obj) { + return parseInt(obj, 10); + }); + configFromArray(config); + } else if (typeof(input) === 'object') { + configFromObject(config); + } else if (typeof(input) === 'number') { + // from milliseconds + config._d = new Date(input); + } else { + utils_hooks__hooks.createFromInputFallback(config); + } + } - if (typeof(locale) === "boolean") { + function createLocalOrUTC (input, format, locale, strict, isUTC) { + var c = {}; + + if (typeof(locale) === 'boolean') { strict = locale; locale = undefined; } // object construction must be done this way. // https://github.com/moment/moment/issues/1423 - c = {}; c._isAMomentObject = true; + c._useUTC = c._isUTC = isUTC; + c._l = locale; c._i = input; c._f = format; - c._l = locale; c._strict = strict; - c._isUTC = false; - c._pf = defaultParsingFlags(); - return makeMoment(c); - }; + return createFromConfig(c); + } - moment.suppressDeprecationWarnings = false; + function local__createLocal (input, format, locale, strict) { + return createLocalOrUTC(input, format, locale, strict, false); + } - moment.createFromInputFallback = deprecate( - 'moment construction falls back to js Date. This is ' + - 'discouraged and will be removed in upcoming major ' + - 'release. Please refer to ' + - 'https://github.com/moment/moment/issues/1407 for more info.', - function (config) { - config._d = new Date(config._i); + var prototypeMin = deprecate( + 'moment().min is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other < this ? this : other; + } else { + return valid__createInvalid(); + } + } + ); + + var prototypeMax = deprecate( + 'moment().max is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', + function () { + var other = local__createLocal.apply(null, arguments); + if (this.isValid() && other.isValid()) { + return other > this ? this : other; + } else { + return valid__createInvalid(); + } } ); @@ -1772,72 +1631,298 @@ moments = moments[0]; } if (!moments.length) { - return moment(); + return local__createLocal(); } res = moments[0]; for (i = 1; i < moments.length; ++i) { - if (moments[i][fn](res)) { + if (!moments[i].isValid() || moments[i][fn](res)) { res = moments[i]; } } return res; } - moment.min = function () { + // TODO: Use [].sort instead? + function min () { var args = [].slice.call(arguments, 0); return pickBy('isBefore', args); - }; + } - moment.max = function () { + function max () { var args = [].slice.call(arguments, 0); return pickBy('isAfter', args); + } + + var now = function () { + return Date.now ? Date.now() : +(new Date()); }; - // creating with utc - moment.utc = function (input, format, locale, strict) { - var c; + function Duration (duration) { + var normalizedInput = normalizeObjectUnits(duration), + years = normalizedInput.year || 0, + quarters = normalizedInput.quarter || 0, + months = normalizedInput.month || 0, + weeks = normalizedInput.week || 0, + days = normalizedInput.day || 0, + hours = normalizedInput.hour || 0, + minutes = normalizedInput.minute || 0, + seconds = normalizedInput.second || 0, + milliseconds = normalizedInput.millisecond || 0; - if (typeof(locale) === "boolean") { - strict = locale; - locale = undefined; + // representation for dateAddRemove + this._milliseconds = +milliseconds + + seconds * 1e3 + // 1000 + minutes * 6e4 + // 1000 * 60 + hours * 36e5; // 1000 * 60 * 60 + // Because of dateAddRemove treats 24 hours as different from a + // day when working around DST, we need to store them separately + this._days = +days + + weeks * 7; + // It is impossible translate months into days without knowing + // which months you are are talking about, so we have to store + // it separately. + this._months = +months + + quarters * 3 + + years * 12; + + this._data = {}; + + this._locale = locale_locales__getLocale(); + + this._bubble(); + } + + function isDuration (obj) { + return obj instanceof Duration; + } + + // FORMATTING + + function offset (token, separator) { + addFormatToken(token, 0, 0, function () { + var offset = this.utcOffset(); + var sign = '+'; + if (offset < 0) { + offset = -offset; + sign = '-'; + } + return sign + zeroFill(~~(offset / 60), 2) + separator + zeroFill(~~(offset) % 60, 2); + }); + } + + offset('Z', ':'); + offset('ZZ', ''); + + // PARSING + + addRegexToken('Z', matchShortOffset); + addRegexToken('ZZ', matchShortOffset); + addParseToken(['Z', 'ZZ'], function (input, array, config) { + config._useUTC = true; + config._tzm = offsetFromString(matchShortOffset, input); + }); + + // HELPERS + + // timezone chunker + // '+10:00' > ['10', '00'] + // '-1530' > ['-15', '30'] + var chunkOffset = /([\+\-]|\d\d)/gi; + + function offsetFromString(matcher, string) { + var matches = ((string || '').match(matcher) || []); + var chunk = matches[matches.length - 1] || []; + var parts = (chunk + '').match(chunkOffset) || ['-', 0, 0]; + var minutes = +(parts[1] * 60) + toInt(parts[2]); + + return parts[0] === '+' ? minutes : -minutes; + } + + // Return a moment from input, that is local/utc/zone equivalent to model. + function cloneWithOffset(input, model) { + var res, diff; + if (model._isUTC) { + res = model.clone(); + diff = (isMoment(input) || isDate(input) ? +input : +local__createLocal(input)) - (+res); + // Use low-level api, because this fn is low-level api. + res._d.setTime(+res._d + diff); + utils_hooks__hooks.updateOffset(res, false); + return res; + } else { + return local__createLocal(input).local(); } - // object construction must be done this way. - // https://github.com/moment/moment/issues/1423 - c = {}; - c._isAMomentObject = true; - c._useUTC = true; - c._isUTC = true; - c._l = locale; - c._i = input; - c._f = format; - c._strict = strict; - c._pf = defaultParsingFlags(); + } - return makeMoment(c).utc(); - }; + function getDateOffset (m) { + // On Firefox.24 Date#getTimezoneOffset returns a floating point. + // https://github.com/moment/moment/pull/1871 + return -Math.round(m._d.getTimezoneOffset() / 15) * 15; + } - // creating with unix timestamp (in seconds) - moment.unix = function (input) { - return moment(input * 1000); - }; + // HOOKS - // duration - moment.duration = function (input, key) { + // This function will be called whenever a moment is mutated. + // It is intended to keep the offset in sync with the timezone. + utils_hooks__hooks.updateOffset = function () {}; + + // MOMENTS + + // keepLocalTime = true means only change the timezone, without + // affecting the local hour. So 5:31:26 +0300 --[utcOffset(2, true)]--> + // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist with offset + // +0200, so we adjust the time as needed, to be valid. + // + // Keeping the time actually adds/subtracts (one hour) + // from the actual represented time. That is why we call updateOffset + // a second time. In case it wants us to change the offset again + // _changeInProgress == true case, then we have to adjust, because + // there is no such time in the given timezone. + function getSetOffset (input, keepLocalTime) { + var offset = this._offset || 0, + localAdjust; + if (!this.isValid()) { + return input != null ? this : NaN; + } + if (input != null) { + if (typeof input === 'string') { + input = offsetFromString(matchShortOffset, input); + } else if (Math.abs(input) < 16) { + input = input * 60; + } + if (!this._isUTC && keepLocalTime) { + localAdjust = getDateOffset(this); + } + this._offset = input; + this._isUTC = true; + if (localAdjust != null) { + this.add(localAdjust, 'm'); + } + if (offset !== input) { + if (!keepLocalTime || this._changeInProgress) { + add_subtract__addSubtract(this, create__createDuration(input - offset, 'm'), 1, false); + } else if (!this._changeInProgress) { + this._changeInProgress = true; + utils_hooks__hooks.updateOffset(this, true); + this._changeInProgress = null; + } + } + return this; + } else { + return this._isUTC ? offset : getDateOffset(this); + } + } + + function getSetZone (input, keepLocalTime) { + if (input != null) { + if (typeof input !== 'string') { + input = -input; + } + + this.utcOffset(input, keepLocalTime); + + return this; + } else { + return -this.utcOffset(); + } + } + + function setOffsetToUTC (keepLocalTime) { + return this.utcOffset(0, keepLocalTime); + } + + function setOffsetToLocal (keepLocalTime) { + if (this._isUTC) { + this.utcOffset(0, keepLocalTime); + this._isUTC = false; + + if (keepLocalTime) { + this.subtract(getDateOffset(this), 'm'); + } + } + return this; + } + + function setOffsetToParsedOffset () { + if (this._tzm) { + this.utcOffset(this._tzm); + } else if (typeof this._i === 'string') { + this.utcOffset(offsetFromString(matchOffset, this._i)); + } + return this; + } + + function hasAlignedHourOffset (input) { + if (!this.isValid()) { + return false; + } + input = input ? local__createLocal(input).utcOffset() : 0; + + return (this.utcOffset() - input) % 60 === 0; + } + + function isDaylightSavingTime () { + return ( + this.utcOffset() > this.clone().month(0).utcOffset() || + this.utcOffset() > this.clone().month(5).utcOffset() + ); + } + + function isDaylightSavingTimeShifted () { + if (!isUndefined(this._isDSTShifted)) { + return this._isDSTShifted; + } + + var c = {}; + + copyConfig(c, this); + c = prepareConfig(c); + + if (c._a) { + var other = c._isUTC ? create_utc__createUTC(c._a) : local__createLocal(c._a); + this._isDSTShifted = this.isValid() && + compareArrays(c._a, other.toArray()) > 0; + } else { + this._isDSTShifted = false; + } + + return this._isDSTShifted; + } + + function isLocal () { + return this.isValid() ? !this._isUTC : false; + } + + function isUtcOffset () { + return this.isValid() ? this._isUTC : false; + } + + function isUtc () { + return this.isValid() ? this._isUTC && this._offset === 0 : false; + } + + // ASP.NET json date format regex + var aspNetRegex = /^(\-)?(?:(\d*)[. ])?(\d+)\:(\d+)(?:\:(\d+)\.?(\d{3})?\d*)?$/; + + // from http://docs.closure-library.googlecode.com/git/closure_goog_date_date.js.source.html + // somewhat more in line with 4.4.3.2 2004 spec, but allows decimal anywhere + // and further modified to allow for strings containing both week and day + var isoRegex = /^(-)?P(?:([0-9,.]*)Y)?(?:([0-9,.]*)M)?(?:([0-9,.]*)W)?(?:([0-9,.]*)D)?(?:T(?:([0-9,.]*)H)?(?:([0-9,.]*)M)?(?:([0-9,.]*)S)?)?$/; + + function create__createDuration (input, key) { var duration = input, // matching against regexp is expensive, do it on demand match = null, sign, ret, - parseIso, diffRes; - if (moment.isDuration(input)) { + if (isDuration(input)) { duration = { - ms: input._milliseconds, - d: input._days, - M: input._months + ms : input._milliseconds, + d : input._days, + M : input._months }; } else if (typeof input === 'number') { duration = {}; @@ -1846,38 +1931,31 @@ } else { duration.milliseconds = input; } - } else if (!!(match = aspNetTimeSpanJsonRegex.exec(input))) { + } else if (!!(match = aspNetRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; duration = { - y: 0, - d: toInt(match[DATE]) * sign, - h: toInt(match[HOUR]) * sign, - m: toInt(match[MINUTE]) * sign, - s: toInt(match[SECOND]) * sign, - ms: toInt(match[MILLISECOND]) * sign + y : 0, + d : toInt(match[DATE]) * sign, + h : toInt(match[HOUR]) * sign, + m : toInt(match[MINUTE]) * sign, + s : toInt(match[SECOND]) * sign, + ms : toInt(match[MILLISECOND]) * sign }; - } else if (!!(match = isoDurationRegex.exec(input))) { + } else if (!!(match = isoRegex.exec(input))) { sign = (match[1] === '-') ? -1 : 1; - parseIso = function (inp) { - // We'd normally use ~~inp for this, but unfortunately it also - // converts floats to ints. - // inp may be undefined, so careful calling replace on it. - var res = inp && parseFloat(inp.replace(',', '.')); - // apply sign while we're at it - return (isNaN(res) ? 0 : res) * sign; - }; duration = { - y: parseIso(match[2]), - M: parseIso(match[3]), - d: parseIso(match[4]), - h: parseIso(match[5]), - m: parseIso(match[6]), - s: parseIso(match[7]), - w: parseIso(match[8]) + y : parseIso(match[2], sign), + M : parseIso(match[3], sign), + w : parseIso(match[4], sign), + d : parseIso(match[5], sign), + h : parseIso(match[6], sign), + m : parseIso(match[7], sign), + s : parseIso(match[8], sign) }; - } else if (typeof duration === 'object' && - ('from' in duration || 'to' in duration)) { - diffRes = momentsDifference(moment(duration.from), moment(duration.to)); + } else if (duration == null) {// checks for null or undefined + duration = {}; + } else if (typeof duration === 'object' && ('from' in duration || 'to' in duration)) { + diffRes = momentsDifference(local__createLocal(duration.from), local__createLocal(duration.to)); duration = {}; duration.ms = diffRes.milliseconds; @@ -1886,876 +1964,1327 @@ ret = new Duration(duration); - if (moment.isDuration(input) && input.hasOwnProperty('_locale')) { + if (isDuration(input) && hasOwnProp(input, '_locale')) { ret._locale = input._locale; } return ret; - }; + } - // version number - moment.version = VERSION; + create__createDuration.fn = Duration.prototype; - // default format - moment.defaultFormat = isoFormat; + function parseIso (inp, sign) { + // We'd normally use ~~inp for this, but unfortunately it also + // converts floats to ints. + // inp may be undefined, so careful calling replace on it. + var res = inp && parseFloat(inp.replace(',', '.')); + // apply sign while we're at it + return (isNaN(res) ? 0 : res) * sign; + } - // constant that refers to the ISO standard - moment.ISO_8601 = function () {}; + function positiveMomentsDifference(base, other) { + var res = {milliseconds: 0, months: 0}; - // Plugins that add properties should also add the key here (null value), - // so we can properly clone ourselves. - moment.momentProperties = momentProperties; - - // This function will be called whenever a moment is mutated. - // It is intended to keep the offset in sync with the timezone. - moment.updateOffset = function () {}; - - // This function allows you to set a threshold for relative time strings - moment.relativeTimeThreshold = function (threshold, limit) { - if (relativeTimeThresholds[threshold] === undefined) { - return false; - } - if (limit === undefined) { - return relativeTimeThresholds[threshold]; - } - relativeTimeThresholds[threshold] = limit; - return true; - }; - - moment.lang = deprecate( - "moment.lang is deprecated. Use moment.locale instead.", - function (key, value) { - return moment.locale(key, value); - } - ); - - // This function will load locale and then set the global locale. If - // no arguments are passed in, it will simply return the current global - // locale key. - moment.locale = function (key, values) { - var data; - if (key) { - if (typeof(values) !== "undefined") { - data = moment.defineLocale(key, values); - } - else { - data = moment.localeData(key); - } - - if (data) { - moment.duration._locale = moment._locale = data; - } + res.months = other.month() - base.month() + + (other.year() - base.year()) * 12; + if (base.clone().add(res.months, 'M').isAfter(other)) { + --res.months; } - return moment._locale._abbr; - }; + res.milliseconds = +other - +(base.clone().add(res.months, 'M')); - moment.defineLocale = function (name, values) { - if (values !== null) { - values.abbr = name; - if (!locales[name]) { - locales[name] = new Locale(); - } - locales[name].set(values); + return res; + } - // backwards compat for now: also set the locale - moment.locale(name); + function momentsDifference(base, other) { + var res; + if (!(base.isValid() && other.isValid())) { + return {milliseconds: 0, months: 0}; + } - return locales[name]; + other = cloneWithOffset(other, base); + if (base.isBefore(other)) { + res = positiveMomentsDifference(base, other); } else { - // useful for testing - delete locales[name]; - return null; - } - }; - - moment.langData = deprecate( - "moment.langData is deprecated. Use moment.localeData instead.", - function (key) { - return moment.localeData(key); - } - ); - - // returns locale data - moment.localeData = function (key) { - var locale; - - if (key && key._locale && key._locale._abbr) { - key = key._locale._abbr; + res = positiveMomentsDifference(other, base); + res.milliseconds = -res.milliseconds; + res.months = -res.months; } - if (!key) { - return moment._locale; - } - - if (!isArray(key)) { - //short-circuit everything else - locale = loadLocale(key); - if (locale) { - return locale; - } - key = [key]; - } - - return chooseLocale(key); - }; - - // compare moment object - moment.isMoment = function (obj) { - return obj instanceof Moment || - (obj != null && obj.hasOwnProperty('_isAMomentObject')); - }; - - // for typechecking Duration objects - moment.isDuration = function (obj) { - return obj instanceof Duration; - }; - - for (i = lists.length - 1; i >= 0; --i) { - makeList(lists[i]); + return res; } - moment.normalizeUnits = function (units) { - return normalizeUnits(units); - }; - - moment.invalid = function (flags) { - var m = moment.utc(NaN); - if (flags != null) { - extend(m._pf, flags); - } - else { - m._pf.userInvalidated = true; - } - - return m; - }; - - moment.parseZone = function () { - return moment.apply(null, arguments).parseZone(); - }; - - moment.parseTwoDigitYear = function (input) { - return toInt(input) + (toInt(input) > 68 ? 1900 : 2000); - }; - - /************************************ - Moment Prototype - ************************************/ - - - extend(moment.fn = Moment.prototype, { - - clone : function () { - return moment(this); - }, - - valueOf : function () { - return +this._d + ((this._offset || 0) * 60000); - }, - - unix : function () { - return Math.floor(+this / 1000); - }, - - toString : function () { - return this.clone().locale('en').format("ddd MMM DD YYYY HH:mm:ss [GMT]ZZ"); - }, - - toDate : function () { - return this._offset ? new Date(+this) : this._d; - }, - - toISOString : function () { - var m = moment(this).utc(); - if (0 < m.year() && m.year() <= 9999) { - return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } else { - return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); - } - }, - - toArray : function () { - var m = this; - return [ - m.year(), - m.month(), - m.date(), - m.hours(), - m.minutes(), - m.seconds(), - m.milliseconds() - ]; - }, - - isValid : function () { - return isValid(this); - }, - - isDSTShifted : function () { - if (this._a) { - return this.isValid() && compareArrays(this._a, (this._isUTC ? moment.utc(this._a) : moment(this._a)).toArray()) > 0; - } - - return false; - }, - - parsingFlags : function () { - return extend({}, this._pf); - }, - - invalidAt: function () { - return this._pf.overflow; - }, - - utc : function (keepLocalTime) { - return this.zone(0, keepLocalTime); - }, - - local : function (keepLocalTime) { - if (this._isUTC) { - this.zone(0, keepLocalTime); - this._isUTC = false; - - if (keepLocalTime) { - this.add(this._d.getTimezoneOffset(), 'm'); - } - } - return this; - }, - - format : function (inputString) { - var output = formatMoment(this, inputString || moment.defaultFormat); - return this.localeData().postformat(output); - }, - - add : createAdder(1, 'add'), - - subtract : createAdder(-1, 'subtract'), - - diff : function (input, units, asFloat) { - var that = makeAs(input, this), - zoneDiff = (this.zone() - that.zone()) * 6e4, - diff, output; - - units = normalizeUnits(units); - - if (units === 'year' || units === 'month') { - // average number of days in the months in the given dates - diff = (this.daysInMonth() + that.daysInMonth()) * 432e5; // 24 * 60 * 60 * 1000 / 2 - // difference in months - output = ((this.year() - that.year()) * 12) + (this.month() - that.month()); - // adjust by taking difference in days, average number of days - // and dst in the given months. - output += ((this - moment(this).startOf('month')) - - (that - moment(that).startOf('month'))) / diff; - // same as above but with zones, to negate all dst - output -= ((this.zone() - moment(this).startOf('month').zone()) - - (that.zone() - moment(that).startOf('month').zone())) * 6e4 / diff; - if (units === 'year') { - output = output / 12; - } - } else { - diff = (this - that); - output = units === 'second' ? diff / 1e3 : // 1000 - units === 'minute' ? diff / 6e4 : // 1000 * 60 - units === 'hour' ? diff / 36e5 : // 1000 * 60 * 60 - units === 'day' ? (diff - zoneDiff) / 864e5 : // 1000 * 60 * 60 * 24, negate dst - units === 'week' ? (diff - zoneDiff) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst - diff; - } - return asFloat ? output : absRound(output); - }, - - from : function (time, withoutSuffix) { - return moment.duration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); - }, - - fromNow : function (withoutSuffix) { - return this.from(moment(), withoutSuffix); - }, - - calendar : function (time) { - // We want to compare the start of today, vs this. - // Getting start-of-today depends on whether we're zone'd or not. - var now = time || moment(), - sod = makeAs(now, this).startOf('day'), - diff = this.diff(sod, 'days', true), - format = diff < -6 ? 'sameElse' : - diff < -1 ? 'lastWeek' : - diff < 0 ? 'lastDay' : - diff < 1 ? 'sameDay' : - diff < 2 ? 'nextDay' : - diff < 7 ? 'nextWeek' : 'sameElse'; - return this.format(this.localeData().calendar(format, this)); - }, - - isLeapYear : function () { - return isLeapYear(this.year()); - }, - - isDST : function () { - return (this.zone() < this.clone().month(0).zone() || - this.zone() < this.clone().month(5).zone()); - }, - - day : function (input) { - var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); - if (input != null) { - input = parseWeekday(input, this.localeData()); - return this.add(input - day, 'd'); - } else { - return day; - } - }, - - month : makeAccessor('Month', true), - - startOf : function (units) { - units = normalizeUnits(units); - // the following switch intentionally omits break keywords - // to utilize falling through the cases. - switch (units) { - case 'year': - this.month(0); - /* falls through */ - case 'quarter': - case 'month': - this.date(1); - /* falls through */ - case 'week': - case 'isoWeek': - case 'day': - this.hours(0); - /* falls through */ - case 'hour': - this.minutes(0); - /* falls through */ - case 'minute': - this.seconds(0); - /* falls through */ - case 'second': - this.milliseconds(0); - /* falls through */ - } - - // weeks are a special case - if (units === 'week') { - this.weekday(0); - } else if (units === 'isoWeek') { - this.isoWeekday(1); - } - - // quarters are also special - if (units === 'quarter') { - this.month(Math.floor(this.month() / 3) * 3); - } - - return this; - }, - - endOf: function (units) { - units = normalizeUnits(units); - return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); - }, - - isAfter: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) > +moment(input).startOf(units); - }, - - isBefore: function (input, units) { - units = typeof units !== 'undefined' ? units : 'millisecond'; - return +this.clone().startOf(units) < +moment(input).startOf(units); - }, - - isSame: function (input, units) { - units = units || 'ms'; - return +this.clone().startOf(units) === +makeAs(input, this).startOf(units); - }, - - min: deprecate( - 'moment().min is deprecated, use moment.min instead. https://github.com/moment/moment/issues/1548', - function (other) { - other = moment.apply(null, arguments); - return other < this ? this : other; - } - ), - - max: deprecate( - 'moment().max is deprecated, use moment.max instead. https://github.com/moment/moment/issues/1548', - function (other) { - other = moment.apply(null, arguments); - return other > this ? this : other; - } - ), - - // keepLocalTime = true means only change the timezone, without - // affecting the local hour. So 5:31:26 +0300 --[zone(2, true)]--> - // 5:31:26 +0200 It is possible that 5:31:26 doesn't exist int zone - // +0200, so we adjust the time as needed, to be valid. - // - // Keeping the time actually adds/subtracts (one hour) - // from the actual represented time. That is why we call updateOffset - // a second time. In case it wants us to change the offset again - // _changeInProgress == true case, then we have to adjust, because - // there is no such time in the given timezone. - zone : function (input, keepLocalTime) { - var offset = this._offset || 0, - localAdjust; - if (input != null) { - if (typeof input === 'string') { - input = timezoneMinutesFromString(input); - } - if (Math.abs(input) < 16) { - input = input * 60; - } - if (!this._isUTC && keepLocalTime) { - localAdjust = this._d.getTimezoneOffset(); - } - this._offset = input; - this._isUTC = true; - if (localAdjust != null) { - this.subtract(localAdjust, 'm'); - } - if (offset !== input) { - if (!keepLocalTime || this._changeInProgress) { - addOrSubtractDurationFromMoment(this, - moment.duration(offset - input, 'm'), 1, false); - } else if (!this._changeInProgress) { - this._changeInProgress = true; - moment.updateOffset(this, true); - this._changeInProgress = null; - } - } - } else { - return this._isUTC ? offset : this._d.getTimezoneOffset(); - } - return this; - }, - - zoneAbbr : function () { - return this._isUTC ? 'UTC' : ''; - }, - - zoneName : function () { - return this._isUTC ? 'Coordinated Universal Time' : ''; - }, - - parseZone : function () { - if (this._tzm) { - this.zone(this._tzm); - } else if (typeof this._i === 'string') { - this.zone(this._i); - } - return this; - }, - - hasAlignedHourOffset : function (input) { - if (!input) { - input = 0; - } - else { - input = moment(input).zone(); - } - - return (this.zone() - input) % 60 === 0; - }, - - daysInMonth : function () { - return daysInMonth(this.year(), this.month()); - }, - - dayOfYear : function (input) { - var dayOfYear = round((moment(this).startOf('day') - moment(this).startOf('year')) / 864e5) + 1; - return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); - }, - - quarter : function (input) { - return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); - }, - - weekYear : function (input) { - var year = weekOfYear(this, this.localeData()._week.dow, this.localeData()._week.doy).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - isoWeekYear : function (input) { - var year = weekOfYear(this, 1, 4).year; - return input == null ? year : this.add((input - year), 'y'); - }, - - week : function (input) { - var week = this.localeData().week(this); - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - isoWeek : function (input) { - var week = weekOfYear(this, 1, 4).week; - return input == null ? week : this.add((input - week) * 7, 'd'); - }, - - weekday : function (input) { - var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; - return input == null ? weekday : this.add(input - weekday, 'd'); - }, - - isoWeekday : function (input) { - // behaves the same as moment#day except - // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) - // as a setter, sunday should belong to the previous week. - return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); - }, - - isoWeeksInYear : function () { - return weeksInYear(this.year(), 1, 4); - }, - - weeksInYear : function () { - var weekInfo = this.localeData()._week; - return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); - }, - - get : function (units) { - units = normalizeUnits(units); - return this[units](); - }, - - set : function (units, value) { - units = normalizeUnits(units); - if (typeof this[units] === 'function') { - this[units](value); - } - return this; - }, - - // If passed a locale key, it will set the locale for this - // instance. Otherwise, it will return the locale configuration - // variables for this instance. - locale : function (key) { - if (key === undefined) { - return this._locale._abbr; - } else { - this._locale = moment.localeData(key); - return this; - } - }, - - lang : deprecate( - "moment().lang() is deprecated. Use moment().localeData() instead.", - function (key) { - if (key === undefined) { - return this.localeData(); - } else { - this._locale = moment.localeData(key); - return this; - } - } - ), - - localeData : function () { - return this._locale; - } - }); - - function rawMonthSetter(mom, value) { - var dayOfMonth; - - // TODO: Move this out of here! - if (typeof value === 'string') { - value = mom.localeData().monthsParse(value); - // TODO: Another silent failure? - if (typeof value !== 'number') { - return mom; - } - } - - dayOfMonth = Math.min(mom.date(), - daysInMonth(mom.year(), value)); - mom._d['set' + (mom._isUTC ? 'UTC' : '') + 'Month'](value, dayOfMonth); - return mom; - } - - function rawGetter(mom, unit) { - return mom._d['get' + (mom._isUTC ? 'UTC' : '') + unit](); - } - - function rawSetter(mom, unit, value) { - if (unit === 'Month') { - return rawMonthSetter(mom, value); + function absRound (number) { + if (number < 0) { + return Math.round(-1 * number) * -1; } else { - return mom._d['set' + (mom._isUTC ? 'UTC' : '') + unit](value); + return Math.round(number); } } - function makeAccessor(unit, keepTime) { - return function (value) { - if (value != null) { - rawSetter(this, unit, value); - moment.updateOffset(this, keepTime); - return this; - } else { - return rawGetter(this, unit); + // TODO: remove 'name' arg after deprecation is removed + function createAdder(direction, name) { + return function (val, period) { + var dur, tmp; + //invert the arguments, but complain about it + if (period !== null && !isNaN(+period)) { + deprecateSimple(name, 'moment().' + name + '(period, number) is deprecated. Please use moment().' + name + '(number, period).'); + tmp = val; val = period; period = tmp; } + + val = typeof val === 'string' ? +val : val; + dur = create__createDuration(val, period); + add_subtract__addSubtract(this, dur, direction); + return this; }; } - moment.fn.millisecond = moment.fn.milliseconds = makeAccessor('Milliseconds', false); - moment.fn.second = moment.fn.seconds = makeAccessor('Seconds', false); - moment.fn.minute = moment.fn.minutes = makeAccessor('Minutes', false); + function add_subtract__addSubtract (mom, duration, isAdding, updateOffset) { + var milliseconds = duration._milliseconds, + days = absRound(duration._days), + months = absRound(duration._months); + + if (!mom.isValid()) { + // No op + return; + } + + updateOffset = updateOffset == null ? true : updateOffset; + + if (milliseconds) { + mom._d.setTime(+mom._d + milliseconds * isAdding); + } + if (days) { + get_set__set(mom, 'Date', get_set__get(mom, 'Date') + days * isAdding); + } + if (months) { + setMonth(mom, get_set__get(mom, 'Month') + months * isAdding); + } + if (updateOffset) { + utils_hooks__hooks.updateOffset(mom, days || months); + } + } + + var add_subtract__add = createAdder(1, 'add'); + var add_subtract__subtract = createAdder(-1, 'subtract'); + + function moment_calendar__calendar (time, formats) { + // We want to compare the start of today, vs this. + // Getting start-of-today depends on whether we're local/utc/offset or not. + var now = time || local__createLocal(), + sod = cloneWithOffset(now, this).startOf('day'), + diff = this.diff(sod, 'days', true), + format = diff < -6 ? 'sameElse' : + diff < -1 ? 'lastWeek' : + diff < 0 ? 'lastDay' : + diff < 1 ? 'sameDay' : + diff < 2 ? 'nextDay' : + diff < 7 ? 'nextWeek' : 'sameElse'; + + var output = formats && (isFunction(formats[format]) ? formats[format]() : formats[format]); + + return this.format(output || this.localeData().calendar(format, this, local__createLocal(now))); + } + + function clone () { + return new Moment(this); + } + + function isAfter (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return +this > +localInput; + } else { + return +localInput < +this.clone().startOf(units); + } + } + + function isBefore (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input); + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(!isUndefined(units) ? units : 'millisecond'); + if (units === 'millisecond') { + return +this < +localInput; + } else { + return +this.clone().endOf(units) < +localInput; + } + } + + function isBetween (from, to, units) { + return this.isAfter(from, units) && this.isBefore(to, units); + } + + function isSame (input, units) { + var localInput = isMoment(input) ? input : local__createLocal(input), + inputMs; + if (!(this.isValid() && localInput.isValid())) { + return false; + } + units = normalizeUnits(units || 'millisecond'); + if (units === 'millisecond') { + return +this === +localInput; + } else { + inputMs = +localInput; + return +(this.clone().startOf(units)) <= inputMs && inputMs <= +(this.clone().endOf(units)); + } + } + + function isSameOrAfter (input, units) { + return this.isSame(input, units) || this.isAfter(input,units); + } + + function isSameOrBefore (input, units) { + return this.isSame(input, units) || this.isBefore(input,units); + } + + function diff (input, units, asFloat) { + var that, + zoneDelta, + delta, output; + + if (!this.isValid()) { + return NaN; + } + + that = cloneWithOffset(input, this); + + if (!that.isValid()) { + return NaN; + } + + zoneDelta = (that.utcOffset() - this.utcOffset()) * 6e4; + + units = normalizeUnits(units); + + if (units === 'year' || units === 'month' || units === 'quarter') { + output = monthDiff(this, that); + if (units === 'quarter') { + output = output / 3; + } else if (units === 'year') { + output = output / 12; + } + } else { + delta = this - that; + output = units === 'second' ? delta / 1e3 : // 1000 + units === 'minute' ? delta / 6e4 : // 1000 * 60 + units === 'hour' ? delta / 36e5 : // 1000 * 60 * 60 + units === 'day' ? (delta - zoneDelta) / 864e5 : // 1000 * 60 * 60 * 24, negate dst + units === 'week' ? (delta - zoneDelta) / 6048e5 : // 1000 * 60 * 60 * 24 * 7, negate dst + delta; + } + return asFloat ? output : absFloor(output); + } + + function monthDiff (a, b) { + // difference in months + var wholeMonthDiff = ((b.year() - a.year()) * 12) + (b.month() - a.month()), + // b is in (anchor - 1 month, anchor + 1 month) + anchor = a.clone().add(wholeMonthDiff, 'months'), + anchor2, adjust; + + if (b - anchor < 0) { + anchor2 = a.clone().add(wholeMonthDiff - 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor - anchor2); + } else { + anchor2 = a.clone().add(wholeMonthDiff + 1, 'months'); + // linear across the month + adjust = (b - anchor) / (anchor2 - anchor); + } + + return -(wholeMonthDiff + adjust); + } + + utils_hooks__hooks.defaultFormat = 'YYYY-MM-DDTHH:mm:ssZ'; + + function toString () { + return this.clone().locale('en').format('ddd MMM DD YYYY HH:mm:ss [GMT]ZZ'); + } + + function moment_format__toISOString () { + var m = this.clone().utc(); + if (0 < m.year() && m.year() <= 9999) { + if (isFunction(Date.prototype.toISOString)) { + // native implementation is ~50x faster, use it when we can + return this.toDate().toISOString(); + } else { + return formatMoment(m, 'YYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } else { + return formatMoment(m, 'YYYYYY-MM-DD[T]HH:mm:ss.SSS[Z]'); + } + } + + function format (inputString) { + var output = formatMoment(this, inputString || utils_hooks__hooks.defaultFormat); + return this.localeData().postformat(output); + } + + function from (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + local__createLocal(time).isValid())) { + return create__createDuration({to: this, from: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function fromNow (withoutSuffix) { + return this.from(local__createLocal(), withoutSuffix); + } + + function to (time, withoutSuffix) { + if (this.isValid() && + ((isMoment(time) && time.isValid()) || + local__createLocal(time).isValid())) { + return create__createDuration({from: this, to: time}).locale(this.locale()).humanize(!withoutSuffix); + } else { + return this.localeData().invalidDate(); + } + } + + function toNow (withoutSuffix) { + return this.to(local__createLocal(), withoutSuffix); + } + + // If passed a locale key, it will set the locale for this + // instance. Otherwise, it will return the locale configuration + // variables for this instance. + function locale (key) { + var newLocaleData; + + if (key === undefined) { + return this._locale._abbr; + } else { + newLocaleData = locale_locales__getLocale(key); + if (newLocaleData != null) { + this._locale = newLocaleData; + } + return this; + } + } + + var lang = deprecate( + 'moment().lang() is deprecated. Instead, use moment().localeData() to get the language configuration. Use moment().locale() to change languages.', + function (key) { + if (key === undefined) { + return this.localeData(); + } else { + return this.locale(key); + } + } + ); + + function localeData () { + return this._locale; + } + + function startOf (units) { + units = normalizeUnits(units); + // the following switch intentionally omits break keywords + // to utilize falling through the cases. + switch (units) { + case 'year': + this.month(0); + /* falls through */ + case 'quarter': + case 'month': + this.date(1); + /* falls through */ + case 'week': + case 'isoWeek': + case 'day': + this.hours(0); + /* falls through */ + case 'hour': + this.minutes(0); + /* falls through */ + case 'minute': + this.seconds(0); + /* falls through */ + case 'second': + this.milliseconds(0); + } + + // weeks are a special case + if (units === 'week') { + this.weekday(0); + } + if (units === 'isoWeek') { + this.isoWeekday(1); + } + + // quarters are also special + if (units === 'quarter') { + this.month(Math.floor(this.month() / 3) * 3); + } + + return this; + } + + function endOf (units) { + units = normalizeUnits(units); + if (units === undefined || units === 'millisecond') { + return this; + } + return this.startOf(units).add(1, (units === 'isoWeek' ? 'week' : units)).subtract(1, 'ms'); + } + + function to_type__valueOf () { + return +this._d - ((this._offset || 0) * 60000); + } + + function unix () { + return Math.floor(+this / 1000); + } + + function toDate () { + return this._offset ? new Date(+this) : this._d; + } + + function toArray () { + var m = this; + return [m.year(), m.month(), m.date(), m.hour(), m.minute(), m.second(), m.millisecond()]; + } + + function toObject () { + var m = this; + return { + years: m.year(), + months: m.month(), + date: m.date(), + hours: m.hours(), + minutes: m.minutes(), + seconds: m.seconds(), + milliseconds: m.milliseconds() + }; + } + + function toJSON () { + // new Date(NaN).toJSON() === null + return this.isValid() ? this.toISOString() : null; + } + + function moment_valid__isValid () { + return valid__isValid(this); + } + + function parsingFlags () { + return extend({}, getParsingFlags(this)); + } + + function invalidAt () { + return getParsingFlags(this).overflow; + } + + function creationData() { + return { + input: this._i, + format: this._f, + locale: this._locale, + isUTC: this._isUTC, + strict: this._strict + }; + } + + // FORMATTING + + addFormatToken(0, ['gg', 2], 0, function () { + return this.weekYear() % 100; + }); + + addFormatToken(0, ['GG', 2], 0, function () { + return this.isoWeekYear() % 100; + }); + + function addWeekYearFormatToken (token, getter) { + addFormatToken(0, [token, token.length], 0, getter); + } + + addWeekYearFormatToken('gggg', 'weekYear'); + addWeekYearFormatToken('ggggg', 'weekYear'); + addWeekYearFormatToken('GGGG', 'isoWeekYear'); + addWeekYearFormatToken('GGGGG', 'isoWeekYear'); + + // ALIASES + + addUnitAlias('weekYear', 'gg'); + addUnitAlias('isoWeekYear', 'GG'); + + // PARSING + + addRegexToken('G', matchSigned); + addRegexToken('g', matchSigned); + addRegexToken('GG', match1to2, match2); + addRegexToken('gg', match1to2, match2); + addRegexToken('GGGG', match1to4, match4); + addRegexToken('gggg', match1to4, match4); + addRegexToken('GGGGG', match1to6, match6); + addRegexToken('ggggg', match1to6, match6); + + addWeekParseToken(['gggg', 'ggggg', 'GGGG', 'GGGGG'], function (input, week, config, token) { + week[token.substr(0, 2)] = toInt(input); + }); + + addWeekParseToken(['gg', 'GG'], function (input, week, config, token) { + week[token] = utils_hooks__hooks.parseTwoDigitYear(input); + }); + + // MOMENTS + + function getSetWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, + this.week(), + this.weekday(), + this.localeData()._week.dow, + this.localeData()._week.doy); + } + + function getSetISOWeekYear (input) { + return getSetWeekYearHelper.call(this, + input, this.isoWeek(), this.isoWeekday(), 1, 4); + } + + function getISOWeeksInYear () { + return weeksInYear(this.year(), 1, 4); + } + + function getWeeksInYear () { + var weekInfo = this.localeData()._week; + return weeksInYear(this.year(), weekInfo.dow, weekInfo.doy); + } + + function getSetWeekYearHelper(input, week, weekday, dow, doy) { + var weeksTarget; + if (input == null) { + return weekOfYear(this, dow, doy).year; + } else { + weeksTarget = weeksInYear(input, dow, doy); + if (week > weeksTarget) { + week = weeksTarget; + } + return setWeekAll.call(this, input, week, weekday, dow, doy); + } + } + + function setWeekAll(weekYear, week, weekday, dow, doy) { + var dayOfYearData = dayOfYearFromWeeks(weekYear, week, weekday, dow, doy), + date = createUTCDate(dayOfYearData.year, 0, dayOfYearData.dayOfYear); + + this.year(date.getUTCFullYear()); + this.month(date.getUTCMonth()); + this.date(date.getUTCDate()); + return this; + } + + // FORMATTING + + addFormatToken('Q', 0, 'Qo', 'quarter'); + + // ALIASES + + addUnitAlias('quarter', 'Q'); + + // PARSING + + addRegexToken('Q', match1); + addParseToken('Q', function (input, array) { + array[MONTH] = (toInt(input) - 1) * 3; + }); + + // MOMENTS + + function getSetQuarter (input) { + return input == null ? Math.ceil((this.month() + 1) / 3) : this.month((input - 1) * 3 + this.month() % 3); + } + + // FORMATTING + + addFormatToken('w', ['ww', 2], 'wo', 'week'); + addFormatToken('W', ['WW', 2], 'Wo', 'isoWeek'); + + // ALIASES + + addUnitAlias('week', 'w'); + addUnitAlias('isoWeek', 'W'); + + // PARSING + + addRegexToken('w', match1to2); + addRegexToken('ww', match1to2, match2); + addRegexToken('W', match1to2); + addRegexToken('WW', match1to2, match2); + + addWeekParseToken(['w', 'ww', 'W', 'WW'], function (input, week, config, token) { + week[token.substr(0, 1)] = toInt(input); + }); + + // HELPERS + + // LOCALES + + function localeWeek (mom) { + return weekOfYear(mom, this._week.dow, this._week.doy).week; + } + + var defaultLocaleWeek = { + dow : 0, // Sunday is the first day of the week. + doy : 6 // The week that contains Jan 1st is the first week of the year. + }; + + function localeFirstDayOfWeek () { + return this._week.dow; + } + + function localeFirstDayOfYear () { + return this._week.doy; + } + + // MOMENTS + + function getSetWeek (input) { + var week = this.localeData().week(this); + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + function getSetISOWeek (input) { + var week = weekOfYear(this, 1, 4).week; + return input == null ? week : this.add((input - week) * 7, 'd'); + } + + // FORMATTING + + addFormatToken('D', ['DD', 2], 'Do', 'date'); + + // ALIASES + + addUnitAlias('date', 'D'); + + // PARSING + + addRegexToken('D', match1to2); + addRegexToken('DD', match1to2, match2); + addRegexToken('Do', function (isStrict, locale) { + return isStrict ? locale._ordinalParse : locale._ordinalParseLenient; + }); + + addParseToken(['D', 'DD'], DATE); + addParseToken('Do', function (input, array) { + array[DATE] = toInt(input.match(match1to2)[0], 10); + }); + + // MOMENTS + + var getSetDayOfMonth = makeGetSet('Date', true); + + // FORMATTING + + addFormatToken('d', 0, 'do', 'day'); + + addFormatToken('dd', 0, 0, function (format) { + return this.localeData().weekdaysMin(this, format); + }); + + addFormatToken('ddd', 0, 0, function (format) { + return this.localeData().weekdaysShort(this, format); + }); + + addFormatToken('dddd', 0, 0, function (format) { + return this.localeData().weekdays(this, format); + }); + + addFormatToken('e', 0, 0, 'weekday'); + addFormatToken('E', 0, 0, 'isoWeekday'); + + // ALIASES + + addUnitAlias('day', 'd'); + addUnitAlias('weekday', 'e'); + addUnitAlias('isoWeekday', 'E'); + + // PARSING + + addRegexToken('d', match1to2); + addRegexToken('e', match1to2); + addRegexToken('E', match1to2); + addRegexToken('dd', matchWord); + addRegexToken('ddd', matchWord); + addRegexToken('dddd', matchWord); + + addWeekParseToken(['dd', 'ddd', 'dddd'], function (input, week, config, token) { + var weekday = config._locale.weekdaysParse(input, token, config._strict); + // if we didn't get a weekday name, mark the date as invalid + if (weekday != null) { + week.d = weekday; + } else { + getParsingFlags(config).invalidWeekday = input; + } + }); + + addWeekParseToken(['d', 'e', 'E'], function (input, week, config, token) { + week[token] = toInt(input); + }); + + // HELPERS + + function parseWeekday(input, locale) { + if (typeof input !== 'string') { + return input; + } + + if (!isNaN(input)) { + return parseInt(input, 10); + } + + input = locale.weekdaysParse(input); + if (typeof input === 'number') { + return input; + } + + return null; + } + + // LOCALES + + var defaultLocaleWeekdays = 'Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday'.split('_'); + function localeWeekdays (m, format) { + return isArray(this._weekdays) ? this._weekdays[m.day()] : + this._weekdays[this._weekdays.isFormat.test(format) ? 'format' : 'standalone'][m.day()]; + } + + var defaultLocaleWeekdaysShort = 'Sun_Mon_Tue_Wed_Thu_Fri_Sat'.split('_'); + function localeWeekdaysShort (m) { + return this._weekdaysShort[m.day()]; + } + + var defaultLocaleWeekdaysMin = 'Su_Mo_Tu_We_Th_Fr_Sa'.split('_'); + function localeWeekdaysMin (m) { + return this._weekdaysMin[m.day()]; + } + + function localeWeekdaysParse (weekdayName, format, strict) { + var i, mom, regex; + + if (!this._weekdaysParse) { + this._weekdaysParse = []; + this._minWeekdaysParse = []; + this._shortWeekdaysParse = []; + this._fullWeekdaysParse = []; + } + + for (i = 0; i < 7; i++) { + // make the regex if we don't have it already + + mom = local__createLocal([2000, 1]).day(i); + if (strict && !this._fullWeekdaysParse[i]) { + this._fullWeekdaysParse[i] = new RegExp('^' + this.weekdays(mom, '').replace('.', '\.?') + '$', 'i'); + this._shortWeekdaysParse[i] = new RegExp('^' + this.weekdaysShort(mom, '').replace('.', '\.?') + '$', 'i'); + this._minWeekdaysParse[i] = new RegExp('^' + this.weekdaysMin(mom, '').replace('.', '\.?') + '$', 'i'); + } + if (!this._weekdaysParse[i]) { + regex = '^' + this.weekdays(mom, '') + '|^' + this.weekdaysShort(mom, '') + '|^' + this.weekdaysMin(mom, ''); + this._weekdaysParse[i] = new RegExp(regex.replace('.', ''), 'i'); + } + // test the regex + if (strict && format === 'dddd' && this._fullWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'ddd' && this._shortWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (strict && format === 'dd' && this._minWeekdaysParse[i].test(weekdayName)) { + return i; + } else if (!strict && this._weekdaysParse[i].test(weekdayName)) { + return i; + } + } + } + + // MOMENTS + + function getSetDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var day = this._isUTC ? this._d.getUTCDay() : this._d.getDay(); + if (input != null) { + input = parseWeekday(input, this.localeData()); + return this.add(input - day, 'd'); + } else { + return day; + } + } + + function getSetLocaleDayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + var weekday = (this.day() + 7 - this.localeData()._week.dow) % 7; + return input == null ? weekday : this.add(input - weekday, 'd'); + } + + function getSetISODayOfWeek (input) { + if (!this.isValid()) { + return input != null ? this : NaN; + } + // behaves the same as moment#day except + // as a getter, returns 7 instead of 0 (1-7 range instead of 0-6) + // as a setter, sunday should belong to the previous week. + return input == null ? this.day() || 7 : this.day(this.day() % 7 ? input : input - 7); + } + + // FORMATTING + + addFormatToken('DDD', ['DDDD', 3], 'DDDo', 'dayOfYear'); + + // ALIASES + + addUnitAlias('dayOfYear', 'DDD'); + + // PARSING + + addRegexToken('DDD', match1to3); + addRegexToken('DDDD', match3); + addParseToken(['DDD', 'DDDD'], function (input, array, config) { + config._dayOfYear = toInt(input); + }); + + // HELPERS + + // MOMENTS + + function getSetDayOfYear (input) { + var dayOfYear = Math.round((this.clone().startOf('day') - this.clone().startOf('year')) / 864e5) + 1; + return input == null ? dayOfYear : this.add((input - dayOfYear), 'd'); + } + + // FORMATTING + + function hFormat() { + return this.hours() % 12 || 12; + } + + addFormatToken('H', ['HH', 2], 0, 'hour'); + addFormatToken('h', ['hh', 2], 0, hFormat); + + addFormatToken('hmm', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2); + }); + + addFormatToken('hmmss', 0, 0, function () { + return '' + hFormat.apply(this) + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); + }); + + addFormatToken('Hmm', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2); + }); + + addFormatToken('Hmmss', 0, 0, function () { + return '' + this.hours() + zeroFill(this.minutes(), 2) + + zeroFill(this.seconds(), 2); + }); + + function meridiem (token, lowercase) { + addFormatToken(token, 0, 0, function () { + return this.localeData().meridiem(this.hours(), this.minutes(), lowercase); + }); + } + + meridiem('a', true); + meridiem('A', false); + + // ALIASES + + addUnitAlias('hour', 'h'); + + // PARSING + + function matchMeridiem (isStrict, locale) { + return locale._meridiemParse; + } + + addRegexToken('a', matchMeridiem); + addRegexToken('A', matchMeridiem); + addRegexToken('H', match1to2); + addRegexToken('h', match1to2); + addRegexToken('HH', match1to2, match2); + addRegexToken('hh', match1to2, match2); + + addRegexToken('hmm', match3to4); + addRegexToken('hmmss', match5to6); + addRegexToken('Hmm', match3to4); + addRegexToken('Hmmss', match5to6); + + addParseToken(['H', 'HH'], HOUR); + addParseToken(['a', 'A'], function (input, array, config) { + config._isPm = config._locale.isPM(input); + config._meridiem = input; + }); + addParseToken(['h', 'hh'], function (input, array, config) { + array[HOUR] = toInt(input); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + getParsingFlags(config).bigHour = true; + }); + addParseToken('Hmm', function (input, array, config) { + var pos = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos)); + array[MINUTE] = toInt(input.substr(pos)); + }); + addParseToken('Hmmss', function (input, array, config) { + var pos1 = input.length - 4; + var pos2 = input.length - 2; + array[HOUR] = toInt(input.substr(0, pos1)); + array[MINUTE] = toInt(input.substr(pos1, 2)); + array[SECOND] = toInt(input.substr(pos2)); + }); + + // LOCALES + + function localeIsPM (input) { + // IE8 Quirks Mode & IE7 Standards Mode do not allow accessing strings like arrays + // Using charAt should be more compatible. + return ((input + '').toLowerCase().charAt(0) === 'p'); + } + + var defaultLocaleMeridiemParse = /[ap]\.?m?\.?/i; + function localeMeridiem (hours, minutes, isLower) { + if (hours > 11) { + return isLower ? 'pm' : 'PM'; + } else { + return isLower ? 'am' : 'AM'; + } + } + + + // MOMENTS + // Setting the hour should keep the time, because the user explicitly // specified which hour he wants. So trying to maintain the same hour (in // a new timezone) makes sense. Adding/subtracting hours does not follow // this rule. - moment.fn.hour = moment.fn.hours = makeAccessor('Hours', true); - // moment.fn.month is defined separately - moment.fn.date = makeAccessor('Date', true); - moment.fn.dates = deprecate('dates accessor is deprecated. Use date instead.', makeAccessor('Date', true)); - moment.fn.year = makeAccessor('FullYear', true); - moment.fn.years = deprecate('years accessor is deprecated. Use year instead.', makeAccessor('FullYear', true)); + var getSetHour = makeGetSet('Hours', true); - // add plural methods - moment.fn.days = moment.fn.day; - moment.fn.months = moment.fn.month; - moment.fn.weeks = moment.fn.week; - moment.fn.isoWeeks = moment.fn.isoWeek; - moment.fn.quarters = moment.fn.quarter; + // FORMATTING - // add aliased format methods - moment.fn.toJSON = moment.fn.toISOString; + addFormatToken('m', ['mm', 2], 0, 'minute'); - /************************************ - Duration Prototype - ************************************/ + // ALIASES + addUnitAlias('minute', 'm'); - function daysToYears (days) { - // 400 years have 146097 days (taking into account leap year rules) - return days * 400 / 146097; - } + // PARSING - function yearsToDays (years) { - // years * 365 + absRound(years / 4) - - // absRound(years / 100) + absRound(years / 400); - return years * 146097 / 400; - } + addRegexToken('m', match1to2); + addRegexToken('mm', match1to2, match2); + addParseToken(['m', 'mm'], MINUTE); - extend(moment.duration.fn = Duration.prototype, { + // MOMENTS - _bubble : function () { - var milliseconds = this._milliseconds, - days = this._days, - months = this._months, - data = this._data, - seconds, minutes, hours, years = 0; + var getSetMinute = makeGetSet('Minutes', false); - // The following code bubbles up values, see the tests for - // examples of what that means. - data.milliseconds = milliseconds % 1000; + // FORMATTING - seconds = absRound(milliseconds / 1000); - data.seconds = seconds % 60; + addFormatToken('s', ['ss', 2], 0, 'second'); - minutes = absRound(seconds / 60); - data.minutes = minutes % 60; + // ALIASES - hours = absRound(minutes / 60); - data.hours = hours % 24; + addUnitAlias('second', 's'); - days += absRound(hours / 24); + // PARSING - // Accurately convert days to years, assume start from year 0. - years = absRound(daysToYears(days)); - days -= absRound(yearsToDays(years)); + addRegexToken('s', match1to2); + addRegexToken('ss', match1to2, match2); + addParseToken(['s', 'ss'], SECOND); - // 30 days to a month - // TODO (iskren): Use anchor date (like 1st Jan) to compute this. - months += absRound(days / 30); - days %= 30; + // MOMENTS - // 12 months -> 1 year - years += absRound(months / 12); - months %= 12; + var getSetSecond = makeGetSet('Seconds', false); - data.days = days; - data.months = months; - data.years = years; - }, + // FORMATTING - abs : function () { - this._milliseconds = Math.abs(this._milliseconds); - this._days = Math.abs(this._days); - this._months = Math.abs(this._months); - - this._data.milliseconds = Math.abs(this._data.milliseconds); - this._data.seconds = Math.abs(this._data.seconds); - this._data.minutes = Math.abs(this._data.minutes); - this._data.hours = Math.abs(this._data.hours); - this._data.months = Math.abs(this._data.months); - this._data.years = Math.abs(this._data.years); - - return this; - }, - - weeks : function () { - return absRound(this.days() / 7); - }, - - valueOf : function () { - return this._milliseconds + - this._days * 864e5 + - (this._months % 12) * 2592e6 + - toInt(this._months / 12) * 31536e6; - }, - - humanize : function (withSuffix) { - var output = relativeTime(this, !withSuffix, this.localeData()); - - if (withSuffix) { - output = this.localeData().pastFuture(+this, output); - } - - return this.localeData().postformat(output); - }, - - add : function (input, val) { - // supports only 2.0-style add(1, 's') or add(moment) - var dur = moment.duration(input, val); - - this._milliseconds += dur._milliseconds; - this._days += dur._days; - this._months += dur._months; - - this._bubble(); - - return this; - }, - - subtract : function (input, val) { - var dur = moment.duration(input, val); - - this._milliseconds -= dur._milliseconds; - this._days -= dur._days; - this._months -= dur._months; - - this._bubble(); - - return this; - }, - - get : function (units) { - units = normalizeUnits(units); - return this[units.toLowerCase() + 's'](); - }, - - as : function (units) { - var days, months; - units = normalizeUnits(units); - - days = this._days + this._milliseconds / 864e5; - if (units === 'month' || units === 'year') { - months = this._months + daysToYears(days) * 12; - return units === 'month' ? months : months / 12; - } else { - days += yearsToDays(this._months / 12); - switch (units) { - case 'week': return days / 7; - case 'day': return days; - case 'hour': return days * 24; - case 'minute': return days * 24 * 60; - case 'second': return days * 24 * 60 * 60; - case 'millisecond': return days * 24 * 60 * 60 * 1000; - default: throw new Error('Unknown unit ' + units); - } - } - }, - - lang : moment.fn.lang, - locale : moment.fn.locale, - - toIsoString : deprecate( - "toIsoString() is deprecated. Please use toISOString() instead " + - "(notice the capitals)", - function () { - return this.toISOString(); - } - ), - - toISOString : function () { - // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js - var years = Math.abs(this.years()), - months = Math.abs(this.months()), - days = Math.abs(this.days()), - hours = Math.abs(this.hours()), - minutes = Math.abs(this.minutes()), - seconds = Math.abs(this.seconds() + this.milliseconds() / 1000); - - if (!this.asSeconds()) { - // this is the same as C#'s (Noda) and python (isodate)... - // but not other JS (goog.date) - return 'P0D'; - } - - return (this.asSeconds() < 0 ? '-' : '') + - 'P' + - (years ? years + 'Y' : '') + - (months ? months + 'M' : '') + - (days ? days + 'D' : '') + - ((hours || minutes || seconds) ? 'T' : '') + - (hours ? hours + 'H' : '') + - (minutes ? minutes + 'M' : '') + - (seconds ? seconds + 'S' : ''); - }, - - localeData : function () { - return this._locale; - } + addFormatToken('S', 0, 0, function () { + return ~~(this.millisecond() / 100); }); - function makeDurationGetter(name) { - moment.duration.fn[name] = function () { - return this._data[name]; - }; + addFormatToken(0, ['SS', 2], 0, function () { + return ~~(this.millisecond() / 10); + }); + + addFormatToken(0, ['SSS', 3], 0, 'millisecond'); + addFormatToken(0, ['SSSS', 4], 0, function () { + return this.millisecond() * 10; + }); + addFormatToken(0, ['SSSSS', 5], 0, function () { + return this.millisecond() * 100; + }); + addFormatToken(0, ['SSSSSS', 6], 0, function () { + return this.millisecond() * 1000; + }); + addFormatToken(0, ['SSSSSSS', 7], 0, function () { + return this.millisecond() * 10000; + }); + addFormatToken(0, ['SSSSSSSS', 8], 0, function () { + return this.millisecond() * 100000; + }); + addFormatToken(0, ['SSSSSSSSS', 9], 0, function () { + return this.millisecond() * 1000000; + }); + + + // ALIASES + + addUnitAlias('millisecond', 'ms'); + + // PARSING + + addRegexToken('S', match1to3, match1); + addRegexToken('SS', match1to3, match2); + addRegexToken('SSS', match1to3, match3); + + var token; + for (token = 'SSSS'; token.length <= 9; token += 'S') { + addRegexToken(token, matchUnsigned); } - for (i in unitMillisecondFactors) { - if (unitMillisecondFactors.hasOwnProperty(i)) { - makeDurationGetter(i.toLowerCase()); + function parseMs(input, array) { + array[MILLISECOND] = toInt(('0.' + input) * 1000); + } + + for (token = 'S'; token.length <= 9; token += 'S') { + addParseToken(token, parseMs); + } + // MOMENTS + + var getSetMillisecond = makeGetSet('Milliseconds', false); + + // FORMATTING + + addFormatToken('z', 0, 0, 'zoneAbbr'); + addFormatToken('zz', 0, 0, 'zoneName'); + + // MOMENTS + + function getZoneAbbr () { + return this._isUTC ? 'UTC' : ''; + } + + function getZoneName () { + return this._isUTC ? 'Coordinated Universal Time' : ''; + } + + var momentPrototype__proto = Moment.prototype; + + momentPrototype__proto.add = add_subtract__add; + momentPrototype__proto.calendar = moment_calendar__calendar; + momentPrototype__proto.clone = clone; + momentPrototype__proto.diff = diff; + momentPrototype__proto.endOf = endOf; + momentPrototype__proto.format = format; + momentPrototype__proto.from = from; + momentPrototype__proto.fromNow = fromNow; + momentPrototype__proto.to = to; + momentPrototype__proto.toNow = toNow; + momentPrototype__proto.get = getSet; + momentPrototype__proto.invalidAt = invalidAt; + momentPrototype__proto.isAfter = isAfter; + momentPrototype__proto.isBefore = isBefore; + momentPrototype__proto.isBetween = isBetween; + momentPrototype__proto.isSame = isSame; + momentPrototype__proto.isSameOrAfter = isSameOrAfter; + momentPrototype__proto.isSameOrBefore = isSameOrBefore; + momentPrototype__proto.isValid = moment_valid__isValid; + momentPrototype__proto.lang = lang; + momentPrototype__proto.locale = locale; + momentPrototype__proto.localeData = localeData; + momentPrototype__proto.max = prototypeMax; + momentPrototype__proto.min = prototypeMin; + momentPrototype__proto.parsingFlags = parsingFlags; + momentPrototype__proto.set = getSet; + momentPrototype__proto.startOf = startOf; + momentPrototype__proto.subtract = add_subtract__subtract; + momentPrototype__proto.toArray = toArray; + momentPrototype__proto.toObject = toObject; + momentPrototype__proto.toDate = toDate; + momentPrototype__proto.toISOString = moment_format__toISOString; + momentPrototype__proto.toJSON = toJSON; + momentPrototype__proto.toString = toString; + momentPrototype__proto.unix = unix; + momentPrototype__proto.valueOf = to_type__valueOf; + momentPrototype__proto.creationData = creationData; + + // Year + momentPrototype__proto.year = getSetYear; + momentPrototype__proto.isLeapYear = getIsLeapYear; + + // Week Year + momentPrototype__proto.weekYear = getSetWeekYear; + momentPrototype__proto.isoWeekYear = getSetISOWeekYear; + + // Quarter + momentPrototype__proto.quarter = momentPrototype__proto.quarters = getSetQuarter; + + // Month + momentPrototype__proto.month = getSetMonth; + momentPrototype__proto.daysInMonth = getDaysInMonth; + + // Week + momentPrototype__proto.week = momentPrototype__proto.weeks = getSetWeek; + momentPrototype__proto.isoWeek = momentPrototype__proto.isoWeeks = getSetISOWeek; + momentPrototype__proto.weeksInYear = getWeeksInYear; + momentPrototype__proto.isoWeeksInYear = getISOWeeksInYear; + + // Day + momentPrototype__proto.date = getSetDayOfMonth; + momentPrototype__proto.day = momentPrototype__proto.days = getSetDayOfWeek; + momentPrototype__proto.weekday = getSetLocaleDayOfWeek; + momentPrototype__proto.isoWeekday = getSetISODayOfWeek; + momentPrototype__proto.dayOfYear = getSetDayOfYear; + + // Hour + momentPrototype__proto.hour = momentPrototype__proto.hours = getSetHour; + + // Minute + momentPrototype__proto.minute = momentPrototype__proto.minutes = getSetMinute; + + // Second + momentPrototype__proto.second = momentPrototype__proto.seconds = getSetSecond; + + // Millisecond + momentPrototype__proto.millisecond = momentPrototype__proto.milliseconds = getSetMillisecond; + + // Offset + momentPrototype__proto.utcOffset = getSetOffset; + momentPrototype__proto.utc = setOffsetToUTC; + momentPrototype__proto.local = setOffsetToLocal; + momentPrototype__proto.parseZone = setOffsetToParsedOffset; + momentPrototype__proto.hasAlignedHourOffset = hasAlignedHourOffset; + momentPrototype__proto.isDST = isDaylightSavingTime; + momentPrototype__proto.isDSTShifted = isDaylightSavingTimeShifted; + momentPrototype__proto.isLocal = isLocal; + momentPrototype__proto.isUtcOffset = isUtcOffset; + momentPrototype__proto.isUtc = isUtc; + momentPrototype__proto.isUTC = isUtc; + + // Timezone + momentPrototype__proto.zoneAbbr = getZoneAbbr; + momentPrototype__proto.zoneName = getZoneName; + + // Deprecations + momentPrototype__proto.dates = deprecate('dates accessor is deprecated. Use date instead.', getSetDayOfMonth); + momentPrototype__proto.months = deprecate('months accessor is deprecated. Use month instead', getSetMonth); + momentPrototype__proto.years = deprecate('years accessor is deprecated. Use year instead', getSetYear); + momentPrototype__proto.zone = deprecate('moment().zone is deprecated, use moment().utcOffset instead. https://github.com/moment/moment/issues/1779', getSetZone); + + var momentPrototype = momentPrototype__proto; + + function moment__createUnix (input) { + return local__createLocal(input * 1000); + } + + function moment__createInZone () { + return local__createLocal.apply(null, arguments).parseZone(); + } + + var defaultCalendar = { + sameDay : '[Today at] LT', + nextDay : '[Tomorrow at] LT', + nextWeek : 'dddd [at] LT', + lastDay : '[Yesterday at] LT', + lastWeek : '[Last] dddd [at] LT', + sameElse : 'L' + }; + + function locale_calendar__calendar (key, mom, now) { + var output = this._calendar[key]; + return isFunction(output) ? output.call(mom, now) : output; + } + + var defaultLongDateFormat = { + LTS : 'h:mm:ss A', + LT : 'h:mm A', + L : 'MM/DD/YYYY', + LL : 'MMMM D, YYYY', + LLL : 'MMMM D, YYYY h:mm A', + LLLL : 'dddd, MMMM D, YYYY h:mm A' + }; + + function longDateFormat (key) { + var format = this._longDateFormat[key], + formatUpper = this._longDateFormat[key.toUpperCase()]; + + if (format || !formatUpper) { + return format; } + + this._longDateFormat[key] = formatUpper.replace(/MMMM|MM|DD|dddd/g, function (val) { + return val.slice(1); + }); + + return this._longDateFormat[key]; } - moment.duration.fn.asMilliseconds = function () { - return this.as('ms'); - }; - moment.duration.fn.asSeconds = function () { - return this.as('s'); - }; - moment.duration.fn.asMinutes = function () { - return this.as('m'); - }; - moment.duration.fn.asHours = function () { - return this.as('h'); - }; - moment.duration.fn.asDays = function () { - return this.as('d'); - }; - moment.duration.fn.asWeeks = function () { - return this.as('weeks'); - }; - moment.duration.fn.asMonths = function () { - return this.as('M'); - }; - moment.duration.fn.asYears = function () { - return this.as('y'); + var defaultInvalidDate = 'Invalid date'; + + function invalidDate () { + return this._invalidDate; + } + + var defaultOrdinal = '%d'; + var defaultOrdinalParse = /\d{1,2}/; + + function ordinal (number) { + return this._ordinal.replace('%d', number); + } + + function preParsePostFormat (string) { + return string; + } + + var defaultRelativeTime = { + future : 'in %s', + past : '%s ago', + s : 'a few seconds', + m : 'a minute', + mm : '%d minutes', + h : 'an hour', + hh : '%d hours', + d : 'a day', + dd : '%d days', + M : 'a month', + MM : '%d months', + y : 'a year', + yy : '%d years' }; - /************************************ - Default Locale - ************************************/ + function relative__relativeTime (number, withoutSuffix, string, isFuture) { + var output = this._relativeTime[string]; + return (isFunction(output)) ? + output(number, withoutSuffix, string, isFuture) : + output.replace(/%d/i, number); + } + function pastFuture (diff, output) { + var format = this._relativeTime[diff > 0 ? 'future' : 'past']; + return isFunction(format) ? format(output) : format.replace(/%s/i, output); + } - // Set default locale, other locale will inherit from English. - moment.locale('en', { + var prototype__proto = Locale.prototype; + + prototype__proto._calendar = defaultCalendar; + prototype__proto.calendar = locale_calendar__calendar; + prototype__proto._longDateFormat = defaultLongDateFormat; + prototype__proto.longDateFormat = longDateFormat; + prototype__proto._invalidDate = defaultInvalidDate; + prototype__proto.invalidDate = invalidDate; + prototype__proto._ordinal = defaultOrdinal; + prototype__proto.ordinal = ordinal; + prototype__proto._ordinalParse = defaultOrdinalParse; + prototype__proto.preparse = preParsePostFormat; + prototype__proto.postformat = preParsePostFormat; + prototype__proto._relativeTime = defaultRelativeTime; + prototype__proto.relativeTime = relative__relativeTime; + prototype__proto.pastFuture = pastFuture; + prototype__proto.set = locale_set__set; + + // Month + prototype__proto.months = localeMonths; + prototype__proto._months = defaultLocaleMonths; + prototype__proto.monthsShort = localeMonthsShort; + prototype__proto._monthsShort = defaultLocaleMonthsShort; + prototype__proto.monthsParse = localeMonthsParse; + prototype__proto._monthsRegex = defaultMonthsRegex; + prototype__proto.monthsRegex = monthsRegex; + prototype__proto._monthsShortRegex = defaultMonthsShortRegex; + prototype__proto.monthsShortRegex = monthsShortRegex; + + // Week + prototype__proto.week = localeWeek; + prototype__proto._week = defaultLocaleWeek; + prototype__proto.firstDayOfYear = localeFirstDayOfYear; + prototype__proto.firstDayOfWeek = localeFirstDayOfWeek; + + // Day of Week + prototype__proto.weekdays = localeWeekdays; + prototype__proto._weekdays = defaultLocaleWeekdays; + prototype__proto.weekdaysMin = localeWeekdaysMin; + prototype__proto._weekdaysMin = defaultLocaleWeekdaysMin; + prototype__proto.weekdaysShort = localeWeekdaysShort; + prototype__proto._weekdaysShort = defaultLocaleWeekdaysShort; + prototype__proto.weekdaysParse = localeWeekdaysParse; + + // Hours + prototype__proto.isPM = localeIsPM; + prototype__proto._meridiemParse = defaultLocaleMeridiemParse; + prototype__proto.meridiem = localeMeridiem; + + function lists__get (format, index, field, setter) { + var locale = locale_locales__getLocale(); + var utc = create_utc__createUTC().set(setter, index); + return locale[field](utc, format); + } + + function list (format, index, field, count, setter) { + if (typeof format === 'number') { + index = format; + format = undefined; + } + + format = format || ''; + + if (index != null) { + return lists__get(format, index, field, setter); + } + + var i; + var out = []; + for (i = 0; i < count; i++) { + out[i] = lists__get(format, i, field, setter); + } + return out; + } + + function lists__listMonths (format, index) { + return list(format, index, 'months', 12, 'month'); + } + + function lists__listMonthsShort (format, index) { + return list(format, index, 'monthsShort', 12, 'month'); + } + + function lists__listWeekdays (format, index) { + return list(format, index, 'weekdays', 7, 'day'); + } + + function lists__listWeekdaysShort (format, index) { + return list(format, index, 'weekdaysShort', 7, 'day'); + } + + function lists__listWeekdaysMin (format, index) { + return list(format, index, 'weekdaysMin', 7, 'day'); + } + + locale_locales__getSetGlobalLocale('en', { + ordinalParse: /\d{1,2}(th|st|nd|rd)/, ordinal : function (number) { var b = number % 10, output = (toInt(number % 100 / 10) === 1) ? 'th' : @@ -2766,43 +3295,394 @@ } }); - /* EMBED_LOCALES */ + // Side effect imports + utils_hooks__hooks.lang = deprecate('moment.lang is deprecated. Use moment.locale instead.', locale_locales__getSetGlobalLocale); + utils_hooks__hooks.langData = deprecate('moment.langData is deprecated. Use moment.localeData instead.', locale_locales__getLocale); - /************************************ - Exposing Moment - ************************************/ + var mathAbs = Math.abs; - function makeGlobal(shouldDeprecate) { - /*global ender:false */ - if (typeof ender !== 'undefined') { - return; - } - oldGlobalMoment = globalScope.moment; - if (shouldDeprecate) { - globalScope.moment = deprecate( - 'Accessing Moment through the global scope is ' + - 'deprecated, and will be removed in an upcoming ' + - 'release.', - moment); + function duration_abs__abs () { + var data = this._data; + + this._milliseconds = mathAbs(this._milliseconds); + this._days = mathAbs(this._days); + this._months = mathAbs(this._months); + + data.milliseconds = mathAbs(data.milliseconds); + data.seconds = mathAbs(data.seconds); + data.minutes = mathAbs(data.minutes); + data.hours = mathAbs(data.hours); + data.months = mathAbs(data.months); + data.years = mathAbs(data.years); + + return this; + } + + function duration_add_subtract__addSubtract (duration, input, value, direction) { + var other = create__createDuration(input, value); + + duration._milliseconds += direction * other._milliseconds; + duration._days += direction * other._days; + duration._months += direction * other._months; + + return duration._bubble(); + } + + // supports only 2.0-style add(1, 's') or add(duration) + function duration_add_subtract__add (input, value) { + return duration_add_subtract__addSubtract(this, input, value, 1); + } + + // supports only 2.0-style subtract(1, 's') or subtract(duration) + function duration_add_subtract__subtract (input, value) { + return duration_add_subtract__addSubtract(this, input, value, -1); + } + + function absCeil (number) { + if (number < 0) { + return Math.floor(number); } else { - globalScope.moment = moment; + return Math.ceil(number); } } - // CommonJS module is defined - if (hasModule) { - module.exports = moment; - } else if (typeof define === 'function' && define.amd) { - define('moment', function (require, exports, module) { - if (module.config && module.config() && module.config().noGlobal === true) { - // release the global variable - globalScope.moment = oldGlobalMoment; - } + function bubble () { + var milliseconds = this._milliseconds; + var days = this._days; + var months = this._months; + var data = this._data; + var seconds, minutes, hours, years, monthsFromDays; - return moment; - }); - makeGlobal(true); - } else { - makeGlobal(); + // if we have a mix of positive and negative values, bubble down first + // check: https://github.com/moment/moment/issues/2166 + if (!((milliseconds >= 0 && days >= 0 && months >= 0) || + (milliseconds <= 0 && days <= 0 && months <= 0))) { + milliseconds += absCeil(monthsToDays(months) + days) * 864e5; + days = 0; + months = 0; + } + + // The following code bubbles up values, see the tests for + // examples of what that means. + data.milliseconds = milliseconds % 1000; + + seconds = absFloor(milliseconds / 1000); + data.seconds = seconds % 60; + + minutes = absFloor(seconds / 60); + data.minutes = minutes % 60; + + hours = absFloor(minutes / 60); + data.hours = hours % 24; + + days += absFloor(hours / 24); + + // convert days to months + monthsFromDays = absFloor(daysToMonths(days)); + months += monthsFromDays; + days -= absCeil(monthsToDays(monthsFromDays)); + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + data.days = days; + data.months = months; + data.years = years; + + return this; } -}).call(this); + + function daysToMonths (days) { + // 400 years have 146097 days (taking into account leap year rules) + // 400 years have 12 months === 4800 + return days * 4800 / 146097; + } + + function monthsToDays (months) { + // the reverse of daysToMonths + return months * 146097 / 4800; + } + + function as (units) { + var days; + var months; + var milliseconds = this._milliseconds; + + units = normalizeUnits(units); + + if (units === 'month' || units === 'year') { + days = this._days + milliseconds / 864e5; + months = this._months + daysToMonths(days); + return units === 'month' ? months : months / 12; + } else { + // handle milliseconds separately because of floating point math errors (issue #1867) + days = this._days + Math.round(monthsToDays(this._months)); + switch (units) { + case 'week' : return days / 7 + milliseconds / 6048e5; + case 'day' : return days + milliseconds / 864e5; + case 'hour' : return days * 24 + milliseconds / 36e5; + case 'minute' : return days * 1440 + milliseconds / 6e4; + case 'second' : return days * 86400 + milliseconds / 1000; + // Math.floor prevents floating point math errors here + case 'millisecond': return Math.floor(days * 864e5) + milliseconds; + default: throw new Error('Unknown unit ' + units); + } + } + } + + // TODO: Use this.as('ms')? + function duration_as__valueOf () { + return ( + this._milliseconds + + this._days * 864e5 + + (this._months % 12) * 2592e6 + + toInt(this._months / 12) * 31536e6 + ); + } + + function makeAs (alias) { + return function () { + return this.as(alias); + }; + } + + var asMilliseconds = makeAs('ms'); + var asSeconds = makeAs('s'); + var asMinutes = makeAs('m'); + var asHours = makeAs('h'); + var asDays = makeAs('d'); + var asWeeks = makeAs('w'); + var asMonths = makeAs('M'); + var asYears = makeAs('y'); + + function duration_get__get (units) { + units = normalizeUnits(units); + return this[units + 's'](); + } + + function makeGetter(name) { + return function () { + return this._data[name]; + }; + } + + var milliseconds = makeGetter('milliseconds'); + var seconds = makeGetter('seconds'); + var minutes = makeGetter('minutes'); + var hours = makeGetter('hours'); + var days = makeGetter('days'); + var months = makeGetter('months'); + var years = makeGetter('years'); + + function weeks () { + return absFloor(this.days() / 7); + } + + var round = Math.round; + var thresholds = { + s: 45, // seconds to minute + m: 45, // minutes to hour + h: 22, // hours to day + d: 26, // days to month + M: 11 // months to year + }; + + // helper function for moment.fn.from, moment.fn.fromNow, and moment.duration.fn.humanize + function substituteTimeAgo(string, number, withoutSuffix, isFuture, locale) { + return locale.relativeTime(number || 1, !!withoutSuffix, string, isFuture); + } + + function duration_humanize__relativeTime (posNegDuration, withoutSuffix, locale) { + var duration = create__createDuration(posNegDuration).abs(); + var seconds = round(duration.as('s')); + var minutes = round(duration.as('m')); + var hours = round(duration.as('h')); + var days = round(duration.as('d')); + var months = round(duration.as('M')); + var years = round(duration.as('y')); + + var a = seconds < thresholds.s && ['s', seconds] || + minutes <= 1 && ['m'] || + minutes < thresholds.m && ['mm', minutes] || + hours <= 1 && ['h'] || + hours < thresholds.h && ['hh', hours] || + days <= 1 && ['d'] || + days < thresholds.d && ['dd', days] || + months <= 1 && ['M'] || + months < thresholds.M && ['MM', months] || + years <= 1 && ['y'] || ['yy', years]; + + a[2] = withoutSuffix; + a[3] = +posNegDuration > 0; + a[4] = locale; + return substituteTimeAgo.apply(null, a); + } + + // This function allows you to set a threshold for relative time strings + function duration_humanize__getSetRelativeTimeThreshold (threshold, limit) { + if (thresholds[threshold] === undefined) { + return false; + } + if (limit === undefined) { + return thresholds[threshold]; + } + thresholds[threshold] = limit; + return true; + } + + function humanize (withSuffix) { + var locale = this.localeData(); + var output = duration_humanize__relativeTime(this, !withSuffix, locale); + + if (withSuffix) { + output = locale.pastFuture(+this, output); + } + + return locale.postformat(output); + } + + var iso_string__abs = Math.abs; + + function iso_string__toISOString() { + // for ISO strings we do not use the normal bubbling rules: + // * milliseconds bubble up until they become hours + // * days do not bubble at all + // * months bubble up until they become years + // This is because there is no context-free conversion between hours and days + // (think of clock changes) + // and also not between days and months (28-31 days per month) + var seconds = iso_string__abs(this._milliseconds) / 1000; + var days = iso_string__abs(this._days); + var months = iso_string__abs(this._months); + var minutes, hours, years; + + // 3600 seconds -> 60 minutes -> 1 hour + minutes = absFloor(seconds / 60); + hours = absFloor(minutes / 60); + seconds %= 60; + minutes %= 60; + + // 12 months -> 1 year + years = absFloor(months / 12); + months %= 12; + + + // inspired by https://github.com/dordille/moment-isoduration/blob/master/moment.isoduration.js + var Y = years; + var M = months; + var D = days; + var h = hours; + var m = minutes; + var s = seconds; + var total = this.asSeconds(); + + if (!total) { + // this is the same as C#'s (Noda) and python (isodate)... + // but not other JS (goog.date) + return 'P0D'; + } + + return (total < 0 ? '-' : '') + + 'P' + + (Y ? Y + 'Y' : '') + + (M ? M + 'M' : '') + + (D ? D + 'D' : '') + + ((h || m || s) ? 'T' : '') + + (h ? h + 'H' : '') + + (m ? m + 'M' : '') + + (s ? s + 'S' : ''); + } + + var duration_prototype__proto = Duration.prototype; + + duration_prototype__proto.abs = duration_abs__abs; + duration_prototype__proto.add = duration_add_subtract__add; + duration_prototype__proto.subtract = duration_add_subtract__subtract; + duration_prototype__proto.as = as; + duration_prototype__proto.asMilliseconds = asMilliseconds; + duration_prototype__proto.asSeconds = asSeconds; + duration_prototype__proto.asMinutes = asMinutes; + duration_prototype__proto.asHours = asHours; + duration_prototype__proto.asDays = asDays; + duration_prototype__proto.asWeeks = asWeeks; + duration_prototype__proto.asMonths = asMonths; + duration_prototype__proto.asYears = asYears; + duration_prototype__proto.valueOf = duration_as__valueOf; + duration_prototype__proto._bubble = bubble; + duration_prototype__proto.get = duration_get__get; + duration_prototype__proto.milliseconds = milliseconds; + duration_prototype__proto.seconds = seconds; + duration_prototype__proto.minutes = minutes; + duration_prototype__proto.hours = hours; + duration_prototype__proto.days = days; + duration_prototype__proto.weeks = weeks; + duration_prototype__proto.months = months; + duration_prototype__proto.years = years; + duration_prototype__proto.humanize = humanize; + duration_prototype__proto.toISOString = iso_string__toISOString; + duration_prototype__proto.toString = iso_string__toISOString; + duration_prototype__proto.toJSON = iso_string__toISOString; + duration_prototype__proto.locale = locale; + duration_prototype__proto.localeData = localeData; + + // Deprecations + duration_prototype__proto.toIsoString = deprecate('toIsoString() is deprecated. Please use toISOString() instead (notice the capitals)', iso_string__toISOString); + duration_prototype__proto.lang = lang; + + // Side effect imports + + // FORMATTING + + addFormatToken('X', 0, 0, 'unix'); + addFormatToken('x', 0, 0, 'valueOf'); + + // PARSING + + addRegexToken('x', matchSigned); + addRegexToken('X', matchTimestamp); + addParseToken('X', function (input, array, config) { + config._d = new Date(parseFloat(input, 10) * 1000); + }); + addParseToken('x', function (input, array, config) { + config._d = new Date(toInt(input)); + }); + + // Side effect imports + + + utils_hooks__hooks.version = '2.12.0'; + + setHookCallback(local__createLocal); + + utils_hooks__hooks.fn = momentPrototype; + utils_hooks__hooks.min = min; + utils_hooks__hooks.max = max; + utils_hooks__hooks.now = now; + utils_hooks__hooks.utc = create_utc__createUTC; + utils_hooks__hooks.unix = moment__createUnix; + utils_hooks__hooks.months = lists__listMonths; + utils_hooks__hooks.isDate = isDate; + utils_hooks__hooks.locale = locale_locales__getSetGlobalLocale; + utils_hooks__hooks.invalid = valid__createInvalid; + utils_hooks__hooks.duration = create__createDuration; + utils_hooks__hooks.isMoment = isMoment; + utils_hooks__hooks.weekdays = lists__listWeekdays; + utils_hooks__hooks.parseZone = moment__createInZone; + utils_hooks__hooks.localeData = locale_locales__getLocale; + utils_hooks__hooks.isDuration = isDuration; + utils_hooks__hooks.monthsShort = lists__listMonthsShort; + utils_hooks__hooks.weekdaysMin = lists__listWeekdaysMin; + utils_hooks__hooks.defineLocale = defineLocale; + utils_hooks__hooks.updateLocale = updateLocale; + utils_hooks__hooks.locales = locale_locales__listLocales; + utils_hooks__hooks.weekdaysShort = lists__listWeekdaysShort; + utils_hooks__hooks.normalizeUnits = normalizeUnits; + utils_hooks__hooks.relativeTimeThreshold = duration_humanize__getSetRelativeTimeThreshold; + utils_hooks__hooks.prototype = momentPrototype; + + var _moment = utils_hooks__hooks; + + return _moment; + +})); From d9231eb805473a3b32e9575c5e25af27f83b9b82 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 9 Mar 2016 12:21:02 +0100 Subject: [PATCH 057/113] github(pr_template): minor guide for PRs --- .github/PULL_REQUEST_TEMPLATE.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index e69de29bb2d..9eacff7c5de 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ +* Link the PR to an issue for new features +* Rebase your PR if it gets out of sync with master \ No newline at end of file From 4a5b753fd8d4b9e4f67f6ef0c10ad287fafd5248 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 9 Mar 2016 13:10:02 +0100 Subject: [PATCH 058/113] feat(templating): refactoring of the refresh values of #4281 --- public/app/features/dashboard/dashboardSrv.js | 4 +-- public/app/features/templating/editorCtrl.js | 8 ++++- .../features/templating/partials/editor.html | 4 +-- .../features/templating/templateValuesSrv.js | 31 ++++++++++--------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index ff53eb93f70..fbd950e60b6 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -404,8 +404,8 @@ function (angular, $, _, moment) { if (oldVersion < 11) { // update template variables _.each(this.templating.list, function(templateVariable) { - if (templateVariable.refresh) { templateVariable.refresh = 'On Dashboard Load'; } - if (!templateVariable.refresh) { templateVariable.refresh = 'Never'; } + if (templateVariable.refresh) { templateVariable.refresh = 1; } + if (!templateVariable.refresh) { templateVariable.refresh = 0; } }); } diff --git a/public/app/features/templating/editorCtrl.js b/public/app/features/templating/editorCtrl.js index 1b31ce9cf1a..b2916e1bcc7 100644 --- a/public/app/features/templating/editorCtrl.js +++ b/public/app/features/templating/editorCtrl.js @@ -12,13 +12,19 @@ function (angular, _) { var replacementDefaults = { type: 'query', datasource: null, - refresh: 'Never', + refresh: 0, name: '', options: [], includeAll: false, multi: false, }; + $scope.refreshOptions = [ + {value: 0, text: "Never"}, + {value: 1, text: "On Dashboard Load"}, + {value: 2, text: "On Time Range Change"}, + ]; + $scope.init = function() { $scope.mode = 'list'; diff --git a/public/app/features/templating/partials/editor.html b/public/app/features/templating/partials/editor.html index bbdabe92fb8..7bd1fd0940f 100644 --- a/public/app/features/templating/partials/editor.html +++ b/public/app/features/templating/partials/editor.html @@ -171,8 +171,8 @@
    Refresh - - When to update the values of this template, will slow down dashboard load / time change + + When to update the values of this variable, will slow down dashboard load / time change
    diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index e5861ad12c1..4ccbefefdef 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -13,22 +13,25 @@ function (angular, _, kbn) { function getNoneOption() { return { text: 'None', value: '', isNone: true }; } - $rootScope.onAppEvent('time-range-changed', function() { - var variable = _.findWhere(self.variables, { type: 'interval' }); - if (variable) { - self.updateAutoInterval(variable); - } - }, $rootScope); - + // update time variant variables $rootScope.onAppEvent('refresh', function() { - var promises = _.chain(self.variables) - .filter(function(variable) { - return variable.refresh === 'On Time Change and Dashboard Load'; - }) - .map(function(variable) { - return self.updateOptions(variable); - }).value(); + + // look for interval variables + var intervalVariable = _.findWhere(self.variables, { type: 'interval' }); + if (intervalVariable) { + self.updateAutoInterval(intervalVariable); + } + + // update variables with refresh === 2 + var promises = self.variables + .filter(function(variable) { + return variable.refresh === 2; + }).map(function(variable) { + return self.updateOptions(variable); + }); + return $q.all(promises); + }, $rootScope); this.init = function(dashboard) { From 6670e6cd29c0051e00aed24009eb38d38780150d Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 9 Mar 2016 16:10:46 +0100 Subject: [PATCH 059/113] feat(cli): allow redirect for plugin-repo.json --- pkg/cmd/grafana-cli/services/services.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index c23c716b62f..1ffc60abf4c 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -12,7 +12,7 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { - res, _ := goreq.Request{Uri: repoUrl}.Do() + res, _ := goreq.Request{Uri: repoUrl, MaxRedirects: 3}.Do() var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) From d6ecf74db497787ea6b946e5ef89d408c5e6a19a Mon Sep 17 00:00:00 2001 From: Paul Mestemaker Date: Wed, 9 Mar 2016 11:25:42 -0800 Subject: [PATCH 060/113] Update ldap.md Added clearer language on which versions offer LDAP support. --- docs/sources/installation/ldap.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/ldap.md b/docs/sources/installation/ldap.md index 82309ec0f19..a5311fb4fa5 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/installation/ldap.md @@ -6,7 +6,7 @@ page_keywords: grafana, ldap, configuration, documentation, integration # LDAP Integration -Grafana 2.1 ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your +Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your Grafana users to login with their LDAP credentials. You can also specify mappings between LDAP group memberships and Grafana Organization user roles. From df2a23d1a7063d7739b949a99001b442604ffc39 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Thu, 10 Mar 2016 13:14:24 +0900 Subject: [PATCH 061/113] fix to use variable.refresh flag --- public/app/features/templating/templateValuesSrv.js | 2 +- public/dashboards/template_vars.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/templating/templateValuesSrv.js b/public/app/features/templating/templateValuesSrv.js index 4ccbefefdef..ce26a35be9b 100644 --- a/public/app/features/templating/templateValuesSrv.js +++ b/public/app/features/templating/templateValuesSrv.js @@ -74,7 +74,7 @@ function (angular, _, kbn) { if (urlValue !== void 0) { return self.setVariableFromUrl(variable, urlValue).then(lock.resolve); } - else if (variable.refresh === 'On Dashboard Load' || variable.refresh === 'On Time Change and Dashboard Load') { + else if (variable.refresh === 1 || variable.refresh === 2) { return self.updateOptions(variable).then(function() { if (_.isEmpty(variable.current) && variable.options.length) { console.log("setting current for %s", variable.name); diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json index 53dd5e3cfbf..8ca81fbdff5 100644 --- a/public/dashboards/template_vars.json +++ b/public/dashboards/template_vars.json @@ -258,6 +258,6 @@ "annotations": { "enable": false }, - "refresh": "Never", + "refresh": 0, "version": 6 } From 1951f3856f2427b4d3462d35881b80f943c5f6f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Mar 2016 09:47:29 +0100 Subject: [PATCH 062/113] feat(plugins): refactored datasourceEditCtrl to typescript, #4298 --- .../app/core/directives/plugin_component.ts | 9 +- public/app/core/routes/routes.ts | 1 + public/app/features/datasources/edit_ctrl.js | 129 ---------------- public/app/features/datasources/edit_ctrl.ts | 140 ++++++++++++++++++ .../features/datasources/partials/edit.html | 41 +++-- 5 files changed, 174 insertions(+), 146 deletions(-) delete mode 100644 public/app/features/datasources/edit_ctrl.js create mode 100644 public/app/features/datasources/edit_ctrl.ts diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 858aab02035..69535327cc8 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -148,12 +148,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // ConfigCtrl case 'datasource-config-ctrl': { - return System.import(scope.datasourceMeta.module).then(function(dsModule) { + var dsMeta = scope.ctrl.datasourceMeta; + return System.import(dsMeta.module).then(function(dsModule) { return { - baseUrl: scope.datasourceMeta.baseUrl, - name: 'ds-config-' + scope.datasourceMeta.id, + baseUrl: dsMeta.baseUrl, + name: 'ds-config-' + dsMeta.id, bindings: {meta: "=", current: "="}, - attrs: {meta: "datasourceMeta", current: "current"}, + attrs: {meta: "ctrl.datasourceMeta", current: "ctrl.current"}, Component: dsModule.ConfigCtrl, }; }); diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 6e3af683c82..2b035650c76 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -57,6 +57,7 @@ function setupAngularRoutes($routeProvider, $locationProvider) { .when('/datasources/edit/:id', { templateUrl: 'public/app/features/datasources/partials/edit.html', controller : 'DataSourceEditCtrl', + controllerAs: 'ctrl', resolve: loadOrgBundle, }) .when('/datasources/new', { diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js deleted file mode 100644 index 4e99dc211ff..00000000000 --- a/public/app/features/datasources/edit_ctrl.js +++ /dev/null @@ -1,129 +0,0 @@ -define([ - 'angular', - 'lodash', - 'app/core/config', -], -function (angular, _, config) { - 'use strict'; - - var module = angular.module('grafana.controllers'); - var datasourceTypes = []; - - module.directive('datasourceHttpSettings', function() { - return { - scope: {current: "="}, - templateUrl: 'public/app/features/datasources/partials/http_settings.html' - }; - }); - - module.controller('DataSourceEditCtrl', function($scope, $q, backendSrv, $routeParams, $location, datasourceSrv) { - - var defaults = { - name: '', - type: 'graphite', - url: '', - access: 'proxy', - jsonData: {} - }; - - $scope.init = function() { - $scope.isNew = true; - $scope.datasources = []; - - $scope.loadDatasourceTypes().then(function() { - if ($routeParams.id) { - $scope.getDatasourceById($routeParams.id); - } else { - $scope.current = angular.copy(defaults); - $scope.typeChanged(); - } - }); - }; - - $scope.loadDatasourceTypes = function() { - if (datasourceTypes.length > 0) { - $scope.types = datasourceTypes; - return $q.when(null); - } - - return backendSrv.get('/api/org/plugins', {enabled: 1, type: 'datasource'}).then(function(plugins) { - datasourceTypes = plugins; - $scope.types = plugins; - }); - }; - - $scope.getDatasourceById = function(id) { - backendSrv.get('/api/datasources/' + id).then(function(ds) { - $scope.isNew = false; - $scope.current = ds; - return $scope.typeChanged(); - }); - }; - - $scope.typeChanged = function() { - return backendSrv.get('/api/org/plugins/' + $scope.current.type + '/settings').then(function(pluginInfo) { - $scope.datasourceMeta = pluginInfo; - }); - }; - - $scope.updateFrontendSettings = function() { - return backendSrv.get('/api/frontend/settings').then(function(settings) { - config.datasources = settings.datasources; - config.defaultDatasource = settings.defaultDatasource; - datasourceSrv.init(); - }); - }; - - $scope.testDatasource = function() { - $scope.testing = { done: false }; - - datasourceSrv.get($scope.current.name).then(function(datasource) { - if (!datasource.testDatasource) { - $scope.testing.message = 'Data source does not support test connection feature.'; - $scope.testing.status = 'warning'; - $scope.testing.title = 'Unknown'; - return; - } - - return datasource.testDatasource().then(function(result) { - $scope.testing.message = result.message; - $scope.testing.status = result.status; - $scope.testing.title = result.title; - }, function(err) { - if (err.statusText) { - $scope.testing.message = err.statusText; - $scope.testing.title = "HTTP Error"; - } else { - $scope.testing.message = err.message; - $scope.testing.title = "Unknown error"; - } - }); - }).finally(function() { - $scope.testing.done = true; - }); - }; - - $scope.saveChanges = function(test) { - if (!$scope.editForm.$valid) { - return; - } - - if ($scope.current.id) { - return backendSrv.put('/api/datasources/' + $scope.current.id, $scope.current).then(function() { - $scope.updateFrontendSettings().then(function() { - if (test) { - $scope.testDatasource(); - } - }); - }); - } else { - return backendSrv.post('/api/datasources', $scope.current).then(function(result) { - $scope.updateFrontendSettings(); - $location.path('datasources/edit/' + result.id); - }); - } - }; - - $scope.init(); - }); -}); diff --git a/public/app/features/datasources/edit_ctrl.ts b/public/app/features/datasources/edit_ctrl.ts new file mode 100644 index 00000000000..eafee0d427a --- /dev/null +++ b/public/app/features/datasources/edit_ctrl.ts @@ -0,0 +1,140 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import config from 'app/core/config'; + +var datasourceTypes = []; + +var defaults = { + name: '', + type: 'graphite', + url: '', + access: 'proxy', + jsonData: {} +}; + +export class DataSourceEditCtrl { + isNew: boolean; + datasources: any[]; + current: any; + types: any; + testing: any; + datasourceMeta: any; + + /** @ngInject */ + constructor( + private $scope, + private $q, + private backendSrv, + private $routeParams, + private $location, + private datasourceSrv) { + + this.isNew = true; + this.datasources = []; + + this.loadDatasourceTypes().then(() => { + if (this.$routeParams.id) { + this.getDatasourceById(this.$routeParams.id); + } else { + this.current = angular.copy(defaults); + this.typeChanged(); + } + }); + } + + loadDatasourceTypes() { + if (datasourceTypes.length > 0) { + this.types = datasourceTypes; + return this.$q.when(null); + } + + return this.backendSrv.get('/api/org/plugins', {enabled: 1, type: 'datasource'}).then(plugins => { + datasourceTypes = plugins; + this.types = plugins; + }); + } + + getDatasourceById(id) { + this.backendSrv.get('/api/datasources/' + id).then(ds => { + this.isNew = false; + this.current = ds; + return this.typeChanged(); + }); + } + + typeChanged() { + return this.backendSrv.get('/api/org/plugins/' + this.current.type + '/settings').then(pluginInfo => { + this.datasourceMeta = pluginInfo; + }); + } + + updateFrontendSettings() { + return this.backendSrv.get('/api/frontend/settings').then(settings => { + config.datasources = settings.datasources; + config.defaultDatasource = settings.defaultDatasource; + this.datasourceSrv.init(); + }); + } + + testDatasource() { + this.testing = { done: false }; + + this.datasourceSrv.get(this.current.name).then(datasource => { + if (!datasource.testDatasource) { + this.testing.message = 'Data source does not support test connection feature.'; + this.testing.status = 'warning'; + this.testing.title = 'Unknown'; + return; + } + + return datasource.testDatasource().then(result => { + this.testing.message = result.message; + this.testing.status = result.status; + this.testing.title = result.title; + }).catch(err => { + if (err.statusText) { + this.testing.message = err.statusText; + this.testing.title = "HTTP Error"; + } else { + this.testing.message = err.message; + this.testing.title = "Unknown error"; + } + }); + }).finally(() => { + this.testing.done = true; + }); + } + + saveChanges(test) { + if (!this.$scope.editForm.$valid) { + return; + } + + if (this.current.id) { + return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(() => { + this.updateFrontendSettings().then(() => { + if (test) { + this.testDatasource(); + } + }); + }); + } else { + return this.backendSrv.post('/api/datasources', this.current).then(result => { + this.updateFrontendSettings(); + this.$location.path('datasources/edit/' + result.id); + }); + } + }; +} + +coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl); + +coreModule.directive('datasourceHttpSettings', function() { + return { + scope: {current: "="}, + templateUrl: 'public/app/features/datasources/partials/http_settings.html' + }; +}); diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index 9176ff4f4ac..c8840321f40 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -1,43 +1,58 @@ - +
    - +
    - -
    -
    - Name - - - The name is used when you select the data source in panels. - The Default data source is preselected in new - panels. - + +
    +
    + Name + + + The name is used when you select the data source in panels. + The Default data source is preselected in new + panels. + - -
    + +
    -
    - Type -
    - -
    -
    -
    +
    + Type +
    + +
    +
    +
    - - - + + + + - - +
    +
    Testing....
    +
    Test results
    +
    +
    {{testing.title}}
    +
    +
    +
    -
    -
    Testing....
    -
    Test results
    -
    -
    {{testing.title}}
    -
    -
    -
    +
    + + + + Cancel +
    -
    - - - - Cancel -
    + +
    + +
    + +
    -
    diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index b079ec320f2..c923cb79c4e 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -4,7 +4,8 @@ "id": "graphite", "includes": [ - {"type": "dashboard", "name": "Carbon Stats", "path": "dashboards/carbon_stats.json"} + {"type": "dashboard", "name": "Carbon Overview", "path": "dashboards/carbon_stats.json"}, + {"type": "dashboard", "name": "Carbon Agent Details", "path": "dashboards/carbon_stats.json"} ], "metrics": true, From 0b6fee138a292ee25db6dea7371529ce2019e2db Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 10 Mar 2016 14:48:28 +0100 Subject: [PATCH 066/113] docs(configuration): add redis as session storage --- docs/sources/installation/configuration.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index b652abcabb6..cdc24dc7c59 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -159,19 +159,19 @@ The database user's password (not applicable for `sqlite3`). For Postgres, use either `disable`, `require` or `verify-full`. For MySQL, use either `true`, `false`, or `skip-verify`. -### ca_cert_path +### ca_cert_path (MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. -### client_key_path +### client_key_path (MySQL only) The path to the client key. Only if server requires client authentication. -### client_cert_path +### client_cert_path (MySQL only) The path to the client cert. Only if server requires client authentication. -### server_cert_name +### server_cert_name (MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. @@ -373,7 +373,7 @@ Set to `true` to enable auto sign up of users who do not exist in Grafana DB. De ### provider -Valid values are `memory`, `file`, `mysql`, `postgres`, `memcache`. Default is `file`. +Valid values are `memory`, `file`, `mysql`, `postgres`, `memcache` or `redis`. Default is `file`. ### provider_config @@ -384,6 +384,7 @@ session provider you have configured. - **mysql:** go-sql-driver/mysql dsn config string, e.g. `user:password@tcp(127.0.0.1:3306)/database_name` - **postgres:** ex: user=a password=b host=localhost port=5432 dbname=c sslmode=disable - **memcache:** ex: 127.0.0.1:11211 +- **redis:** ex: `addr=127.0.0.1:6379,pool_size=100,db=grafana` If you use MySQL or Postgres as the session store you need to create the session table manually. @@ -415,10 +416,10 @@ How long sessions lasts in seconds. Defaults to `86400` (24 hours). ### reporting_enabled -When enabled Grafana will send anonymous usage statistics to +When enabled Grafana will send anonymous usage statistics to `stats.grafana.org`. No IP addresses are being tracked, only simple counters to track running instances, versions, dashboard & error counts. It is very helpful -to us, so please leave this enabled. Counters are sent every 24 hours. Default +to us, so please leave this enabled. Counters are sent every 24 hours. Default value is `true`. ### google_analytics_ua_id From 43073da7eb11444ae751ce34528117db4ccbc916 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 10 Mar 2016 16:02:10 +0100 Subject: [PATCH 067/113] feat(renderer): add timeout for the renderer api closes #4325 --- pkg/api/render.go | 1 + pkg/components/renderer/renderer.go | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/pkg/api/render.go b/pkg/api/render.go index 728128acaab..9ed0c5ee6d7 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -31,6 +31,7 @@ func RenderToPng(c *middleware.Context) { Width: queryReader.Get("width", "800"), Height: queryReader.Get("height", "400"), SessionId: c.Session.ID(), + Timeout: queryReader.Get("timeout", "15"), } renderOpts.Url = setting.ToAbsUrl(renderOpts.Url) diff --git a/pkg/components/renderer/renderer.go b/pkg/components/renderer/renderer.go index d72ceca9c3d..f81da43c295 100644 --- a/pkg/components/renderer/renderer.go +++ b/pkg/components/renderer/renderer.go @@ -11,6 +11,7 @@ import ( "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" + "strconv" ) type RenderOpts struct { @@ -18,6 +19,7 @@ type RenderOpts struct { Width string Height string SessionId string + Timeout string } func RenderToPng(params *RenderOpts) (string, error) { @@ -60,8 +62,13 @@ func RenderToPng(params *RenderOpts) (string, error) { close(done) }() + timeout, err := strconv.Atoi(params.Timeout) + if err != nil { + timeout = 15 + } + select { - case <-time.After(15 * time.Second): + case <-time.After(time.Duration(timeout) * time.Second): if err := cmd.Process.Kill(); err != nil { log.Error(4, "failed to kill: %v", err) } From fee0745e983a25140b127b6ca32c45fdf88bdda2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 10 Mar 2016 14:43:21 +0100 Subject: [PATCH 068/113] feat(cli): support for asking grafana net about plugins --- .../grafana-cli/commands/install_command.go | 18 ++++++++++-------- pkg/cmd/grafana-cli/commands/ls_command.go | 4 ++-- .../grafana-cli/commands/ls_command_test.go | 6 +++--- pkg/cmd/grafana-cli/main.go | 2 +- pkg/cmd/grafana-cli/services/services.go | 3 ++- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index c58ed12668f..49e7d781389 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -4,6 +4,7 @@ import ( "archive/zip" "bytes" "errors" + "fmt" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" s "github.com/grafana/grafana/pkg/cmd/grafana-cli/services" @@ -64,30 +65,31 @@ func InstallPlugin(pluginName, version string, c CommandLine) error { return err } - url := v.Url - commit := v.Commit - if version == "" { version = v.Version } - downloadURL := url + "/archive/" + commit + ".zip" + downloadURL := fmt.Sprintf("%s/%s/versions/%s/download", + c.GlobalString("repo"), + pluginName, + version) log.Infof("installing %v @ %v\n", plugin.Id, version) log.Infof("from url: %v\n", downloadURL) - log.Infof("on commit: %v\n", commit) log.Infof("into: %v\n", pluginFolder) err = downloadFile(plugin.Id, pluginFolder, downloadURL) - if err == nil { - log.Infof("Installed %v successfully ✔\n", plugin.Id) + if err != nil { + return err } + log.Infof("Installed %v successfully ✔\n", plugin.Id) + res, _ := s.ReadPlugin(pluginFolder, pluginName) for _, v := range res.Dependency.Plugins { InstallPlugin(v.Id, version, c) - log.Infof("Installed Dependency: %v ✔\n", v.Id) + log.Infof("Installed dependency: %v ✔\n", v.Id) } return err diff --git a/pkg/cmd/grafana-cli/commands/ls_command.go b/pkg/cmd/grafana-cli/commands/ls_command.go index 5a90df89d8a..05dd57bfd2a 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command.go +++ b/pkg/cmd/grafana-cli/commands/ls_command.go @@ -10,7 +10,7 @@ import ( var ls_getPlugins func(path string) []m.InstalledPlugin = s.GetLocalPlugins -var validateLsCommmand = func(pluginDir string) error { +var validateLsCommand = func(pluginDir string) error { if pluginDir == "" { return errors.New("missing path flag") } @@ -31,7 +31,7 @@ var validateLsCommmand = func(pluginDir string) error { func lsCommand(c CommandLine) error { pluginDir := c.GlobalString("path") - if err := validateLsCommmand(pluginDir); err != nil { + if err := validateLsCommand(pluginDir); err != nil { return err } diff --git a/pkg/cmd/grafana-cli/commands/ls_command_test.go b/pkg/cmd/grafana-cli/commands/ls_command_test.go index fa49375234d..650c6270bf0 100644 --- a/pkg/cmd/grafana-cli/commands/ls_command_test.go +++ b/pkg/cmd/grafana-cli/commands/ls_command_test.go @@ -9,10 +9,10 @@ import ( ) func TestMissingPath(t *testing.T) { - var org = validateLsCommmand + var org = validateLsCommand Convey("ls command", t, func() { - validateLsCommmand = org + validateLsCommand = org Convey("Missing path", func() { commandLine := &commandstest.FakeCommandLine{ @@ -61,7 +61,7 @@ func TestMissingPath(t *testing.T) { }, } - validateLsCommmand = func(pluginDir string) error { + validateLsCommand = func(pluginDir string) error { return errors.New("dummie error") } diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 12bebd46898..4e74578c604 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -41,7 +41,7 @@ func main() { cli.StringFlag{ Name: "repo", Usage: "url to the plugin repository", - Value: "https://raw.githubusercontent.com/grafana/grafana-plugin-repository/master/repo.json", + Value: "", }, cli.BoolFlag{ Name: "debug, d", diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index 1ffc60abf4c..fc77570b77c 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -12,7 +12,8 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { - res, _ := goreq.Request{Uri: repoUrl, MaxRedirects: 3}.Do() + + res, _ := goreq.Request{Uri: repoUrl + "/repo", MaxRedirects: 3}.Do() var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) From c09d506245f0d2a6572289b61280ea0c4d959075 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 10 Mar 2016 16:38:16 +0100 Subject: [PATCH 069/113] feat(navigation): hide new and import for viewers closes #4326 --- pkg/api/index.go | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index 7cd2842b050..691c50f04f4 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -48,18 +48,23 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { data.User.LightTheme = true } + dashboardChildNavs := []*dtos.NavLink{ + {Text: "Home", Url: setting.AppSubUrl + "/"}, + {Text: "Playlists", Url: setting.AppSubUrl + "/playlists"}, + {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"}, + } + + if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}) + } + data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ - Text: "Dashboards", - Icon: "icon-gf icon-gf-dashboard", - Url: setting.AppSubUrl + "/", - Children: []*dtos.NavLink{ - {Text: "Home", Url: setting.AppSubUrl + "/"}, - {Text: "Playlists", Url: setting.AppSubUrl + "/playlists"}, - {Text: "Snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots"}, - {Divider: true}, - {Text: "New", Url: setting.AppSubUrl + "/dashboard/new"}, - {Text: "Import", Url: setting.AppSubUrl + "/import/dashboard"}, - }, + Text: "Dashboards", + Icon: "icon-gf icon-gf-dashboard", + Url: setting.AppSubUrl + "/", + Children: dashboardChildNavs, }) if c.OrgRole == m.ROLE_ADMIN { From 788aafff3cbbf635227cb1326c7c034c48ac5842 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 10 Mar 2016 17:16:01 +0100 Subject: [PATCH 070/113] feat(cli): disable dependecy downloads until needed --- pkg/cmd/grafana-cli/commands/install_command.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 49e7d781389..4dfbfd80e31 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -85,13 +85,13 @@ func InstallPlugin(pluginName, version string, c CommandLine) error { log.Infof("Installed %v successfully ✔\n", plugin.Id) + /* Enable once we need support for downloading depedencies res, _ := s.ReadPlugin(pluginFolder, pluginName) - for _, v := range res.Dependency.Plugins { InstallPlugin(v.Id, version, c) log.Infof("Installed dependency: %v ✔\n", v.Id) } - + */ return err } From 60adcedebe6c9a48535bac83f767179f420dc1ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 10 Mar 2016 19:57:48 +0100 Subject: [PATCH 071/113] feat(plugins): worked on plugin dashboard import handling --- examples/nginx-app/package.json | 2 +- examples/nginx-app/plugin.json | 7 +- .../nginx-app/src/dashboards/connections.json | 5 ++ .../nginx-app/src/dashboards/dashboard.js | 17 ---- examples/nginx-app/src/dashboards/memory.json | 5 ++ .../dashboards/nginx_connection_stats.json | 0 pkg/models/dashboards.go | 7 +- pkg/plugins/dashboards.go | 77 +++++++++++++++++++ pkg/plugins/dashboards_test.go | 53 +++++++++++++ pkg/plugins/models.go | 16 ++++ pkg/plugins/plugins_test.go | 9 ++- .../dashboard/import_list/import_list.ts | 21 +++-- .../features/datasources/partials/edit.html | 6 +- tests/app-plugin-json/plugin.json | 48 ------------ 14 files changed, 189 insertions(+), 84 deletions(-) create mode 100644 examples/nginx-app/src/dashboards/connections.json delete mode 100644 examples/nginx-app/src/dashboards/dashboard.js create mode 100644 examples/nginx-app/src/dashboards/memory.json delete mode 100644 examples/nginx-app/src/dashboards/nginx_connection_stats.json create mode 100644 pkg/plugins/dashboards.go create mode 100644 pkg/plugins/dashboards_test.go delete mode 100644 tests/app-plugin-json/plugin.json diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json index 32c070f96b0..91c53734ec8 100644 --- a/examples/nginx-app/package.json +++ b/examples/nginx-app/package.json @@ -31,7 +31,7 @@ "dependencies": { "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0", + "lodash": "~4.0.0" }, "homepage": "https://github.com/raintank/kentik-app-poc#readme" } diff --git a/examples/nginx-app/plugin.json b/examples/nginx-app/plugin.json index 65f01ad62fc..6de4fbd4daa 100644 --- a/examples/nginx-app/plugin.json +++ b/examples/nginx-app/plugin.json @@ -26,6 +26,10 @@ "small": "img/logo_small.png", "large": "img/logo_large.png" }, + "screenshots": [ + {"name": "img1", "path": "img/screenshot1.png"}, + {"name": "img2", "path": "img/screenshot2.png"} + ], "links": [ {"name": "Project site", "url": "http://project.com"}, {"name": "License & Terms", "url": "http://license.com"} @@ -35,7 +39,8 @@ }, "includes": [ - {"type": "dashboard", "name": "Nginx Connection stats", "path": "dashboards/nginx_connection_stats.json"}, + {"type": "dashboard", "name": "Nginx Connections", "path": "dashboards/connections.json"}, + {"type": "dashboard", "name": "Nginx Memory", "path": "dashboards/memory.json"}, {"type": "panel", "name": "Nginx Panel"}, {"type": "datasource", "name": "Nginx Datasource"} ], diff --git a/examples/nginx-app/src/dashboards/connections.json b/examples/nginx-app/src/dashboards/connections.json new file mode 100644 index 00000000000..f1f62826f73 --- /dev/null +++ b/examples/nginx-app/src/dashboards/connections.json @@ -0,0 +1,5 @@ +{ + "title": "Nginx Connections", + "revision": "1.5", + "schemaVersion": 11 +} diff --git a/examples/nginx-app/src/dashboards/dashboard.js b/examples/nginx-app/src/dashboards/dashboard.js deleted file mode 100644 index 794e2c5217b..00000000000 --- a/examples/nginx-app/src/dashboards/dashboard.js +++ /dev/null @@ -1,17 +0,0 @@ -require([ -], function () { - - function Dashboard() { - - this.getInputs = function() { - - }; - - this.buildDashboard = function() { - - }; - } - - return Dashboard; -}); - diff --git a/examples/nginx-app/src/dashboards/memory.json b/examples/nginx-app/src/dashboards/memory.json new file mode 100644 index 00000000000..b79cb4d7dba --- /dev/null +++ b/examples/nginx-app/src/dashboards/memory.json @@ -0,0 +1,5 @@ +{ + "title": "Nginx Memory", + "revision": "2.0", + "schemaVersion": 11 +} diff --git a/examples/nginx-app/src/dashboards/nginx_connection_stats.json b/examples/nginx-app/src/dashboards/nginx_connection_stats.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 1015dfbe28c..3e87f504a77 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -102,8 +102,11 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { } // GetString a -func (dash *Dashboard) GetString(prop string) string { - return dash.Data[prop].(string) +func (dash *Dashboard) GetString(prop string, defaultValue string) string { + if val, exists := dash.Data[prop]; exists { + return val.(string) + } + return defaultValue } // UpdateSlug updates the slug diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go new file mode 100644 index 00000000000..f253db480a0 --- /dev/null +++ b/pkg/plugins/dashboards.go @@ -0,0 +1,77 @@ +package plugins + +import ( + "encoding/json" + "os" + "path/filepath" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +type PluginDashboardInfoDTO struct { + Title string + InstalledURI string + InstalledRevision string + Revision string + Description string +} + +func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { + plugin, exists := Plugins[pluginId] + + if !exists { + return nil, &PluginNotFoundError{pluginId} + } + + result := make([]*PluginDashboardInfoDTO, 0) + + for _, include := range plugin.Includes { + if include.Type == PluginTypeDashboard { + if dashInfo, err := getDashboardImportStatus(orgId, plugin, include); err != nil { + return nil, err + } else { + result = append(result, dashInfo) + } + } + } + + return result, nil +} + +func getDashboardImportStatus(orgId int64, plugin *PluginBase, dashInclude *PluginInclude) (*PluginDashboardInfoDTO, error) { + res := &PluginDashboardInfoDTO{} + + dashboardFilePath := filepath.Join(plugin.PluginDir, dashInclude.Path) + reader, err := os.Open(dashboardFilePath) + if err != nil { + return nil, err + } + + defer reader.Close() + + jsonParser := json.NewDecoder(reader) + var data map[string]interface{} + + if err := jsonParser.Decode(&data); err != nil { + return nil, err + } + + dashboard := m.NewDashboardFromJson(data) + + res.Title = dashboard.Title + res.Revision = dashboard.GetString("revision", "1.0") + + query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug} + + if err := bus.Dispatch(&query); err != nil { + if err != m.ErrDashboardNotFound { + return nil, err + } + } else { + res.InstalledURI = "db/" + query.Result.Slug + res.InstalledRevision = query.Result.GetString("revision", "1.0") + } + + return res, nil +} diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go new file mode 100644 index 00000000000..b8ab51940a1 --- /dev/null +++ b/pkg/plugins/dashboards_test.go @@ -0,0 +1,53 @@ +package plugins + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ini.v1" +) + +func TestPluginDashboards(t *testing.T) { + + Convey("When asking plugin dashboard info", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.nginx-app") + sec.NewKey("path", "../../examples/nginx-app") + err := Init() + + So(err, ShouldBeNil) + + bus.AddHandler("test", func(query *m.GetDashboardQuery) error { + if query.Slug == "nginx-connections" { + dash := m.NewDashboard("Nginx Connections") + dash.Data["revision"] = "1.1" + query.Result = dash + return nil + } + + return m.ErrDashboardNotFound + }) + + dashboards, err := GetPluginDashboards(1, "nginx-app") + + So(err, ShouldBeNil) + + Convey("should return 2 dashboarrd", func() { + So(len(dashboards), ShouldEqual, 2) + }) + + Convey("should include installed version info", func() { + So(dashboards[0].Title, ShouldEqual, "Nginx Connections") + So(dashboards[0].Revision, ShouldEqual, "1.5") + So(dashboards[0].InstalledRevision, ShouldEqual, "1.1") + So(dashboards[0].InstalledURI, ShouldEqual, "db/nginx-connections") + + So(dashboards[1].Revision, ShouldEqual, "2.0") + So(dashboards[1].InstalledRevision, ShouldEqual, "") + }) + }) + +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 0addaac9bb7..ac9fb5e04dc 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -3,12 +3,28 @@ package plugins import ( "encoding/json" "errors" + "fmt" "strings" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) +var ( + PluginTypeApp = "app" + PluginTypeDatasource = "datasource" + PluginTypePanel = "panel" + PluginTypeDashboard = "dashboard" +) + +type PluginNotFoundError struct { + PluginId string +} + +func (e *PluginNotFoundError) Error() string { + return fmt.Sprintf("Plugin with id %s not found", e.PluginId) +} + type PluginLoader interface { Load(decoder *json.Decoder, pluginDir string) error } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 08ff3cbdfdd..b7f4b845fb5 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -27,14 +27,15 @@ func TestPluginScans(t *testing.T) { Convey("When reading app plugin definition", t, func() { setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.app-test") - sec.NewKey("path", "../../tests/app-plugin-json") + sec, _ := setting.Cfg.NewSection("plugin.nginx-app") + sec.NewKey("path", "../../examples/nginx-app") err := Init() So(err, ShouldBeNil) So(len(Apps), ShouldBeGreaterThan, 0) - So(Apps["app-example"].Info.Logos.Large, ShouldEqual, "public/plugins/app-example/img/logo_large.png") - So(Apps["app-example"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/app-example/img/screenshot2.png") + + So(Apps["nginx-app"].Info.Logos.Large, ShouldEqual, "public/plugins/nginx-app/img/logo_large.png") + So(Apps["nginx-app"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/nginx-app/img/screenshot2.png") }) } diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/dashboard/import_list/import_list.ts index b4fe3c36352..09a5b65de76 100644 --- a/public/app/features/dashboard/import_list/import_list.ts +++ b/public/app/features/dashboard/import_list/import_list.ts @@ -1,10 +1,10 @@ /// import angular from 'angular'; +import _ from 'lodash'; import coreModule from 'app/core/core_module'; class DashboardScriptLoader { - } export class DashImportListCtrl { @@ -12,9 +12,19 @@ export class DashImportListCtrl { plugin: any; constructor(private $http) { + this.dashboards = []; - this.dashboards = this.plugin.includes.filter(val => val.type === 'dashboard'); + this.plugin.includes + .filter(val => val.type === 'dashboard') + .forEach(this.getDashbordImportStatus.bind(this)); + } + getDashbordImportStatus(dash) { + var dashUrl = this.plugin.baseUrl + '/' + dash.path; + this.$http.get(dashUrl).then(res => { + this.load(res.data); + + }); } load(json) { @@ -22,10 +32,7 @@ export class DashImportListCtrl { console.log(model); } - import() { - // this.$http.get(url).then(res => { - // this.load(res.data); - // }); + import(dash) { } } @@ -41,7 +48,7 @@ var template = ` {{dash.name}} - + diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html index fa1e0cee104..46177d998cc 100644 --- a/public/app/features/datasources/partials/edit.html +++ b/public/app/features/datasources/partials/edit.html @@ -7,15 +7,14 @@

    Add data source

    Edit data source

    -
    - +
    - diff --git a/tests/app-plugin-json/plugin.json b/tests/app-plugin-json/plugin.json deleted file mode 100644 index 59f41766eda..00000000000 --- a/tests/app-plugin-json/plugin.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "type": "app", - "name": "App Example", - "id": "app-example", - - "staticRoot": ".", - "module": "app", - - "pages": [ - {"name": "Example1", "url": "/app-example", "reqRole": "Editor"} - ], - - "css": { - "light": "css/plugin.dark.css", - "dark": "css/plugin.light.css" - }, - - "info": { - "description": "Example Grafana App", - "author": { - "name": "Raintank Inc.", - "url": "http://raintank.io" - }, - "keywords": ["example"], - "logos": { - "small": "img/logo_small.png", - "large": "img/logo_large.png" - }, - "screenshots": [ - {"name": "img1", "path": "img/screenshot1.png"}, - {"name": "img2", "path": "img/screenshot2.png"} - ], - "links": [ - {"name": "Project site", "url": "http://project.com"}, - {"name": "License & Terms", "url": "http://license.com"} - ], - "version": "1.0.0", - "updated": "2015-02-10" - }, - - "dependencies": { - "grafanaVersion": "2.6.x", - "plugins": [ - {"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"}, - {"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"} - ] - } -} From 8c6890c95fa4a824d5ace0cddc7783b8b2a4f688 Mon Sep 17 00:00:00 2001 From: Tom Dyas Date: Thu, 10 Mar 2016 17:46:59 -0500 Subject: [PATCH 072/113] remove extra comma in nginx-app's package.json otherwise grunt fails due to a syntax error in package.json --- examples/nginx-app/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json index 32c070f96b0..91c53734ec8 100644 --- a/examples/nginx-app/package.json +++ b/examples/nginx-app/package.json @@ -31,7 +31,7 @@ "dependencies": { "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0", + "lodash": "~4.0.0" }, "homepage": "https://github.com/raintank/kentik-app-poc#readme" } From 2c5abed2f1bc3a9a2c9c81c58fe9c4ed3a810eff Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 11 Mar 2016 12:11:01 +0900 Subject: [PATCH 073/113] (cloudwatch) add metrics/dimensions of AWS/Events and AWS/Logs --- pkg/api/cloudwatch/metrics.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index a4e97cdb347..54ab565bede 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -55,8 +55,10 @@ func init() { "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB", "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"}, "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"}, + "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"}, "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"}, + "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -85,8 +87,10 @@ func init() { "AWS/ELB": {"LoadBalancerName", "AvailabilityZone"}, "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, "AWS/ES": {}, + "AWS/Events": {"RuleName"}, "AWS/Kinesis": {"StreamName"}, "AWS/Lambda": {"FunctionName"}, + "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, From 336c1b7b6e17292d033181cfa0d51e633ed705b1 Mon Sep 17 00:00:00 2001 From: simnv Date: Fri, 11 Mar 2016 08:30:56 +0500 Subject: [PATCH 074/113] Fix groupby selection Default groupby selection for influxdb is escaped not like a regex, but as a glob. It works fine with one value in variable, but when you select multiple values, query is incorrect: > SHOW TAG KEYS FROM "os.disk.fs.space_total" WHERE "host" =~ /{host-1,host-2}$/ --- public/app/plugins/datasource/influxdb/datasource.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 84c7a83394c..1136c9709bd 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -104,7 +104,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) this.metricFindQuery = function (query) { var interpolated; try { - interpolated = templateSrv.replace(query); + interpolated = templateSrv.replace(query, null, 'regex'); } catch (err) { return $q.reject(err); } From b72cf90ed43b29f36f79a38fd5be5bc7da6fc32f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 08:39:05 +0100 Subject: [PATCH 075/113] example(panel): rename to match new convetion --- .../css/styles.css | 0 .../{panel-boilerplate-es5 => boilerplate-es5-panel}/module.js | 0 .../{panel-boilerplate-es5 => boilerplate-es5-panel}/panel.html | 0 .../{panel-boilerplate-es5 => boilerplate-es5-panel}/plugin.json | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename examples/{panel-boilerplate-es5 => boilerplate-es5-panel}/css/styles.css (100%) rename examples/{panel-boilerplate-es5 => boilerplate-es5-panel}/module.js (100%) rename examples/{panel-boilerplate-es5 => boilerplate-es5-panel}/panel.html (100%) rename examples/{panel-boilerplate-es5 => boilerplate-es5-panel}/plugin.json (100%) diff --git a/examples/panel-boilerplate-es5/css/styles.css b/examples/boilerplate-es5-panel/css/styles.css similarity index 100% rename from examples/panel-boilerplate-es5/css/styles.css rename to examples/boilerplate-es5-panel/css/styles.css diff --git a/examples/panel-boilerplate-es5/module.js b/examples/boilerplate-es5-panel/module.js similarity index 100% rename from examples/panel-boilerplate-es5/module.js rename to examples/boilerplate-es5-panel/module.js diff --git a/examples/panel-boilerplate-es5/panel.html b/examples/boilerplate-es5-panel/panel.html similarity index 100% rename from examples/panel-boilerplate-es5/panel.html rename to examples/boilerplate-es5-panel/panel.html diff --git a/examples/panel-boilerplate-es5/plugin.json b/examples/boilerplate-es5-panel/plugin.json similarity index 100% rename from examples/panel-boilerplate-es5/plugin.json rename to examples/boilerplate-es5-panel/plugin.json From 0052e9d13623961b4abe3a7c18fa88030b9427d8 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 11 Mar 2016 14:58:29 +0900 Subject: [PATCH 076/113] (cloudwatch) add ec2_instance_attribute() query --- .../datasource/cloudwatch/datasource.js | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 06c723e416a..5d093640b1e 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -143,7 +143,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { return this.awsRequest({ region: region, action: 'DescribeInstances', - parameters: { filter: filters, instanceIds: instanceIds } + parameters: { filters: filters, instanceIds: instanceIds } }); }; @@ -205,6 +205,28 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } + var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?([^)]+)\)/); + if (ec2InstanceAttributeQuery) { + region = templateSrv.replace(ec2InstanceAttributeQuery[1]); + var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3])); + var filter = _.map(filterJson, function(f) { + return { + Name: f.slice(0, f.indexOf('=')), + Values: f.slice(f.indexOf('=') + 1).split(',') + }; + }); + var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]); + + return this.performEC2DescribeInstances(region, filter, null).then(function(result) { + var attributes = _.chain(result.Reservations) + .map(function(reservations) { + return _.pluck(reservations.Instances, targetAttributeName); + }) + .flatten().value(); + return transformSuggestData(attributes); + }); + } + return $q.when([]); }; From a16c799da931319f24178ed06f9932b999575544 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 08:52:14 +0100 Subject: [PATCH 077/113] fix(examples): update link in readme --- examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/README.md b/examples/README.md index 2e8a46f2aa3..04a544be399 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,3 @@ ## Example plugin implementations -[datasource-plugin-genericdatsource](https://github.com/grafana/datasource-plugin-genericdatasource/tree/3.0) \ No newline at end of file +[simple-json-datasource](https://github.com/grafana/simple-json-datasource) \ No newline at end of file From 25f08ddd39661b571d767a8ecae07f13cd6eca1d Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 11 Mar 2016 16:59:06 +0900 Subject: [PATCH 078/113] (cloudwatch) change parameter form of ec2_instance_attribute() --- public/app/plugins/datasource/cloudwatch/datasource.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 5d093640b1e..cdaeebb83b8 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -205,14 +205,14 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } - var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?([^)]+)\)/); + var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/); if (ec2InstanceAttributeQuery) { region = templateSrv.replace(ec2InstanceAttributeQuery[1]); var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3])); - var filter = _.map(filterJson, function(f) { + var filter = _.map(filterJson, function(values, name) { return { - Name: f.slice(0, f.indexOf('=')), - Values: f.slice(f.indexOf('=') + 1).split(',') + Name: name, + Values: values }; }); var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]); From b989e2ce37fdaa9b14f142e6c61b309123f6aa0a Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 11 Mar 2016 17:04:42 +0900 Subject: [PATCH 079/113] minor fix --- public/app/plugins/datasource/cloudwatch/datasource.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index cdaeebb83b8..704cbd41e72 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -209,7 +209,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { if (ec2InstanceAttributeQuery) { region = templateSrv.replace(ec2InstanceAttributeQuery[1]); var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3])); - var filter = _.map(filterJson, function(values, name) { + var filters = _.map(filterJson, function(values, name) { return { Name: name, Values: values @@ -217,7 +217,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]); - return this.performEC2DescribeInstances(region, filter, null).then(function(result) { + return this.performEC2DescribeInstances(region, filters, null).then(function(result) { var attributes = _.chain(result.Reservations) .map(function(reservations) { return _.pluck(reservations.Instances, targetAttributeName); From 2de439bd1ebf6adbfcd4c962fe57ee6c16bf8c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 09:57:20 +0100 Subject: [PATCH 080/113] feat(plugins): progress on dashboard installs , #4298 --- pkg/api/api.go | 14 +++-- pkg/api/dtos/plugins.go | 6 ++ pkg/api/{plugin_setting.go => plugins.go} | 31 ++++++++++ .../{plugin_setting.go => plugin_settings.go} | 0 pkg/plugins/dashboard_installer.go | 56 +++++++++++++++++++ pkg/plugins/dashboards.go | 36 ++++++++---- pkg/plugins/models.go | 2 +- .../dashboard/import_list/import_list.ts | 39 +++++++------ public/app/features/datasources/edit_ctrl.ts | 4 +- public/app/features/plugins/edit_ctrl.ts | 2 +- public/app/features/plugins/list_ctrl.ts | 2 +- 11 files changed, 154 insertions(+), 38 deletions(-) rename pkg/api/{plugin_setting.go => plugins.go} (77%) rename pkg/models/{plugin_setting.go => plugin_settings.go} (100%) create mode 100644 pkg/plugins/dashboard_installer.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 0915bbd678d..44b56ea62f6 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -126,10 +126,6 @@ func Register(r *macaron.Macaron) { r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) - // apps - r.Get("/plugins", wrap(GetPluginList)) - r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) - r.Post("/plugins/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) // create new org @@ -177,6 +173,16 @@ func Register(r *macaron.Macaron) { r.Get("/", wrap(GetDataSourceByName)) }, reqOrgAdmin) + r.Group("/plugins", func() { + r.Get("/", wrap(GetPluginList)) + + r.Get("/dashboards/:pluginId", wrap(GetPluginDashboards)) + r.Post("/dashboards/install", bind(dtos.InstallPluginDashboardCmd{}), wrap(InstallPluginDashboard)) + + r.Get("/:pluginId/settings", wrap(GetPluginSettingById)) + r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) + }, reqOrgAdmin) + r.Get("/frontend/settings/", GetFrontendSettings) r.Any("/datasources/proxy/:id/*", reqSignedIn, ProxyDataSourceRequest) r.Any("/datasources/proxy/:id", reqSignedIn, ProxyDataSourceRequest) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 61b149d73e4..144b9b09755 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -25,3 +25,9 @@ type PluginListItem struct { Pinned bool `json:"pinned"` Info *plugins.PluginInfo `json:"info"` } + +type InstallPluginDashboardCmd struct { + PluginId string `json:"pluginId"` + Path string `json:"path"` + Inputs map[string]interface{} `json:"inputs"` +} diff --git a/pkg/api/plugin_setting.go b/pkg/api/plugins.go similarity index 77% rename from pkg/api/plugin_setting.go rename to pkg/api/plugins.go index 1ceecdfac18..1ba0dde6c9f 100644 --- a/pkg/api/plugin_setting.go +++ b/pkg/api/plugins.go @@ -107,3 +107,34 @@ func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Re return ApiSuccess("Plugin settings updated") } + +func GetPluginDashboards(c *middleware.Context) Response { + pluginId := c.Params(":pluginId") + + if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + if notfound, ok := err.(plugins.PluginNotFoundError); ok { + return ApiError(404, notfound.Error(), nil) + } + + return ApiError(500, "Failed to get plugin dashboards", err) + } else { + return Json(200, list) + } +} + +func InstallPluginDashboard(c *middleware.Context, apiCmd dtos.InstallPluginDashboardCmd) Response { + + cmd := plugins.InstallPluginDashboardCommand{ + OrgId: c.OrgId, + UserId: c.UserId, + PluginId: apiCmd.PluginId, + Path: apiCmd.Path, + Inputs: apiCmd.Inputs, + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to install dashboard", err) + } + + return Json(200, cmd.Result) +} diff --git a/pkg/models/plugin_setting.go b/pkg/models/plugin_settings.go similarity index 100% rename from pkg/models/plugin_setting.go rename to pkg/models/plugin_settings.go diff --git a/pkg/plugins/dashboard_installer.go b/pkg/plugins/dashboard_installer.go new file mode 100644 index 00000000000..9f52b518d70 --- /dev/null +++ b/pkg/plugins/dashboard_installer.go @@ -0,0 +1,56 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +type InstallPluginDashboardCommand struct { + Path string `json:"string"` + Inputs map[string]interface{} `json:"inputs"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` + PluginId string `json:"-"` + Result *PluginDashboardInfoDTO +} + +func init() { + bus.AddHandler("plugins", InstallPluginDashboard) +} + +func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error { + plugin, exists := Plugins[cmd.PluginId] + + if !exists { + return PluginNotFoundError{cmd.PluginId} + } + + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { + return err + } + + saveCmd := m.SaveDashboardCommand{ + Dashboard: dashboard.Data, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + } + + if err := bus.Dispatch(&saveCmd); err != nil { + return err + } + + cmd.Result = &PluginDashboardInfoDTO{ + PluginId: cmd.PluginId, + Title: dashboard.Title, + Path: cmd.Path, + Revision: dashboard.GetString("revision", "1.0"), + InstalledURI: "db/" + saveCmd.Result.Slug, + InstalledRevision: dashboard.GetString("revision", "1.0"), + } + + return nil +} diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index f253db480a0..9acb64cec1b 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -10,25 +10,27 @@ import ( ) type PluginDashboardInfoDTO struct { - Title string - InstalledURI string - InstalledRevision string - Revision string - Description string + PluginId string `json:"pluginId"` + Title string `json:"title"` + InstalledURI string `json:"installedURI"` + InstalledRevision string `json:"installedRevision"` + Revision string `json:"revision"` + Description string `json:"description"` + Path string `json:"path"` } func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { plugin, exists := Plugins[pluginId] if !exists { - return nil, &PluginNotFoundError{pluginId} + return nil, PluginNotFoundError{pluginId} } result := make([]*PluginDashboardInfoDTO, 0) for _, include := range plugin.Includes { if include.Type == PluginTypeDashboard { - if dashInfo, err := getDashboardImportStatus(orgId, plugin, include); err != nil { + if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil { return nil, err } else { result = append(result, dashInfo) @@ -39,10 +41,9 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT return result, nil } -func getDashboardImportStatus(orgId int64, plugin *PluginBase, dashInclude *PluginInclude) (*PluginDashboardInfoDTO, error) { - res := &PluginDashboardInfoDTO{} +func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) { - dashboardFilePath := filepath.Join(plugin.PluginDir, dashInclude.Path) + dashboardFilePath := filepath.Join(plugin.PluginDir, path) reader, err := os.Open(dashboardFilePath) if err != nil { return nil, err @@ -57,8 +58,21 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, dashInclude *Plug return nil, err } - dashboard := m.NewDashboardFromJson(data) + return m.NewDashboardFromJson(data), nil +} +func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*PluginDashboardInfoDTO, error) { + res := &PluginDashboardInfoDTO{} + + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin, path); err != nil { + return nil, err + } + + res.Path = path + res.PluginId = plugin.Id res.Title = dashboard.Title res.Revision = dashboard.GetString("revision", "1.0") diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index ac9fb5e04dc..502c355eda8 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -21,7 +21,7 @@ type PluginNotFoundError struct { PluginId string } -func (e *PluginNotFoundError) Error() string { +func (e PluginNotFoundError) Error() string { return fmt.Sprintf("Plugin with id %s not found", e.PluginId) } diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/dashboard/import_list/import_list.ts index 09a5b65de76..7d88d7f9f0a 100644 --- a/public/app/features/dashboard/import_list/import_list.ts +++ b/public/app/features/dashboard/import_list/import_list.ts @@ -11,29 +11,26 @@ export class DashImportListCtrl { dashboards: any[]; plugin: any; - constructor(private $http) { + constructor(private $http, private backendSrv, private $rootScope) { this.dashboards = []; - this.plugin.includes - .filter(val => val.type === 'dashboard') - .forEach(this.getDashbordImportStatus.bind(this)); - } - - getDashbordImportStatus(dash) { - var dashUrl = this.plugin.baseUrl + '/' + dash.path; - this.$http.get(dashUrl).then(res => { - this.load(res.data); - + backendSrv.get(`/api/plugins/dashboards/${this.plugin.id}`).then(dashboards => { + this.dashboards = dashboards; }); } - load(json) { - var model = angular.fromJson(json); - console.log(model); + import(dash) { + var installCmd = { + pluginId: this.plugin.id, + path: dash.path, + inputs: {} + }; + + this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => { + console.log(res); + }); } - import(dash) { - } } var template = ` @@ -45,11 +42,17 @@ var template = ` - {{dash.name}} + {{dash.title}} + + + {{dash.revision}} + + + {{dash.installedRevision}} - diff --git a/public/app/features/datasources/edit_ctrl.ts b/public/app/features/datasources/edit_ctrl.ts index b766d6cda84..3ad74c628ab 100644 --- a/public/app/features/datasources/edit_ctrl.ts +++ b/public/app/features/datasources/edit_ctrl.ts @@ -54,7 +54,7 @@ export class DataSourceEditCtrl { return this.$q.when(null); } - return this.backendSrv.get('/api/org/plugins', {enabled: 1, type: 'datasource'}).then(plugins => { + return this.backendSrv.get('/api/plugins', {enabled: 1, type: 'datasource'}).then(plugins => { datasourceTypes = plugins; this.types = plugins; }); @@ -70,7 +70,7 @@ export class DataSourceEditCtrl { typeChanged() { this.hasDashboards = false; - return this.backendSrv.get('/api/org/plugins/' + this.current.type + '/settings').then(pluginInfo => { + return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => { this.datasourceMeta = pluginInfo; this.hasDashboards = _.findWhere(pluginInfo.includes, {type: 'dashboard'}); }); diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/edit_ctrl.ts index f8e7c96792c..2d4362835d4 100644 --- a/public/app/features/plugins/edit_ctrl.ts +++ b/public/app/features/plugins/edit_ctrl.ts @@ -22,7 +22,7 @@ export class PluginEditCtrl { } init() { - return this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(result => { + return this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(result => { this.model = result; this.pluginIcon = this.getPluginIcon(this.model.type); diff --git a/public/app/features/plugins/list_ctrl.ts b/public/app/features/plugins/list_ctrl.ts index a1bacc09c14..2693a4d8755 100644 --- a/public/app/features/plugins/list_ctrl.ts +++ b/public/app/features/plugins/list_ctrl.ts @@ -8,7 +8,7 @@ export class PluginListCtrl { /** @ngInject */ constructor(private backendSrv: any) { - this.backendSrv.get('api/org/plugins').then(plugins => { + this.backendSrv.get('api/plugins', {embedded: 0}).then(plugins => { this.plugins = plugins; }); } From 4f52bc138fed3f2b378cb98452b389040c4a5e2e Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 10:06:44 +0100 Subject: [PATCH 081/113] docs(examples): move app example into new repo --- examples/README.md | 3 +- examples/nginx-app/.gitignore | 7 --- examples/nginx-app/.jscs.json | 13 ----- examples/nginx-app/.jshintrc | 36 ------------ examples/nginx-app/Gruntfile.js | 54 ------------------ examples/nginx-app/package.json | 37 ------------ examples/nginx-app/plugin.json | 50 ---------------- examples/nginx-app/readme.md | 7 --- examples/nginx-app/src/components/config.html | 3 - examples/nginx-app/src/components/config.js | 6 -- examples/nginx-app/src/components/logs.html | 3 - examples/nginx-app/src/components/logs.js | 6 -- examples/nginx-app/src/components/stream.html | 3 - examples/nginx-app/src/components/stream.js | 6 -- examples/nginx-app/src/css/dark.css | 0 examples/nginx-app/src/css/light.css | 0 .../nginx-app/src/dashboards/dashboard.js | 17 ------ .../dashboards/nginx_connection_stats.json | 0 .../nginx-app/src/datasource/datasource.js | 12 ---- examples/nginx-app/src/datasource/module.js | 5 -- examples/nginx-app/src/datasource/plugin.json | 5 -- examples/nginx-app/src/img/logo_large.png | Bin 14460 -> 0 bytes examples/nginx-app/src/img/logo_small.png | Bin 6430 -> 0 bytes examples/nginx-app/src/module.js | 9 --- examples/nginx-app/src/panel/module.js | 15 ----- examples/nginx-app/src/panel/plugin.json | 5 -- 26 files changed, 2 insertions(+), 300 deletions(-) delete mode 100644 examples/nginx-app/.gitignore delete mode 100644 examples/nginx-app/.jscs.json delete mode 100644 examples/nginx-app/.jshintrc delete mode 100644 examples/nginx-app/Gruntfile.js delete mode 100644 examples/nginx-app/package.json delete mode 100644 examples/nginx-app/plugin.json delete mode 100644 examples/nginx-app/readme.md delete mode 100644 examples/nginx-app/src/components/config.html delete mode 100644 examples/nginx-app/src/components/config.js delete mode 100644 examples/nginx-app/src/components/logs.html delete mode 100644 examples/nginx-app/src/components/logs.js delete mode 100644 examples/nginx-app/src/components/stream.html delete mode 100644 examples/nginx-app/src/components/stream.js delete mode 100644 examples/nginx-app/src/css/dark.css delete mode 100644 examples/nginx-app/src/css/light.css delete mode 100644 examples/nginx-app/src/dashboards/dashboard.js delete mode 100644 examples/nginx-app/src/dashboards/nginx_connection_stats.json delete mode 100644 examples/nginx-app/src/datasource/datasource.js delete mode 100644 examples/nginx-app/src/datasource/module.js delete mode 100644 examples/nginx-app/src/datasource/plugin.json delete mode 100644 examples/nginx-app/src/img/logo_large.png delete mode 100644 examples/nginx-app/src/img/logo_small.png delete mode 100644 examples/nginx-app/src/module.js delete mode 100644 examples/nginx-app/src/panel/module.js delete mode 100644 examples/nginx-app/src/panel/plugin.json diff --git a/examples/README.md b/examples/README.md index 04a544be399..64341a7fdbc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,4 @@ ## Example plugin implementations -[simple-json-datasource](https://github.com/grafana/simple-json-datasource) \ No newline at end of file +datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource) +app: [example-app](https://github.com/grafana/example-app) \ No newline at end of file diff --git a/examples/nginx-app/.gitignore b/examples/nginx-app/.gitignore deleted file mode 100644 index 8c2c350441b..00000000000 --- a/examples/nginx-app/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store - -node_modules -tmp/* -npm-debug.log -dist/* - diff --git a/examples/nginx-app/.jscs.json b/examples/nginx-app/.jscs.json deleted file mode 100644 index dcf694dcc63..00000000000 --- a/examples/nginx-app/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} \ No newline at end of file diff --git a/examples/nginx-app/.jshintrc b/examples/nginx-app/.jshintrc deleted file mode 100644 index 3725af83afc..00000000000 --- a/examples/nginx-app/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "browser": true, - "esnext": true, - - "bitwise":false, - "curly": true, - "eqnull": true, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/examples/nginx-app/Gruntfile.js b/examples/nginx-app/Gruntfile.js deleted file mode 100644 index b88d857cde3..00000000000 --- a/examples/nginx-app/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.loadNpmTasks('grunt-execute'); - grunt.loadNpmTasks('grunt-contrib-clean'); - - grunt.initConfig({ - - clean: ["dist"], - - copy: { - src_to_dist: { - cwd: 'src', - expand: true, - src: ['**/*', '!**/*.js', '!**/*.scss'], - dest: 'dist' - }, - pluginDef: { - expand: true, - src: ['plugin.json', 'readme.md'], - dest: 'dist', - } - }, - - watch: { - rebuild_all: { - files: ['src/**/*', 'plugin.json', 'readme.md'], - tasks: ['default'], - options: {spawn: false} - }, - }, - - babel: { - options: { - sourceMap: true, - presets: ["es2015"], - plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], - }, - dist: { - files: [{ - cwd: 'src', - expand: true, - src: ['**/*.js'], - dest: 'dist', - ext:'.js' - }] - }, - }, - - }); - - grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']); -}; diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json deleted file mode 100644 index 91c53734ec8..00000000000 --- a/examples/nginx-app/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "kentik-app", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/raintank/kentik-app-poc.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/raintank/kentik-app-poc/issues" - }, - "devDependencies": { - "grunt": "~0.4.5", - "babel": "~6.5.1", - "grunt-babel": "~6.0.0", - "grunt-contrib-copy": "~0.8.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-uglify": "~0.11.0", - "grunt-systemjs-builder": "^0.2.5", - "load-grunt-tasks": "~3.2.0", - "grunt-execute": "~0.2.2", - "grunt-contrib-clean": "~0.6.0" - }, - "dependencies": { - "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", - "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0" - }, - "homepage": "https://github.com/raintank/kentik-app-poc#readme" -} diff --git a/examples/nginx-app/plugin.json b/examples/nginx-app/plugin.json deleted file mode 100644 index 65f01ad62fc..00000000000 --- a/examples/nginx-app/plugin.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "type": "app", - "name": "Nginx", - "id": "nginx-app", - - "staticRoot": ".", - - "pages": [ - { "name": "Live stream", "component": "StreamPageCtrl", "role": "Editor"}, - { "name": "Log view", "component": "LogsPageCtrl", "role": "Viewer"} - ], - - "css": { - "dark": "css/dark.css", - "light": "css/light.css" - }, - - "info": { - "description": "Official Grafana Nginx App & Dashboard bundle", - "author": { - "name": "Nginx Inc.", - "url": "http://nginx.com" - }, - "keywords": ["nginx"], - "logos": { - "small": "img/logo_small.png", - "large": "img/logo_large.png" - }, - "links": [ - {"name": "Project site", "url": "http://project.com"}, - {"name": "License & Terms", "url": "http://license.com"} - ], - "version": "1.0.0", - "updated": "2015-02-10" - }, - - "includes": [ - {"type": "dashboard", "name": "Nginx Connection stats", "path": "dashboards/nginx_connection_stats.json"}, - {"type": "panel", "name": "Nginx Panel"}, - {"type": "datasource", "name": "Nginx Datasource"} - ], - - "dependencies": { - "grafanaVersion": "3.x.x", - "plugins": [ - {"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"}, - {"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"} - ] - } -} diff --git a/examples/nginx-app/readme.md b/examples/nginx-app/readme.md deleted file mode 100644 index c267cfbb1fc..00000000000 --- a/examples/nginx-app/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -## Overview - -This application is an example app. - -### Awesome - -Even though it does not have any features it is still pretty awesome. diff --git a/examples/nginx-app/src/components/config.html b/examples/nginx-app/src/components/config.html deleted file mode 100644 index c531ec36d76..00000000000 --- a/examples/nginx-app/src/components/config.html +++ /dev/null @@ -1,3 +0,0 @@ -

    - Nginx config! -

    diff --git a/examples/nginx-app/src/components/config.js b/examples/nginx-app/src/components/config.js deleted file mode 100644 index bb8f007b9bc..00000000000 --- a/examples/nginx-app/src/components/config.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class NginxAppConfigCtrl { -} -NginxAppConfigCtrl.templateUrl = 'components/config.html'; - - diff --git a/examples/nginx-app/src/components/logs.html b/examples/nginx-app/src/components/logs.html deleted file mode 100644 index ca215772bf5..00000000000 --- a/examples/nginx-app/src/components/logs.html +++ /dev/null @@ -1,3 +0,0 @@ -

    - Logs page! -

    diff --git a/examples/nginx-app/src/components/logs.js b/examples/nginx-app/src/components/logs.js deleted file mode 100644 index 5b67290381b..00000000000 --- a/examples/nginx-app/src/components/logs.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class LogsPageCtrl { -} -LogsPageCtrl.templateUrl = 'components/logs.html'; - - diff --git a/examples/nginx-app/src/components/stream.html b/examples/nginx-app/src/components/stream.html deleted file mode 100644 index ad70ca4df50..00000000000 --- a/examples/nginx-app/src/components/stream.html +++ /dev/null @@ -1,3 +0,0 @@ -

    - Stream page! -

    diff --git a/examples/nginx-app/src/components/stream.js b/examples/nginx-app/src/components/stream.js deleted file mode 100644 index 8684b36c64d..00000000000 --- a/examples/nginx-app/src/components/stream.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class StreamPageCtrl { -} -StreamPageCtrl.templateUrl = 'components/stream.html'; - - diff --git a/examples/nginx-app/src/css/dark.css b/examples/nginx-app/src/css/dark.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/css/light.css b/examples/nginx-app/src/css/light.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/dashboards/dashboard.js b/examples/nginx-app/src/dashboards/dashboard.js deleted file mode 100644 index 794e2c5217b..00000000000 --- a/examples/nginx-app/src/dashboards/dashboard.js +++ /dev/null @@ -1,17 +0,0 @@ -require([ -], function () { - - function Dashboard() { - - this.getInputs = function() { - - }; - - this.buildDashboard = function() { - - }; - } - - return Dashboard; -}); - diff --git a/examples/nginx-app/src/dashboards/nginx_connection_stats.json b/examples/nginx-app/src/dashboards/nginx_connection_stats.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/datasource/datasource.js b/examples/nginx-app/src/datasource/datasource.js deleted file mode 100644 index 7f4ed363707..00000000000 --- a/examples/nginx-app/src/datasource/datasource.js +++ /dev/null @@ -1,12 +0,0 @@ -export default class NginxDatasource { - - constructor() {} - - query(options) { - return []; - } - - testDatasource() { - return false; - } -} diff --git a/examples/nginx-app/src/datasource/module.js b/examples/nginx-app/src/datasource/module.js deleted file mode 100644 index a3473e59889..00000000000 --- a/examples/nginx-app/src/datasource/module.js +++ /dev/null @@ -1,5 +0,0 @@ -import {Datasource} from './datasource'; - -export { - Datasource -}; \ No newline at end of file diff --git a/examples/nginx-app/src/datasource/plugin.json b/examples/nginx-app/src/datasource/plugin.json deleted file mode 100644 index ffce9492418..00000000000 --- a/examples/nginx-app/src/datasource/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "datasource", - "name": "Nginx Datasource", - "id": "nginx-datasource" -} diff --git a/examples/nginx-app/src/img/logo_large.png b/examples/nginx-app/src/img/logo_large.png deleted file mode 100644 index c28955960e4d842f8d47496e9382e7ceadae2100..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14460 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 TE(&}n2vU?&eO@MG67+un{$ko6 diff --git a/examples/nginx-app/src/img/logo_small.png b/examples/nginx-app/src/img/logo_small.png deleted file mode 100644 index a6040f66f3da060c679acd661cf8b1f53eb6720d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6430 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 diff --git a/examples/nginx-app/src/module.js b/examples/nginx-app/src/module.js deleted file mode 100644 index b5aeecc6ccf..00000000000 --- a/examples/nginx-app/src/module.js +++ /dev/null @@ -1,9 +0,0 @@ -import {LogsPageCtrl} from './components/logs'; -import {StreamPageCtrl} from './components/stream'; -import {NginxAppConfigCtrl} from './components/config'; - -export { - NginxAppConfigCtrl as ConfigCtrl, - StreamPageCtrl, - LogsPageCtrl -}; diff --git a/examples/nginx-app/src/panel/module.js b/examples/nginx-app/src/panel/module.js deleted file mode 100644 index 899586da81b..00000000000 --- a/examples/nginx-app/src/panel/module.js +++ /dev/null @@ -1,15 +0,0 @@ -import {PanelCtrl} from 'app/plugins/sdk'; - -class NginxPanelCtrl extends PanelCtrl { - - constructor($scope, $injector) { - super($scope, $injector); - } - -} -NginxPanelCtrl.template = '

    nginx!

    '; - -export { - NginxPanelCtrl as PanelCtrl -}; - diff --git a/examples/nginx-app/src/panel/plugin.json b/examples/nginx-app/src/panel/plugin.json deleted file mode 100644 index f3548c987f3..00000000000 --- a/examples/nginx-app/src/panel/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "panel", - "name": "Nginx Panel", - "id": "nginx-panel" -} From 01788e86e5b63e0508924e788ea1ef9866b790db Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 11 Mar 2016 18:14:29 +0900 Subject: [PATCH 082/113] (cloudwatch) add document for new query --- docs/sources/datasources/cloudwatch.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 4d9d8b38c1c..351b08eb2aa 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -64,9 +64,19 @@ Name | Description `metrics(namespace)` | Returns a list of metrics in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. `dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. +`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`. +`ec2_instance_attribute(region, attribute_name, filters)` | Returns a list of attribute matching the specified `region`, `attribute_name`, `filters`. For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). +The `ec2_instance_attribute` query take `filters` in JSON format. +You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }` + +Example `ec2_instance_attribute()` query + + ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) + ![](/img/v2/cloudwatch_templating.png) ## Cost From fc54c01f01dc9e5136b65d8e477cfa2458d24043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 12:31:56 +0100 Subject: [PATCH 083/113] feat(plugins): more work on plugin dashboard install, #4298 --- pkg/api/dtos/plugins.go | 7 +-- pkg/plugins/dashboard_installer.go | 3 +- pkg/plugins/dashboards.go | 6 ++- public/app/core/routes/routes.ts | 6 +-- public/app/features/dashboard/all.js | 1 - public/app/features/datasources/all.js | 4 -- public/app/features/org/all.js | 1 - public/app/features/plugins/all.ts | 9 ++-- .../edit_ctrl.ts => plugins/ds_edit_ctrl.ts} | 2 +- .../list_ctrl.ts => plugins/ds_list_ctrl.ts} | 0 .../plugins/import_list/import_list.html | 37 ++++++++++++++++ .../import_list/import_list.ts | 44 +++++-------------- .../partials/ds_edit.html} | 0 .../partials/ds_http_settings.html} | 0 .../partials/ds_list.html} | 0 .../partials/{edit.html => plugin_edit.html} | 0 .../partials/{list.html => plugin_list.html} | 0 .../partials/{page.html => plugin_page.html} | 0 .../{edit_ctrl.ts => plugin_edit_ctrl.ts} | 0 .../{list_ctrl.ts => plugin_list_ctrl.ts} | 0 .../{page_ctrl.ts => plugin_page_ctrl.ts} | 0 .../graphite/dashboards/carbon_stats.json | 2 +- .../plugins/datasource/graphite/plugin.json | 3 +- 23 files changed, 70 insertions(+), 55 deletions(-) delete mode 100644 public/app/features/datasources/all.js rename public/app/features/{datasources/edit_ctrl.ts => plugins/ds_edit_ctrl.ts} (97%) rename public/app/features/{datasources/list_ctrl.ts => plugins/ds_list_ctrl.ts} (100%) create mode 100644 public/app/features/plugins/import_list/import_list.html rename public/app/features/{dashboard => plugins}/import_list/import_list.ts (56%) rename public/app/features/{datasources/partials/edit.html => plugins/partials/ds_edit.html} (100%) rename public/app/features/{datasources/partials/http_settings.html => plugins/partials/ds_http_settings.html} (100%) rename public/app/features/{datasources/partials/list.html => plugins/partials/ds_list.html} (100%) rename public/app/features/plugins/partials/{edit.html => plugin_edit.html} (100%) rename public/app/features/plugins/partials/{list.html => plugin_list.html} (100%) rename public/app/features/plugins/partials/{page.html => plugin_page.html} (100%) rename public/app/features/plugins/{edit_ctrl.ts => plugin_edit_ctrl.ts} (100%) rename public/app/features/plugins/{list_ctrl.ts => plugin_list_ctrl.ts} (100%) rename public/app/features/plugins/{page_ctrl.ts => plugin_page_ctrl.ts} (100%) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 144b9b09755..5f3dac76296 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -27,7 +27,8 @@ type PluginListItem struct { } type InstallPluginDashboardCmd struct { - PluginId string `json:"pluginId"` - Path string `json:"path"` - Inputs map[string]interface{} `json:"inputs"` + PluginId string `json:"pluginId"` + Path string `json:"path"` + Reinstall bool `json:"reinstall"` + Inputs map[string]interface{} `json:"inputs"` } diff --git a/pkg/plugins/dashboard_installer.go b/pkg/plugins/dashboard_installer.go index 9f52b518d70..279ab209989 100644 --- a/pkg/plugins/dashboard_installer.go +++ b/pkg/plugins/dashboard_installer.go @@ -48,8 +48,9 @@ func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error { Title: dashboard.Title, Path: cmd.Path, Revision: dashboard.GetString("revision", "1.0"), - InstalledURI: "db/" + saveCmd.Result.Slug, + InstalledUri: "db/" + saveCmd.Result.Slug, InstalledRevision: dashboard.GetString("revision", "1.0"), + Installed: true, } return nil diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 9acb64cec1b..1697ad808f2 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -12,7 +12,8 @@ import ( type PluginDashboardInfoDTO struct { PluginId string `json:"pluginId"` Title string `json:"title"` - InstalledURI string `json:"installedURI"` + Installed bool `json:"installed"` + InstalledUri string `json:"installedUri"` InstalledRevision string `json:"installedRevision"` Revision string `json:"revision"` Description string `json:"description"` @@ -83,7 +84,8 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl return nil, err } } else { - res.InstalledURI = "db/" + query.Result.Slug + res.Installed = true + res.InstalledUri = "db/" + query.Result.Slug res.InstalledRevision = query.Result.GetString("revision", "1.0") } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 91ef4cd8b25..5ec55c311e5 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -52,19 +52,19 @@ function setupAngularRoutes($routeProvider, $locationProvider) { templateUrl: 'public/app/features/datasources/partials/list.html', controller : 'DataSourcesCtrl', controllerAs: 'ctrl', - resolve: loadOrgBundle, + resolve: loadPluginsBundle, }) .when('/datasources/edit/:id', { templateUrl: 'public/app/features/datasources/partials/edit.html', controller : 'DataSourceEditCtrl', controllerAs: 'ctrl', - resolve: loadOrgBundle, + resolve: loadPluginsBundle, }) .when('/datasources/new', { templateUrl: 'public/app/features/datasources/partials/edit.html', controller : 'DataSourceEditCtrl', controllerAs: 'ctrl', - resolve: loadOrgBundle, + resolve: loadPluginsBundle, }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index b96f6693324..d110019add6 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -13,7 +13,6 @@ define([ './timeSrv', './unsavedChangesSrv', './timepicker/timepicker', - './import_list/import_list', './graphiteImportCtrl', './dynamicDashboardSrv', './importCtrl', diff --git a/public/app/features/datasources/all.js b/public/app/features/datasources/all.js deleted file mode 100644 index b181fd475c2..00000000000 --- a/public/app/features/datasources/all.js +++ /dev/null @@ -1,4 +0,0 @@ -define([ - './list_ctrl', - './edit_ctrl', -], function () {}); diff --git a/public/app/features/org/all.js b/public/app/features/org/all.js index cebd0dd1def..e04634d709a 100644 --- a/public/app/features/org/all.js +++ b/public/app/features/org/all.js @@ -4,5 +4,4 @@ define([ './userInviteCtrl', './orgApiKeysCtrl', './orgDetailsCtrl', - '../datasources/all', ], function () {}); diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index 9d54165e56b..346fb2b30ef 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,3 +1,6 @@ -import './edit_ctrl'; -import './page_ctrl'; -import './list_ctrl'; +import './plugin_edit_ctrl'; +import './plugin_page_ctrl'; +import './plugin_list_ctrl'; +import './import_list/import_list'; +import './ds_edit_ctrl'; +import './ds_list_ctrl'; diff --git a/public/app/features/datasources/edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts similarity index 97% rename from public/app/features/datasources/edit_ctrl.ts rename to public/app/features/plugins/ds_edit_ctrl.ts index 3ad74c628ab..1e07b3a814a 100644 --- a/public/app/features/datasources/edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -140,6 +140,6 @@ coreModule.controller('DataSourceEditCtrl', DataSourceEditCtrl); coreModule.directive('datasourceHttpSettings', function() { return { scope: {current: "="}, - templateUrl: 'public/app/features/datasources/partials/http_settings.html' + templateUrl: 'public/app/features/plugins/partials/ds_http_settings.html' }; }); diff --git a/public/app/features/datasources/list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts similarity index 100% rename from public/app/features/datasources/list_ctrl.ts rename to public/app/features/plugins/ds_list_ctrl.ts diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html new file mode 100644 index 00000000000..d6c1bb914ce --- /dev/null +++ b/public/app/features/plugins/import_list/import_list.html @@ -0,0 +1,37 @@ +
    + + + + + + + + + + +
    + + + + {{dash.title}} + + + {{dash.title}} + + + v{{dash.revision}} + + Installed v{{dash.installedRevision}} + + + + +
    +
    + diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts similarity index 56% rename from public/app/features/dashboard/import_list/import_list.ts rename to public/app/features/plugins/import_list/import_list.ts index 7d88d7f9f0a..62145623fa2 100644 --- a/public/app/features/dashboard/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -4,9 +4,6 @@ import angular from 'angular'; import _ from 'lodash'; import coreModule from 'app/core/core_module'; -class DashboardScriptLoader { -} - export class DashImportListCtrl { dashboards: any[]; plugin: any; @@ -19,50 +16,32 @@ export class DashImportListCtrl { }); } - import(dash) { + import(dash, reinstall) { var installCmd = { pluginId: this.plugin.id, path: dash.path, + reinstall: reinstall, inputs: {} }; this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => { - console.log(res); + this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); + _.extend(dash, res); }); } + remove(dash) { + this.backendSrv.delete('/api/dashboards/' + dash.installedUri).then(() => { + this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]); + dash.installed = false; + }); + } } -var template = ` -
    - - - - - - - - - - -
    - - - {{dash.title}} - - {{dash.revision}} - - {{dash.installedRevision}} - - -
    -
    -`; - export function dashboardImportList() { return { restrict: 'E', - template: template, + templateUrl: 'public/app/features/plugins/import_list/import_list.html', controller: DashImportListCtrl, bindToController: true, controllerAs: 'ctrl', @@ -72,7 +51,6 @@ export function dashboardImportList() { }; } - coreModule.directive('dashboardImportList', dashboardImportList); diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/plugins/partials/ds_edit.html similarity index 100% rename from public/app/features/datasources/partials/edit.html rename to public/app/features/plugins/partials/ds_edit.html diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html similarity index 100% rename from public/app/features/datasources/partials/http_settings.html rename to public/app/features/plugins/partials/ds_http_settings.html diff --git a/public/app/features/datasources/partials/list.html b/public/app/features/plugins/partials/ds_list.html similarity index 100% rename from public/app/features/datasources/partials/list.html rename to public/app/features/plugins/partials/ds_list.html diff --git a/public/app/features/plugins/partials/edit.html b/public/app/features/plugins/partials/plugin_edit.html similarity index 100% rename from public/app/features/plugins/partials/edit.html rename to public/app/features/plugins/partials/plugin_edit.html diff --git a/public/app/features/plugins/partials/list.html b/public/app/features/plugins/partials/plugin_list.html similarity index 100% rename from public/app/features/plugins/partials/list.html rename to public/app/features/plugins/partials/plugin_list.html diff --git a/public/app/features/plugins/partials/page.html b/public/app/features/plugins/partials/plugin_page.html similarity index 100% rename from public/app/features/plugins/partials/page.html rename to public/app/features/plugins/partials/plugin_page.html diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts similarity index 100% rename from public/app/features/plugins/edit_ctrl.ts rename to public/app/features/plugins/plugin_edit_ctrl.ts diff --git a/public/app/features/plugins/list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts similarity index 100% rename from public/app/features/plugins/list_ctrl.ts rename to public/app/features/plugins/plugin_list_ctrl.ts diff --git a/public/app/features/plugins/page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts similarity index 100% rename from public/app/features/plugins/page_ctrl.ts rename to public/app/features/plugins/plugin_page_ctrl.ts diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json index 642f32b6e6a..ad2c32be759 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json @@ -6,7 +6,7 @@ } }, - "title": "Carbon stats", + "title": "Carbon Cache Stats", "version": 1, "rows": [ { diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index c923cb79c4e..f7715f9c5b9 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -4,8 +4,7 @@ "id": "graphite", "includes": [ - {"type": "dashboard", "name": "Carbon Overview", "path": "dashboards/carbon_stats.json"}, - {"type": "dashboard", "name": "Carbon Agent Details", "path": "dashboards/carbon_stats.json"} + {"type": "dashboard", "name": "Carbon Cache Stats", "path": "dashboards/carbon_stats.json"} ], "metrics": true, From d2b0bad1cffa0df4a2ea59c4064c2f57a8e8e542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 13:02:21 +0100 Subject: [PATCH 084/113] fix(): fixed changed partials paths --- public/app/core/routes/routes.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 5ec55c311e5..92cf32448a1 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -49,19 +49,19 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardImportCtrl', }) .when('/datasources', { - templateUrl: 'public/app/features/datasources/partials/list.html', + templateUrl: 'public/app/features/plugins/partials/ds_list.html', controller : 'DataSourcesCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/datasources/edit/:id', { - templateUrl: 'public/app/features/datasources/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', controller : 'DataSourceEditCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/datasources/new', { - templateUrl: 'public/app/features/datasources/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/ds_edit.html', controller : 'DataSourceEditCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, @@ -168,19 +168,19 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/plugins', { - templateUrl: 'public/app/features/plugins/partials/list.html', + templateUrl: 'public/app/features/plugins/partials/plugin_list.html', controller: 'PluginListCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/plugins/:pluginId/edit', { - templateUrl: 'public/app/features/plugins/partials/edit.html', + templateUrl: 'public/app/features/plugins/partials/plugin_edit.html', controller: 'PluginEditCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, }) .when('/plugins/:pluginId/page/:slug', { - templateUrl: 'public/app/features/plugins/partials/page.html', + templateUrl: 'public/app/features/plugins/partials/plugin_page.html', controller: 'AppPageCtrl', controllerAs: 'ctrl', resolve: loadPluginsBundle, From 8da702c2e7977fdf6d8bf357adf657afd1879762 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 13:59:09 +0100 Subject: [PATCH 085/113] feat(cli): add grafana net url --- pkg/cmd/grafana-cli/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 4e74578c604..b277714fe9b 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -41,7 +41,7 @@ func main() { cli.StringFlag{ Name: "repo", Usage: "url to the plugin repository", - Value: "", + Value: "https://grafana-net.raintank.io/api/plugins", }, cli.BoolFlag{ Name: "debug, d", From f5bb2b11e5d8afe3d06bd542bde1496bc70f892f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 14:11:25 +0100 Subject: [PATCH 086/113] feat(cli): improve error handling for missing plugin dir --- pkg/cmd/grafana-cli/commands/commands.go | 1 + pkg/cmd/grafana-cli/commands/install_command.go | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 4993d8aa045..f1b36c90ef2 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -11,6 +11,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C cmd := &contextCommandLine{context} if err := command(cmd); err != nil { + log.Error("\nError: ") log.Errorf("%s\n\n", err) cmd.ShowHelp() diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index 4dfbfd80e31..9b9faaa3fe4 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -28,7 +28,15 @@ func validateInput(c CommandLine, pluginFolder string) error { } fileInfo, err := os.Stat(pluginDir) - if err != nil && !fileInfo.IsDir() { + if err != nil { + if err = os.MkdirAll(pluginDir, os.ModePerm); err != nil { + return errors.New("path is not a directory") + } + + return nil + } + + if !fileInfo.IsDir() { return errors.New("path is not a directory") } From 5094c1db2a970293fc788257caa7c0e8ea203a82 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 14:34:48 +0100 Subject: [PATCH 087/113] feat(cli): improves error message for 401 requests --- pkg/cmd/grafana-cli/services/services.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index fc77570b77c..cd03f755075 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -3,6 +3,7 @@ package services import ( "encoding/json" "errors" + "fmt" "github.com/franela/goreq" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" @@ -12,8 +13,12 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { + fullUrl := repoUrl + "/repo" + res, _ := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do() - res, _ := goreq.Request{Uri: repoUrl + "/repo", MaxRedirects: 3}.Do() + if res.StatusCode != 200 { + return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode) + } var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) From 85466c7ee1ce5299ed67943a42ee7c8f10fef3ae Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 17:04:26 +0100 Subject: [PATCH 088/113] fix(plugins): fixes broken link --- public/app/features/plugins/plugin_edit_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/plugin_edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts index 2d4362835d4..71beb9d0268 100644 --- a/public/app/features/plugins/plugin_edit_ctrl.ts +++ b/public/app/features/plugins/plugin_edit_ctrl.ts @@ -78,7 +78,7 @@ export class PluginEditCtrl { secureJsonData: self.model.secureJsonData, }, {}); - return self.backendSrv.post(`/api/org/plugins/${self.pluginId}/settings`, updateCmd); + return self.backendSrv.post(`/api/plugins/${self.pluginId}/settings`, updateCmd); }); // if set, performt he postUpdate hook. If a promise is returned it will block From c1e4d0590c40206c28a72fe52b647d5f02a2f1e3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 11 Mar 2016 17:13:35 +0100 Subject: [PATCH 089/113] fix(plugins): fixes broken plugin link --- public/app/features/plugins/plugin_page_ctrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/plugin_page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts index 9157f14202e..6a840717a80 100644 --- a/public/app/features/plugins/plugin_page_ctrl.ts +++ b/public/app/features/plugins/plugin_page_ctrl.ts @@ -11,7 +11,7 @@ export class AppPageCtrl { /** @ngInject */ constructor(private backendSrv, private $routeParams: any, private $rootScope) { this.pluginId = $routeParams.pluginId; - this.backendSrv.get(`/api/org/plugins/${this.pluginId}/settings`).then(app => { + this.backendSrv.get(`/api/plugins/${this.pluginId}/settings`).then(app => { this.appModel = app; this.page = _.findWhere(app.pages, {slug: this.$routeParams.slug}); if (!this.page) { From c5a817194a0a669434516e7c942b55721dc1c368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 17:31:57 +0100 Subject: [PATCH 090/113] more work on dashboard importing and templating --- pkg/api/api.go | 2 +- pkg/api/dtos/plugins.go | 10 +- pkg/api/plugins.go | 4 +- pkg/components/dynmap/dynmap.go | 813 ++++++++++++++++++ pkg/components/dynmap/dynmap_test.go | 313 +++++++ pkg/plugins/dashboard_importer.go | 130 +++ pkg/plugins/dashboard_importer_test.go | 81 ++ pkg/plugins/dashboard_installer.go | 57 -- .../plugins/import_list/import_list.ts | 15 +- .../features/plugins/partials/ds_edit.html | 2 +- .../graphite/dashboards/carbon_stats.json | 1 + tests/test-app/dashboards/connections.json | 21 +- 12 files changed, 1380 insertions(+), 69 deletions(-) create mode 100644 pkg/components/dynmap/dynmap.go create mode 100644 pkg/components/dynmap/dynmap_test.go create mode 100644 pkg/plugins/dashboard_importer.go create mode 100644 pkg/plugins/dashboard_importer_test.go delete mode 100644 pkg/plugins/dashboard_installer.go diff --git a/pkg/api/api.go b/pkg/api/api.go index d4bb22149f8..c350b278679 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -176,7 +176,7 @@ func Register(r *macaron.Macaron) { r.Get("/", wrap(GetPluginList)) r.Get("/dashboards/:pluginId", wrap(GetPluginDashboards)) - r.Post("/dashboards/install", bind(dtos.InstallPluginDashboardCmd{}), wrap(InstallPluginDashboard)) + r.Post("/dashboards/import", bind(dtos.ImportDashboardCommand{}), wrap(ImportDashboard)) r.Get("/:pluginId/settings", wrap(GetPluginSettingById)) r.Post("/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 5f3dac76296..7008c939aef 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -26,9 +26,9 @@ type PluginListItem struct { Info *plugins.PluginInfo `json:"info"` } -type InstallPluginDashboardCmd struct { - PluginId string `json:"pluginId"` - Path string `json:"path"` - Reinstall bool `json:"reinstall"` - Inputs map[string]interface{} `json:"inputs"` +type ImportDashboardCommand struct { + PluginId string `json:"pluginId"` + Path string `json:"path"` + Reinstall bool `json:"reinstall"` + Inputs []plugins.ImportDashboardInput `json:"inputs"` } diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 1ba0dde6c9f..2eb27c3c8ed 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -122,9 +122,9 @@ func GetPluginDashboards(c *middleware.Context) Response { } } -func InstallPluginDashboard(c *middleware.Context, apiCmd dtos.InstallPluginDashboardCmd) Response { +func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Response { - cmd := plugins.InstallPluginDashboardCommand{ + cmd := plugins.ImportDashboardCommand{ OrgId: c.OrgId, UserId: c.UserId, PluginId: apiCmd.PluginId, diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go new file mode 100644 index 00000000000..bd7c8627bf0 --- /dev/null +++ b/pkg/components/dynmap/dynmap.go @@ -0,0 +1,813 @@ +// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go +// MIT Licence + +package dynmap + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "strings" +) + +// Error values returned when validation functions fail +var ( + ErrNotNull = errors.New("is not null") + ErrNotArray = errors.New("Not an array") + ErrNotNumber = errors.New("not a number") + ErrNotBool = errors.New("no bool") + ErrNotObject = errors.New("not an object") + ErrNotObjectArray = errors.New("not an object array") + ErrNotString = errors.New("not a string") +) + +type KeyNotFoundError struct { + Key string +} + +func (k KeyNotFoundError) Error() string { + if k.Key != "" { + return fmt.Sprintf("key '%s' not found", k.Key) + } + + return "key not found" +} + +// Value represents an arbitrary JSON value. +// It may contain a bool, number, string, object, array or null. +type Value struct { + data interface{} + exists bool // Used to separate nil and non-existing values +} + +// Object represents an object JSON object. +// It inherets from Value but with an additional method to access +// a map representation of it's content. It's useful when iterating. +type Object struct { + Value + m map[string]*Value + valid bool +} + +// Returns the golang map. +// Needed when iterating through the values of the object. +func (v *Object) Map() map[string]*Value { + return v.m +} + +func NewFromMap(data map[string]interface{}) *Object { + val := &Value{data: data, exists: true} + obj, _ := val.Object() + return obj +} + +func NewObject() *Object { + val := &Value{data: make(map[string]interface{}), exists: true} + obj, _ := val.Object() + return obj +} + +// Creates a new value from an io.reader. +// Returns an error if the reader does not contain valid json. +// Useful for parsing the body of a net/http response. +// Example: NewFromReader(res.Body) +func NewValueFromReader(reader io.Reader) (*Value, error) { + j := new(Value) + d := json.NewDecoder(reader) + d.UseNumber() + err := d.Decode(&j.data) + return j, err +} + +// Creates a new value from bytes. +// Returns an error if the bytes are not valid json. +func NewValueFromBytes(b []byte) (*Value, error) { + r := bytes.NewReader(b) + return NewValueFromReader(r) +} + +func objectFromValue(v *Value, err error) (*Object, error) { + if err != nil { + return nil, err + } + + o, err := v.Object() + + if err != nil { + return nil, err + } + + return o, nil +} + +func NewObjectFromBytes(b []byte) (*Object, error) { + return objectFromValue(NewValueFromBytes(b)) +} + +func NewObjectFromReader(reader io.Reader) (*Object, error) { + return objectFromValue(NewValueFromReader(reader)) +} + +// Marshal into bytes. +func (v *Value) Marshal() ([]byte, error) { + return json.Marshal(v.data) +} + +// Get the interyling data as interface +func (v *Value) Interface() interface{} { + return v.data +} + +// Private Get +func (v *Value) get(key string) (*Value, error) { + + // Assume this is an object + obj, err := v.Object() + + if err == nil { + child, ok := obj.Map()[key] + if ok { + return child, nil + } else { + return nil, KeyNotFoundError{key} + } + } + + return nil, err +} + +// Private get path +func (v *Value) getPath(keys []string) (*Value, error) { + current := v + var err error + for _, key := range keys { + current, err = current.get(key) + + if err != nil { + return nil, err + } + } + return current, nil +} + +// Gets the value at key path. +// Returns error if the value does not exist. +// Consider using the more specific Get(..) methods instead. +// Example: +// value, err := GetValue("address", "street") +func (v *Object) GetValue(keys ...string) (*Value, error) { + return v.getPath(keys) +} + +// Gets the value at key path and attempts to typecast the value into an object. +// Returns error if the value is not a json object. +// Example: +// object, err := GetObject("person", "address") +func (v *Object) GetObject(keys ...string) (*Object, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + obj, err := child.Object() + + if err != nil { + return nil, err + } else { + return obj, nil + } + + } +} + +// Gets the value at key path and attempts to typecast the value into a string. +// Returns error if the value is not a json string. +// Example: +// string, err := GetString("address", "street") +func (v *Object) GetString(keys ...string) (string, error) { + child, err := v.getPath(keys) + + if err != nil { + return "", err + } else { + return child.String() + } +} + +func (v *Object) MustGetString(path string, def string) string { + keys := strings.Split(path, ".") + if str, err := v.GetString(keys...); err != nil { + return def + } else { + return str + } +} + +// Gets the value at key path and attempts to typecast the value into null. +// Returns error if the value is not json null. +// Example: +// err := GetNull("address", "street") +func (v *Object) GetNull(keys ...string) error { + child, err := v.getPath(keys) + + if err != nil { + return err + } + + return child.Null() +} + +// Gets the value at key path and attempts to typecast the value into a number. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetNumber(keys ...string) (json.Number, error) { + child, err := v.getPath(keys) + + if err != nil { + return "", err + } else { + + n, err := child.Number() + + if err != nil { + return "", err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetFloat64(keys ...string) (float64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + n, err := child.Float64() + + if err != nil { + return 0, err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// n, err := GetNumber("address", "street_number") +func (v *Object) GetInt64(keys ...string) (int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + n, err := child.Int64() + + if err != nil { + return 0, err + } else { + return n, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into a float64. +// Returns error if the value is not a json number. +// Example: +// v, err := GetInterface("address", "anything") +func (v *Object) GetInterface(keys ...string) (interface{}, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + return child.Interface(), nil + } +} + +// Gets the value at key path and attempts to typecast the value into a bool. +// Returns error if the value is not a json boolean. +// Example: +// married, err := GetBoolean("person", "married") +func (v *Object) GetBoolean(keys ...string) (bool, error) { + child, err := v.getPath(keys) + + if err != nil { + return false, err + } + + return child.Boolean() +} + +// Gets the value at key path and attempts to typecast the value into an array. +// Returns error if the value is not a json array. +// Consider using the more specific GetArray() since it may reduce later type casts. +// Example: +// friends, err := GetValueArray("person", "friends") +// for i, friend := range friends { +// ... // friend will be of type Value here +// } +func (v *Object) GetValueArray(keys ...string) ([]*Value, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + return child.Array() + + } +} + +// Gets the value at key path and attempts to typecast the value into an array of objects. +// Returns error if the value is not a json array or if any of the contained objects are not objects. +// Example: +// friends, err := GetObjectArray("person", "friends") +// for i, friend := range friends { +// ... // friend will be of type Object here +// } +func (v *Object) GetObjectArray(keys ...string) ([]*Object, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]*Object, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem. + Object() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of string. +// Returns error if the value is not a json array or if any of the contained objects are not strings. +// Gets the value at key path and attempts to typecast the value into an array of objects. +// Returns error if the value is not a json array or if any of the contained objects are not objects. +// Example: +// friendNames, err := GetStringArray("person", "friend_names") +// for i, friendName := range friendNames { +// ... // friendName will be of type string here +// } +func (v *Object) GetStringArray(keys ...string) ([]string, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]string, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.String() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of numbers. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +// Example: +// friendAges, err := GetNumberArray("person", "friend_ages") +// for i, friendAge := range friendAges { +// ... // friendAge will be of type float64 here +// } +func (v *Object) GetNumberArray(keys ...string) ([]json.Number, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]json.Number, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Number() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of floats. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +func (v *Object) GetFloat64Array(keys ...string) ([]float64, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]float64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Float64() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of ints. +// Returns error if the value is not a json array or if any of the contained objects are not numbers. +func (v *Object) GetInt64Array(keys ...string) ([]int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]int64, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Int64() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of bools. +// Returns error if the value is not a json array or if any of the contained objects are not booleans. +func (v *Object) GetBooleanArray(keys ...string) ([]bool, error) { + child, err := v.getPath(keys) + + if err != nil { + return nil, err + } else { + + array, err := child.Array() + + if err != nil { + return nil, err + } else { + + typedArray := make([]bool, len(array)) + + for index, arrayItem := range array { + typedArrayItem, err := arrayItem.Boolean() + + if err != nil { + return nil, err + } else { + typedArray[index] = typedArrayItem + } + + } + return typedArray, nil + } + } +} + +// Gets the value at key path and attempts to typecast the value into an array of nulls. +// Returns length, or an error if the value is not a json array or if any of the contained objects are not nulls. +func (v *Object) GetNullArray(keys ...string) (int64, error) { + child, err := v.getPath(keys) + + if err != nil { + return 0, err + } else { + + array, err := child.Array() + + if err != nil { + return 0, err + } else { + + var length int64 = 0 + + for _, arrayItem := range array { + err := arrayItem.Null() + + if err != nil { + return 0, err + } else { + length++ + } + + } + return length, nil + } + } +} + +// Returns an error if the value is not actually null +func (v *Value) Null() error { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case nil: + valid = v.exists // Valid only if j also exists, since other values could possibly also be nil + break + } + + if valid { + return nil + } + + return ErrNotNull + +} + +// Attempts to typecast the current value into an array. +// Returns error if the current value is not a json array. +// Example: +// friendsArray, err := friendsValue.Array() +func (v *Value) Array() ([]*Value, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case []interface{}: + valid = true + break + } + + // Unsure if this is a good way to use slices, it's probably not + var slice []*Value + + if valid { + + for _, element := range v.data.([]interface{}) { + child := Value{element, true} + slice = append(slice, &child) + } + + return slice, nil + } + + return slice, ErrNotArray + +} + +// Attempts to typecast the current value into a number. +// Returns error if the current value is not a json number. +// Example: +// ageNumber, err := ageValue.Number() +func (v *Value) Number() (json.Number, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case json.Number: + valid = true + break + } + + if valid { + return v.data.(json.Number), nil + } + + return "", ErrNotNumber +} + +// Attempts to typecast the current value into a float64. +// Returns error if the current value is not a json number. +// Example: +// percentage, err := v.Float64() +func (v *Value) Float64() (float64, error) { + n, err := v.Number() + + if err != nil { + return 0, err + } + + return n.Float64() +} + +// Attempts to typecast the current value into a int64. +// Returns error if the current value is not a json number. +// Example: +// id, err := v.Int64() +func (v *Value) Int64() (int64, error) { + n, err := v.Number() + + if err != nil { + return 0, err + } + + return n.Int64() +} + +// Attempts to typecast the current value into a bool. +// Returns error if the current value is not a json boolean. +// Example: +// marriedBool, err := marriedValue.Boolean() +func (v *Value) Boolean() (bool, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case bool: + valid = true + break + } + + if valid { + return v.data.(bool), nil + } + + return false, ErrNotBool +} + +// Attempts to typecast the current value into an object. +// Returns error if the current value is not a json object. +// Example: +// friendObject, err := friendValue.Object() +func (v *Value) Object() (*Object, error) { + + var valid bool + + // Check the type of this data + switch v.data.(type) { + case map[string]interface{}: + valid = true + break + } + + if valid { + obj := new(Object) + obj.valid = valid + + m := make(map[string]*Value) + + if valid { + for key, element := range v.data.(map[string]interface{}) { + m[key] = &Value{element, true} + + } + } + + obj.data = v.data + obj.m = m + + return obj, nil + } + + return nil, ErrNotObject +} + +// Attempts to typecast the current value into an object arrau. +// Returns error if the current value is not an array of json objects +// Example: +// friendObjects, err := friendValues.ObjectArray() +func (v *Value) ObjectArray() ([]*Object, error) { + + var valid bool + + // Check the type of this data + switch v.data.(type) { + case []interface{}: + valid = true + break + } + + // Unsure if this is a good way to use slices, it's probably not + var slice []*Object + + if valid { + + for _, element := range v.data.([]interface{}) { + childValue := Value{element, true} + childObject, err := childValue.Object() + + if err != nil { + return nil, ErrNotObjectArray + } + slice = append(slice, childObject) + } + + return slice, nil + } + + return nil, ErrNotObjectArray + +} + +// Attempts to typecast the current value into a string. +// Returns error if the current value is not a json string +// Example: +// nameObject, err := nameValue.String() +func (v *Value) String() (string, error) { + var valid bool + + // Check the type of this data + switch v.data.(type) { + case string: + valid = true + break + } + + if valid { + return v.data.(string), nil + } + + return "", ErrNotString +} + +// Returns the value a json formatted string. +// Note: The method named String() is used by golang's log method for logging. +// Example: +func (v *Object) String() string { + + f, err := json.Marshal(v.data) + if err != nil { + return err.Error() + } + + return string(f) + +} + +func (v *Object) SetValue(key string, value interface{}) *Value { + data := v.Interface().(map[string]interface{}) + data[key] = value + + return &Value{ + data: value, + exists: true, + } +} diff --git a/pkg/components/dynmap/dynmap_test.go b/pkg/components/dynmap/dynmap_test.go new file mode 100644 index 00000000000..cc002ea06e0 --- /dev/null +++ b/pkg/components/dynmap/dynmap_test.go @@ -0,0 +1,313 @@ +// uses code from https://github.com/antonholmquist/jason/blob/master/jason.go +// MIT Licence + +package dynmap + +import ( + "log" + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +type Assert struct { + T *testing.T +} + +func NewAssert(t *testing.T) *Assert { + return &Assert{ + T: t, + } +} + +func (assert *Assert) True(value bool, message string) { + if value == false { + log.Panicln("Assert: ", message) + } +} + +func TestFirst(t *testing.T) { + + assert := NewAssert(t) + + testJSON := `{ + "name": "anton", + "age": 29, + "nothing": null, + "true": true, + "false": false, + "list": [ + "first", + "second" + ], + "list2": [ + { + "street": "Street 42", + "city": "Stockholm" + }, + { + "street": "Street 42", + "city": "Stockholm" + } + ], + "address": { + "street": "Street 42", + "city": "Stockholm" + }, + "country": { + "name": "Sweden" + } + }` + + j, err := NewObjectFromBytes([]byte(testJSON)) + + a, err := j.GetObject("address") + assert.True(a != nil && err == nil, "failed to create json from string") + + assert.True(err == nil, "failed to create json from string") + + s, err := j.GetString("name") + assert.True(s == "anton" && err == nil, "name should be a string") + + s = j.MustGetString("name", "fallback") + assert.True(s == "anton", "must get string") + + s = j.MustGetString("adsasdas", "fallback") + assert.True(s == "fallback", "must get string return fallback") + + s, err = j.GetString("name") + assert.True(s == "anton" && err == nil, "name shoud match") + + s, err = j.GetString("address", "street") + assert.True(s == "Street 42" && err == nil, "street shoud match") + //log.Println("s: ", s.String()) + + _, err = j.GetNumber("age") + assert.True(err == nil, "age should be a number") + + n, err := j.GetInt64("age") + assert.True(n == 29 && err == nil, "age mismatch") + + ageInterface, err := j.GetInterface("age") + assert.True(ageInterface != nil, "should be defined") + assert.True(err == nil, "age interface error") + + invalidInterface, err := j.GetInterface("not_existing") + assert.True(invalidInterface == nil, "should not give error here") + assert.True(err != nil, "should give error here") + + age, err := j.GetValue("age") + assert.True(age != nil && err == nil, "age should exist") + + age2, err := j.GetValue("age2") + assert.True(age2 == nil && err != nil, "age2 should not exist") + + address, err := j.GetObject("address") + assert.True(address != nil && err == nil, "address should be an object") + + //log.Println("address: ", address) + + s, err = address.GetString("street") + + addressAsString, err := j.GetString("address") + assert.True(addressAsString == "" && err != nil, "address should not be an string") + + s, err = j.GetString("address", "street") + assert.True(s == "Street 42" && err == nil, "street mismatching") + + s, err = j.GetString("address", "name2") + assert.True(s == "" && err != nil, "nonexistent string fail") + + b, err := j.GetBoolean("true") + assert.True(b == true && err == nil, "bool true test") + + b, err = j.GetBoolean("false") + assert.True(b == false && err == nil, "bool false test") + + b, err = j.GetBoolean("invalid_field") + assert.True(b == false && err != nil, "bool invalid test") + + list, err := j.GetValueArray("list") + assert.True(list != nil && err == nil, "list should be an array") + + list2, err := j.GetValueArray("list2") + assert.True(list2 != nil && err == nil, "list2 should be an array") + + list2Array, err := j.GetValueArray("list2") + assert.True(err == nil, "List2 should not return error on AsArray") + assert.True(len(list2Array) == 2, "List2 should should have length 2") + + list2Value, err := j.GetValue("list2") + assert.True(err == nil, "List2 should not return error on value") + + list2ObjectArray, err := list2Value.ObjectArray() + assert.True(err == nil, "list2Value should not return error on ObjectArray") + assert.True(len(list2ObjectArray) == 2, "list2ObjectArray should should have length 2") + + for _, elementValue := range list2Array { + //assert.True(element.IsObject() == true, "first fail") + + element, err := elementValue.Object() + + s, err = element.GetString("street") + assert.True(s == "Street 42" && err == nil, "second fail") + } + + obj, err := j.GetObject("country") + assert.True(obj != nil && err == nil, "country should not return error on AsObject") + for key, value := range obj.Map() { + + assert.True(key == "name", "country name key incorrect") + + s, err = value.String() + assert.True(s == "Sweden" && err == nil, "country name should be Sweden") + } +} + +func TestSecond(t *testing.T) { + json := ` + { + "data": [ + { + "id": "X999_Y999", + "from": { + "name": "Tom Brady", "id": "X12" + }, + "message": "Looking forward to 2010!", + "actions": [ + { + "name": "Comment", + "link": "http://www.facebook.com/X999/posts/Y999" + }, + { + "name": "Like", + "link": "http://www.facebook.com/X999/posts/Y999" + } + ], + "type": "status", + "created_time": "2010-08-02T21:27:44+0000", + "updated_time": "2010-08-02T21:27:44+0000" + }, + { + "id": "X998_Y998", + "from": { + "name": "Peyton Manning", "id": "X18" + }, + "message": "Where's my contract?", + "actions": [ + { + "name": "Comment", + "link": "http://www.facebook.com/X998/posts/Y998" + }, + { + "name": "Like", + "link": "http://www.facebook.com/X998/posts/Y998" + } + ], + "type": "status", + "created_time": "2010-08-02T21:27:44+0000", + "updated_time": "2010-08-02T21:27:44+0000" + } + ] + }` + + assert := NewAssert(t) + j, err := NewObjectFromBytes([]byte(json)) + + assert.True(j != nil && err == nil, "failed to parse json") + + dataObject, err := j.GetObject("data") + assert.True(dataObject == nil && err != nil, "data should not be an object") + + dataArray, err := j.GetObjectArray("data") + assert.True(dataArray != nil && err == nil, "data should be an object array") + + for index, dataItem := range dataArray { + + if index == 0 { + id, err := dataItem.GetString("id") + assert.True(id == "X999_Y999" && err == nil, "item id mismatch") + + fromName, err := dataItem.GetString("from", "name") + assert.True(fromName == "Tom Brady" && err == nil, "fromName mismatch") + + actions, err := dataItem.GetObjectArray("actions") + + for index, action := range actions { + + if index == 1 { + name, err := action.GetString("name") + assert.True(name == "Like" && err == nil, "name mismatch") + + link, err := action.GetString("link") + assert.True(link == "http://www.facebook.com/X999/posts/Y999" && err == nil, "Like mismatch") + + } + + } + } else if index == 1 { + id, err := dataItem.GetString("id") + assert.True(id == "X998_Y998" && err == nil, "item id mismatch") + } + + } + +} + +func TestErrors(t *testing.T) { + json := ` + { + "string": "hello", + "number": 1, + "array": [1,2,3] + }` + + errstr := "expected an error getting %s, but got '%s'" + + j, err := NewObjectFromBytes([]byte(json)) + if err != nil { + t.Fatal("failed to parse json") + } + + if _, err = j.GetObject("string"); err != ErrNotObject { + t.Errorf(errstr, "object", err) + } + + if err = j.GetNull("string"); err != ErrNotNull { + t.Errorf(errstr, "null", err) + } + + if _, err = j.GetStringArray("string"); err != ErrNotArray { + t.Errorf(errstr, "array", err) + } + + if _, err = j.GetStringArray("array"); err != ErrNotString { + t.Errorf(errstr, "string array", err) + } + + if _, err = j.GetNumber("array"); err != ErrNotNumber { + t.Errorf(errstr, "number", err) + } + + if _, err = j.GetBoolean("array"); err != ErrNotBool { + t.Errorf(errstr, "boolean", err) + } + + if _, err = j.GetString("number"); err != ErrNotString { + t.Errorf(errstr, "string", err) + } + + _, err = j.GetString("not_found") + if e, ok := err.(KeyNotFoundError); !ok { + t.Errorf(errstr, "key not found error", e) + } + +} + +func TestWriting(t *testing.T) { + Convey("When writing", t, func() { + j, _ := NewObjectFromBytes([]byte(`{}`)) + j.SetValue("prop", "value") + So(j.MustGetString("prop", ""), ShouldEqual, "value") + }) +} diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go new file mode 100644 index 00000000000..a9ee7a6b46c --- /dev/null +++ b/pkg/plugins/dashboard_importer.go @@ -0,0 +1,130 @@ +package plugins + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/dynmap" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" +) + +type ImportDashboardCommand struct { + Path string `json:"string"` + Inputs []ImportDashboardInput `json:"inputs"` + + OrgId int64 `json:"-"` + UserId int64 `json:"-"` + PluginId string `json:"-"` + Result *PluginDashboardInfoDTO +} + +type ImportDashboardInput struct { + Type string `json:"type"` + PluginId string `json:"pluginId"` + Name string `json:"name"` + Value string `json:"value"` +} + +func init() { + bus.AddHandler("plugins", ImportDashboard) +} + +func ImportDashboard(cmd *ImportDashboardCommand) error { + plugin, exists := Plugins[cmd.PluginId] + + if !exists { + return PluginNotFoundError{cmd.PluginId} + } + + var dashboard *m.Dashboard + var err error + + if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { + return err + } + + saveCmd := m.SaveDashboardCommand{ + Dashboard: dashboard.Data, + OrgId: cmd.OrgId, + UserId: cmd.UserId, + } + + if err := bus.Dispatch(&saveCmd); err != nil { + return err + } + + cmd.Result = &PluginDashboardInfoDTO{ + PluginId: cmd.PluginId, + Title: dashboard.Title, + Path: cmd.Path, + Revision: dashboard.GetString("revision", "1.0"), + InstalledUri: "db/" + saveCmd.Result.Slug, + InstalledRevision: dashboard.GetString("revision", "1.0"), + Installed: true, + } + + return nil +} + +type DashTemplateEvaluator struct { + template *dynmap.Object + inputs []ImportDashboardInput + variables map[string]string + result *dynmap.Object +} + +// func (this *DashTemplateEvaluator) getObject(path string) map[string]interface{} { +// if obj, exists := this.template[path]; exists { +// return obj.(map[string]interface{}) +// } +// return nil +// } + +func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Object) *ImportDashboardInput { + inputType, _ := varDef.GetString("type") + + for _, input := range this.inputs { + if inputType == input.Type && (input.Name == varName || input.Name == "*") { + return &input + } + } + + return nil +} + +func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) { + this.result = dynmap.NewObject() + this.variables = make(map[string]string) + + // check that we have all inputs we need + if requiredInputs, err := this.template.GetObject("__inputs"); err == nil { + for varName, value := range requiredInputs.Map() { + varDef, _ := value.Object() + input := this.findInput(varName, varDef) + this.variables[varName] = input.Value + } + } + + this.EvalObject(this.template, this.result) + return this.result, nil +} + +func (this *DashTemplateEvaluator) EvalObject(source *dynmap.Object, writer *dynmap.Object) { + + for key, value := range source.Map() { + if key == "__input" { + continue + } + + goValue := value.Interface() + switch goValue.(type) { + case string: + writer.SetValue(key, goValue) + case map[string]interface{}: + childSource, _ := value.Object() + childWriter, _ := writer.SetValue(key, map[string]interface{}{}).Object() + this.EvalObject(childSource, childWriter) + default: + log.Error(3, "Unknown json type key: %v , type: %v", key, goValue) + } + } +} diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go new file mode 100644 index 00000000000..90f4ba2847d --- /dev/null +++ b/pkg/plugins/dashboard_importer_test.go @@ -0,0 +1,81 @@ +package plugins + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/dynmap" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ini.v1" +) + +func TestDashboardImport(t *testing.T) { + + Convey("When importing plugin dashboard", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.test-app") + sec.NewKey("path", "../../tests/test-app") + err := Init() + + So(err, ShouldBeNil) + + var importedDash *m.Dashboard + bus.AddHandler("test", func(cmd *m.SaveDashboardCommand) error { + importedDash = cmd.GetDashboardModel() + cmd.Result = importedDash + return nil + }) + + cmd := ImportDashboardCommand{ + PluginId: "test-app", + Path: "dashboards/connections.json", + OrgId: 1, + UserId: 1, + Inputs: []ImportDashboardInput{}, + } + + err = ImportDashboard(&cmd) + So(err, ShouldBeNil) + + Convey("should install dashboard", func() { + So(importedDash, ShouldNotBeNil) + rows := importedDash.Data["rows"].([]interface{}) + row1 := rows[0].(map[string]interface{}) + panels := row1["panels"].([]interface{}) + panel := panels[0].(map[string]interface{}) + + So(panel["datasource"], ShouldEqual, "graphite") + So(importedDash.Data["__inputs"], ShouldBeNil) + }) + }) + + Convey("When evaling dashboard template", t, func() { + template, _ := dynmap.NewObjectFromBytes([]byte(`{ + "__input": { + "graphite": { + "type": "datasource" + } + }, + "test": { + "prop": "$__graphite" + } + }`)) + + evaluator := &DashTemplateEvaluator{ + template: template, + inputs: []ImportDashboardInput{ + {Name: "*", Type: "datasource", Value: "my-server"}, + }, + } + + res, err := evaluator.Eval() + So(err, ShouldBeNil) + + Convey("should render template", func() { + So(res.MustGetString("test.prop", ""), ShouldEqual, "my-server") + }) + }) + +} diff --git a/pkg/plugins/dashboard_installer.go b/pkg/plugins/dashboard_installer.go deleted file mode 100644 index 279ab209989..00000000000 --- a/pkg/plugins/dashboard_installer.go +++ /dev/null @@ -1,57 +0,0 @@ -package plugins - -import ( - "github.com/grafana/grafana/pkg/bus" - m "github.com/grafana/grafana/pkg/models" -) - -type InstallPluginDashboardCommand struct { - Path string `json:"string"` - Inputs map[string]interface{} `json:"inputs"` - - OrgId int64 `json:"-"` - UserId int64 `json:"-"` - PluginId string `json:"-"` - Result *PluginDashboardInfoDTO -} - -func init() { - bus.AddHandler("plugins", InstallPluginDashboard) -} - -func InstallPluginDashboard(cmd *InstallPluginDashboardCommand) error { - plugin, exists := Plugins[cmd.PluginId] - - if !exists { - return PluginNotFoundError{cmd.PluginId} - } - - var dashboard *m.Dashboard - var err error - - if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { - return err - } - - saveCmd := m.SaveDashboardCommand{ - Dashboard: dashboard.Data, - OrgId: cmd.OrgId, - UserId: cmd.UserId, - } - - if err := bus.Dispatch(&saveCmd); err != nil { - return err - } - - cmd.Result = &PluginDashboardInfoDTO{ - PluginId: cmd.PluginId, - Title: dashboard.Title, - Path: cmd.Path, - Revision: dashboard.GetString("revision", "1.0"), - InstalledUri: "db/" + saveCmd.Result.Slug, - InstalledRevision: dashboard.GetString("revision", "1.0"), - Installed: true, - } - - return nil -} diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 62145623fa2..004fde69bd1 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -7,6 +7,7 @@ import coreModule from 'app/core/core_module'; export class DashImportListCtrl { dashboards: any[]; plugin: any; + datasource: any; constructor(private $http, private backendSrv, private $rootScope) { this.dashboards = []; @@ -21,9 +22,18 @@ export class DashImportListCtrl { pluginId: this.plugin.id, path: dash.path, reinstall: reinstall, - inputs: {} + inputs: [] }; + if (this.datasource) { + installCmd.inputs.push({ + name: '*', + type: 'datasource', + pluginId: this.datasource.type, + value: this.datasource.name + }); + } + this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => { this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); _.extend(dash, res); @@ -46,7 +56,8 @@ export function dashboardImportList() { bindToController: true, controllerAs: 'ctrl', scope: { - plugin: "=" + plugin: "=", + datasource: "=" } }; } diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 46177d998cc..d519b78ae55 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -74,7 +74,7 @@
    - +
    diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json index ad2c32be759..e206d8be57d 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json @@ -2,6 +2,7 @@ "__inputs": { "graphite": { "type": "datasource", + "pluginId": "graphite", "description": "Graphite datasource" } }, diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index f1f62826f73..feb90cae71d 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -1,5 +1,24 @@ { + "__inputs": { + "graphite": { + "type": "datasource", + "pluginId": "graphite", + "description": "Graphite datasource" + } + }, + "title": "Nginx Connections", "revision": "1.5", - "schemaVersion": 11 + "schemaVersion": 11, + + "rows": [ + { + "panels": [ + { + "type": "graph", + "datasource": "$__graphite" + } + ] + } + ] } From 1f9f439acb1015307fe53e52202864d9b6c69e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 23:28:33 +0100 Subject: [PATCH 091/113] lib(): added simplejson lib --- pkg/api/cloudwatch/metrics.go | 7 +- pkg/api/dtos/models.go | 31 +- pkg/api/frontendsettings.go | 2 +- pkg/components/dynmap/dynmap.go | 4 + pkg/components/simplejson/simplejson.go | 461 ++++++++++++++++++ pkg/components/simplejson/simplejson_go11.go | 91 ++++ pkg/components/simplejson/simplejson_test.go | 248 ++++++++++ pkg/models/datasource.go | 56 ++- pkg/plugins/dashboard_importer.go | 54 +- pkg/plugins/dashboard_importer_test.go | 16 +- public/app/features/plugins/ds_edit_ctrl.ts | 3 +- .../features/plugins/partials/ds_edit.html | 14 +- .../graphite/dashboards/carbon_stats.json | 2 +- 13 files changed, 921 insertions(+), 68 deletions(-) create mode 100644 pkg/components/simplejson/simplejson.go create mode 100644 pkg/components/simplejson/simplejson_go11.go create mode 100644 pkg/components/simplejson/simplejson_test.go diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 54ab565bede..f5e5274a202 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -130,11 +130,14 @@ func handleGetNamespaces(req *cwRequest, c *middleware.Context) { for key := range metricsMap { keys = append(keys, key) } - if customMetricsNamespaces, ok := req.DataSource.JsonData["customMetricsNamespaces"].(string); ok { - for _, key := range strings.Split(customMetricsNamespaces, ",") { + + customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString() + if customNamespaces != "" { + for _, key := range strings.Split(customNamespaces, ",") { keys = append(keys, key) } } + sort.Sort(sort.StringSlice(keys)) result := []interface{}{} diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 36e51fdc28a..d5534ea6ed4 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) @@ -57,21 +58,21 @@ type DashboardFullWithMeta struct { } type DataSource struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Name string `json:"name"` - Type string `json:"type"` - Access m.DsAccess `json:"access"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData,omitempty"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` } type MetricQueryResultDto struct { diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 8c86f98220a..c84d7faccff 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -59,7 +59,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro defaultDatasource = ds.Name } - if len(ds.JsonData) > 0 { + if len(ds.JsonData.MustMap()) > 0 { dsMap["jsonData"] = ds.JsonData } diff --git a/pkg/components/dynmap/dynmap.go b/pkg/components/dynmap/dynmap.go index bd7c8627bf0..797694845cd 100644 --- a/pkg/components/dynmap/dynmap.go +++ b/pkg/components/dynmap/dynmap.go @@ -120,6 +120,10 @@ func (v *Value) Interface() interface{} { return v.data } +func (v *Value) StringMap() map[string]interface{} { + return v.data.(map[string]interface{}) +} + // Private Get func (v *Value) get(key string) (*Value, error) { diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go new file mode 100644 index 00000000000..1ea8aa346aa --- /dev/null +++ b/pkg/components/simplejson/simplejson.go @@ -0,0 +1,461 @@ +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "log" +) + +// returns the current implementation version +func Version() string { + return "0.5.0" +} + +type Json struct { + data interface{} +} + +func (j *Json) FromDB(data []byte) error { + j.data = make(map[string]interface{}) + + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +func (j *Json) ToDB() ([]byte, error) { + return j.Encode() +} + +// NewJson returns a pointer to a new `Json` object +// after unmarshaling `body` bytes +func NewJson(body []byte) (*Json, error) { + j := new(Json) + err := j.UnmarshalJSON(body) + if err != nil { + return nil, err + } + return j, nil +} + +// New returns a pointer to a new, empty `Json` object +func New() *Json { + return &Json{ + data: make(map[string]interface{}), + } +} + +// Interface returns the underlying data +func (j *Json) Interface() interface{} { + return j.data +} + +// Encode returns its marshaled data as `[]byte` +func (j *Json) Encode() ([]byte, error) { + return j.MarshalJSON() +} + +// EncodePretty returns its marshaled data as `[]byte` with indentation +func (j *Json) EncodePretty() ([]byte, error) { + return json.MarshalIndent(&j.data, "", " ") +} + +// Implements the json.Marshaler interface. +func (j *Json) MarshalJSON() ([]byte, error) { + fmt.Printf("MarshalJSON") + return json.Marshal(&j.data) +} + +// Set modifies `Json` map by `key` and `value` +// Useful for changing single key/value in a `Json` object easily. +func (j *Json) Set(key string, val interface{}) { + m, err := j.Map() + if err != nil { + return + } + m[key] = val +} + +// SetPath modifies `Json`, recursively checking/creating map keys for the supplied path, +// and then finally writing in the value +func (j *Json) SetPath(branch []string, val interface{}) { + if len(branch) == 0 { + j.data = val + return + } + + // in order to insert our branch, we need map[string]interface{} + if _, ok := (j.data).(map[string]interface{}); !ok { + // have to replace with something suitable + j.data = make(map[string]interface{}) + } + curr := j.data.(map[string]interface{}) + + for i := 0; i < len(branch)-1; i++ { + b := branch[i] + // key exists? + if _, ok := curr[b]; !ok { + n := make(map[string]interface{}) + curr[b] = n + curr = n + continue + } + + // make sure the value is the right sort of thing + if _, ok := curr[b].(map[string]interface{}); !ok { + // have to replace with something suitable + n := make(map[string]interface{}) + curr[b] = n + } + + curr = curr[b].(map[string]interface{}) + } + + // add remaining k/v + curr[branch[len(branch)-1]] = val +} + +// Del modifies `Json` map by deleting `key` if it is present. +func (j *Json) Del(key string) { + m, err := j.Map() + if err != nil { + return + } + delete(m, key) +} + +// Get returns a pointer to a new `Json` object +// for `key` in its `map` representation +// +// useful for chaining operations (to traverse a nested JSON): +// js.Get("top_level").Get("dict").Get("value").Int() +func (j *Json) Get(key string) *Json { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val} + } + } + return &Json{nil} +} + +// GetPath searches for the item as specified by the branch +// without the need to deep dive using Get()'s. +// +// js.GetPath("top_level", "dict") +func (j *Json) GetPath(branch ...string) *Json { + jin := j + for _, p := range branch { + jin = jin.Get(p) + } + return jin +} + +// GetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation +// +// this is the analog to Get when accessing elements of +// a json array instead of a json object: +// js.Get("top_level").Get("array").GetIndex(1).Get("key").Int() +func (j *Json) GetIndex(index int) *Json { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]} + } + } + return &Json{nil} +} + +// CheckGet returns a pointer to a new `Json` object and +// a `bool` identifying success or failure +// +// useful for chained operations when success is important: +// if data, ok := js.Get("top_level").CheckGet("inner"); ok { +// log.Println(data) +// } +func (j *Json) CheckGet(key string) (*Json, bool) { + m, err := j.Map() + if err == nil { + if val, ok := m[key]; ok { + return &Json{val}, true + } + } + return nil, false +} + +// Map type asserts to `map` +func (j *Json) Map() (map[string]interface{}, error) { + if m, ok := (j.data).(map[string]interface{}); ok { + return m, nil + } + return nil, errors.New("type assertion to map[string]interface{} failed") +} + +// Array type asserts to an `array` +func (j *Json) Array() ([]interface{}, error) { + if a, ok := (j.data).([]interface{}); ok { + return a, nil + } + return nil, errors.New("type assertion to []interface{} failed") +} + +// Bool type asserts to `bool` +func (j *Json) Bool() (bool, error) { + if s, ok := (j.data).(bool); ok { + return s, nil + } + return false, errors.New("type assertion to bool failed") +} + +// String type asserts to `string` +func (j *Json) String() (string, error) { + if s, ok := (j.data).(string); ok { + return s, nil + } + return "", errors.New("type assertion to string failed") +} + +// Bytes type asserts to `[]byte` +func (j *Json) Bytes() ([]byte, error) { + if s, ok := (j.data).(string); ok { + return []byte(s), nil + } + return nil, errors.New("type assertion to []byte failed") +} + +// StringArray type asserts to an `array` of `string` +func (j *Json) StringArray() ([]string, error) { + arr, err := j.Array() + if err != nil { + return nil, err + } + retArr := make([]string, 0, len(arr)) + for _, a := range arr { + if a == nil { + retArr = append(retArr, "") + continue + } + s, ok := a.(string) + if !ok { + return nil, err + } + retArr = append(retArr, s) + } + return retArr, nil +} + +// MustArray guarantees the return of a `[]interface{}` (with optional default) +// +// useful when you want to interate over array values in a succinct manner: +// for i, v := range js.Get("results").MustArray() { +// fmt.Println(i, v) +// } +func (j *Json) MustArray(args ...[]interface{}) []interface{} { + var def []interface{} + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustArray() received too many arguments %d", len(args)) + } + + a, err := j.Array() + if err == nil { + return a + } + + return def +} + +// MustMap guarantees the return of a `map[string]interface{}` (with optional default) +// +// useful when you want to interate over map values in a succinct manner: +// for k, v := range js.Get("dictionary").MustMap() { +// fmt.Println(k, v) +// } +func (j *Json) MustMap(args ...map[string]interface{}) map[string]interface{} { + var def map[string]interface{} + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustMap() received too many arguments %d", len(args)) + } + + a, err := j.Map() + if err == nil { + return a + } + + return def +} + +// MustString guarantees the return of a `string` (with optional default) +// +// useful when you explicitly want a `string` in a single value return context: +// myFunc(js.Get("param1").MustString(), js.Get("optional_param").MustString("my_default")) +func (j *Json) MustString(args ...string) string { + var def string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustString() received too many arguments %d", len(args)) + } + + s, err := j.String() + if err == nil { + return s + } + + return def +} + +// MustStringArray guarantees the return of a `[]string` (with optional default) +// +// useful when you want to interate over array values in a succinct manner: +// for i, s := range js.Get("results").MustStringArray() { +// fmt.Println(i, s) +// } +func (j *Json) MustStringArray(args ...[]string) []string { + var def []string + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustStringArray() received too many arguments %d", len(args)) + } + + a, err := j.StringArray() + if err == nil { + return a + } + + return def +} + +// MustInt guarantees the return of an `int` (with optional default) +// +// useful when you explicitly want an `int` in a single value return context: +// myFunc(js.Get("param1").MustInt(), js.Get("optional_param").MustInt(5150)) +func (j *Json) MustInt(args ...int) int { + var def int + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt() received too many arguments %d", len(args)) + } + + i, err := j.Int() + if err == nil { + return i + } + + return def +} + +// MustFloat64 guarantees the return of a `float64` (with optional default) +// +// useful when you explicitly want a `float64` in a single value return context: +// myFunc(js.Get("param1").MustFloat64(), js.Get("optional_param").MustFloat64(5.150)) +func (j *Json) MustFloat64(args ...float64) float64 { + var def float64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustFloat64() received too many arguments %d", len(args)) + } + + f, err := j.Float64() + if err == nil { + return f + } + + return def +} + +// MustBool guarantees the return of a `bool` (with optional default) +// +// useful when you explicitly want a `bool` in a single value return context: +// myFunc(js.Get("param1").MustBool(), js.Get("optional_param").MustBool(true)) +func (j *Json) MustBool(args ...bool) bool { + var def bool + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustBool() received too many arguments %d", len(args)) + } + + b, err := j.Bool() + if err == nil { + return b + } + + return def +} + +// MustInt64 guarantees the return of an `int64` (with optional default) +// +// useful when you explicitly want an `int64` in a single value return context: +// myFunc(js.Get("param1").MustInt64(), js.Get("optional_param").MustInt64(5150)) +func (j *Json) MustInt64(args ...int64) int64 { + var def int64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustInt64() received too many arguments %d", len(args)) + } + + i, err := j.Int64() + if err == nil { + return i + } + + return def +} + +// MustUInt64 guarantees the return of an `uint64` (with optional default) +// +// useful when you explicitly want an `uint64` in a single value return context: +// myFunc(js.Get("param1").MustUint64(), js.Get("optional_param").MustUint64(5150)) +func (j *Json) MustUint64(args ...uint64) uint64 { + var def uint64 + + switch len(args) { + case 0: + case 1: + def = args[0] + default: + log.Panicf("MustUint64() received too many arguments %d", len(args)) + } + + i, err := j.Uint64() + if err == nil { + return i + } + + return def +} diff --git a/pkg/components/simplejson/simplejson_go11.go b/pkg/components/simplejson/simplejson_go11.go new file mode 100644 index 00000000000..3a88b29e449 --- /dev/null +++ b/pkg/components/simplejson/simplejson_go11.go @@ -0,0 +1,91 @@ +// +build go1.1 + +package simplejson + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "reflect" + "strconv" +) + +// Implements the json.Unmarshaler interface. +func (j *Json) UnmarshalJSON(p []byte) error { + fmt.Printf("UnmarshalJSON") + dec := json.NewDecoder(bytes.NewBuffer(p)) + dec.UseNumber() + return dec.Decode(&j.data) +} + +// NewFromReader returns a *Json by decoding from an io.Reader +func NewFromReader(r io.Reader) (*Json, error) { + j := new(Json) + dec := json.NewDecoder(r) + dec.UseNumber() + err := dec.Decode(&j.data) + return j, err +} + +// Float64 coerces into a float64 +func (j *Json) Float64() (float64, error) { + switch j.data.(type) { + case json.Number: + return j.data.(json.Number).Float64() + case float32, float64: + return reflect.ValueOf(j.data).Float(), nil + case int, int8, int16, int32, int64: + return float64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return float64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int coerces into an int +func (j *Json) Int() (int, error) { + switch j.data.(type) { + case json.Number: + i, err := j.data.(json.Number).Int64() + return int(i), err + case float32, float64: + return int(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return int(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return int(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Int64 coerces into an int64 +func (j *Json) Int64() (int64, error) { + switch j.data.(type) { + case json.Number: + return j.data.(json.Number).Int64() + case float32, float64: + return int64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return reflect.ValueOf(j.data).Int(), nil + case uint, uint8, uint16, uint32, uint64: + return int64(reflect.ValueOf(j.data).Uint()), nil + } + return 0, errors.New("invalid value type") +} + +// Uint64 coerces into an uint64 +func (j *Json) Uint64() (uint64, error) { + switch j.data.(type) { + case json.Number: + return strconv.ParseUint(j.data.(json.Number).String(), 10, 64) + case float32, float64: + return uint64(reflect.ValueOf(j.data).Float()), nil + case int, int8, int16, int32, int64: + return uint64(reflect.ValueOf(j.data).Int()), nil + case uint, uint8, uint16, uint32, uint64: + return reflect.ValueOf(j.data).Uint(), nil + } + return 0, errors.New("invalid value type") +} diff --git a/pkg/components/simplejson/simplejson_test.go b/pkg/components/simplejson/simplejson_test.go new file mode 100644 index 00000000000..b46ffff1873 --- /dev/null +++ b/pkg/components/simplejson/simplejson_test.go @@ -0,0 +1,248 @@ +package simplejson + +import ( + "encoding/json" + "testing" + + "github.com/bmizerany/assert" +) + +func TestSimplejson(t *testing.T) { + var ok bool + var err error + + js, err := NewJson([]byte(`{ + "test": { + "string_array": ["asdf", "ghjk", "zxcv"], + "string_array_null": ["abc", null, "efg"], + "array": [1, "2", 3], + "arraywithsubs": [{"subkeyone": 1}, + {"subkeytwo": 2, "subkeythree": 3}], + "int": 10, + "float": 5.150, + "string": "simplejson", + "bool": true, + "sub_obj": {"a": 1} + } + }`)) + + assert.NotEqual(t, nil, js) + assert.Equal(t, nil, err) + + _, ok = js.CheckGet("test") + assert.Equal(t, true, ok) + + _, ok = js.CheckGet("missing_key") + assert.Equal(t, false, ok) + + aws := js.Get("test").Get("arraywithsubs") + assert.NotEqual(t, nil, aws) + var awsval int + awsval, _ = aws.GetIndex(0).Get("subkeyone").Int() + assert.Equal(t, 1, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeytwo").Int() + assert.Equal(t, 2, awsval) + awsval, _ = aws.GetIndex(1).Get("subkeythree").Int() + assert.Equal(t, 3, awsval) + + i, _ := js.Get("test").Get("int").Int() + assert.Equal(t, 10, i) + + f, _ := js.Get("test").Get("float").Float64() + assert.Equal(t, 5.150, f) + + s, _ := js.Get("test").Get("string").String() + assert.Equal(t, "simplejson", s) + + b, _ := js.Get("test").Get("bool").Bool() + assert.Equal(t, true, b) + + mi := js.Get("test").Get("int").MustInt() + assert.Equal(t, 10, mi) + + mi2 := js.Get("test").Get("missing_int").MustInt(5150) + assert.Equal(t, 5150, mi2) + + ms := js.Get("test").Get("string").MustString() + assert.Equal(t, "simplejson", ms) + + ms2 := js.Get("test").Get("missing_string").MustString("fyea") + assert.Equal(t, "fyea", ms2) + + ma2 := js.Get("test").Get("missing_array").MustArray([]interface{}{"1", 2, "3"}) + assert.Equal(t, ma2, []interface{}{"1", 2, "3"}) + + msa := js.Get("test").Get("string_array").MustStringArray() + assert.Equal(t, msa[0], "asdf") + assert.Equal(t, msa[1], "ghjk") + assert.Equal(t, msa[2], "zxcv") + + msa2 := js.Get("test").Get("string_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa2[0], "asdf") + assert.Equal(t, msa2[1], "ghjk") + assert.Equal(t, msa2[2], "zxcv") + + msa3 := js.Get("test").Get("missing_array").MustStringArray([]string{"1", "2", "3"}) + assert.Equal(t, msa3, []string{"1", "2", "3"}) + + mm2 := js.Get("test").Get("missing_map").MustMap(map[string]interface{}{"found": false}) + assert.Equal(t, mm2, map[string]interface{}{"found": false}) + + strs, err := js.Get("test").Get("string_array").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs[0], "asdf") + assert.Equal(t, strs[1], "ghjk") + assert.Equal(t, strs[2], "zxcv") + + strs2, err := js.Get("test").Get("string_array_null").StringArray() + assert.Equal(t, err, nil) + assert.Equal(t, strs2[0], "abc") + assert.Equal(t, strs2[1], "") + assert.Equal(t, strs2[2], "efg") + + gp, _ := js.GetPath("test", "string").String() + assert.Equal(t, "simplejson", gp) + + gp2, _ := js.GetPath("test", "int").Int() + assert.Equal(t, 10, gp2) + + assert.Equal(t, js.Get("test").Get("bool").MustBool(), true) + + js.Set("float2", 300.0) + assert.Equal(t, js.Get("float2").MustFloat64(), 300.0) + + js.Set("test2", "setTest") + assert.Equal(t, "setTest", js.Get("test2").MustString()) + + js.Del("test2") + assert.NotEqual(t, "setTest", js.Get("test2").MustString()) + + js.Get("test").Get("sub_obj").Set("a", 2) + assert.Equal(t, 2, js.Get("test").Get("sub_obj").Get("a").MustInt()) + + js.GetPath("test", "sub_obj").Set("a", 3) + assert.Equal(t, 3, js.GetPath("test", "sub_obj", "a").MustInt()) +} + +func TestStdlibInterfaces(t *testing.T) { + val := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + val2 := new(struct { + Name string `json:"name"` + Params *Json `json:"params"` + }) + + raw := `{"name":"myobject","params":{"string":"simplejson"}}` + + assert.Equal(t, nil, json.Unmarshal([]byte(raw), val)) + + assert.Equal(t, "myobject", val.Name) + assert.NotEqual(t, nil, val.Params.data) + s, _ := val.Params.Get("string").String() + assert.Equal(t, "simplejson", s) + + p, err := json.Marshal(val) + assert.Equal(t, nil, err) + assert.Equal(t, nil, json.Unmarshal(p, val2)) + assert.Equal(t, val, val2) // stable +} + +func TestSet(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.Set("baz", "bing") + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestReplace(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + err = js.UnmarshalJSON([]byte(`{"baz":"bing"}`)) + assert.Equal(t, nil, err) + + s, err := js.GetPath("baz").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bing", s) +} + +func TestSetPath(t *testing.T) { + js, err := NewJson([]byte(`{}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"foo", "bar"}, "baz") + + s, err := js.GetPath("foo", "bar").String() + assert.Equal(t, nil, err) + assert.Equal(t, "baz", s) +} + +func TestSetPathNoPath(t *testing.T) { + js, err := NewJson([]byte(`{"some":"data","some_number":1.0,"some_bool":false}`)) + assert.Equal(t, nil, err) + + f := js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 1.0) + + js.SetPath([]string{}, map[string]interface{}{"foo": "bar"}) + + s, err := js.GetPath("foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) + + f = js.GetPath("some_number").MustFloat64(99.0) + assert.Equal(t, f, 99.0) +} + +func TestPathWillAugmentExisting(t *testing.T) { + js, err := NewJson([]byte(`{"this":{"a":"aa","b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "d"}, "dd") + + cases := []struct { + path []string + outcome string + }{ + { + path: []string{"this", "a"}, + outcome: "aa", + }, + { + path: []string{"this", "b"}, + outcome: "bb", + }, + { + path: []string{"this", "c"}, + outcome: "cc", + }, + { + path: []string{"this", "d"}, + outcome: "dd", + }, + } + + for _, tc := range cases { + s, err := js.GetPath(tc.path...).String() + assert.Equal(t, nil, err) + assert.Equal(t, tc.outcome, s) + } +} + +func TestPathWillOverwriteExisting(t *testing.T) { + // notice how "a" is 0.1 - but then we'll try to set at path a, foo + js, err := NewJson([]byte(`{"this":{"a":0.1,"b":"bb","c":"cc"}}`)) + assert.Equal(t, nil, err) + + js.SetPath([]string{"this", "a", "foo"}, "bar") + + s, err := js.GetPath("this", "a", "foo").String() + assert.Equal(t, nil, err) + assert.Equal(t, "bar", s) +} diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 88abd03c319..2e9d98e9700 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -3,6 +3,8 @@ package models import ( "errors" "time" + + "github.com/grafana/grafana/pkg/components/simplejson" ) const ( @@ -42,7 +44,7 @@ type DataSource struct { BasicAuthPassword string WithCredentials bool IsDefault bool - JsonData map[string]interface{} + JsonData *simplejson.Json Created time.Time Updated time.Time @@ -74,19 +76,19 @@ func IsKnownDataSourcePlugin(dsType string) bool { // Also acts as api DTO type AddDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - Database string `json:"database"` - User string `json:"user"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + Database string `json:"database"` + User string `json:"user"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` OrgId int64 `json:"-"` @@ -95,19 +97,19 @@ type AddDataSourceCommand struct { // Also acts as api DTO type UpdateDataSourceCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - Access DsAccess `json:"access" binding:"Required"` - Url string `json:"url"` - Password string `json:"password"` - User string `json:"user"` - Database string `json:"database"` - BasicAuth bool `json:"basicAuth"` - BasicAuthUser string `json:"basicAuthUser"` - BasicAuthPassword string `json:"basicAuthPassword"` - WithCredentials bool `json:"withCredentials"` - IsDefault bool `json:"isDefault"` - JsonData map[string]interface{} `json:"jsonData"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + Access DsAccess `json:"access" binding:"Required"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + BasicAuthUser string `json:"basicAuthUser"` + BasicAuthPassword string `json:"basicAuthPassword"` + WithCredentials bool `json:"withCredentials"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData"` OrgId int64 `json:"-"` Id int64 `json:"-"` diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index a9ee7a6b46c..15e055faffd 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -1,6 +1,10 @@ package plugins import ( + "fmt" + "reflect" + "regexp" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/dynmap" "github.com/grafana/grafana/pkg/log" @@ -24,6 +28,14 @@ type ImportDashboardInput struct { Value string `json:"value"` } +type DashboardInputMissingError struct { + VariableName string +} + +func (e DashboardInputMissingError) Error() string { + return fmt.Sprintf("Dashbord input variable: %v missing from import command", e.VariableName) +} + func init() { bus.AddHandler("plugins", ImportDashboard) } @@ -42,8 +54,19 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { return err } + template := dynmap.NewFromMap(dashboard.Data) + evaluator := &DashTemplateEvaluator{ + template: template, + inputs: cmd.Inputs, + } + + generatedDash, err := evaluator.Eval() + if err != nil { + return err + } + saveCmd := m.SaveDashboardCommand{ - Dashboard: dashboard.Data, + Dashboard: generatedDash.StringMap(), OrgId: cmd.OrgId, UserId: cmd.UserId, } @@ -70,15 +93,9 @@ type DashTemplateEvaluator struct { inputs []ImportDashboardInput variables map[string]string result *dynmap.Object + varRegex *regexp.Regexp } -// func (this *DashTemplateEvaluator) getObject(path string) map[string]interface{} { -// if obj, exists := this.template[path]; exists { -// return obj.(map[string]interface{}) -// } -// return nil -// } - func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Object) *ImportDashboardInput { inputType, _ := varDef.GetString("type") @@ -94,14 +111,22 @@ func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Obje func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) { this.result = dynmap.NewObject() this.variables = make(map[string]string) + this.varRegex, _ = regexp.Compile("\\$__(\\w+)") // check that we have all inputs we need if requiredInputs, err := this.template.GetObject("__inputs"); err == nil { for varName, value := range requiredInputs.Map() { varDef, _ := value.Object() input := this.findInput(varName, varDef) - this.variables[varName] = input.Value + + if input == nil { + return nil, &DashboardInputMissingError{VariableName: varName} + } + + this.variables["$__"+varName] = input.Value } + } else { + log.Info("Import: dashboard has no __import section") } this.EvalObject(this.template, this.result) @@ -111,19 +136,24 @@ func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) { func (this *DashTemplateEvaluator) EvalObject(source *dynmap.Object, writer *dynmap.Object) { for key, value := range source.Map() { - if key == "__input" { + if key == "__inputs" { continue } goValue := value.Interface() - switch goValue.(type) { + + switch v := goValue.(type) { case string: - writer.SetValue(key, goValue) + interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { + return this.variables[match] + }) + writer.SetValue(key, interpolated) case map[string]interface{}: childSource, _ := value.Object() childWriter, _ := writer.SetValue(key, map[string]interface{}{}).Object() this.EvalObject(childSource, childWriter) default: + log.Info("type: %v", reflect.TypeOf(goValue)) log.Error(3, "Unknown json type key: %v , type: %v", key, goValue) } } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 90f4ba2847d..472de6b4e26 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -33,7 +33,9 @@ func TestDashboardImport(t *testing.T) { Path: "dashboards/connections.json", OrgId: 1, UserId: 1, - Inputs: []ImportDashboardInput{}, + Inputs: []ImportDashboardInput{ + {Name: "*", Type: "datasource"}, + }, } err = ImportDashboard(&cmd) @@ -41,6 +43,10 @@ func TestDashboardImport(t *testing.T) { Convey("should install dashboard", func() { So(importedDash, ShouldNotBeNil) + + dashData := dynmap.NewFromMap(importedDash.Data) + So(dashData.String(), ShouldEqual, "") + rows := importedDash.Data["rows"].([]interface{}) row1 := rows[0].(map[string]interface{}) panels := row1["panels"].([]interface{}) @@ -53,7 +59,7 @@ func TestDashboardImport(t *testing.T) { Convey("When evaling dashboard template", t, func() { template, _ := dynmap.NewObjectFromBytes([]byte(`{ - "__input": { + "__inputs": { "graphite": { "type": "datasource" } @@ -76,6 +82,12 @@ func TestDashboardImport(t *testing.T) { Convey("should render template", func() { So(res.MustGetString("test.prop", ""), ShouldEqual, "my-server") }) + + Convey("should not include inputs in output", func() { + _, err := res.GetObject("__inputs") + So(err, ShouldNotBeNil) + }) + }) } diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 1e07b3a814a..d2ca7a82dc4 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -24,6 +24,7 @@ export class DataSourceEditCtrl { datasourceMeta: any; tabIndex: number; hasDashboards: boolean; + editForm: any; /** @ngInject */ constructor( @@ -114,7 +115,7 @@ export class DataSourceEditCtrl { } saveChanges(test) { - if (!this.$scope.editForm.$valid) { + if (!this.editForm.$valid) { return; } diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index d519b78ae55..ff2f981e58b 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -25,7 +25,7 @@
    -
    +
    Name @@ -52,12 +52,12 @@ -
    -
    Testing....
    -
    Test results
    -
    -
    {{testing.title}}
    -
    +
    +
    Testing....
    +
    Test results
    +
    +
    {{ctrl.testing.title}}
    +
    diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json index e206d8be57d..3cdfb0049c9 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json @@ -14,7 +14,7 @@ "panels": [ { "type": "graph", - "datasource": "__$graphite" + "datasource": "$__graphite" } ] } From 749da42ffcb740d27d8d465ef3bdd84c35e06fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 23:29:45 +0100 Subject: [PATCH 092/113] updated lib --- Godeps/Godeps.json | 18 +- .../github.com/bmizerany/assert/.gitignore | 7 + .../src/github.com/bmizerany/assert/README.md | 45 +++ .../src/github.com/bmizerany/assert/assert.go | 76 ++++ .../bmizerany/assert/assert_test.go | 15 + .../bmizerany/assert/example/point.go | 5 + .../bmizerany/assert/example/point_test.go | 13 + .../src/github.com/gopherjs/gopherjs/js/js.go | 168 --------- .../src/github.com/kr/pretty/.gitignore | 4 + .../src/github.com/kr/pretty/License | 21 ++ .../src/github.com/kr/pretty/Readme | 9 + .../src/github.com/kr/pretty/diff.go | 158 ++++++++ .../src/github.com/kr/pretty/diff_test.go | 74 ++++ .../src/github.com/kr/pretty/example_test.go | 20 ++ .../src/github.com/kr/pretty/formatter.go | 337 ++++++++++++++++++ .../github.com/kr/pretty/formatter_test.go | 261 ++++++++++++++ .../src/github.com/kr/pretty/pretty.go | 98 +++++ .../src/github.com/kr/pretty/zero.go | 41 +++ .../_workspace/src/github.com/kr/text/License | 19 + .../_workspace/src/github.com/kr/text/Readme | 3 + .../src/github.com/kr/text/colwriter/Readme | 5 + .../github.com/kr/text/colwriter/column.go | 147 ++++++++ .../kr/text/colwriter/column_test.go | 90 +++++ .../_workspace/src/github.com/kr/text/doc.go | 3 + .../src/github.com/kr/text/indent.go | 74 ++++ .../src/github.com/kr/text/indent_test.go | 119 +++++++ .../src/github.com/kr/text/mc/Readme | 9 + .../src/github.com/kr/text/mc/mc.go | 62 ++++ .../_workspace/src/github.com/kr/text/wrap.go | 86 +++++ .../src/github.com/kr/text/wrap_test.go | 62 ++++ 30 files changed, 1877 insertions(+), 172 deletions(-) create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/README.md create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/assert.go create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go create mode 100644 Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go delete mode 100644 Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/.gitignore create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/License create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/Readme create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/diff.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/diff_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/example_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/formatter.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/pretty.go create mode 100644 Godeps/_workspace/src/github.com/kr/pretty/zero.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/License create mode 100644 Godeps/_workspace/src/github.com/kr/text/Readme create mode 100644 Godeps/_workspace/src/github.com/kr/text/colwriter/Readme create mode 100644 Godeps/_workspace/src/github.com/kr/text/colwriter/column.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/doc.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/indent.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/indent_test.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/mc/Readme create mode 100644 Godeps/_workspace/src/github.com/kr/text/mc/mc.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/wrap.go create mode 100644 Godeps/_workspace/src/github.com/kr/text/wrap_test.go diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index b75fdfb574a..bc1cb68bb5b 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -64,6 +64,11 @@ "Comment": "v1.0.0", "Rev": "abb928e07c4108683d6b4d0b6ca08fe6bc0eee5f" }, + { + "ImportPath": "github.com/bmizerany/assert", + "Comment": "release.r60-6-ge17e998", + "Rev": "e17e99893cb6509f428e1728281c2ad60a6b31e3" + }, { "ImportPath": "github.com/bradfitz/gomemcache/memcache", "Comment": "release.r60-40-g72a6864", @@ -123,10 +128,6 @@ "Comment": "v0.4.4-44-gf561133", "Rev": "f56113384f2c63dfe4cd8e768e349f1c35122b58" }, - { - "ImportPath": "github.com/gopherjs/gopherjs/js", - "Rev": "14d893dca2e4adb93a5ccc9494040acc0821cd8d" - }, { "ImportPath": "github.com/gosimple/slug", "Rev": "8d258463b4459f161f51d6a357edacd3eef9d663" @@ -160,6 +161,15 @@ "ImportPath": "github.com/klauspost/crc32", "Rev": "6834731faf32e62a2dd809d99fb24d1e4ae5a92d" }, + { + "ImportPath": "github.com/kr/pretty", + "Comment": "go.weekly.2011-12-22-27-ge6ac2fc", + "Rev": "e6ac2fc51e89a3249e82157fa0bb7a18ef9dd5bb" + }, + { + "ImportPath": "github.com/kr/text", + "Rev": "bb797dc4fb8320488f47bf11de07a733d7233e1f" + }, { "ImportPath": "github.com/lib/pq", "Comment": "go1.0-cutoff-13-g19eeca3", diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore b/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore new file mode 100644 index 00000000000..b6fadf4ebe7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/.gitignore @@ -0,0 +1,7 @@ +_go_.* +_gotest_.* +_obj +_test +_testmain.go +*.out +*.[568] diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/README.md b/Godeps/_workspace/src/github.com/bmizerany/assert/README.md new file mode 100644 index 00000000000..8b6b6fc4fc6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/README.md @@ -0,0 +1,45 @@ +# Assert (c) Blake Mizerany and Keith Rarick -- MIT LICENCE + +## Assertions for Go tests + +## Install + + $ go get github.com/bmizerany/assert + +## Use + +**point.go** + + package point + + type Point struct { + x, y int + } + +**point_test.go** + + + package point + + import ( + "testing" + "github.com/bmizerany/assert" + ) + + func TestAsserts(t *testing.T) { + p1 := Point{1, 1} + p2 := Point{2, 1} + + assert.Equal(t, p1, p2) + } + +**output** + $ go test + --- FAIL: TestAsserts (0.00 seconds) + assert.go:15: /Users/flavio.barbosa/dev/stewie/src/point_test.go:12 + assert.go:24: ! X: 1 != 2 + FAIL + +## Docs + + http://github.com/bmizerany/assert diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go b/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go new file mode 100644 index 00000000000..7409f985e80 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/assert.go @@ -0,0 +1,76 @@ +package assert +// Testing helpers for doozer. + +import ( + "github.com/kr/pretty" + "reflect" + "testing" + "runtime" + "fmt" +) + +func assert(t *testing.T, result bool, f func(), cd int) { + if !result { + _, file, line, _ := runtime.Caller(cd + 1) + t.Errorf("%s:%d", file, line) + f() + t.FailNow() + } +} + +func equal(t *testing.T, exp, got interface{}, cd int, args ...interface{}) { + fn := func() { + for _, desc := range pretty.Diff(exp, got) { + t.Error("!", desc) + } + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + result := reflect.DeepEqual(exp, got) + assert(t, result, fn, cd+1) +} + +func tt(t *testing.T, result bool, cd int, args ...interface{}) { + fn := func() { + t.Errorf("! Failure") + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + assert(t, result, fn, cd+1) +} + +func T(t *testing.T, result bool, args ...interface{}) { + tt(t, result, 1, args...) +} + +func Tf(t *testing.T, result bool, format string, args ...interface{}) { + tt(t, result, 1, fmt.Sprintf(format, args...)) +} + +func Equal(t *testing.T, exp, got interface{}, args ...interface{}) { + equal(t, exp, got, 1, args...) +} + +func Equalf(t *testing.T, exp, got interface{}, format string, args ...interface{}) { + equal(t, exp, got, 1, fmt.Sprintf(format, args...)) +} + +func NotEqual(t *testing.T, exp, got interface{}, args ...interface{}) { + fn := func() { + t.Errorf("! Unexpected: <%#v>", exp) + if len(args) > 0 { + t.Error("!", " -", fmt.Sprint(args...)) + } + } + result := !reflect.DeepEqual(exp, got) + assert(t, result, fn, 1) +} + +func Panic(t *testing.T, err interface{}, fn func()) { + defer func() { + equal(t, err, recover(), 3) + }() + fn() +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go b/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go new file mode 100644 index 00000000000..162a590c62f --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/assert_test.go @@ -0,0 +1,15 @@ +package assert + +import ( + "testing" +) + +func TestLineNumbers(t *testing.T) { + Equal(t, "foo", "foo", "msg!") + //Equal(t, "foo", "bar", "this should blow up") +} + +func TestNotEqual(t *testing.T) { + NotEqual(t, "foo", "bar", "msg!") + //NotEqual(t, "foo", "foo", "this should blow up") +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go new file mode 100644 index 00000000000..15789fe10f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point.go @@ -0,0 +1,5 @@ +package point + +type Point struct { + X, Y int +} diff --git a/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go new file mode 100644 index 00000000000..34e791a43c9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/bmizerany/assert/example/point_test.go @@ -0,0 +1,13 @@ +package point + +import ( + "testing" + "assert" +) + +func TestAsserts(t *testing.T) { + p1 := Point{1, 1} + p2 := Point{2, 1} + + assert.Equal(t, p1, p2) +} diff --git a/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go b/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go deleted file mode 100644 index 5367d3d0fa7..00000000000 --- a/Godeps/_workspace/src/github.com/gopherjs/gopherjs/js/js.go +++ /dev/null @@ -1,168 +0,0 @@ -// Package js provides functions for interacting with native JavaScript APIs. Calls to these functions are treated specially by GopherJS and translated directly to their corresponding JavaScript syntax. -// -// Use MakeWrapper to expose methods to JavaScript. When passing values directly, the following type conversions are performed: -// -// | Go type | JavaScript type | Conversions back to interface{} | -// | --------------------- | --------------------- | ------------------------------- | -// | bool | Boolean | bool | -// | integers and floats | Number | float64 | -// | string | String | string | -// | []int8 | Int8Array | []int8 | -// | []int16 | Int16Array | []int16 | -// | []int32, []int | Int32Array | []int | -// | []uint8 | Uint8Array | []uint8 | -// | []uint16 | Uint16Array | []uint16 | -// | []uint32, []uint | Uint32Array | []uint | -// | []float32 | Float32Array | []float32 | -// | []float64 | Float64Array | []float64 | -// | all other slices | Array | []interface{} | -// | arrays | see slice type | see slice type | -// | functions | Function | func(...interface{}) *js.Object | -// | time.Time | Date | time.Time | -// | - | instanceof Node | *js.Object | -// | maps, structs | instanceof Object | map[string]interface{} | -// -// Additionally, for a struct containing a *js.Object field, only the content of the field will be passed to JavaScript and vice versa. -package js - -// Object is a container for a native JavaScript object. Calls to its methods are treated specially by GopherJS and translated directly to their JavaScript syntax. A nil pointer to Object is equal to JavaScript's "null". Object can not be used as a map key. -type Object struct{ object *Object } - -// Get returns the object's property with the given key. -func (o *Object) Get(key string) *Object { return o.object.Get(key) } - -// Set assigns the value to the object's property with the given key. -func (o *Object) Set(key string, value interface{}) { o.object.Set(key, value) } - -// Delete removes the object's property with the given key. -func (o *Object) Delete(key string) { o.object.Delete(key) } - -// Length returns the object's "length" property, converted to int. -func (o *Object) Length() int { return o.object.Length() } - -// Index returns the i'th element of an array. -func (o *Object) Index(i int) *Object { return o.object.Index(i) } - -// SetIndex sets the i'th element of an array. -func (o *Object) SetIndex(i int, value interface{}) { o.object.SetIndex(i, value) } - -// Call calls the object's method with the given name. -func (o *Object) Call(name string, args ...interface{}) *Object { return o.object.Call(name, args...) } - -// Invoke calls the object itself. This will fail if it is not a function. -func (o *Object) Invoke(args ...interface{}) *Object { return o.object.Invoke(args...) } - -// New creates a new instance of this type object. This will fail if it not a function (constructor). -func (o *Object) New(args ...interface{}) *Object { return o.object.New(args...) } - -// Bool returns the object converted to bool according to JavaScript type conversions. -func (o *Object) Bool() bool { return o.object.Bool() } - -// String returns the object converted to string according to JavaScript type conversions. -func (o *Object) String() string { return o.object.String() } - -// Int returns the object converted to int according to JavaScript type conversions (parseInt). -func (o *Object) Int() int { return o.object.Int() } - -// Int64 returns the object converted to int64 according to JavaScript type conversions (parseInt). -func (o *Object) Int64() int64 { return o.object.Int64() } - -// Uint64 returns the object converted to uint64 according to JavaScript type conversions (parseInt). -func (o *Object) Uint64() uint64 { return o.object.Uint64() } - -// Float returns the object converted to float64 according to JavaScript type conversions (parseFloat). -func (o *Object) Float() float64 { return o.object.Float() } - -// Interface returns the object converted to interface{}. See GopherJS' README for details. -func (o *Object) Interface() interface{} { return o.object.Interface() } - -// Unsafe returns the object as an uintptr, which can be converted via unsafe.Pointer. Not intended for public use. -func (o *Object) Unsafe() uintptr { return o.object.Unsafe() } - -// Error encapsulates JavaScript errors. Those are turned into a Go panic and may be recovered, giving an *Error that holds the JavaScript error object. -type Error struct { - *Object -} - -// Error returns the message of the encapsulated JavaScript error object. -func (err *Error) Error() string { - return "JavaScript error: " + err.Get("message").String() -} - -// Stack returns the stack property of the encapsulated JavaScript error object. -func (err *Error) Stack() string { - return err.Get("stack").String() -} - -// Global gives JavaScript's global object ("window" for browsers and "GLOBAL" for Node.js). -var Global *Object - -// Module gives the value of the "module" variable set by Node.js. Hint: Set a module export with 'js.Module.Get("exports").Set("exportName", ...)'. -var Module *Object - -// Undefined gives the JavaScript value "undefined". -var Undefined *Object - -// Debugger gets compiled to JavaScript's "debugger;" statement. -func Debugger() {} - -// InternalObject returns the internal JavaScript object that represents i. Not intended for public use. -func InternalObject(i interface{}) *Object { - return nil -} - -// MakeFunc wraps a function and gives access to the values of JavaScript's "this" and "arguments" keywords. -func MakeFunc(func(this *Object, arguments []*Object) interface{}) *Object { - return nil -} - -// Keys returns the keys of the given JavaScript object. -func Keys(o *Object) []string { - if o == nil || o == Undefined { - return nil - } - a := Global.Get("Object").Call("keys", o) - s := make([]string, a.Length()) - for i := 0; i < a.Length(); i++ { - s[i] = a.Index(i).String() - } - return s -} - -// MakeWrapper creates a JavaScript object which has wrappers for the exported methods of i. Use explicit getter and setter methods to expose struct fields to JavaScript. -func MakeWrapper(i interface{}) *Object { - v := InternalObject(i) - o := Global.Get("Object").New() - o.Set("__internal_object__", v) - methods := v.Get("constructor").Get("methods") - for i := 0; i < methods.Length(); i++ { - m := methods.Index(i) - if m.Get("pkg").String() != "" { // not exported - continue - } - o.Set(m.Get("name").String(), func(args ...*Object) *Object { - return Global.Call("$externalizeFunction", v.Get(m.Get("prop").String()), m.Get("typ"), true).Call("apply", v, args) - }) - } - return o -} - -// NewArrayBuffer creates a JavaScript ArrayBuffer from a byte slice. -func NewArrayBuffer(b []byte) *Object { - slice := InternalObject(b) - offset := slice.Get("$offset").Int() - length := slice.Get("$length").Int() - return slice.Get("$array").Get("buffer").Call("slice", offset, offset+length) -} - -// M is a simple map type. It is intended as a shorthand for JavaScript objects (before conversion). -type M map[string]interface{} - -// S is a simple slice type. It is intended as a shorthand for JavaScript arrays (before conversion). -type S []interface{} - -func init() { - // avoid dead code elimination - e := Error{} - _ = e -} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/.gitignore b/Godeps/_workspace/src/github.com/kr/pretty/.gitignore new file mode 100644 index 00000000000..1f0a99f2f2b --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/.gitignore @@ -0,0 +1,4 @@ +[568].out +_go* +_test* +_obj diff --git a/Godeps/_workspace/src/github.com/kr/pretty/License b/Godeps/_workspace/src/github.com/kr/pretty/License new file mode 100644 index 00000000000..05c783ccf68 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/License @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/pretty/Readme b/Godeps/_workspace/src/github.com/kr/pretty/Readme new file mode 100644 index 00000000000..c589fc622b3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/Readme @@ -0,0 +1,9 @@ +package pretty + + import "github.com/kr/pretty" + + Package pretty provides pretty-printing for Go values. + +Documentation + + http://godoc.org/github.com/kr/pretty diff --git a/Godeps/_workspace/src/github.com/kr/pretty/diff.go b/Godeps/_workspace/src/github.com/kr/pretty/diff.go new file mode 100644 index 00000000000..8fe8e2405a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/diff.go @@ -0,0 +1,158 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" +) + +type sbuf []string + +func (s *sbuf) Write(b []byte) (int, error) { + *s = append(*s, string(b)) + return len(b), nil +} + +// Diff returns a slice where each element describes +// a difference between a and b. +func Diff(a, b interface{}) (desc []string) { + Fdiff((*sbuf)(&desc), a, b) + return desc +} + +// Fdiff writes to w a description of the differences between a and b. +func Fdiff(w io.Writer, a, b interface{}) { + diffWriter{w: w}.diff(reflect.ValueOf(a), reflect.ValueOf(b)) +} + +type diffWriter struct { + w io.Writer + l string // label +} + +func (w diffWriter) printf(f string, a ...interface{}) { + var l string + if w.l != "" { + l = w.l + ": " + } + fmt.Fprintf(w.w, l+f, a...) +} + +func (w diffWriter) diff(av, bv reflect.Value) { + if !av.IsValid() && bv.IsValid() { + w.printf("nil != %#v", bv.Interface()) + return + } + if av.IsValid() && !bv.IsValid() { + w.printf("%#v != nil", av.Interface()) + return + } + if !av.IsValid() && !bv.IsValid() { + return + } + + at := av.Type() + bt := bv.Type() + if at != bt { + w.printf("%v != %v", at, bt) + return + } + + // numeric types, including bool + if at.Kind() < reflect.Array { + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%#v != %#v", a, b) + } + return + } + + switch at.Kind() { + case reflect.String: + a, b := av.Interface(), bv.Interface() + if a != b { + w.printf("%q != %q", a, b) + } + case reflect.Ptr: + switch { + case av.IsNil() && !bv.IsNil(): + w.printf("nil != %v", bv.Interface()) + case !av.IsNil() && bv.IsNil(): + w.printf("%v != nil", av.Interface()) + case !av.IsNil() && !bv.IsNil(): + w.diff(av.Elem(), bv.Elem()) + } + case reflect.Struct: + for i := 0; i < av.NumField(); i++ { + w.relabel(at.Field(i).Name).diff(av.Field(i), bv.Field(i)) + } + case reflect.Slice: + lenA := av.Len() + lenB := bv.Len() + if lenA != lenB { + w.printf("%s[%d] != %s[%d]", av.Type(), lenA, bv.Type(), lenB) + break + } + for i := 0; i < lenA; i++ { + w.relabel(fmt.Sprintf("[%d]", i)).diff(av.Index(i), bv.Index(i)) + } + case reflect.Map: + ak, both, bk := keyDiff(av.MapKeys(), bv.MapKeys()) + for _, k := range ak { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("%q != (missing)", av.MapIndex(k)) + } + for _, k := range both { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.diff(av.MapIndex(k), bv.MapIndex(k)) + } + for _, k := range bk { + w := w.relabel(fmt.Sprintf("[%#v]", k.Interface())) + w.printf("(missing) != %q", bv.MapIndex(k)) + } + case reflect.Interface: + w.diff(reflect.ValueOf(av.Interface()), reflect.ValueOf(bv.Interface())) + default: + if !reflect.DeepEqual(av.Interface(), bv.Interface()) { + w.printf("%# v != %# v", Formatter(av.Interface()), Formatter(bv.Interface())) + } + } +} + +func (d diffWriter) relabel(name string) (d1 diffWriter) { + d1 = d + if d.l != "" && name[0] != '[' { + d1.l += "." + } + d1.l += name + return d1 +} + +func keyDiff(a, b []reflect.Value) (ak, both, bk []reflect.Value) { + for _, av := range a { + inBoth := false + for _, bv := range b { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + both = append(both, av) + break + } + } + if !inBoth { + ak = append(ak, av) + } + } + for _, bv := range b { + inBoth := false + for _, av := range a { + if reflect.DeepEqual(av.Interface(), bv.Interface()) { + inBoth = true + break + } + } + if !inBoth { + bk = append(bk, bv) + } + } + return +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go b/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go new file mode 100644 index 00000000000..3c388f13ca7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/diff_test.go @@ -0,0 +1,74 @@ +package pretty + +import ( + "testing" +) + +type difftest struct { + a interface{} + b interface{} + exp []string +} + +type S struct { + A int + S *S + I interface{} + C []int +} + +var diffs = []difftest{ + {a: nil, b: nil}, + {a: S{A: 1}, b: S{A: 1}}, + + {0, "", []string{`int != string`}}, + {0, 1, []string{`0 != 1`}}, + {S{}, new(S), []string{`pretty.S != *pretty.S`}}, + {"a", "b", []string{`"a" != "b"`}}, + {S{}, S{A: 1}, []string{`A: 0 != 1`}}, + {new(S), &S{A: 1}, []string{`A: 0 != 1`}}, + {S{S: new(S)}, S{S: &S{A: 1}}, []string{`S.A: 0 != 1`}}, + {S{}, S{I: 0}, []string{`I: nil != 0`}}, + {S{I: 1}, S{I: "x"}, []string{`I: int != string`}}, + {S{}, S{C: []int{1}}, []string{`C: []int[0] != []int[1]`}}, + {S{C: []int{}}, S{C: []int{1}}, []string{`C: []int[0] != []int[1]`}}, + {S{C: []int{1, 2, 3}}, S{C: []int{1, 2, 4}}, []string{`C[2]: 3 != 4`}}, + {S{}, S{A: 1, S: new(S)}, []string{`A: 0 != 1`, `S: nil != &{0 []}`}}, +} + +func TestDiff(t *testing.T) { + for _, tt := range diffs { + got := Diff(tt.a, tt.b) + eq := len(got) == len(tt.exp) + if eq { + for i := range got { + eq = eq && got[i] == tt.exp[i] + } + } + if !eq { + t.Errorf("diffing % #v", tt.a) + t.Errorf("with % #v", tt.b) + diffdiff(t, got, tt.exp) + continue + } + } +} + +func diffdiff(t *testing.T, got, exp []string) { + minus(t, "unexpected:", got, exp) + minus(t, "missing:", exp, got) +} + +func minus(t *testing.T, s string, a, b []string) { + var i, j int + for i = 0; i < len(a); i++ { + for j = 0; j < len(b); j++ { + if a[i] == b[j] { + break + } + } + if j == len(b) { + t.Error(s, a[i]) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/example_test.go b/Godeps/_workspace/src/github.com/kr/pretty/example_test.go new file mode 100644 index 00000000000..ecf40f3fcc6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/example_test.go @@ -0,0 +1,20 @@ +package pretty_test + +import ( + "fmt" + "github.com/kr/pretty" +) + +func Example() { + type myType struct { + a, b int + } + var x = []myType{{1, 2}, {3, 4}, {5, 6}} + fmt.Printf("%# v", pretty.Formatter(x)) + // output: + // []pretty_test.myType{ + // {a:1, b:2}, + // {a:3, b:4}, + // {a:5, b:6}, + // } +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/formatter.go b/Godeps/_workspace/src/github.com/kr/pretty/formatter.go new file mode 100644 index 00000000000..8dacda25fa8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/formatter.go @@ -0,0 +1,337 @@ +package pretty + +import ( + "fmt" + "io" + "reflect" + "strconv" + "text/tabwriter" + + "github.com/kr/text" +) + +const ( + limit = 50 +) + +type formatter struct { + x interface{} + force bool + quote bool +} + +// Formatter makes a wrapper, f, that will format x as go source with line +// breaks and tabs. Object f responds to the "%v" formatting verb when both the +// "#" and " " (space) flags are set, for example: +// +// fmt.Sprintf("%# v", Formatter(x)) +// +// If one of these two flags is not set, or any other verb is used, f will +// format x according to the usual rules of package fmt. +// In particular, if x satisfies fmt.Formatter, then x.Format will be called. +func Formatter(x interface{}) (f fmt.Formatter) { + return formatter{x: x, quote: true} +} + +func (fo formatter) String() string { + return fmt.Sprint(fo.x) // unwrap it +} + +func (fo formatter) passThrough(f fmt.State, c rune) { + s := "%" + for i := 0; i < 128; i++ { + if f.Flag(i) { + s += string(i) + } + } + if w, ok := f.Width(); ok { + s += fmt.Sprintf("%d", w) + } + if p, ok := f.Precision(); ok { + s += fmt.Sprintf(".%d", p) + } + s += string(c) + fmt.Fprintf(f, s, fo.x) +} + +func (fo formatter) Format(f fmt.State, c rune) { + if fo.force || c == 'v' && f.Flag('#') && f.Flag(' ') { + w := tabwriter.NewWriter(f, 4, 4, 1, ' ', 0) + p := &printer{tw: w, Writer: w, visited: make(map[visit]int)} + p.printValue(reflect.ValueOf(fo.x), true, fo.quote) + w.Flush() + return + } + fo.passThrough(f, c) +} + +type printer struct { + io.Writer + tw *tabwriter.Writer + visited map[visit]int + depth int +} + +func (p *printer) indent() *printer { + q := *p + q.tw = tabwriter.NewWriter(p.Writer, 4, 4, 1, ' ', 0) + q.Writer = text.NewIndentWriter(q.tw, []byte{'\t'}) + return &q +} + +func (p *printer) printInline(v reflect.Value, x interface{}, showType bool) { + if showType { + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, "(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } +} + +// printValue must keep track of already-printed pointer values to avoid +// infinite recursion. +type visit struct { + v uintptr + typ reflect.Type +} + +func (p *printer) printValue(v reflect.Value, showType, quote bool) { + if p.depth > 10 { + io.WriteString(p, "!%v(DEPTH EXCEEDED)") + return + } + + switch v.Kind() { + case reflect.Bool: + p.printInline(v, v.Bool(), showType) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + p.printInline(v, v.Int(), showType) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + p.printInline(v, v.Uint(), showType) + case reflect.Float32, reflect.Float64: + p.printInline(v, v.Float(), showType) + case reflect.Complex64, reflect.Complex128: + fmt.Fprintf(p, "%#v", v.Complex()) + case reflect.String: + p.fmtString(v.String(), quote) + case reflect.Map: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + keys := v.MapKeys() + for i := 0; i < v.Len(); i++ { + showTypeInStruct := true + k := keys[i] + mv := v.MapIndex(k) + pp.printValue(k, false, true) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = t.Elem().Kind() == reflect.Interface + pp.printValue(mv, showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Struct: + t := v.Type() + if v.CanAddr() { + addr := v.UnsafeAddr() + vis := visit{addr, t} + if vd, ok := p.visited[vis]; ok && vd < p.depth { + p.fmtString(t.String()+"{(CYCLIC REFERENCE)}", false) + break // don't print v again + } + p.visited[vis] = p.depth + } + + if showType { + io.WriteString(p, t.String()) + } + writeByte(p, '{') + if nonzero(v) { + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.NumField(); i++ { + showTypeInStruct := true + if f := t.Field(i); f.Name != "" { + io.WriteString(pp, f.Name) + writeByte(pp, ':') + if expand { + writeByte(pp, '\t') + } + showTypeInStruct = labelType(f.Type) + } + pp.printValue(getField(v, i), showTypeInStruct, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.NumField()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + } + writeByte(p, '}') + case reflect.Interface: + switch e := v.Elem(); { + case e.Kind() == reflect.Invalid: + io.WriteString(p, "nil") + case e.IsValid(): + pp := *p + pp.depth++ + pp.printValue(e, showType, true) + default: + io.WriteString(p, v.Type().String()) + io.WriteString(p, "(nil)") + } + case reflect.Array, reflect.Slice: + t := v.Type() + if showType { + io.WriteString(p, t.String()) + } + if v.Kind() == reflect.Slice && v.IsNil() && showType { + io.WriteString(p, "(nil)") + break + } + if v.Kind() == reflect.Slice && v.IsNil() { + io.WriteString(p, "nil") + break + } + writeByte(p, '{') + expand := !canInline(v.Type()) + pp := p + if expand { + writeByte(p, '\n') + pp = p.indent() + } + for i := 0; i < v.Len(); i++ { + showTypeInSlice := t.Elem().Kind() == reflect.Interface + pp.printValue(v.Index(i), showTypeInSlice, true) + if expand { + io.WriteString(pp, ",\n") + } else if i < v.Len()-1 { + io.WriteString(pp, ", ") + } + } + if expand { + pp.tw.Flush() + } + writeByte(p, '}') + case reflect.Ptr: + e := v.Elem() + if !e.IsValid() { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + io.WriteString(p, ")(nil)") + } else { + pp := *p + pp.depth++ + writeByte(pp, '&') + pp.printValue(e, true, true) + } + case reflect.Chan: + x := v.Pointer() + if showType { + writeByte(p, '(') + io.WriteString(p, v.Type().String()) + fmt.Fprintf(p, ")(%#v)", x) + } else { + fmt.Fprintf(p, "%#v", x) + } + case reflect.Func: + io.WriteString(p, v.Type().String()) + io.WriteString(p, " {...}") + case reflect.UnsafePointer: + p.printInline(v, v.Pointer(), showType) + case reflect.Invalid: + io.WriteString(p, "nil") + } +} + +func canInline(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map: + return !canExpand(t.Elem()) + case reflect.Struct: + for i := 0; i < t.NumField(); i++ { + if canExpand(t.Field(i).Type) { + return false + } + } + return true + case reflect.Interface: + return false + case reflect.Array, reflect.Slice: + return !canExpand(t.Elem()) + case reflect.Ptr: + return false + case reflect.Chan, reflect.Func, reflect.UnsafePointer: + return false + } + return true +} + +func canExpand(t reflect.Type) bool { + switch t.Kind() { + case reflect.Map, reflect.Struct, + reflect.Interface, reflect.Array, reflect.Slice, + reflect.Ptr: + return true + } + return false +} + +func labelType(t reflect.Type) bool { + switch t.Kind() { + case reflect.Interface, reflect.Struct: + return true + } + return false +} + +func (p *printer) fmtString(s string, quote bool) { + if quote { + s = strconv.Quote(s) + } + io.WriteString(p, s) +} + +func tryDeepEqual(a, b interface{}) bool { + defer func() { recover() }() + return reflect.DeepEqual(a, b) +} + +func writeByte(w io.Writer, b byte) { + w.Write([]byte{b}) +} + +func getField(v reflect.Value, i int) reflect.Value { + val := v.Field(i) + if val.Kind() == reflect.Interface && !val.IsNil() { + val = val.Elem() + } + return val +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go b/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go new file mode 100644 index 00000000000..5f3204e8e87 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/formatter_test.go @@ -0,0 +1,261 @@ +package pretty + +import ( + "fmt" + "io" + "strings" + "testing" + "unsafe" +) + +type test struct { + v interface{} + s string +} + +type LongStructTypeName struct { + longFieldName interface{} + otherLongFieldName interface{} +} + +type SA struct { + t *T + v T +} + +type T struct { + x, y int +} + +type F int + +func (f F) Format(s fmt.State, c rune) { + fmt.Fprintf(s, "F(%d)", int(f)) +} + +var long = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var gosyntax = []test{ + {nil, `nil`}, + {"", `""`}, + {"a", `"a"`}, + {1, "int(1)"}, + {1.0, "float64(1)"}, + {[]int(nil), "[]int(nil)"}, + {[0]int{}, "[0]int{}"}, + {complex(1, 0), "(1+0i)"}, + //{make(chan int), "(chan int)(0x1234)"}, + {unsafe.Pointer(uintptr(unsafe.Pointer(&long))), fmt.Sprintf("unsafe.Pointer(0x%02x)", uintptr(unsafe.Pointer(&long)))}, + {func(int) {}, "func(int) {...}"}, + {map[int]int{1: 1}, "map[int]int{1:1}"}, + {int32(1), "int32(1)"}, + {io.EOF, `&errors.errorString{s:"EOF"}`}, + {[]string{"a"}, `[]string{"a"}`}, + { + []string{long}, + `[]string{"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"}`, + }, + {F(5), "pretty.F(5)"}, + { + SA{&T{1, 2}, T{3, 4}}, + `pretty.SA{ + t: &pretty.T{x:1, y:2}, + v: pretty.T{x:3, y:4}, +}`, + }, + { + map[int][]byte{1: {}}, + `map[int][]uint8{ + 1: {}, +}`, + }, + { + map[int]T{1: {}}, + `map[int]pretty.T{ + 1: {}, +}`, + }, + { + long, + `"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"`, + }, + { + LongStructTypeName{ + longFieldName: LongStructTypeName{}, + otherLongFieldName: long, + }, + `pretty.LongStructTypeName{ + longFieldName: pretty.LongStructTypeName{}, + otherLongFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", +}`, + }, + { + &LongStructTypeName{ + longFieldName: &LongStructTypeName{}, + otherLongFieldName: (*LongStructTypeName)(nil), + }, + `&pretty.LongStructTypeName{ + longFieldName: &pretty.LongStructTypeName{}, + otherLongFieldName: (*pretty.LongStructTypeName)(nil), +}`, + }, + { + []LongStructTypeName{ + {nil, nil}, + {3, 3}, + {long, nil}, + }, + `[]pretty.LongStructTypeName{ + {}, + { + longFieldName: int(3), + otherLongFieldName: int(3), + }, + { + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, + { + []interface{}{ + LongStructTypeName{nil, nil}, + []byte{1, 2, 3}, + T{3, 4}, + LongStructTypeName{long, nil}, + }, + `[]interface {}{ + pretty.LongStructTypeName{}, + []uint8{0x1, 0x2, 0x3}, + pretty.T{x:3, y:4}, + pretty.LongStructTypeName{ + longFieldName: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", + otherLongFieldName: nil, + }, +}`, + }, +} + +func TestGoSyntax(t *testing.T) { + for _, tt := range gosyntax { + s := fmt.Sprintf("%# v", Formatter(tt.v)) + if tt.s != s { + t.Errorf("expected %q", tt.s) + t.Errorf("got %q", s) + t.Errorf("expraw\n%s", tt.s) + t.Errorf("gotraw\n%s", s) + } + } +} + +type I struct { + i int + R interface{} +} + +func (i *I) I() *I { return i.R.(*I) } + +func TestCycle(t *testing.T) { + type A struct{ *A } + v := &A{} + v.A = v + + // panics from stack overflow without cycle detection + t.Logf("Example cycle:\n%# v", Formatter(v)) + + p := &A{} + s := fmt.Sprintf("%# v", Formatter([]*A{p, p})) + if strings.Contains(s, "CYCLIC") { + t.Errorf("Repeated address detected as cyclic reference:\n%s", s) + } + + type R struct { + i int + *R + } + r := &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + }, + }, + } + r.R.R.R = r + t.Logf("Example longer cycle:\n%# v", Formatter(r)) + + r = &R{ + i: 1, + R: &R{ + i: 2, + R: &R{ + i: 3, + R: &R{ + i: 4, + R: &R{ + i: 5, + R: &R{ + i: 6, + R: &R{ + i: 7, + R: &R{ + i: 8, + R: &R{ + i: 9, + R: &R{ + i: 10, + R: &R{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + // here be pirates + r.R.R.R.R.R.R.R.R.R.R.R = r + t.Logf("Example very long cycle:\n%# v", Formatter(r)) + + i := &I{ + i: 1, + R: &I{ + i: 2, + R: &I{ + i: 3, + R: &I{ + i: 4, + R: &I{ + i: 5, + R: &I{ + i: 6, + R: &I{ + i: 7, + R: &I{ + i: 8, + R: &I{ + i: 9, + R: &I{ + i: 10, + R: &I{ + i: 11, + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } + iv := i.I().I().I().I().I().I().I().I().I().I() + *iv = *i + t.Logf("Example long interface cycle:\n%# v", Formatter(i)) +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/pretty.go b/Godeps/_workspace/src/github.com/kr/pretty/pretty.go new file mode 100644 index 00000000000..d3df8686ce8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/pretty.go @@ -0,0 +1,98 @@ +// Package pretty provides pretty-printing for Go values. This is +// useful during debugging, to avoid wrapping long output lines in +// the terminal. +// +// It provides a function, Formatter, that can be used with any +// function that accepts a format string. It also provides +// convenience wrappers for functions in packages fmt and log. +package pretty + +import ( + "fmt" + "io" + "log" +) + +// Errorf is a convenience wrapper for fmt.Errorf. +// +// Calling Errorf(f, x, y) is equivalent to +// fmt.Errorf(f, Formatter(x), Formatter(y)). +func Errorf(format string, a ...interface{}) error { + return fmt.Errorf(format, wrap(a, false)...) +} + +// Fprintf is a convenience wrapper for fmt.Fprintf. +// +// Calling Fprintf(w, f, x, y) is equivalent to +// fmt.Fprintf(w, f, Formatter(x), Formatter(y)). +func Fprintf(w io.Writer, format string, a ...interface{}) (n int, error error) { + return fmt.Fprintf(w, format, wrap(a, false)...) +} + +// Log is a convenience wrapper for log.Printf. +// +// Calling Log(x, y) is equivalent to +// log.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Log(a ...interface{}) { + log.Print(wrap(a, true)...) +} + +// Logf is a convenience wrapper for log.Printf. +// +// Calling Logf(f, x, y) is equivalent to +// log.Printf(f, Formatter(x), Formatter(y)). +func Logf(format string, a ...interface{}) { + log.Printf(format, wrap(a, false)...) +} + +// Logln is a convenience wrapper for log.Printf. +// +// Calling Logln(x, y) is equivalent to +// log.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Logln(a ...interface{}) { + log.Println(wrap(a, true)...) +} + +// Print pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Print(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Print(a ...interface{}) (n int, errno error) { + return fmt.Print(wrap(a, true)...) +} + +// Printf is a convenience wrapper for fmt.Printf. +// +// Calling Printf(f, x, y) is equivalent to +// fmt.Printf(f, Formatter(x), Formatter(y)). +func Printf(format string, a ...interface{}) (n int, errno error) { + return fmt.Printf(format, wrap(a, false)...) +} + +// Println pretty-prints its operands and writes to standard output. +// +// Calling Print(x, y) is equivalent to +// fmt.Println(Formatter(x), Formatter(y)), but each operand is +// formatted with "%# v". +func Println(a ...interface{}) (n int, errno error) { + return fmt.Println(wrap(a, true)...) +} + +// Sprintf is a convenience wrapper for fmt.Sprintf. +// +// Calling Sprintf(f, x, y) is equivalent to +// fmt.Sprintf(f, Formatter(x), Formatter(y)). +func Sprintf(format string, a ...interface{}) string { + return fmt.Sprintf(format, wrap(a, false)...) +} + +func wrap(a []interface{}, force bool) []interface{} { + w := make([]interface{}, len(a)) + for i, x := range a { + w[i] = formatter{x: x, force: force} + } + return w +} diff --git a/Godeps/_workspace/src/github.com/kr/pretty/zero.go b/Godeps/_workspace/src/github.com/kr/pretty/zero.go new file mode 100644 index 00000000000..abb5b6fc14c --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/pretty/zero.go @@ -0,0 +1,41 @@ +package pretty + +import ( + "reflect" +) + +func nonzero(v reflect.Value) bool { + switch v.Kind() { + case reflect.Bool: + return v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() != 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() != 0 + case reflect.Float32, reflect.Float64: + return v.Float() != 0 + case reflect.Complex64, reflect.Complex128: + return v.Complex() != complex(0, 0) + case reflect.String: + return v.String() != "" + case reflect.Struct: + for i := 0; i < v.NumField(); i++ { + if nonzero(getField(v, i)) { + return true + } + } + return false + case reflect.Array: + for i := 0; i < v.Len(); i++ { + if nonzero(v.Index(i)) { + return true + } + } + return false + case reflect.Map, reflect.Interface, reflect.Slice, reflect.Ptr, reflect.Chan, reflect.Func: + return !v.IsNil() + case reflect.UnsafePointer: + return v.Pointer() != 0 + } + return true +} diff --git a/Godeps/_workspace/src/github.com/kr/text/License b/Godeps/_workspace/src/github.com/kr/text/License new file mode 100644 index 00000000000..480a3280599 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/License @@ -0,0 +1,19 @@ +Copyright 2012 Keith Rarick + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/kr/text/Readme b/Godeps/_workspace/src/github.com/kr/text/Readme new file mode 100644 index 00000000000..7e6e7c0687b --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/Readme @@ -0,0 +1,3 @@ +This is a Go package for manipulating paragraphs of text. + +See http://go.pkgdoc.org/github.com/kr/text for full documentation. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme new file mode 100644 index 00000000000..1c1f4e68393 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/Readme @@ -0,0 +1,5 @@ +Package colwriter provides a write filter that formats +input lines in multiple columns. + +The package is a straightforward translation from +/src/cmd/draw/mc.c in Plan 9 from User Space. diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go new file mode 100644 index 00000000000..7302ce9f7a8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column.go @@ -0,0 +1,147 @@ +// Package colwriter provides a write filter that formats +// input lines in multiple columns. +// +// The package is a straightforward translation from +// /src/cmd/draw/mc.c in Plan 9 from User Space. +package colwriter + +import ( + "bytes" + "io" + "unicode/utf8" +) + +const ( + tab = 4 +) + +const ( + // Print each input line ending in a colon ':' separately. + BreakOnColon uint = 1 << iota +) + +// A Writer is a filter that arranges input lines in as many columns as will +// fit in its width. Tab '\t' chars in the input are translated to sequences +// of spaces ending at multiples of 4 positions. +// +// If BreakOnColon is set, each input line ending in a colon ':' is written +// separately. +// +// The Writer assumes that all Unicode code points have the same width; this +// may not be true in some fonts. +type Writer struct { + w io.Writer + buf []byte + width int + flag uint +} + +// NewWriter allocates and initializes a new Writer writing to w. +// Parameter width controls the total number of characters on each line +// across all columns. +func NewWriter(w io.Writer, width int, flag uint) *Writer { + return &Writer{ + w: w, + width: width, + flag: flag, + } +} + +// Write writes p to the writer w. The only errors returned are ones +// encountered while writing to the underlying output stream. +func (w *Writer) Write(p []byte) (n int, err error) { + var linelen int + var lastWasColon bool + for i, c := range p { + w.buf = append(w.buf, c) + linelen++ + if c == '\t' { + w.buf[len(w.buf)-1] = ' ' + for linelen%tab != 0 { + w.buf = append(w.buf, ' ') + linelen++ + } + } + if w.flag&BreakOnColon != 0 && c == ':' { + lastWasColon = true + } else if lastWasColon { + if c == '\n' { + pos := bytes.LastIndex(w.buf[:len(w.buf)-1], []byte{'\n'}) + if pos < 0 { + pos = 0 + } + line := w.buf[pos:] + w.buf = w.buf[:pos] + if err = w.columnate(); err != nil { + if len(line) < i { + return i - len(line), err + } + return 0, err + } + if n, err := w.w.Write(line); err != nil { + if r := len(line) - n; r < i { + return i - r, err + } + return 0, err + } + } + lastWasColon = false + } + if c == '\n' { + linelen = 0 + } + } + return len(p), nil +} + +// Flush should be called after the last call to Write to ensure that any data +// buffered in the Writer is written to output. +func (w *Writer) Flush() error { + return w.columnate() +} + +func (w *Writer) columnate() error { + words := bytes.Split(w.buf, []byte{'\n'}) + w.buf = nil + if len(words[len(words)-1]) == 0 { + words = words[:len(words)-1] + } + maxwidth := 0 + for _, wd := range words { + if n := utf8.RuneCount(wd); n > maxwidth { + maxwidth = n + } + } + maxwidth++ // space char + wordsPerLine := w.width / maxwidth + if wordsPerLine <= 0 { + wordsPerLine = 1 + } + nlines := (len(words) + wordsPerLine - 1) / wordsPerLine + for i := 0; i < nlines; i++ { + col := 0 + endcol := 0 + for j := i; j < len(words); j += nlines { + endcol += maxwidth + _, err := w.w.Write(words[j]) + if err != nil { + return err + } + col += utf8.RuneCount(words[j]) + if j+nlines < len(words) { + for col < endcol { + _, err := w.w.Write([]byte{' '}) + if err != nil { + return err + } + col++ + } + } + } + _, err := w.w.Write([]byte{'\n'}) + if err != nil { + return err + } + } + return nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go new file mode 100644 index 00000000000..ce388f5a260 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/colwriter/column_test.go @@ -0,0 +1,90 @@ +package colwriter + +import ( + "bytes" + "testing" +) + +var src = ` +.git +.gitignore +.godir +Procfile: +README.md +api.go +apps.go +auth.go +darwin.go +data.go +dyno.go: +env.go +git.go +help.go +hkdist +linux.go +ls.go +main.go +plugin.go +run.go +scale.go +ssh.go +tail.go +term +unix.go +update.go +version.go +windows.go +`[1:] + +var tests = []struct { + wid int + flag uint + src string + want string +}{ + {80, 0, "", ""}, + {80, 0, src, ` +.git README.md darwin.go git.go ls.go scale.go unix.go +.gitignore api.go data.go help.go main.go ssh.go update.go +.godir apps.go dyno.go: hkdist plugin.go tail.go version.go +Procfile: auth.go env.go linux.go run.go term windows.go +`[1:]}, + {80, BreakOnColon, src, ` +.git .gitignore .godir + +Procfile: +README.md api.go apps.go auth.go darwin.go data.go + +dyno.go: +env.go hkdist main.go scale.go term version.go +git.go linux.go plugin.go ssh.go unix.go windows.go +help.go ls.go run.go tail.go update.go +`[1:]}, + {20, 0, ` +Hello +Γειά σου +안녕 +今日は +`[1:], ` +Hello 안녕 +Γειά σου 今日は +`[1:]}, +} + +func TestWriter(t *testing.T) { + for _, test := range tests { + b := new(bytes.Buffer) + w := NewWriter(b, test.wid, test.flag) + if _, err := w.Write([]byte(test.src)); err != nil { + t.Error(err) + } + if err := w.Flush(); err != nil { + t.Error(err) + } + if g := b.String(); test.want != g { + t.Log("\n" + test.want) + t.Log("\n" + g) + t.Errorf("%q != %q", test.want, g) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/doc.go b/Godeps/_workspace/src/github.com/kr/text/doc.go new file mode 100644 index 00000000000..cf4c198f955 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/doc.go @@ -0,0 +1,3 @@ +// Package text provides rudimentary functions for manipulating text in +// paragraphs. +package text diff --git a/Godeps/_workspace/src/github.com/kr/text/indent.go b/Godeps/_workspace/src/github.com/kr/text/indent.go new file mode 100644 index 00000000000..4ebac45c092 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent.go @@ -0,0 +1,74 @@ +package text + +import ( + "io" +) + +// Indent inserts prefix at the beginning of each non-empty line of s. The +// end-of-line marker is NL. +func Indent(s, prefix string) string { + return string(IndentBytes([]byte(s), []byte(prefix))) +} + +// IndentBytes inserts prefix at the beginning of each non-empty line of b. +// The end-of-line marker is NL. +func IndentBytes(b, prefix []byte) []byte { + var res []byte + bol := true + for _, c := range b { + if bol && c != '\n' { + res = append(res, prefix...) + } + res = append(res, c) + bol = c == '\n' + } + return res +} + +// Writer indents each line of its input. +type indentWriter struct { + w io.Writer + bol bool + pre [][]byte + sel int + off int +} + +// NewIndentWriter makes a new write filter that indents the input +// lines. Each line is prefixed in order with the corresponding +// element of pre. If there are more lines than elements, the last +// element of pre is repeated for each subsequent line. +func NewIndentWriter(w io.Writer, pre ...[]byte) io.Writer { + return &indentWriter{ + w: w, + pre: pre, + bol: true, + } +} + +// The only errors returned are from the underlying indentWriter. +func (w *indentWriter) Write(p []byte) (n int, err error) { + for _, c := range p { + if w.bol { + var i int + i, err = w.w.Write(w.pre[w.sel][w.off:]) + w.off += i + if err != nil { + return n, err + } + } + _, err = w.w.Write([]byte{c}) + if err != nil { + return n, err + } + n++ + w.bol = c == '\n' + if w.bol { + w.off = 0 + if w.sel < len(w.pre)-1 { + w.sel++ + } + } + } + return n, nil +} diff --git a/Godeps/_workspace/src/github.com/kr/text/indent_test.go b/Godeps/_workspace/src/github.com/kr/text/indent_test.go new file mode 100644 index 00000000000..5c723eee855 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/indent_test.go @@ -0,0 +1,119 @@ +package text + +import ( + "bytes" + "testing" +) + +type T struct { + inp, exp, pre string +} + +var tests = []T{ + { + "The quick brown fox\njumps over the lazy\ndog.\nBut not quickly.\n", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\nxxxBut not quickly.\n", + "xxx", + }, + { + "The quick brown fox\njumps over the lazy\ndog.\n\nBut not quickly.", + "xxxThe quick brown fox\nxxxjumps over the lazy\nxxxdog.\n\nxxxBut not quickly.", + "xxx", + }, +} + +func TestIndent(t *testing.T) { + for _, test := range tests { + got := Indent(test.inp, test.pre) + if got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + } + } +} + +type IndentWriterTest struct { + inp, exp string + pre []string +} + +var ts = []IndentWriterTest{ + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxxThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. +But not quickly. +`[1:], + ` +xxaThe quick brown fox +xxbjumps over the lazy +xxcdog. +xxxBut not quickly. +`[1:], + []string{"xxa", "xxb", "xxc", "xxx"}, + }, + { + ` +The quick brown fox +jumps over the lazy +dog. + +But not quickly.`[1:], + ` +xxaThe quick brown fox +xxxjumps over the lazy +xxxdog. +xxx +xxxBut not quickly.`[1:], + []string{"xxa", "xxx"}, + }, +} + +func TestIndentWriter(t *testing.T) { + for _, test := range ts { + b := new(bytes.Buffer) + pre := make([][]byte, len(test.pre)) + for i := range test.pre { + pre[i] = []byte(test.pre[i]) + } + w := NewIndentWriter(b, pre...) + if _, err := w.Write([]byte(test.inp)); err != nil { + t.Error(err) + } + if got := b.String(); got != test.exp { + t.Errorf("mismatch %q != %q", got, test.exp) + t.Log(got) + t.Log(test.exp) + } + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/Readme b/Godeps/_workspace/src/github.com/kr/text/mc/Readme new file mode 100644 index 00000000000..519ddc00a13 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/Readme @@ -0,0 +1,9 @@ +Command mc prints in multiple columns. + + Usage: mc [-] [-N] [file...] + +Mc splits the input into as many columns as will fit in N +print positions. If the output is a tty, the default N is +the number of characters in a terminal line; otherwise the +default N is 80. Under option - each input line ending in +a colon ':' is printed separately. diff --git a/Godeps/_workspace/src/github.com/kr/text/mc/mc.go b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go new file mode 100644 index 00000000000..00169a30f16 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/mc/mc.go @@ -0,0 +1,62 @@ +// Command mc prints in multiple columns. +// +// Usage: mc [-] [-N] [file...] +// +// Mc splits the input into as many columns as will fit in N +// print positions. If the output is a tty, the default N is +// the number of characters in a terminal line; otherwise the +// default N is 80. Under option - each input line ending in +// a colon ':' is printed separately. +package main + +import ( + "github.com/kr/pty" + "github.com/kr/text/colwriter" + "io" + "log" + "os" + "strconv" +) + +func main() { + var width int + var flag uint + args := os.Args[1:] + for len(args) > 0 && len(args[0]) > 0 && args[0][0] == '-' { + if len(args[0]) > 1 { + width, _ = strconv.Atoi(args[0][1:]) + } else { + flag |= colwriter.BreakOnColon + } + args = args[1:] + } + if width < 1 { + _, width, _ = pty.Getsize(os.Stdout) + } + if width < 1 { + width = 80 + } + + w := colwriter.NewWriter(os.Stdout, width, flag) + if len(args) > 0 { + for _, s := range args { + if f, err := os.Open(s); err == nil { + copyin(w, f) + f.Close() + } else { + log.Println(err) + } + } + } else { + copyin(w, os.Stdin) + } +} + +func copyin(w *colwriter.Writer, r io.Reader) { + if _, err := io.Copy(w, r); err != nil { + log.Println(err) + } + if err := w.Flush(); err != nil { + log.Println(err) + } +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap.go b/Godeps/_workspace/src/github.com/kr/text/wrap.go new file mode 100644 index 00000000000..b09bb03736d --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap.go @@ -0,0 +1,86 @@ +package text + +import ( + "bytes" + "math" +) + +var ( + nl = []byte{'\n'} + sp = []byte{' '} +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func Wrap(s string, lim int) string { + return string(WrapBytes([]byte(s), lim)) +} + +// WrapBytes wraps b into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapBytes(b []byte, lim int) []byte { + words := bytes.Split(bytes.Replace(bytes.TrimSpace(b), nl, sp, -1), sp) + var lines [][]byte + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, bytes.Join(line, sp)) + } + return bytes.Join(lines, nl) +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, either +// Wrap or WrapBytes will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each byte as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words [][]byte, spc, lim, pen int) [][][]byte { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = len(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + len(words[j]) + } + } + + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim || i == n-1 { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + + var lines [][][]byte + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} diff --git a/Godeps/_workspace/src/github.com/kr/text/wrap_test.go b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go new file mode 100644 index 00000000000..634b6e8ebb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/kr/text/wrap_test.go @@ -0,0 +1,62 @@ +package text + +import ( + "bytes" + "testing" +) + +var text = "The quick brown fox jumps over the lazy dog." + +func TestWrap(t *testing.T) { + exp := [][]string{ + {"The", "quick", "brown", "fox"}, + {"jumps", "over", "the", "lazy", "dog."}, + } + words := bytes.Split([]byte(text), sp) + got := WrapWords(words, 1, 24, defaultPenalty) + if len(exp) != len(got) { + t.Fail() + } + for i := range exp { + if len(exp[i]) != len(got[i]) { + t.Fail() + } + for j := range exp[i] { + if exp[i][j] != string(got[i][j]) { + t.Fatal(i, exp[i][j], got[i][j]) + } + } + } +} + +func TestWrapNarrow(t *testing.T) { + exp := "The\nquick\nbrown\nfox\njumps\nover\nthe\nlazy\ndog." + if Wrap(text, 5) != exp { + t.Fail() + } +} + +func TestWrapOneLine(t *testing.T) { + exp := "The quick brown fox jumps over the lazy dog." + if Wrap(text, 500) != exp { + t.Fail() + } +} + +func TestWrapBug1(t *testing.T) { + cases := []struct { + limit int + text string + want string + }{ + {4, "aaaaa", "aaaaa"}, + {4, "a aaaaa", "a\naaaaa"}, + } + + for _, test := range cases { + got := Wrap(test.text, test.limit) + if got != test.want { + t.Errorf("Wrap(%q, %d) = %q want %q", test.text, test.limit, got, test.want) + } + } +} From 40b2f00dc5b1dfee5800b292c3bd894747e365c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 11 Mar 2016 23:39:42 +0100 Subject: [PATCH 093/113] fix(): fixed from db -> json --- pkg/components/simplejson/simplejson.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 1ea8aa346aa..2a96d06d694 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -26,6 +26,10 @@ func (j *Json) FromDB(data []byte) error { } func (j *Json) ToDB() ([]byte, error) { + if j == nil || j.data == nil { + return nil, nil + } + return j.Encode() } From 3fb0b71822bf7f7a1943acd88efe9fd965561dcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 12 Mar 2016 00:13:06 +0100 Subject: [PATCH 094/113] refactor(): refactoring json usage --- pkg/components/simplejson/simplejson.go | 7 +++- pkg/models/dashboards.go | 52 ++++++++++--------------- pkg/models/dashboards_test.go | 8 ++-- pkg/plugins/dashboard_importer.go | 46 +++++++++++----------- pkg/plugins/dashboard_importer_test.go | 23 +++++------ pkg/plugins/dashboards.go | 8 ++-- pkg/plugins/dashboards_test.go | 2 +- pkg/services/search/json_index.go | 8 ++-- 8 files changed, 68 insertions(+), 86 deletions(-) diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 2a96d06d694..85e2f955943 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -4,7 +4,6 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "log" ) @@ -51,6 +50,11 @@ func New() *Json { } } +// New returns a pointer to a new, empty `Json` object +func NewFromAny(data interface{}) *Json { + return &Json{data: data} +} + // Interface returns the underlying data func (j *Json) Interface() interface{} { return j.data @@ -68,7 +72,6 @@ func (j *Json) EncodePretty() ([]byte, error) { // Implements the json.Marshaler interface. func (j *Json) MarshalJSON() ([]byte, error) { - fmt.Printf("MarshalJSON") return json.Marshal(&j.data) } diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 3e87f504a77..6243c729624 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -6,6 +6,7 @@ import ( "time" "github.com/gosimple/slug" + "github.com/grafana/grafana/pkg/components/simplejson" ) // Typed errors @@ -37,14 +38,14 @@ type Dashboard struct { CreatedBy int64 Title string - Data map[string]interface{} + Data *simplejson.Json } // NewDashboard creates a new dashboard func NewDashboard(title string) *Dashboard { dash := &Dashboard{} - dash.Data = make(map[string]interface{}) - dash.Data["title"] = title + dash.Data = simplejson.New() + dash.Data.Set("title", title) dash.Title = title dash.Created = time.Now() dash.Updated = time.Now() @@ -54,34 +55,24 @@ func NewDashboard(title string) *Dashboard { // GetTags turns the tags in data json into go string array func (dash *Dashboard) GetTags() []string { - jsonTags := dash.Data["tags"] - if jsonTags == nil || jsonTags == "" { - return []string{} - } - - arr := jsonTags.([]interface{}) - b := make([]string, len(arr)) - for i := range arr { - b[i] = arr[i].(string) - } - return b + return dash.Data.Get("tags").MustStringArray() } -func NewDashboardFromJson(data map[string]interface{}) *Dashboard { +func NewDashboardFromJson(data *simplejson.Json) *Dashboard { dash := &Dashboard{} dash.Data = data - dash.Title = dash.Data["title"].(string) + dash.Title = dash.Data.Get("title").MustString() dash.UpdateSlug() - if dash.Data["id"] != nil { - dash.Id = int64(dash.Data["id"].(float64)) + if id, err := dash.Data.Get("id").Float64(); err == nil { + dash.Id = int64(id) - if dash.Data["version"] != nil { - dash.Version = int(dash.Data["version"].(float64)) + if version, err := dash.Data.Get("version").Float64(); err == nil { + dash.Version = int(version) dash.Updated = time.Now() } } else { - dash.Data["version"] = 0 + dash.Data.Set("version", 0) dash.Created = time.Now() dash.Updated = time.Now() } @@ -92,9 +83,11 @@ func NewDashboardFromJson(data map[string]interface{}) *Dashboard { // GetDashboardModel turns the command into the savable model func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { dash := NewDashboardFromJson(cmd.Dashboard) - if dash.Data["version"] == 0 { + + if dash.Data.Get("version").MustInt(0) == 0 { dash.CreatedBy = cmd.UserId } + dash.UpdatedBy = cmd.UserId dash.OrgId = cmd.OrgId dash.UpdateSlug() @@ -103,15 +96,12 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { // GetString a func (dash *Dashboard) GetString(prop string, defaultValue string) string { - if val, exists := dash.Data[prop]; exists { - return val.(string) - } - return defaultValue + return dash.Data.Get(prop).MustString(defaultValue) } // UpdateSlug updates the slug func (dash *Dashboard) UpdateSlug() { - title := strings.ToLower(dash.Data["title"].(string)) + title := strings.ToLower(dash.Data.Get("title").MustString()) dash.Slug = slug.Make(title) } @@ -120,10 +110,10 @@ func (dash *Dashboard) UpdateSlug() { // type SaveDashboardCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - UserId int64 `json:"userId"` - OrgId int64 `json:"-"` - Overwrite bool `json:"overwrite"` + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + UserId int64 `json:"userId"` + OrgId int64 `json:"-"` + Overwrite bool `json:"overwrite"` Result *Dashboard } diff --git a/pkg/models/dashboards_test.go b/pkg/models/dashboards_test.go index b0b6796c4d8..ee16508dc8a 100644 --- a/pkg/models/dashboards_test.go +++ b/pkg/models/dashboards_test.go @@ -3,6 +3,7 @@ package models import ( "testing" + "github.com/grafana/grafana/pkg/components/simplejson" . "github.com/smartystreets/goconvey/convey" ) @@ -16,12 +17,11 @@ func TestDashboardModel(t *testing.T) { }) Convey("Given a dashboard json", t, func() { - json := map[string]interface{}{ - "title": "test dash", - } + json := simplejson.New() + json.Set("title", "test dash") Convey("With tags as string value", func() { - json["tags"] = "" + json.Set("tags", "") dash := NewDashboardFromJson(json) So(len(dash.GetTags()), ShouldEqual, 0) diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 15e055faffd..2a640e814e9 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -6,7 +6,7 @@ import ( "regexp" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/dynmap" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) @@ -54,9 +54,8 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { return err } - template := dynmap.NewFromMap(dashboard.Data) evaluator := &DashTemplateEvaluator{ - template: template, + template: dashboard.Data, inputs: cmd.Inputs, } @@ -66,7 +65,7 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { } saveCmd := m.SaveDashboardCommand{ - Dashboard: generatedDash.StringMap(), + Dashboard: generatedDash, OrgId: cmd.OrgId, UserId: cmd.UserId, } @@ -89,15 +88,15 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { } type DashTemplateEvaluator struct { - template *dynmap.Object + template *simplejson.Json inputs []ImportDashboardInput variables map[string]string - result *dynmap.Object + result *simplejson.Json varRegex *regexp.Regexp } -func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Object) *ImportDashboardInput { - inputType, _ := varDef.GetString("type") +func (this *DashTemplateEvaluator) findInput(varName string, varDef *simplejson.Json) *ImportDashboardInput { + inputType := varDef.Get("type").MustString() for _, input := range this.inputs { if inputType == input.Type && (input.Name == varName || input.Name == "*") { @@ -108,16 +107,15 @@ func (this *DashTemplateEvaluator) findInput(varName string, varDef *dynmap.Obje return nil } -func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) { - this.result = dynmap.NewObject() +func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { + this.result = simplejson.New() this.variables = make(map[string]string) this.varRegex, _ = regexp.Compile("\\$__(\\w+)") // check that we have all inputs we need - if requiredInputs, err := this.template.GetObject("__inputs"); err == nil { - for varName, value := range requiredInputs.Map() { - varDef, _ := value.Object() - input := this.findInput(varName, varDef) + if inputDefs := this.template.Get("__inputs"); inputDefs != nil { + for varName, value := range inputDefs.MustMap() { + input := this.findInput(varName, simplejson.NewFromAny(value)) if input == nil { return nil, &DashboardInputMissingError{VariableName: varName} @@ -133,28 +131,28 @@ func (this *DashTemplateEvaluator) Eval() (*dynmap.Object, error) { return this.result, nil } -func (this *DashTemplateEvaluator) EvalObject(source *dynmap.Object, writer *dynmap.Object) { +func (this *DashTemplateEvaluator) EvalObject(source *simplejson.Json, writer *simplejson.Json) { - for key, value := range source.Map() { + for key, value := range source.MustMap() { if key == "__inputs" { continue } - goValue := value.Interface() - - switch v := goValue.(type) { + switch v := value.(type) { case string: interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { return this.variables[match] }) - writer.SetValue(key, interpolated) + writer.Set(key, interpolated) case map[string]interface{}: - childSource, _ := value.Object() - childWriter, _ := writer.SetValue(key, map[string]interface{}{}).Object() + childSource := simplejson.NewFromAny(value) + childWriter := simplejson.New() + writer.Set(key, childWriter.Interface()) this.EvalObject(childSource, childWriter) + case []interface{}: default: - log.Info("type: %v", reflect.TypeOf(goValue)) - log.Error(3, "Unknown json type key: %v , type: %v", key, goValue) + log.Info("type: %v", reflect.TypeOf(value)) + log.Error(3, "Unknown json type key: %v , type: %v", key, value) } } } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 472de6b4e26..3e4c307cbb9 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/components/dynmap" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -44,21 +44,16 @@ func TestDashboardImport(t *testing.T) { Convey("should install dashboard", func() { So(importedDash, ShouldNotBeNil) - dashData := dynmap.NewFromMap(importedDash.Data) - So(dashData.String(), ShouldEqual, "") + dashStr, _ := importedDash.Data.EncodePretty() + So(string(dashStr), ShouldEqual, "") - rows := importedDash.Data["rows"].([]interface{}) - row1 := rows[0].(map[string]interface{}) - panels := row1["panels"].([]interface{}) - panel := panels[0].(map[string]interface{}) - - So(panel["datasource"], ShouldEqual, "graphite") - So(importedDash.Data["__inputs"], ShouldBeNil) + // So(panel["datasource"], ShouldEqual, "graphite") + // So(importedDash.Data["__inputs"], ShouldBeNil) }) }) Convey("When evaling dashboard template", t, func() { - template, _ := dynmap.NewObjectFromBytes([]byte(`{ + template, _ := simplejson.NewJson([]byte(`{ "__inputs": { "graphite": { "type": "datasource" @@ -80,12 +75,12 @@ func TestDashboardImport(t *testing.T) { So(err, ShouldBeNil) Convey("should render template", func() { - So(res.MustGetString("test.prop", ""), ShouldEqual, "my-server") + So(res.GetPath("test", "prop").MustString(), ShouldEqual, "my-server") }) Convey("should not include inputs in output", func() { - _, err := res.GetObject("__inputs") - So(err, ShouldNotBeNil) + inputs := res.Get("__inputs") + So(inputs.Interface(), ShouldBeNil) }) }) diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 1697ad808f2..932196a42a9 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -1,11 +1,11 @@ package plugins import ( - "encoding/json" "os" "path/filepath" "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" ) @@ -52,10 +52,8 @@ func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) defer reader.Close() - jsonParser := json.NewDecoder(reader) - var data map[string]interface{} - - if err := jsonParser.Decode(&data); err != nil { + data, err := simplejson.NewFromReader(reader) + if err != nil { return nil, err } diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go index 58cbe2f4920..bdd08ceefd2 100644 --- a/pkg/plugins/dashboards_test.go +++ b/pkg/plugins/dashboards_test.go @@ -23,7 +23,7 @@ func TestPluginDashboards(t *testing.T) { bus.AddHandler("test", func(query *m.GetDashboardQuery) error { if query.Slug == "nginx-connections" { dash := m.NewDashboard("Nginx Connections") - dash.Data["revision"] = "1.1" + dash.Data.Set("revision", "1.1") query.Result = dash return nil } diff --git a/pkg/services/search/json_index.go b/pkg/services/search/json_index.go index e70c662438d..79c238b27f9 100644 --- a/pkg/services/search/json_index.go +++ b/pkg/services/search/json_index.go @@ -1,12 +1,12 @@ package search import ( - "encoding/json" "os" "path/filepath" "strings" "time" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) @@ -120,10 +120,8 @@ func loadDashboardFromFile(filename string) (*JsonDashIndexItem, error) { } defer reader.Close() - jsonParser := json.NewDecoder(reader) - var data map[string]interface{} - - if err := jsonParser.Decode(&data); err != nil { + data, err := simplejson.NewFromReader(reader) + if err != nil { return nil, err } From 28f4f3a8c884217d950fd1a0282b70385f3218f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 12 Mar 2016 00:16:08 +0100 Subject: [PATCH 095/113] fix(datasource edit): Editing datasources broken earlier today, fixes #4343 --- public/app/features/plugins/ds_edit_ctrl.ts | 3 ++- public/app/features/plugins/partials/ds_edit.html | 14 +++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index 1e07b3a814a..d2ca7a82dc4 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -24,6 +24,7 @@ export class DataSourceEditCtrl { datasourceMeta: any; tabIndex: number; hasDashboards: boolean; + editForm: any; /** @ngInject */ constructor( @@ -114,7 +115,7 @@ export class DataSourceEditCtrl { } saveChanges(test) { - if (!this.$scope.editForm.$valid) { + if (!this.editForm.$valid) { return; } diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 46177d998cc..4024b77b2c4 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -25,7 +25,7 @@
    - +
    Name @@ -52,12 +52,12 @@ -
    -
    Testing....
    -
    Test results
    -
    -
    {{testing.title}}
    -
    +
    +
    Testing....
    +
    Test results
    +
    +
    {{ctrl.testing.title}}
    +
    From 0398face05aac33ed42453570b919e9a125c32ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 12 Mar 2016 10:13:49 +0100 Subject: [PATCH 096/113] feat(plugins): dashboard import for data sources is working! #4298 --- pkg/api/dashboard_snapshot.go | 2 - pkg/api/dtos/models.go | 4 +- pkg/components/simplejson/simplejson_go11.go | 2 - pkg/models/dashboard_snapshot.go | 14 +- pkg/plugins/dashboard_importer.go | 55 +++--- pkg/plugins/dashboard_importer_test.go | 15 +- pkg/services/sqlstore/dashboard.go | 4 +- .../sqlstore/dashboard_snapshot_test.go | 7 +- pkg/services/sqlstore/dashboard_test.go | 17 +- .../app/features/dashboard/dashnav/dashnav.ts | 4 +- .../plugins/import_list/import_list.html | 6 +- .../plugins/import_list/import_list.ts | 2 +- .../features/plugins/partials/ds_edit.html | 2 +- .../graphite/dashboards/carbon_metrics.json | 176 ++++++++++++++++++ .../graphite/dashboards/carbon_stats.json | 22 --- .../plugins/datasource/graphite/plugin.json | 2 +- tests/test-app/dashboards/connections.json | 4 + .../dashboards/connections_result.json | 20 ++ 18 files changed, 278 insertions(+), 80 deletions(-) create mode 100644 public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json delete mode 100644 public/app/plugins/datasource/graphite/dashboards/carbon_stats.json create mode 100644 tests/test-app/dashboards/connections_result.json diff --git a/pkg/api/dashboard_snapshot.go b/pkg/api/dashboard_snapshot.go index 8ed848a7b11..3c369ca5b7f 100644 --- a/pkg/api/dashboard_snapshot.go +++ b/pkg/api/dashboard_snapshot.go @@ -53,7 +53,6 @@ func CreateDashboardSnapshot(c *middleware.Context, cmd m.CreateDashboardSnapsho } func GetDashboardSnapshot(c *middleware.Context) { - key := c.Params(":key") query := &m.GetDashboardSnapshotQuery{Key: key} @@ -136,5 +135,4 @@ func SearchDashboardSnapshots(c *middleware.Context) Response { } return Json(200, dtos) - //return Json(200, searchQuery.Result) } diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index d5534ea6ed4..26295dd3d3c 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -53,8 +53,8 @@ type DashboardMeta struct { } type DashboardFullWithMeta struct { - Meta DashboardMeta `json:"meta"` - Dashboard map[string]interface{} `json:"dashboard"` + Meta DashboardMeta `json:"meta"` + Dashboard *simplejson.Json `json:"dashboard"` } type DataSource struct { diff --git a/pkg/components/simplejson/simplejson_go11.go b/pkg/components/simplejson/simplejson_go11.go index 3a88b29e449..1c479532cf0 100644 --- a/pkg/components/simplejson/simplejson_go11.go +++ b/pkg/components/simplejson/simplejson_go11.go @@ -6,7 +6,6 @@ import ( "bytes" "encoding/json" "errors" - "fmt" "io" "reflect" "strconv" @@ -14,7 +13,6 @@ import ( // Implements the json.Unmarshaler interface. func (j *Json) UnmarshalJSON(p []byte) error { - fmt.Printf("UnmarshalJSON") dec := json.NewDecoder(bytes.NewBuffer(p)) dec.UseNumber() return dec.Decode(&j.data) diff --git a/pkg/models/dashboard_snapshot.go b/pkg/models/dashboard_snapshot.go index 9bfbd06c1ef..f920e91f2e4 100644 --- a/pkg/models/dashboard_snapshot.go +++ b/pkg/models/dashboard_snapshot.go @@ -1,6 +1,10 @@ package models -import "time" +import ( + "time" + + "github.com/grafana/grafana/pkg/components/simplejson" +) // DashboardSnapshot model type DashboardSnapshot struct { @@ -17,7 +21,7 @@ type DashboardSnapshot struct { Created time.Time Updated time.Time - Dashboard map[string]interface{} + Dashboard *simplejson.Json } // DashboardSnapshotDTO without dashboard map @@ -40,9 +44,9 @@ type DashboardSnapshotDTO struct { // COMMANDS type CreateDashboardSnapshotCommand struct { - Dashboard map[string]interface{} `json:"dashboard" binding:"Required"` - Name string `json:"name" binding:"Required"` - Expires int64 `json:"expires"` + Dashboard *simplejson.Json `json:"dashboard" binding:"Required"` + Name string `json:"name" binding:"Required"` + Expires int64 `json:"expires"` // these are passed when storing an external snapshot ref External bool `json:"external"` diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 2a640e814e9..b5dc286f6d1 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -1,8 +1,8 @@ package plugins import ( + "encoding/json" "fmt" - "reflect" "regexp" "github.com/grafana/grafana/pkg/bus" @@ -127,32 +127,45 @@ func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { log.Info("Import: dashboard has no __import section") } - this.EvalObject(this.template, this.result) - return this.result, nil + return simplejson.NewFromAny(this.evalObject(this.template)), nil } -func (this *DashTemplateEvaluator) EvalObject(source *simplejson.Json, writer *simplejson.Json) { +func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{} { + + sourceValue := source.Interface() + + switch v := sourceValue.(type) { + case string: + interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { + return this.variables[match] + }) + return interpolated + case bool: + return v + case json.Number: + return v + case map[string]interface{}: + return this.evalObject(source) + case []interface{}: + array := make([]interface{}, 0) + for _, item := range v { + array = append(array, this.evalValue(simplejson.NewFromAny(item))) + } + return array + } + + return nil +} + +func (this *DashTemplateEvaluator) evalObject(source *simplejson.Json) interface{} { + result := make(map[string]interface{}) for key, value := range source.MustMap() { if key == "__inputs" { continue } - - switch v := value.(type) { - case string: - interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { - return this.variables[match] - }) - writer.Set(key, interpolated) - case map[string]interface{}: - childSource := simplejson.NewFromAny(value) - childWriter := simplejson.New() - writer.Set(key, childWriter.Interface()) - this.EvalObject(childSource, childWriter) - case []interface{}: - default: - log.Info("type: %v", reflect.TypeOf(value)) - log.Error(3, "Unknown json type key: %v , type: %v", key, value) - } + result[key] = this.evalValue(simplejson.NewFromAny(value)) } + + return result } diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 3e4c307cbb9..9b5605277d4 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -1,6 +1,7 @@ package plugins import ( + "io/ioutil" "testing" "github.com/grafana/grafana/pkg/bus" @@ -34,7 +35,7 @@ func TestDashboardImport(t *testing.T) { OrgId: 1, UserId: 1, Inputs: []ImportDashboardInput{ - {Name: "*", Type: "datasource"}, + {Name: "*", Type: "datasource", Value: "graphite"}, }, } @@ -44,11 +45,15 @@ func TestDashboardImport(t *testing.T) { Convey("should install dashboard", func() { So(importedDash, ShouldNotBeNil) - dashStr, _ := importedDash.Data.EncodePretty() - So(string(dashStr), ShouldEqual, "") + resultStr, _ := importedDash.Data.EncodePretty() + expectedBytes, _ := ioutil.ReadFile("../../tests/test-app/dashboards/connections_result.json") + expectedJson, _ := simplejson.NewJson(expectedBytes) + expectedStr, _ := expectedJson.EncodePretty() - // So(panel["datasource"], ShouldEqual, "graphite") - // So(importedDash.Data["__inputs"], ShouldBeNil) + So(string(resultStr), ShouldEqual, string(expectedStr)) + + panel := importedDash.Data.Get("rows").GetIndex(0).Get("panels").GetIndex(0) + So(panel.Get("datasource").MustString(), ShouldEqual, "graphite") }) }) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index a33ef10d725..396d507cfd2 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -69,7 +69,7 @@ func SaveDashboard(cmd *m.SaveDashboardCommand) error { affectedRows, err = sess.Insert(dash) } else { dash.Version += 1 - dash.Data["version"] = dash.Version + dash.Data.Set("version", dash.Version) affectedRows, err = sess.Id(dash.Id).Update(dash) } @@ -108,7 +108,7 @@ func GetDashboard(query *m.GetDashboardQuery) error { return m.ErrDashboardNotFound } - dashboard.Data["id"] = dashboard.Id + dashboard.Data.Set("id", dashboard.Id) query.Result = &dashboard return nil diff --git a/pkg/services/sqlstore/dashboard_snapshot_test.go b/pkg/services/sqlstore/dashboard_snapshot_test.go index 5301f0f1cc9..50375088b4b 100644 --- a/pkg/services/sqlstore/dashboard_snapshot_test.go +++ b/pkg/services/sqlstore/dashboard_snapshot_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" ) @@ -16,9 +17,9 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { Convey("Given saved snaphot", func() { cmd := m.CreateDashboardSnapshotCommand{ Key: "hej", - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "hello": "mupp", - }, + }), } err := CreateDashboardSnapshot(&cmd) So(err, ShouldBeNil) @@ -29,7 +30,7 @@ func TestDashboardSnapshotDBAccess(t *testing.T) { So(err, ShouldBeNil) So(query.Result, ShouldNotBeNil) - So(query.Result.Dashboard["hello"], ShouldEqual, "mupp") + So(query.Result.Dashboard.Get("hello").MustString(), ShouldEqual, "mupp") }) }) diff --git a/pkg/services/sqlstore/dashboard_test.go b/pkg/services/sqlstore/dashboard_test.go index 2d35a0742de..609639f7788 100644 --- a/pkg/services/sqlstore/dashboard_test.go +++ b/pkg/services/sqlstore/dashboard_test.go @@ -5,6 +5,7 @@ import ( . "github.com/smartystreets/goconvey/convey" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/search" ) @@ -12,11 +13,11 @@ import ( func insertTestDashboard(title string, orgId int64, tags ...interface{}) *m.Dashboard { cmd := m.SaveDashboardCommand{ OrgId: orgId, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": title, "tags": tags, - }, + }), } err := SaveDashboard(&cmd) @@ -58,11 +59,11 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 1, Overwrite: true, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(123412321), "title": "Expect error", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) @@ -76,11 +77,11 @@ func TestDashboardDataAccess(t *testing.T) { cmd := m.SaveDashboardCommand{ OrgId: 2, Overwrite: true, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": float64(query.Result.Id), "title": "Expect error", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) @@ -135,11 +136,11 @@ func TestDashboardDataAccess(t *testing.T) { Convey("Should not be able to save dashboard with same name", func() { cmd := m.SaveDashboardCommand{ OrgId: 1, - Dashboard: map[string]interface{}{ + Dashboard: simplejson.NewFromAny(map[string]interface{}{ "id": nil, "title": "test dash 23", "tags": []interface{}{}, - }, + }), } err := SaveDashboard(&cmd) diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index dcc2f40d0dc..2d5b66cb13c 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -137,8 +137,8 @@ export class DashNavCtrl { $scope.deleteDashboard = function() { $scope.appEvent('confirm-modal', { - title: 'Delete dashboard', - text: 'Do you want to delete dashboard?', + title: 'Delete', + text: 'Do you want to delete this dashboard?', text2: $scope.dashboard.title, icon: 'fa-trash', yesText: 'Delete', diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index d6c1bb914ce..f9dc4ff28d0 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -21,13 +21,13 @@ diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts index 004fde69bd1..19f847a58d0 100644 --- a/public/app/features/plugins/import_list/import_list.ts +++ b/public/app/features/plugins/import_list/import_list.ts @@ -34,7 +34,7 @@ export class DashImportListCtrl { }); } - this.backendSrv.post(`/api/plugins/dashboards/install`, installCmd).then(res => { + this.backendSrv.post(`/api/plugins/dashboards/import`, installCmd).then(res => { this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]); _.extend(dash, res); }); diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index ff2f981e58b..3305bfd67af 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -74,7 +74,7 @@
    - +
    diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json new file mode 100644 index 00000000000..b9f2db18927 --- /dev/null +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json @@ -0,0 +1,176 @@ +{ + "__inputs": { + "graphite": { + "type": "datasource", + "pluginId": "graphite", + "description": "Graphite datasource" + } + }, + + "revision": "1.0", + "title": "Graphite Carbon Metrics", + "tags": ["graphite", "carbon"], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": false, + "sharedCrosshair": false, + "rows": [ + { + "collapsable": true, + "collapse": false, + "editable": true, + "height": "350px", + "notice": false, + "panels": [ + { + "aliasColors": {}, + "annotate": { + "enable": false + }, + "bars": false, + "datasource": "$__graphite", + "editable": true, + "fill": 0, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "max": null, + "min": 0, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 1, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "loadingEditor": false, + "nullPointMode": "null as zero", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "resolution": 100, + "scale": 1, + "seriesOverrides": [ + { + "alias": "Points Per Update", + "yaxis": 2 + }, + { + "alias": "CPU", + "yaxis": 2 + } + ], + "span": 12, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "target": "alias(sumSeries(carbon.agents.*.updateOperations),\"Updates\") " + }, + { + "refId": "B", + "target": "alias(sumSeries(carbon.agents.*.metricsReceived),'Metrics Received')" + }, + { + "refId": "C", + "target": "alias(sumSeries(carbon.agents.*.committedPoints),'Committed Points')" + }, + { + "refId": "D", + "target": "alias(sumSeries(carbon.agents.*.pointsPerUpdate),'Points Per Update')" + }, + { + "refId": "E", + "target": "alias(averageSeries(carbon.agents.*.cpuUsage),'CPU')" + }, + { + "refId": "F", + "target": "alias(sumSeries(carbon.agents.*.creates),'Creates')" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Graphite Carbon Metrics", + "tooltip": { + "query_as_alias": true, + "shared": false, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "zerofill": true + } + ], + "title": "Row1" + } + ], + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "notice": false, + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "templating": { + "enable": false, + "list": [] + }, + "annotations": { + "enable": false, + "list": [] + }, + "refresh": false, + "schemaVersion": 8, + "version": 2, + "links": [] +} diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json deleted file mode 100644 index 3cdfb0049c9..00000000000 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "__inputs": { - "graphite": { - "type": "datasource", - "pluginId": "graphite", - "description": "Graphite datasource" - } - }, - - "title": "Carbon Cache Stats", - "version": 1, - "rows": [ - { - "panels": [ - { - "type": "graph", - "datasource": "$__graphite" - } - ] - } - ] -} diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index f7715f9c5b9..a1c35094701 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -4,7 +4,7 @@ "id": "graphite", "includes": [ - {"type": "dashboard", "name": "Carbon Cache Stats", "path": "dashboards/carbon_stats.json"} + {"type": "dashboard", "name": "Graphite Carbon Metrics", "path": "dashboards/carbon_metrics.json"} ], "metrics": true, diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index feb90cae71d..2f5102fba3e 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -10,6 +10,10 @@ "title": "Nginx Connections", "revision": "1.5", "schemaVersion": 11, + "tags": ["tag1", "tag2"], + "number_array": [1,2,3,10.33], + "boolean_true": true, + "boolean_false": false, "rows": [ { diff --git a/tests/test-app/dashboards/connections_result.json b/tests/test-app/dashboards/connections_result.json new file mode 100644 index 00000000000..1c662f6c269 --- /dev/null +++ b/tests/test-app/dashboards/connections_result.json @@ -0,0 +1,20 @@ +{ + "revision": "1.5", + "tags": ["tag1", "tag2"], + "boolean_false": false, + "boolean_true": true, + "number_array": [1,2,3,10.33], + "rows": [ + { + "panels": [ + { + "datasource": "graphite", + "type": "graph" + } + ] + } + ], + "schemaVersion": 11, + "title": "Nginx Connections", + "version": 0 +} From d575a5ce1230c3ed51cc148aa7e962a706219b6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 12 Mar 2016 10:40:55 +0100 Subject: [PATCH 097/113] feat(plugins): added prometheus dashboard to prometheus data source, #4298 --- pkg/plugins/dashboard_importer.go | 33 +- pkg/plugins/dashboard_importer_test.go | 9 +- .../graphite/dashboards/carbon_metrics.json | 12 +- .../dashboards/prometheus_stats.json | 577 ++++++++++++++++++ .../plugins/datasource/prometheus/plugin.json | 4 + tests/test-app/dashboards/connections.json | 12 +- 6 files changed, 615 insertions(+), 32 deletions(-) create mode 100644 public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index b5dc286f6d1..834bfabd048 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -7,7 +7,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" ) @@ -95,11 +94,10 @@ type DashTemplateEvaluator struct { varRegex *regexp.Regexp } -func (this *DashTemplateEvaluator) findInput(varName string, varDef *simplejson.Json) *ImportDashboardInput { - inputType := varDef.Get("type").MustString() +func (this *DashTemplateEvaluator) findInput(varName string, varType string) *ImportDashboardInput { for _, input := range this.inputs { - if inputType == input.Type && (input.Name == varName || input.Name == "*") { + if varType == input.Type && (input.Name == varName || input.Name == "*") { return &input } } @@ -110,21 +108,20 @@ func (this *DashTemplateEvaluator) findInput(varName string, varDef *simplejson. func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { this.result = simplejson.New() this.variables = make(map[string]string) - this.varRegex, _ = regexp.Compile("\\$__(\\w+)") + this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`) // check that we have all inputs we need - if inputDefs := this.template.Get("__inputs"); inputDefs != nil { - for varName, value := range inputDefs.MustMap() { - input := this.findInput(varName, simplejson.NewFromAny(value)) + for _, inputDef := range this.template.Get("__inputs").MustArray() { + inputDefJson := simplejson.NewFromAny(inputDef) + inputName := inputDefJson.Get("name").MustString() + inputType := inputDefJson.Get("type").MustString() + input := this.findInput(inputName, inputType) - if input == nil { - return nil, &DashboardInputMissingError{VariableName: varName} - } - - this.variables["$__"+varName] = input.Value + if input == nil { + return nil, &DashboardInputMissingError{VariableName: inputName} } - } else { - log.Info("Import: dashboard has no __import section") + + this.variables["${"+inputName+"}"] = input.Value } return simplejson.NewFromAny(this.evalObject(this.template)), nil @@ -137,7 +134,11 @@ func (this *DashTemplateEvaluator) evalValue(source *simplejson.Json) interface{ switch v := sourceValue.(type) { case string: interpolated := this.varRegex.ReplaceAllStringFunc(v, func(match string) string { - return this.variables[match] + if replacement, exists := this.variables[match]; exists { + return replacement + } else { + return match + } }) return interpolated case bool: diff --git a/pkg/plugins/dashboard_importer_test.go b/pkg/plugins/dashboard_importer_test.go index 9b5605277d4..d2897fad1cd 100644 --- a/pkg/plugins/dashboard_importer_test.go +++ b/pkg/plugins/dashboard_importer_test.go @@ -59,13 +59,14 @@ func TestDashboardImport(t *testing.T) { Convey("When evaling dashboard template", t, func() { template, _ := simplejson.NewJson([]byte(`{ - "__inputs": { - "graphite": { + "__inputs": [ + { + "name": "DS_NAME", "type": "datasource" } - }, + ], "test": { - "prop": "$__graphite" + "prop": "${DS_NAME}" } }`)) diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json index b9f2db18927..845fc1930bd 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_metrics.json @@ -1,11 +1,11 @@ { - "__inputs": { - "graphite": { + "__inputs": [ + { + "name": "DS_NAME", "type": "datasource", - "pluginId": "graphite", - "description": "Graphite datasource" + "pluginId": "graphite" } - }, + ], "revision": "1.0", "title": "Graphite Carbon Metrics", @@ -29,7 +29,7 @@ "enable": false }, "bars": false, - "datasource": "$__graphite", + "datasource": "${DS_NAME}", "editable": true, "fill": 0, "grid": { diff --git a/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json new file mode 100644 index 00000000000..e008317a96e --- /dev/null +++ b/public/app/plugins/datasource/prometheus/dashboards/prometheus_stats.json @@ -0,0 +1,577 @@ +{ + "__inputs": [ + { + "name": "DS_NAME", + "type": "datasource", + "pluginId": "prometheus" + } + ], + + "revision": "1.0", + "title": "Prometheus Stats", + "tags": ["prometheus"], + "style": "dark", + "timezone": "browser", + "editable": true, + "hideControls": true, + "sharedCrosshair": false, + "rows": [ + { + "collapse": false, + "editable": true, + "height": 178, + "panels": [ + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(245, 54, 54, 0.9)", + "rgba(237, 129, 40, 0.89)", + "rgba(50, 172, 45, 0.97)" + ], + "datasource": "${DS_NAME}", + "decimals": 1, + "editable": true, + "error": false, + "format": "s", + "id": 5, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": false + }, + "targets": [ + { + "expr": "(time() - process_start_time_seconds{job=\"prometheus\"})", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "", + "title": "Uptime", + "type": "singlestat", + "valueFontSize": "80%", + "valueMaps": [ + { + "op": "=", + "text": "N/A", + "value": "null" + } + ], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": false, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "format": "none", + "id": 6, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "expr": "prometheus_local_storage_memory_series", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "0,1,5", + "title": "Local Storage Memory Series", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [], + "valueName": "current" + }, + { + "cacheTimeout": null, + "colorBackground": false, + "colorValue": true, + "colors": [ + "rgba(50, 172, 45, 0.97)", + "rgba(237, 129, 40, 0.89)", + "rgba(245, 54, 54, 0.9)" + ], + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "format": "none", + "id": 7, + "interval": null, + "links": [], + "maxDataPoints": 100, + "nullPointMode": "connected", + "nullText": null, + "postfix": "", + "postfixFontSize": "50%", + "prefix": "", + "prefixFontSize": "50%", + "span": 3, + "sparkline": { + "fillColor": "rgba(31, 118, 189, 0.18)", + "full": false, + "lineColor": "rgb(31, 120, 193)", + "show": true + }, + "targets": [ + { + "expr": "prometheus_local_storage_indexing_queue_length", + "intervalFactor": 2, + "refId": "A" + } + ], + "thresholds": "0,500,4000", + "title": "Interal Storage Queue Length", + "type": "singlestat", + "valueFontSize": "70%", + "valueMaps": [ + { + "op": "=", + "text": "Empty", + "value": "0" + } + ], + "valueName": "current" + }, + { + "content": "\"Prometheus\nPrometheus\n\n

    You're using Prometheus, an open-source systems monitoring and alerting toolkit originally built at SoundCloud. For more information, check out the Grafana and Prometheus projects.

    ", + "editable": true, + "error": false, + "id": 9, + "links": [], + "mode": "html", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": 227, + "panels": [ + { + "aliasColors": { + "prometheus": "#C15C17", + "{instance=\"localhost:9090\",job=\"prometheus\"}": "#C15C17" + }, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 2, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 9, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(prometheus_local_storage_ingested_samples_total[5m])", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{job}}", + "metric": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Samples ingested (rate-5m)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "content": "#### Samples Ingested\nThis graph displays the count of samples ingested by the Prometheus server, as measured over the last 5 minutes, per time series in the range vector. When troubleshooting an issue on IRC or Github, this is often the first stat requested by the Prometheus team. ", + "editable": true, + "error": false, + "id": 8, + "links": [], + "mode": "markdown", + "span": 2.995914043583536, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": { + "prometheus": "#F9BA8F", + "{instance=\"localhost:9090\",interval=\"5s\",job=\"prometheus\"}": "#F9BA8F" + }, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 5, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "rate(prometheus_target_interval_length_seconds_count[5m])", + "intervalFactor": 2, + "legendFormat": "{{job}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Target Scrapes (last 5m)", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "aliasColors": {}, + "bars": false, + "datasource": "${DS_NAME}", + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 14, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 4, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "prometheus_target_interval_length_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{quantile}} ({{interval}})", + "metric": "", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Scrape Duration", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "short", + "short" + ] + }, + { + "content": "#### Scrapes\nPrometheus scrapes metrics from instrumented jobs, either directly or via an intermediary push gateway for short-lived jobs. Target scrapes will show how frequently targets are scraped, as measured over the last 5 minutes, per time series in the range vector. Scrape Duration will show how long the scrapes are taking, with percentiles available as series. ", + "editable": true, + "error": false, + "id": 11, + "links": [], + "mode": "markdown", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + }, + { + "collapse": false, + "editable": true, + "height": "250px", + "panels": [ + { + "aliasColors": {}, + "bars": false, + "datasource": "${DS_NAME}", + "decimals": null, + "editable": true, + "error": false, + "fill": 1, + "grid": { + "leftLogBase": 1, + "leftMax": null, + "leftMin": null, + "rightLogBase": 1, + "rightMax": null, + "rightMin": null, + "threshold1": null, + "threshold1Color": "rgba(216, 200, 27, 0.27)", + "threshold2": null, + "threshold2Color": "rgba(234, 112, 112, 0.22)" + }, + "id": 12, + "leftYAxisLabel": "", + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "hideEmpty": true, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "span": 9, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{quantile}}", + "refId": "A" + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Rule Eval Duration", + "tooltip": { + "shared": true, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "y-axis": true, + "y_formats": [ + "percentunit", + "short" + ] + }, + { + "content": "#### Rule Evaluation Duration\nThis graph panel plots the duration for all evaluations to execute. The 50th percentile, 90th percentile and 99th percentile are shown as three separate series to help identify outliers that may be skewing the data.", + "editable": true, + "error": false, + "id": 15, + "links": [], + "mode": "markdown", + "span": 3, + "style": {}, + "title": "", + "transparent": true, + "type": "text" + } + ], + "title": "New row" + } + ], + "time": { + "from": "now-5m", + "to": "now" + }, + "timepicker": { + "now": true, + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "templating": { + "list": [] + }, + "annotations": { + "list": [] + }, + "refresh": false, + "schemaVersion": 7, + "links": [ + { + "icon": "info", + "tags": [], + "targetBlank": true, + "title": "Grafana Docs", + "tooltip": "", + "type": "link", + "url": "http://www.grafana.org/docs" + }, + { + "icon": "info", + "tags": [], + "targetBlank": true, + "title": "Prometheus Docs", + "type": "link", + "url": "http://prometheus.io/docs/introduction/overview/" + } + ] +} diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index 66dd9145e6a..2ff198d5129 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -3,6 +3,10 @@ "name": "Prometheus", "id": "prometheus", + "includes": [ + {"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"} + ], + "metrics": true, "annotations": true } diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json index 2f5102fba3e..ee5e40140b3 100644 --- a/tests/test-app/dashboards/connections.json +++ b/tests/test-app/dashboards/connections.json @@ -1,11 +1,11 @@ { - "__inputs": { - "graphite": { + "__inputs": [ + { + "name": "DS_NAME", "type": "datasource", - "pluginId": "graphite", - "description": "Graphite datasource" + "pluginId": "graphite" } - }, + ], "title": "Nginx Connections", "revision": "1.5", @@ -20,7 +20,7 @@ "panels": [ { "type": "graph", - "datasource": "$__graphite" + "datasource": "${DS_NAME}" } ] } From 4a3c19c666e7d627fdf8ab7359f9c4d7bca33953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 12 Mar 2016 10:49:30 +0100 Subject: [PATCH 098/113] plugins(): dashboards, changed wording installed to imported when showing imported version in dashboard list --- public/app/features/plugins/import_list/import_list.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html index f9dc4ff28d0..acb6654d520 100644 --- a/public/app/features/plugins/import_list/import_list.html +++ b/public/app/features/plugins/import_list/import_list.html @@ -17,7 +17,7 @@ v{{dash.revision}} - Installed v{{dash.installedRevision}} + Imported v{{dash.installedRevision}}
    -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    +
    + + +
    +
    +
    + +
    +
    diff --git a/public/app/features/plugins/plugin_list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts index 2693a4d8755..9e9b679c6c1 100644 --- a/public/app/features/plugins/plugin_list_ctrl.ts +++ b/public/app/features/plugins/plugin_list_ctrl.ts @@ -4,11 +4,28 @@ import angular from 'angular'; export class PluginListCtrl { plugins: any[]; + tabIndex: number; /** @ngInject */ - constructor(private backendSrv: any) { + constructor(private backendSrv: any, $location) { + this.tabIndex = 0; - this.backendSrv.get('api/plugins', {embedded: 0}).then(plugins => { + var pluginType = $location.search().type || 'panel'; + switch (pluginType) { + case "datasource": { + this.tabIndex = 1; + break; + } + case "app": { + this.tabIndex = 2; + break; + } + case "panel": + default: + this.tabIndex = 0; + } + + this.backendSrv.get('api/plugins', {embedded: 0, type: pluginType}).then(plugins => { this.plugins = plugins; }); } From fc6ad2059a68c302cc6046d89551841fa3b416a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 10:03:18 +0100 Subject: [PATCH 103/113] docs(): updated doc version --- docs/Makefile | 2 +- docs/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index f24a63065b3..b1a72adc3f8 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -44,7 +44,7 @@ docs-test: docs-build $(DOCKER_RUN_DOCS) "$(DOCKER_DOCS_IMAGE)" ./test.sh docs-build: - git fetch https://github.com/grafana/grafana.git docs-2.5 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files + git fetch https://github.com/grafana/grafana.git docs-2.6 && git diff --name-status FETCH_HEAD...HEAD -- . > changed-files echo "$(GIT_BRANCH)" > GIT_BRANCH echo "$(GITCOMMIT)" > GITCOMMIT docker build -t "$(DOCKER_DOCS_IMAGE)" . diff --git a/docs/VERSION b/docs/VERSION index e70b4523ae7..4a36342fcab 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -2.6.0 +3.0.0 From 4aba3bc62bbfcc87419b4e31599b38bb235cbe26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 10:18:43 +0100 Subject: [PATCH 104/113] fix(opentsdb): fixed templating issue for OpenTSDB, fixes #4312, fixes #4311 --- public/app/features/templating/templateSrv.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/public/app/features/templating/templateSrv.js b/public/app/features/templating/templateSrv.js index 3527c386220..ece69302196 100644 --- a/public/app/features/templating/templateSrv.js +++ b/public/app/features/templating/templateSrv.js @@ -43,6 +43,9 @@ function (angular, _) { } this.formatValue = function(value, format, variable) { + // for some scopedVars there is no variable + variable = variable || {}; + if (typeof format === 'function') { return format(value, variable, this.formatValue); } @@ -126,9 +129,6 @@ function (angular, _) { return target.replace(this._regex, function(match, g1, g2) { variable = self._index[g1 || g2]; - if (!variable) { - return match; - } if (scopedVars) { value = scopedVars[g1 || g2]; @@ -137,6 +137,10 @@ function (angular, _) { } } + if (!variable) { + return match; + } + systemValue = self._grafanaVariables[variable.current.value]; if (systemValue) { return self.formatValue(systemValue, format, variable); From f77bd59e1da4300c757284c293808ebb4b97c677 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 14 Mar 2016 11:33:38 +0100 Subject: [PATCH 105/113] fix(datasource): fixes testdatasource bug --- public/app/features/plugins/partials/ds_edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 3305bfd67af..8822000df91 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -64,7 +64,7 @@
    - Cancel From 953a07252afdfe6f7dbe2de7a69b1ac27ab78268 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 14 Mar 2016 11:43:35 +0100 Subject: [PATCH 106/113] Revert "fix(datasource): fixes testdatasource bug" This reverts commit f77bd59e1da4300c757284c293808ebb4b97c677. --- public/app/features/plugins/partials/ds_edit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html index 8822000df91..3305bfd67af 100644 --- a/public/app/features/plugins/partials/ds_edit.html +++ b/public/app/features/plugins/partials/ds_edit.html @@ -64,7 +64,7 @@
    - Cancel From ca5a34ad1ee4a8004135c49e31e466aff91e9521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Mar 2016 12:00:56 +0100 Subject: [PATCH 107/113] fix(datasource): filter out built in data sources from data source type dropdown --- pkg/api/plugins.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 195dd9483b8..2ee77ff0a92 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -48,6 +48,13 @@ func GetPluginList(c *middleware.Context) Response { continue } + // filter out built in data sources + if ds, exists := plugins.DataSources[pluginDef.Id]; exists { + if ds.BuiltIn { + continue + } + } + result = append(result, listItem) } From 8f97e8d5edb0fa02e9207800c70d5d166ecd134a Mon Sep 17 00:00:00 2001 From: Sean Kenny Date: Mon, 14 Mar 2016 19:40:23 +0000 Subject: [PATCH 108/113] Fix minor typo in tooltip --- .../plugins/datasource/elasticsearch/partials/bucket_agg.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html index 5c1b538cf9a..3ddbfed3f80 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html +++ b/public/app/plugins/datasource/elasticsearch/partials/bucket_agg.html @@ -66,7 +66,7 @@
  • - +
  • From e905c2c6b589dbbef9ee3e9a4ed67a8550ad9112 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 14 Mar 2016 17:16:10 +0100 Subject: [PATCH 109/113] feat(plugin.json): remove staticRoot field --- pkg/plugins/frontend_plugin.go | 15 +++++++++------ pkg/plugins/models.go | 18 ++++++++---------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index aab3f851f60..cd7f9703aae 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -3,9 +3,9 @@ package plugins import ( "net/url" "path" - "path/filepath" "strings" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -14,10 +14,9 @@ type FrontendPluginBase struct { } func (fp *FrontendPluginBase) initFrontendPlugin() { - if fp.StaticRoot != "" { - fp.StaticRootAbs = filepath.Join(fp.PluginDir, fp.StaticRoot) + if isInternalPlugin(fp.PluginDir) { StaticRoutes = append(StaticRoutes, &PluginStaticRoute{ - Directory: fp.StaticRootAbs, + Directory: fp.PluginDir, PluginId: fp.Id, }) } @@ -33,7 +32,7 @@ func (fp *FrontendPluginBase) initFrontendPlugin() { } func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { - appSubPath := strings.Replace(fp.PluginDir, app.StaticRootAbs, "", 1) + appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1) fp.IncludedInAppId = app.Id fp.BaseUrl = app.BaseUrl fp.Module = util.JoinUrlFragments("plugins/"+app.Id, appSubPath) + "/module" @@ -41,7 +40,7 @@ func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { func (fp *FrontendPluginBase) handleModuleDefaults() { - if fp.StaticRoot != "" { + if isInternalPlugin(fp.PluginDir) { fp.Module = path.Join("plugins", fp.Id, "module") fp.BaseUrl = path.Join("public/plugins", fp.Id) return @@ -51,6 +50,10 @@ func (fp *FrontendPluginBase) handleModuleDefaults() { fp.BaseUrl = path.Join("public/app/plugins", fp.Type, fp.Id) } +func isInternalPlugin(pluginDir string) bool { + return !strings.Contains(pluginDir, setting.StaticRootPath) +} + func evalRelativePluginUrlPath(pathStr string, baseUrl string) string { if pathStr == "" { return "" diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 07c3d321ec7..8443e91931d 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -30,16 +30,14 @@ type PluginLoader interface { } type PluginBase struct { - Type string `json:"type"` - Name string `json:"name"` - Id string `json:"id"` - Info PluginInfo `json:"info"` - Dependencies PluginDependencies `json:"dependencies"` - Includes []*PluginInclude `json:"includes"` - Module string `json:"module"` - BaseUrl string `json:"baseUrl"` - StaticRoot string `json:"staticRoot"` - StaticRootAbs string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + Id string `json:"id"` + Info PluginInfo `json:"info"` + Dependencies PluginDependencies `json:"dependencies"` + Includes []*PluginInclude `json:"includes"` + Module string `json:"module"` + BaseUrl string `json:"baseUrl"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` From 82eb32a71ecc1cc2f20d2cf039243f143a67805a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Mar 2016 09:15:24 +0100 Subject: [PATCH 110/113] ux(): initial pass at new card / list design, #4364 --- pkg/plugins/frontend_plugin.go | 12 ++- .../plugins/partials/plugin_list.html | 55 ++++++------- .../cloudwatch/img/amazon-web-services.png | Bin 0 -> 9559 bytes .../plugins/datasource/cloudwatch/plugin.json | 13 +++- .../datasource/elasticsearch/plugin.json | 2 +- .../plugins/datasource/graphite/plugin.json | 9 ++- .../datasource/influxdb/img/influxdb_logo.svg | 15 ++++ .../plugins/datasource/influxdb/plugin.json | 13 +++- .../datasource/opentsdb/img/opentsdb_logo.png | Bin 0 -> 11644 bytes .../plugins/datasource/opentsdb/plugin.json | 13 +++- .../prometheus/img/prometheus_logo.svg | 19 +++++ .../plugins/datasource/prometheus/plugin.json | 13 +++- public/app/plugins/panel/dashlist/plugin.json | 9 ++- public/app/plugins/panel/graph/plugin.json | 9 ++- .../app/plugins/panel/singlestat/plugin.json | 9 ++- public/app/plugins/panel/table/plugin.json | 9 ++- public/app/plugins/panel/text/plugin.json | 9 ++- public/img/plugin-default-logo_dark.svg | 26 +++++++ public/img/plugin-default-logo_light.svg | 24 ++++++ public/sass/_grafana.scss | 1 + public/sass/_variables.dark.scss | 13 +++- public/sass/_variables.light.scss | 6 ++ public/sass/_variables.scss | 2 +- public/sass/base/_fonts.scss | 7 ++ public/sass/components/_cards.scss | 73 ++++++++++++++++++ 25 files changed, 311 insertions(+), 50 deletions(-) create mode 100644 public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png create mode 100644 public/app/plugins/datasource/influxdb/img/influxdb_logo.svg create mode 100644 public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png create mode 100644 public/app/plugins/datasource/prometheus/img/prometheus_logo.svg create mode 100644 public/img/plugin-default-logo_dark.svg create mode 100644 public/img/plugin-default-logo_light.svg create mode 100644 public/sass/components/_cards.scss diff --git a/pkg/plugins/frontend_plugin.go b/pkg/plugins/frontend_plugin.go index aab3f851f60..5e1b4556a16 100644 --- a/pkg/plugins/frontend_plugin.go +++ b/pkg/plugins/frontend_plugin.go @@ -24,14 +24,22 @@ func (fp *FrontendPluginBase) initFrontendPlugin() { fp.handleModuleDefaults() - fp.Info.Logos.Small = evalRelativePluginUrlPath(fp.Info.Logos.Small, fp.BaseUrl) - fp.Info.Logos.Large = evalRelativePluginUrlPath(fp.Info.Logos.Large, fp.BaseUrl) + fp.Info.Logos.Small = getPluginLogoUrl(fp.Info.Logos.Small, fp.BaseUrl) + fp.Info.Logos.Large = getPluginLogoUrl(fp.Info.Logos.Large, fp.BaseUrl) for i := 0; i < len(fp.Info.Screenshots); i++ { fp.Info.Screenshots[i].Path = evalRelativePluginUrlPath(fp.Info.Screenshots[i].Path, fp.BaseUrl) } } +func getPluginLogoUrl(path, baseUrl string) string { + if path == "" { + return "public/img/plugin-default-logo_dark.svg" + } + + return evalRelativePluginUrlPath(path, baseUrl) +} + func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) { appSubPath := strings.Replace(fp.PluginDir, app.StaticRootAbs, "", 1) fp.IncludedInAppId = app.Id diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html index 5c3d38b3459..e11860235e5 100644 --- a/public/app/features/plugins/partials/plugin_list.html +++ b/public/app/features/plugins/partials/plugin_list.html @@ -26,37 +26,26 @@
    - - - - - - - - - - - - - - - - - -
    NameType
    - - {{plugin.name}} - - - {{plugin.type}} - - Enabled - Pinned - - - - Edit - -
    -
    +
    +
      +
    1. + +
      + + {{plugin.type}} +
      +
      +
      + +
      +
      +
      {{plugin.name}}
      +
      By {{plugin.info.author.name}}
      +
      +
      +
      +
    2. +
    +
    +
    diff --git a/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png b/public/app/plugins/datasource/cloudwatch/img/amazon-web-services.png new file mode 100644 index 0000000000000000000000000000000000000000..f11f40347510903c178610d44d261ef530db2dc5 GIT binary patch literal 9559 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 literal 0 HcmV?d00001 diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index 49c5341bd21..eb2f97335da 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -4,5 +4,16 @@ "id": "cloudwatch", "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/amazon-web-services.png", + "large": "img/amazon-web-services.png" + } + } } diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index 3cd345e5cdf..d4b07ac8e00 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -6,7 +6,7 @@ "info": { "description": "Elasticsearch Data Source for Grafana", "author": { - "name": "Grafana Core", + "name": "Grafana Project", "url": "http://grafana.org" }, "keywords": ["elasticsearch"], diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index a1c35094701..a561a99372b 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -8,5 +8,12 @@ ], "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg new file mode 100644 index 00000000000..6fad144f8c7 --- /dev/null +++ b/public/app/plugins/datasource/influxdb/img/influxdb_logo.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/app/plugins/datasource/influxdb/plugin.json b/public/app/plugins/datasource/influxdb/plugin.json index dd60a16b715..605ce168831 100644 --- a/public/app/plugins/datasource/influxdb/plugin.json +++ b/public/app/plugins/datasource/influxdb/plugin.json @@ -5,5 +5,16 @@ "defaultMatchFormat": "regex values", "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/influxdb_logo.svg", + "large": "img/influxdb_logo.svg" + } + } } diff --git a/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png b/public/app/plugins/datasource/opentsdb/img/opentsdb_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..519d56a476c4c24558ca0bca4b50f9adfa948adc GIT binary patch literal 11644 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 Pqyewx)#NH;NP literal 0 HcmV?d00001 diff --git a/public/app/plugins/datasource/opentsdb/plugin.json b/public/app/plugins/datasource/opentsdb/plugin.json index 333e9d07536..02ba02e6e89 100644 --- a/public/app/plugins/datasource/opentsdb/plugin.json +++ b/public/app/plugins/datasource/opentsdb/plugin.json @@ -5,5 +5,16 @@ "metrics": true, "defaultMatchFormat": "pipe", - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/opentsdb_logo.png", + "large": "img/opentsdb_logo.png" + } + } } diff --git a/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg new file mode 100644 index 00000000000..29005ec3860 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/img/prometheus_logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index 2ff198d5129..f39f8691661 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -8,5 +8,16 @@ ], "metrics": true, - "annotations": true + "annotations": true, + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + }, + "logos": { + "small": "img/prometheus_logo.svg", + "large": "img/prometheus_logo.svg" + } + } } diff --git a/public/app/plugins/panel/dashlist/plugin.json b/public/app/plugins/panel/dashlist/plugin.json index 1f1266d8444..9cac424ac68 100644 --- a/public/app/plugins/panel/dashlist/plugin.json +++ b/public/app/plugins/panel/dashlist/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Dashboard list", - "id": "dashlist" + "id": "dashlist", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/graph/plugin.json b/public/app/plugins/panel/graph/plugin.json index f603e5e4b06..baa9fe12a39 100644 --- a/public/app/plugins/panel/graph/plugin.json +++ b/public/app/plugins/panel/graph/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Graph", - "id": "graph" + "id": "graph", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/singlestat/plugin.json b/public/app/plugins/panel/singlestat/plugin.json index 8372e4b2c2d..ea9ae32853a 100644 --- a/public/app/plugins/panel/singlestat/plugin.json +++ b/public/app/plugins/panel/singlestat/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Singlestat", - "id": "singlestat" + "id": "singlestat", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/table/plugin.json b/public/app/plugins/panel/table/plugin.json index 7f8c0bb23cf..e25efdfbe36 100644 --- a/public/app/plugins/panel/table/plugin.json +++ b/public/app/plugins/panel/table/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Table", - "id": "table" + "id": "table", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/app/plugins/panel/text/plugin.json b/public/app/plugins/panel/text/plugin.json index 4cf046cec36..485f42942f2 100644 --- a/public/app/plugins/panel/text/plugin.json +++ b/public/app/plugins/panel/text/plugin.json @@ -1,5 +1,12 @@ { "type": "panel", "name": "Text", - "id": "text" + "id": "text", + + "info": { + "author": { + "name": "Grafana Project", + "url": "http://grafana.org" + } + } } diff --git a/public/img/plugin-default-logo_dark.svg b/public/img/plugin-default-logo_dark.svg new file mode 100644 index 00000000000..d665aa30b07 --- /dev/null +++ b/public/img/plugin-default-logo_dark.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/img/plugin-default-logo_light.svg b/public/img/plugin-default-logo_light.svg new file mode 100644 index 00000000000..4573494ff16 --- /dev/null +++ b/public/img/plugin-default-logo_light.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/public/sass/_grafana.scss b/public/sass/_grafana.scss index 42956f3c3fa..f8973c65dc7 100644 --- a/public/sass/_grafana.scss +++ b/public/sass/_grafana.scss @@ -30,6 +30,7 @@ @import "layout/page"; // COMPONENTS +@import "components/cards"; @import "components/buttons"; @import "components/navs"; @import "components/tabs"; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 8823b336d83..8efd9f7e55d 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -67,9 +67,9 @@ $link-hover-color: $white; // Typography // ------------------------- -$headings-color: darken($white,11%); -$abbr-border-color: $gray-3 !default; -$text-muted: darken($link-color,30%); +$headings-color: darken($white,11%); +$abbr-border-color: $gray-3 !default; +$text-muted: darken($link-color,30%); $blockquote-small-color: $gray-3 !default; $blockquote-border-color: $gray-4 !default; @@ -252,3 +252,10 @@ $popover-help-color: $text-color; // images $checkboxImageUrl: '../img/checkbox.png'; + +// cards +$card-background: linear-gradient(135deg, #2f2f2f, #262626); +$card-background-hover: linear-gradient(135deg, #343434, #262626); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .3); + + diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index af66e2c2389..279e284b0be 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -279,3 +279,9 @@ $popover-help-color: $gray-6; // images $checkboxImageUrl: '../img/checkbox_white.png'; + +// cards +$card-background: linear-gradient(135deg, $gray-5, $gray-6); +$card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); +$card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); + diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 4f1002bdc67..9f96591a4b9 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -4,7 +4,7 @@ // // Quickly modify global styling by enabling or disabling optional features. -$enable-flex: false !default; +$enable-flex: true !default; $enable-rounded: true !default; $enable-shadows: false !default; $enable-gradients: false !default; diff --git a/public/sass/base/_fonts.scss b/public/sass/base/_fonts.scss index f7d172a523f..e442cbdce6c 100644 --- a/public/sass/base/_fonts.scss +++ b/public/sass/base/_fonts.scss @@ -176,3 +176,10 @@ content: "\e903"; } +.icon-gf-app:before { + content: "\e902"; +} +.icon-gf-datasource:before { + content: "\e607"; +} + diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss new file mode 100644 index 00000000000..12d3b0166ff --- /dev/null +++ b/public/sass/components/_cards.scss @@ -0,0 +1,73 @@ + + +.card-section { + margin-bottom: $spacer*2; +} + +.card-list { + display: flex; + flex-direction: row; + flex-wrap: wrap; + list-style-type: none; +} + +.card-item-wrapper { + width: 100%; + padding: 0 1.5rem 1.5rem 0rem; +} + +.card-item { + display: block; + height: 100%; + min-height: 12rem; + background: $card-background; + box-shadow: $card-shadow; + padding: 1rem; + border-radius: 4px; + + &:hover { + background: $card-background-hover; + } +} + +.card-item-figure { + margin: 0 $spacer $spacer 0; + img { + width: 6rem; + } +} + +.card-item-body { + display: flex; +} + +.card-item-details { +} + +.card-item-header { + color: $text-color-weak; + margin-bottom: $spacer; +} + +.card-item-name { + color: $headings-color; + font-size: $font-size-h3; +} + +.card-item-sub-name { + color: $text-color-weak; +} + +@include media-breakpoint-up(md) { + .card-item-wrapper { + width: 50%; + } +} + +@include media-breakpoint-up(lg) { + .card-item-wrapper { + width: 33%; + } +} + + From cba7c1726f1c4a7d073b0674b3a884327822f4d5 Mon Sep 17 00:00:00 2001 From: Trent White Date: Tue, 15 Mar 2016 10:08:23 -0400 Subject: [PATCH 111/113] add graphite logo to datasource card --- .../datasource/graphite/img/graphite_logo.png | Bin 0 -> 6285 bytes .../app/plugins/datasource/graphite/plugin.json | 6 +++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/graphite/img/graphite_logo.png diff --git a/public/app/plugins/datasource/graphite/img/graphite_logo.png b/public/app/plugins/datasource/graphite/img/graphite_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..e70b40720c45c17b4bb571df5f73d6c3c11cdcf4 GIT binary patch literal 6285 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 Date: Tue, 15 Mar 2016 15:28:08 +0100 Subject: [PATCH 112/113] ux(): added card layout selector and list view mode, #4364 --- .../layout_selector/layout_selector.ts | 74 ++++++++++++++ public/app/core/core.ts | 2 + .../plugins/partials/plugin_list.html | 8 +- public/sass/components/_cards.scss | 96 +++++++++++++++---- 4 files changed, 158 insertions(+), 22 deletions(-) create mode 100644 public/app/core/components/layout_selector/layout_selector.ts diff --git a/public/app/core/components/layout_selector/layout_selector.ts b/public/app/core/components/layout_selector/layout_selector.ts new file mode 100644 index 00000000000..e9d90aa1f48 --- /dev/null +++ b/public/app/core/components/layout_selector/layout_selector.ts @@ -0,0 +1,74 @@ +/// + +import config from 'app/core/config'; +import store from 'app/core/store'; +import _ from 'lodash'; +import $ from 'jquery'; +import coreModule from 'app/core/core_module'; + +var template = ` +
    + + +
    +`; + +export class LayoutSelectorCtrl { + mode: string; + + /** @ngInject **/ + constructor(private $rootScope) { + this.mode = store.get('grafana.list.layout.mode') || 'grid'; + } + + listView() { + this.mode = 'list'; + store.set('grafana.list.layout.mode', 'list'); + this.$rootScope.appEvent('layout-mode-changed', 'list'); + } + + gridView() { + this.mode = 'grid'; + store.set('grafana.list.layout.mode', 'grid'); + this.$rootScope.appEvent('layout-mode-changed', 'grid'); + } + +} + +/** @ngInject **/ +export function layoutSelector() { + return { + restrict: 'E', + controller: LayoutSelectorCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: {}, + template: template, + }; +} + +/** @ngInject **/ +export function layoutMode($rootScope) { + return { + restrict: 'A', + scope: {}, + link: function(scope, elem) { + var layout = store.get('grafana.list.layout.mode') || 'grid'; + var className = 'card-list-layout-' + layout; + elem.addClass(className); + + $rootScope.onAppEvent('layout-mode-changed', (evt, newLayout) => { + elem.removeClass(className); + className = 'card-list-layout-' + newLayout; + elem.addClass(className); + }, scope); + } + }; +} + +coreModule.directive('layoutSelector', layoutSelector); +coreModule.directive('layoutMode', layoutMode); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 32ddee02dc7..2ab7ea01410 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -28,6 +28,7 @@ import {infoPopover} from './components/info_popover'; import {colorPicker} from './components/colorpicker'; import {navbarDirective} from './components/navbar/navbar'; import {arrayJoin} from './directives/array_join'; +import {layoutSelector} from './components/layout_selector/layout_selector'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -42,5 +43,6 @@ export { navbarDirective, searchDirective, colorPicker, + layoutSelector, infoPopover }; diff --git a/public/app/features/plugins/partials/plugin_list.html b/public/app/features/plugins/partials/plugin_list.html index e11860235e5..aa86e9e6dd2 100644 --- a/public/app/features/plugins/partials/plugin_list.html +++ b/public/app/features/plugins/partials/plugin_list.html @@ -26,8 +26,10 @@
    -
    -
      +
      + + +
      1. @@ -36,7 +38,7 @@
        - +
        {{plugin.name}}
        diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 12d3b0166ff..4e5b43f60f5 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -1,4 +1,27 @@ +.layout-selector { + text-align: right; + margin: 0 $spacer*1.5 $spacer; + position: relative; + top: -0.5rem; + button { + background: $input-label-bg; + color: $text-color-weak; + border: none; + padding: 0.5rem; + line-height: 1; + font-size: 130%; + + &:focus { + outline: none; + } + + &.active { + background-color: lighten($input-label-bg, 5%); + color: $link-color; + } + } +} .card-section { margin-bottom: $spacer*2; @@ -11,15 +34,9 @@ list-style-type: none; } -.card-item-wrapper { - width: 100%; - padding: 0 1.5rem 1.5rem 0rem; -} - .card-item { display: block; height: 100%; - min-height: 12rem; background: $card-background; box-shadow: $card-shadow; padding: 1rem; @@ -30,13 +47,6 @@ } } -.card-item-figure { - margin: 0 $spacer $spacer 0; - img { - width: 6rem; - } -} - .card-item-body { display: flex; } @@ -51,23 +61,71 @@ .card-item-name { color: $headings-color; - font-size: $font-size-h3; } .card-item-sub-name { color: $text-color-weak; } -@include media-breakpoint-up(md) { +.card-list-layout-grid { + .card-item-wrapper { - width: 50%; + width: 100%; + padding: 0 1.5rem 1.5rem 0rem; + } + + .card-item-figure { + margin: 0 $spacer $spacer 0; + img { + width: 6rem; + } + } + + .card-item-name { + font-size: $font-size-h3; + } + + @include media-breakpoint-up(md) { + .card-item-wrapper { + width: 50%; + } + } + + @include media-breakpoint-up(lg) { + .card-item-wrapper { + width: 33.333333%; + } } } -@include media-breakpoint-up(lg) { +.card-list-layout-list { + .card-item-wrapper { - width: 33%; + width: 100%; + padding: 0 0 1.5rem 0rem; + } + + .card-item-header { + float: right; + } + + .card-item-figure { + margin: 0 $spacer 0 0; + img { + width: 3.5rem; + } + } + + .card-item-name { + font-size: $font-size-h4; + } + + .card-item-sub-name { + font-size: $font-size-sm; + } + + .layout-selector { + margin-right: 0; } } - From 1abdd170d326595b4818920e4b837dfc330ad07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Mar 2016 15:52:29 +0100 Subject: [PATCH 113/113] ux(): fixes to cards and sorting plugin list, #4364 --- pkg/api/dtos/plugins.go | 14 ++++++++++++++ pkg/api/plugins.go | 7 +++++-- public/sass/components/_cards.scss | 2 ++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 7008c939aef..88683155006 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -26,6 +26,20 @@ type PluginListItem struct { Info *plugins.PluginInfo `json:"info"` } +type PluginList []PluginListItem + +func (slice PluginList) Len() int { + return len(slice) +} + +func (slice PluginList) Less(i, j int) bool { + return slice[i].Name < slice[j].Name +} + +func (slice PluginList) Swap(i, j int) { + slice[i], slice[j] = slice[j], slice[i] +} + type ImportDashboardCommand struct { PluginId string `json:"pluginId"` Path string `json:"path"` diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 2ee77ff0a92..56d0d99296c 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -1,6 +1,8 @@ package api import ( + "sort" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" @@ -19,7 +21,7 @@ func GetPluginList(c *middleware.Context) Response { return ApiError(500, "Failed to get list of plugins", err) } - result := make([]*dtos.PluginListItem, 0) + result := make(dtos.PluginList, 0) for _, pluginDef := range plugins.Plugins { // filter out app sub plugins if embeddedFilter == "0" && pluginDef.IncludedInAppId != "" { @@ -31,7 +33,7 @@ func GetPluginList(c *middleware.Context) Response { continue } - listItem := &dtos.PluginListItem{ + listItem := dtos.PluginListItem{ Id: pluginDef.Id, Name: pluginDef.Name, Type: pluginDef.Type, @@ -58,6 +60,7 @@ func GetPluginList(c *middleware.Context) Response { result = append(result, listItem) } + sort.Sort(result) return Json(200, result) } diff --git a/public/sass/components/_cards.scss b/public/sass/components/_cards.scss index 4e5b43f60f5..8272de2fa3b 100644 --- a/public/sass/components/_cards.scss +++ b/public/sass/components/_cards.scss @@ -76,6 +76,8 @@ .card-item-figure { margin: 0 $spacer $spacer 0; + height: 6rem; + img { width: 6rem; }