Просмотр исходного кода

Merge branch 'master' into cli_colors

bergquist 9 лет назад
Родитель
Сommit
740478344b
75 измененных файлов с 703 добавлено и 585 удалено
  1. 10 0
      docs/sources/datasources/cloudwatch.md
  2. 2 1
      examples/README.md
  3. 0 0
      examples/boilerplate-es5-panel/css/styles.css
  4. 0 0
      examples/boilerplate-es5-panel/module.js
  5. 0 0
      examples/boilerplate-es5-panel/panel.html
  6. 0 0
      examples/boilerplate-es5-panel/plugin.json
  7. 0 7
      examples/nginx-app/.gitignore
  8. 0 13
      examples/nginx-app/.jscs.json
  9. 0 36
      examples/nginx-app/.jshintrc
  10. 0 54
      examples/nginx-app/Gruntfile.js
  11. 0 37
      examples/nginx-app/package.json
  12. 0 7
      examples/nginx-app/readme.md
  13. 0 3
      examples/nginx-app/src/components/config.html
  14. 0 6
      examples/nginx-app/src/components/config.js
  15. 0 3
      examples/nginx-app/src/components/logs.html
  16. 0 6
      examples/nginx-app/src/components/logs.js
  17. 0 3
      examples/nginx-app/src/components/stream.html
  18. 0 6
      examples/nginx-app/src/components/stream.js
  19. 0 0
      examples/nginx-app/src/css/dark.css
  20. 0 0
      examples/nginx-app/src/css/light.css
  21. 0 17
      examples/nginx-app/src/dashboards/dashboard.js
  22. 0 0
      examples/nginx-app/src/dashboards/nginx_connection_stats.json
  23. 0 12
      examples/nginx-app/src/datasource/datasource.js
  24. 0 5
      examples/nginx-app/src/datasource/module.js
  25. 0 5
      examples/nginx-app/src/datasource/plugin.json
  26. BIN
      examples/nginx-app/src/img/logo_large.png
  27. BIN
      examples/nginx-app/src/img/logo_small.png
  28. 0 9
      examples/nginx-app/src/module.js
  29. 0 15
      examples/nginx-app/src/panel/module.js
  30. 0 5
      examples/nginx-app/src/panel/plugin.json
  31. 10 4
      pkg/api/api.go
  32. 4 0
      pkg/api/cloudwatch/metrics.go
  33. 7 0
      pkg/api/dtos/plugins.go
  34. 31 0
      pkg/api/plugins.go
  35. 1 0
      pkg/cmd/grafana-cli/commands/commands.go
  36. 9 1
      pkg/cmd/grafana-cli/commands/install_command.go
  37. 1 1
      pkg/cmd/grafana-cli/main.go
  38. 6 1
      pkg/cmd/grafana-cli/services/services.go
  39. 5 2
      pkg/models/dashboards.go
  40. 0 0
      pkg/models/plugin_settings.go
  41. 57 0
      pkg/plugins/dashboard_installer.go
  42. 93 0
      pkg/plugins/dashboards.go
  43. 53 0
      pkg/plugins/dashboards_test.go
  44. 16 0
      pkg/plugins/models.go
  45. 5 4
      pkg/plugins/plugins_test.go
  46. 5 4
      public/app/core/directives/plugin_component.ts
  47. 11 9
      public/app/core/routes/routes.ts
  48. 0 1
      public/app/features/dashboard/all.js
  49. 0 53
      public/app/features/dashboard/import_list/import_list.ts
  50. 0 4
      public/app/features/datasources/all.js
  51. 0 123
      public/app/features/datasources/edit_ctrl.js
  52. 0 63
      public/app/features/datasources/partials/edit.html
  53. 0 1
      public/app/features/org/all.js
  54. 6 3
      public/app/features/plugins/all.ts
  55. 145 0
      public/app/features/plugins/ds_edit_ctrl.ts
  56. 0 0
      public/app/features/plugins/ds_list_ctrl.ts
  57. 37 0
      public/app/features/plugins/import_list/import_list.html
  58. 58 0
      public/app/features/plugins/import_list/import_list.ts
  59. 81 0
      public/app/features/plugins/partials/ds_edit.html
  60. 0 0
      public/app/features/plugins/partials/ds_http_settings.html
  61. 0 0
      public/app/features/plugins/partials/ds_list.html
  62. 0 0
      public/app/features/plugins/partials/plugin_edit.html
  63. 0 0
      public/app/features/plugins/partials/plugin_list.html
  64. 0 0
      public/app/features/plugins/partials/plugin_page.html
  65. 1 1
      public/app/features/plugins/plugin_edit_ctrl.ts
  66. 1 1
      public/app/features/plugins/plugin_list_ctrl.ts
  67. 0 0
      public/app/features/plugins/plugin_page_ctrl.ts
  68. 23 1
      public/app/plugins/datasource/cloudwatch/datasource.js
  69. 1 1
      public/app/plugins/datasource/graphite/dashboards/carbon_stats.json
  70. 1 1
      public/app/plugins/datasource/graphite/plugin.json
  71. 1 1
      public/app/plugins/datasource/influxdb/datasource.ts
  72. 0 48
      tests/app-plugin-json/plugin.json
  73. 5 0
      tests/test-app/dashboards/connections.json
  74. 5 0
      tests/test-app/dashboards/memory.json
  75. 12 7
      tests/test-app/plugin.json

+ 10 - 0
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

+ 2 - 1
examples/README.md

@@ -1,3 +1,4 @@
 ## Example plugin implementations
 
-[datasource-plugin-genericdatsource](https://github.com/grafana/datasource-plugin-genericdatasource/tree/3.0)
+datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource)
+app:  [example-app](https://github.com/grafana/example-app)

+ 0 - 0
examples/panel-boilerplate-es5/css/styles.css → examples/boilerplate-es5-panel/css/styles.css


+ 0 - 0
examples/panel-boilerplate-es5/module.js → examples/boilerplate-es5-panel/module.js


+ 0 - 0
examples/panel-boilerplate-es5/panel.html → examples/boilerplate-es5-panel/panel.html


+ 0 - 0
examples/panel-boilerplate-es5/plugin.json → examples/boilerplate-es5-panel/plugin.json


+ 0 - 7
examples/nginx-app/.gitignore

@@ -1,7 +0,0 @@
-.DS_Store
-
-node_modules
-tmp/*
-npm-debug.log
-dist/*
-

+ 0 - 13
examples/nginx-app/.jscs.json

@@ -1,13 +0,0 @@
-{
-    "disallowImplicitTypeConversion": ["string"],
-    "disallowKeywords": ["with"],
-    "disallowMultipleLineBreaks": true,
-    "disallowMixedSpacesAndTabs": true,
-    "disallowTrailingWhitespace": true,
-    "requireSpacesInFunctionExpression": {
-        "beforeOpeningCurlyBrace": true
-    },
-    "disallowSpacesInsideArrayBrackets": true,
-    "disallowSpacesInsideParentheses": true,
-    "validateIndentation": 2
-}

+ 0 - 36
examples/nginx-app/.jshintrc

@@ -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
-  }
-}

+ 0 - 54
examples/nginx-app/Gruntfile.js

@@ -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']);
-};

+ 0 - 37
examples/nginx-app/package.json

@@ -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"
-}

+ 0 - 7
examples/nginx-app/readme.md

@@ -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.

+ 0 - 3
examples/nginx-app/src/components/config.html

@@ -1,3 +0,0 @@
-<h3>
-	Nginx config!
-</h3>

+ 0 - 6
examples/nginx-app/src/components/config.js

@@ -1,6 +0,0 @@
-
-export class NginxAppConfigCtrl {
-}
-NginxAppConfigCtrl.templateUrl = 'components/config.html';
-
-

+ 0 - 3
examples/nginx-app/src/components/logs.html

@@ -1,3 +0,0 @@
-<h3>
-	Logs page!
-</h3>

+ 0 - 6
examples/nginx-app/src/components/logs.js

@@ -1,6 +0,0 @@
-
-export class LogsPageCtrl {
-}
-LogsPageCtrl.templateUrl = 'components/logs.html';
-
-

+ 0 - 3
examples/nginx-app/src/components/stream.html

@@ -1,3 +0,0 @@
-<h3>
-	Stream page!
-</h3>

+ 0 - 6
examples/nginx-app/src/components/stream.js

@@ -1,6 +0,0 @@
-
-export class StreamPageCtrl {
-}
-StreamPageCtrl.templateUrl = 'components/stream.html';
-
-

+ 0 - 0
examples/nginx-app/src/css/dark.css


+ 0 - 0
examples/nginx-app/src/css/light.css


+ 0 - 17
examples/nginx-app/src/dashboards/dashboard.js

@@ -1,17 +0,0 @@
-require([
-], function () {
-
-  function Dashboard() {
-
-    this.getInputs = function() {
-
-    };
-
-    this.buildDashboard = function() {
-
-    };
-  }
-
-  return Dashboard;
-});
-

+ 0 - 0
examples/nginx-app/src/dashboards/nginx_connection_stats.json


+ 0 - 12
examples/nginx-app/src/datasource/datasource.js

@@ -1,12 +0,0 @@
-export default class NginxDatasource {
-
-  constructor() {}
-
-  query(options) {
-    return [];
-  }
-
-  testDatasource() {
-    return false;
-  }
-}

+ 0 - 5
examples/nginx-app/src/datasource/module.js

@@ -1,5 +0,0 @@
-import {Datasource} from  './datasource';
-
-export {
-  Datasource
-};

+ 0 - 5
examples/nginx-app/src/datasource/plugin.json

@@ -1,5 +0,0 @@
-{
-  "type": "datasource",
-  "name": "Nginx Datasource",
-  "id": "nginx-datasource"
-}

BIN
examples/nginx-app/src/img/logo_large.png


BIN
examples/nginx-app/src/img/logo_small.png


+ 0 - 9
examples/nginx-app/src/module.js

@@ -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
-};

+ 0 - 15
examples/nginx-app/src/panel/module.js

@@ -1,15 +0,0 @@
-import {PanelCtrl} from  'app/plugins/sdk';
-
-class NginxPanelCtrl extends PanelCtrl {
-
-  constructor($scope, $injector) {
-    super($scope, $injector);
-  }
-
-}
-NginxPanelCtrl.template = '<h2>nginx!</h2>';
-
-export {
-  NginxPanelCtrl as PanelCtrl
-};
-

+ 0 - 5
examples/nginx-app/src/panel/plugin.json

@@ -1,5 +0,0 @@
-{
-  "type": "panel",
-  "name": "Nginx Panel",
-  "id": "nginx-panel"
-}

+ 10 - 4
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
@@ -176,6 +172,16 @@ func Register(r *macaron.Macaron) {
 
 		r.Get("/datasources/id/:name", wrap(GetDataSourceIdByName), reqSignedIn)
 
+		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)

+ 4 - 0
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"},

+ 7 - 0
pkg/api/dtos/plugins.go

@@ -25,3 +25,10 @@ type PluginListItem struct {
 	Pinned  bool                `json:"pinned"`
 	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"`
+}

+ 31 - 0
pkg/api/plugin_setting.go → 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)
+}

+ 1 - 0
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()

+ 9 - 1
pkg/cmd/grafana-cli/commands/install_command.go

@@ -29,7 +29,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")
 	}
 

+ 1 - 1
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",

+ 6 - 1
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)

+ 5 - 2
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

+ 0 - 0
pkg/models/plugin_setting.go → pkg/models/plugin_settings.go


+ 57 - 0
pkg/plugins/dashboard_installer.go

@@ -0,0 +1,57 @@
+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
+}

+ 93 - 0
pkg/plugins/dashboards.go

@@ -0,0 +1,93 @@
+package plugins
+
+import (
+	"encoding/json"
+	"os"
+	"path/filepath"
+
+	"github.com/grafana/grafana/pkg/bus"
+	m "github.com/grafana/grafana/pkg/models"
+)
+
+type PluginDashboardInfoDTO struct {
+	PluginId          string `json:"pluginId"`
+	Title             string `json:"title"`
+	Installed         bool   `json:"installed"`
+	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}
+	}
+
+	result := make([]*PluginDashboardInfoDTO, 0)
+
+	for _, include := range plugin.Includes {
+		if include.Type == PluginTypeDashboard {
+			if dashInfo, err := getDashboardImportStatus(orgId, plugin, include.Path); err != nil {
+				return nil, err
+			} else {
+				result = append(result, dashInfo)
+			}
+		}
+	}
+
+	return result, nil
+}
+
+func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) {
+
+	dashboardFilePath := filepath.Join(plugin.PluginDir, 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
+	}
+
+	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")
+
+	query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug}
+
+	if err := bus.Dispatch(&query); err != nil {
+		if err != m.ErrDashboardNotFound {
+			return nil, err
+		}
+	} else {
+		res.Installed = true
+		res.InstalledUri = "db/" + query.Result.Slug
+		res.InstalledRevision = query.Result.GetString("revision", "1.0")
+	}
+
+	return res, nil
+}

+ 53 - 0
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.test-app")
+		sec.NewKey("path", "../../tests/test-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, "test-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, "")
+		})
+	})
+
+}

+ 16 - 0
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
 }

+ 5 - 4
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", "../../tests/test-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["test-app"].Info.Logos.Large, ShouldEqual, "public/plugins/test-app/img/logo_large.png")
+		So(Apps["test-app"].Info.Screenshots[1].Path, ShouldEqual, "public/plugins/test-app/img/screenshot2.png")
 	})
 
 }

+ 5 - 4
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,
           };
         });

+ 11 - 9
public/app/core/routes/routes.ts

@@ -49,20 +49,22 @@ 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: loadOrgBundle,
+    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',
-    resolve: loadOrgBundle,
+    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',
-    resolve: loadOrgBundle,
+    controllerAs: 'ctrl',
+    resolve: loadPluginsBundle,
   })
   .when('/org', {
     templateUrl: 'public/app/features/org/partials/orgDetails.html',
@@ -166,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,

+ 0 - 1
public/app/features/dashboard/all.js

@@ -13,7 +13,6 @@ define([
   './timeSrv',
   './unsavedChangesSrv',
   './timepicker/timepicker',
-  './import_list/import_list',
   './graphiteImportCtrl',
   './dynamicDashboardSrv',
   './importCtrl',

+ 0 - 53
public/app/features/dashboard/import_list/import_list.ts

@@ -1,53 +0,0 @@
-///<reference path="../../../headers/common.d.ts" />
-
-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 = `
-<h3 class="page-heading">Dashboards</h3>
-<div class="gf-form-group">
-  <button class="btn btn-mini btn-inverse" ng-click="ctrl.import(dash)">Import</button>
-</div>
-`;
-
-export function dashboardImportList() {
-  return {
-    restrict: 'E',
-    template: template,
-    controller: DashImportListCtrl,
-    bindToController: true,
-    controllerAs: 'ctrl',
-    scope: {
-      plugin: "="
-    }
-  };
-}
-
-
-coreModule.directive('dashboardImportList', dashboardImportList);
-
-
-
-

+ 0 - 4
public/app/features/datasources/all.js

@@ -1,4 +0,0 @@
-define([
-  './list_ctrl',
-  './edit_ctrl',
-], function () {});

+ 0 - 123
public/app/features/datasources/edit_ctrl.js

@@ -1,123 +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();
-  });
-});

+ 0 - 63
public/app/features/datasources/partials/edit.html

@@ -1,63 +0,0 @@
-<navbar
-title="Data Sources"
-title-url="datasources"
-icon="icon-gf icon-gf-datasources">
-</navbar>
-
-<div class="page-container">
-	<div class="page-header">
-		<h1 ng-show="isNew">Add data source</h1>
-		<h1 ng-show="!isNew">Edit data source</h1>
-	</div>
-
-	<form name="editForm">
-		<div class="gf-form-group">
-			<div class="gf-form">
-				<span class="gf-form-label width-7">Name</span>
-				<input class="gf-form-input max-width-21" type="text" ng-model="current.name" placeholder="My data source name" required>
-				<info-popover offset="0px -95px">
-					The name is used when you select the data source in panels.
-					The <code>Default</code> data source is preselected in new
-					panels.
-				</info-popover>
-
-				<editor-checkbox text="Default" model="current.isDefault"></editor-checkbox>
-			</div>
-
-			<div class="gf-form">
-				<span class="gf-form-label width-7">Type</span>
-				<div class="gf-form-select-wrapper">
-					<select class="gf-form-input gf-size-auto" ng-model="current.type" ng-options="v.id as v.name for v in types" ng-change="typeChanged()"></select>
-				</div>
-			</div>
-		</div>
-
-
-		<rebuild-on-change property="datasourceMeta.id">
-			<plugin-component type="datasource-config-ctrl">
-			</plugin-component>
-		</rebuild-on-change>
-
-		<div ng-if="testing" style="margin-top: 25px">
-			<h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
-			<h5 ng-show="testing.done">Test results</h5>
-			<div class="alert-{{testing.status}} alert">
-				<div class="alert-title">{{testing.title}}</div>
-				<div ng-bind='testing.message'></div>
-			</div>
-		</div>
-
-		<!-- <dashboard&#45;import&#45;list plugin="current"></dashboard&#45;import&#45;list> -->
-
-		<div class="gf-form-button-row">
-			<button type="submit" class="btn btn-success" ng-show="isNew" ng-click="saveChanges()">Add</button>
-			<button type="submit" class="btn btn-success" ng-show="!isNew" ng-click="saveChanges()">Save</button>
-			<button type="submit" class="btn btn-secondary" ng-show="!isNew" ng-click="saveChanges(true)">
-				Test Connection
-			</button>
-			<a class="btn btn-link" href="datasources">Cancel</a>
-		</div>
-
-	</form>
-</div>
-

+ 0 - 1
public/app/features/org/all.js

@@ -4,5 +4,4 @@ define([
   './userInviteCtrl',
   './orgApiKeysCtrl',
   './orgDetailsCtrl',
-  '../datasources/all',
 ], function () {});

+ 6 - 3
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';

+ 145 - 0
public/app/features/plugins/ds_edit_ctrl.ts

@@ -0,0 +1,145 @@
+///<reference path="../../headers/common.d.ts" />
+
+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;
+  tabIndex: number;
+  hasDashboards: boolean;
+
+  /** @ngInject */
+  constructor(
+    private $scope,
+    private $q,
+    private backendSrv,
+    private $routeParams,
+    private $location,
+    private datasourceSrv) {
+
+      this.isNew = true;
+      this.datasources = [];
+      this.tabIndex = 0;
+
+      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/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() {
+      this.hasDashboards = false;
+      return this.backendSrv.get('/api/plugins/' + this.current.type + '/settings').then(pluginInfo => {
+        this.datasourceMeta = pluginInfo;
+        this.hasDashboards = _.findWhere(pluginInfo.includes, {type: 'dashboard'});
+      });
+    }
+
+    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/plugins/partials/ds_http_settings.html'
+  };
+});

+ 0 - 0
public/app/features/datasources/list_ctrl.ts → public/app/features/plugins/ds_list_ctrl.ts


+ 37 - 0
public/app/features/plugins/import_list/import_list.html

@@ -0,0 +1,37 @@
+<div class="gf-form-group" ng-if="ctrl.dashboards.length">
+	<table class="filter-table">
+		<tbody>
+			<tr ng-repeat="dash in ctrl.dashboards">
+				<td class="width-1">
+					<i class="icon-gf icon-gf-dashboard"></i>
+				</td>
+				<td>
+					<a href="dashboard/{{dash.installedUri}}" ng-show="dash.installed">
+						{{dash.title}}
+					</a>
+					<span ng-show="!dash.installed">
+						{{dash.title}}
+					</span>
+				</td>
+				<td>
+					v{{dash.revision}}
+				</td>
+				<td ng-if="dash.installed">
+					Installed v{{dash.installedRevision}}
+				</td>
+				<td style="text-align: right">
+					<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.installed">
+						Install
+					</button>
+					<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.installed">
+						Re-Install
+					</button>
+					<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.installed">
+						Un-install
+					</button>
+				</td>
+			</tr>
+		</tbody>
+	</table>
+</div>
+

+ 58 - 0
public/app/features/plugins/import_list/import_list.ts

@@ -0,0 +1,58 @@
+///<reference path="../../../headers/common.d.ts" />
+
+import angular from 'angular';
+import _ from 'lodash';
+import coreModule from 'app/core/core_module';
+
+export class DashImportListCtrl {
+  dashboards: any[];
+  plugin: any;
+
+  constructor(private $http, private backendSrv, private $rootScope) {
+    this.dashboards = [];
+
+    backendSrv.get(`/api/plugins/dashboards/${this.plugin.id}`).then(dashboards => {
+      this.dashboards = dashboards;
+    });
+  }
+
+  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 => {
+      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;
+    });
+  }
+}
+
+export function dashboardImportList() {
+  return {
+    restrict: 'E',
+    templateUrl: 'public/app/features/plugins/import_list/import_list.html',
+    controller: DashImportListCtrl,
+    bindToController: true,
+    controllerAs: 'ctrl',
+    scope: {
+      plugin: "="
+    }
+  };
+}
+
+coreModule.directive('dashboardImportList', dashboardImportList);
+
+
+
+

+ 81 - 0
public/app/features/plugins/partials/ds_edit.html

@@ -0,0 +1,81 @@
+<navbar title="Data Sources" title-url="datasources" icon="icon-gf icon-gf-datasources">
+</navbar>
+
+<div class="page-container">
+
+  <div class="page-header">
+		<h1 ng-show="isNew">Add data source</h1>
+		<h1 ng-show="!isNew">Edit data source</h1>
+
+		<div class="page-header-tabs" ng-show="ctrl.hasDashboards">
+			<ul class="gf-tabs">
+				<li class="gf-tabs-item">
+					<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 0" ng-class="{active: ctrl.tabIndex === 0}">
+					  Config
+					</a>
+				</li>
+				<li class="gf-tabs-item">
+					<a class="gf-tabs-link" ng-click="ctrl.tabIndex = 1" ng-class="{active: ctrl.tabIndex === 1}">
+					  Dashboards
+					</a>
+				</li>
+			</ul>
+		</div>
+	</div>
+
+  <div ng-if="ctrl.tabIndex === 0" class="tab-content">
+
+    <form name="editForm">
+      <div class="gf-form-group">
+        <div class="gf-form">
+          <span class="gf-form-label width-7">Name</span>
+          <input class="gf-form-input max-width-21" type="text" ng-model="ctrl.current.name" placeholder="My data source name" required>
+          <info-popover offset="0px -95px">
+            The name is used when you select the data source in panels.
+            The <code>Default</code> data source is preselected in new
+            panels.
+          </info-popover>
+
+          <editor-checkbox text="Default" model="ctrl.current.isDefault"></editor-checkbox>
+        </div>
+
+        <div class="gf-form">
+          <span class="gf-form-label width-7">Type</span>
+          <div class="gf-form-select-wrapper">
+            <select class="gf-form-input gf-size-auto" ng-model="ctrl.current.type" ng-options="v.id as v.name for v in ctrl.types" ng-change="ctrl.typeChanged()"></select>
+          </div>
+        </div>
+      </div>
+
+      <rebuild-on-change property="ctrl.datasourceMeta.id">
+        <plugin-component type="datasource-config-ctrl">
+        </plugin-component>
+      </rebuild-on-change>
+
+      <div ng-if="testing" style="margin-top: 25px">
+        <h5 ng-show="!testing.done">Testing.... <i class="fa fa-spiner fa-spin"></i></h5>
+        <h5 ng-show="testing.done">Test results</h5>
+        <div class="alert-{{testing.status}} alert">
+          <div class="alert-title">{{testing.title}}</div>
+          <div ng-bind='testing.message'></div>
+        </div>
+      </div>
+
+      <div class="gf-form-button-row">
+        <button type="submit" class="btn btn-success" ng-show="ctrl.isNew" ng-click="ctrl.saveChanges()">Add</button>
+        <button type="submit" class="btn btn-success" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges()">Save</button>
+        <button type="submit" class="btn btn-secondary" ng-show="!ctrl.isNew" ng-click="ctrl.saveChanges(true)">
+          Test Connection
+        </button>
+        <a class="btn btn-link" href="datasources">Cancel</a>
+      </div>
+
+    </form>
+  </div>
+
+  <div ng-if="ctrl.tabIndex === 1" class="tab-content">
+    <dashboard-import-list plugin="ctrl.datasourceMeta"></dashboard-import-list>
+  </div>
+
+</div>
+

+ 0 - 0
public/app/features/datasources/partials/http_settings.html → public/app/features/plugins/partials/ds_http_settings.html


+ 0 - 0
public/app/features/datasources/partials/list.html → public/app/features/plugins/partials/ds_list.html


+ 0 - 0
public/app/features/plugins/partials/edit.html → public/app/features/plugins/partials/plugin_edit.html


+ 0 - 0
public/app/features/plugins/partials/list.html → public/app/features/plugins/partials/plugin_list.html


+ 0 - 0
public/app/features/plugins/partials/page.html → public/app/features/plugins/partials/plugin_page.html


+ 1 - 1
public/app/features/plugins/edit_ctrl.ts → public/app/features/plugins/plugin_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);
 

+ 1 - 1
public/app/features/plugins/list_ctrl.ts → public/app/features/plugins/plugin_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;
     });
   }

+ 0 - 0
public/app/features/plugins/page_ctrl.ts → public/app/features/plugins/plugin_page_ctrl.ts


+ 23 - 1
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 filters = _.map(filterJson, function(values, name) {
+          return {
+            Name: name,
+            Values: values
+          };
+        });
+        var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]);
+
+        return this.performEC2DescribeInstances(region, filters, 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([]);
     };
 

+ 1 - 1
public/app/plugins/datasource/graphite/dashboards/carbon_stats.json

@@ -6,7 +6,7 @@
     }
   },
 
-  "title": "Carbon stats",
+  "title": "Carbon Cache Stats",
   "version": 1,
   "rows": [
     {

+ 1 - 1
public/app/plugins/datasource/graphite/plugin.json

@@ -4,7 +4,7 @@
   "id": "graphite",
 
   "includes": [
-    {"type": "dashboard", "name": "Carbon Stats", "path": "dashboards/carbon_stats.json"}
+    {"type": "dashboard", "name": "Carbon Cache Stats", "path": "dashboards/carbon_stats.json"}
   ],
 
   "metrics": true,

+ 1 - 1
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);
     }

+ 0 - 48
tests/app-plugin-json/plugin.json

@@ -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"}
-    ]
-  }
-}

+ 5 - 0
tests/test-app/dashboards/connections.json

@@ -0,0 +1,5 @@
+{
+  "title": "Nginx Connections",
+  "revision": "1.5",
+  "schemaVersion": 11
+}

+ 5 - 0
tests/test-app/dashboards/memory.json

@@ -0,0 +1,5 @@
+{
+  "title": "Nginx Memory",
+  "revision": "2.0",
+  "schemaVersion": 11
+}

+ 12 - 7
examples/nginx-app/plugin.json → tests/test-app/plugin.json

@@ -1,7 +1,7 @@
 {
   "type": "app",
-  "name": "Nginx",
-  "id": "nginx-app",
+  "name": "Test App",
+  "id": "test-app",
 
   "staticRoot": ".",
 
@@ -16,16 +16,20 @@
   },
 
   "info": {
-    "description": "Official Grafana Nginx App & Dashboard bundle",
+    "description": "Official Grafana Test App & Dashboard bundle",
     "author": {
-      "name": "Nginx Inc.",
-      "url": "http://nginx.com"
+      "name": "Test Inc.",
+      "url": "http://test.com"
     },
-    "keywords": ["nginx"],
+    "keywords": ["test"],
     "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"}
@@ -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"}
   ],