mirror of https://github.com/grafana/grafana.git
				
				
				
			Merge branch 'master' into cli_colors
This commit is contained in:
		
						commit
						740478344b
					
				|  | @ -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" ] }) | ||||
| 
 | ||||
|  | ||||
| 
 | ||||
| ## Cost | ||||
|  |  | |||
|  | @ -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) | ||||
|  | @ -1,7 +0,0 @@ | |||
| .DS_Store | ||||
| 
 | ||||
| node_modules | ||||
| tmp/* | ||||
| npm-debug.log | ||||
| dist/* | ||||
| 
 | ||||
|  | @ -1,13 +0,0 @@ | |||
| { | ||||
|     "disallowImplicitTypeConversion": ["string"], | ||||
|     "disallowKeywords": ["with"], | ||||
|     "disallowMultipleLineBreaks": true, | ||||
|     "disallowMixedSpacesAndTabs": true, | ||||
|     "disallowTrailingWhitespace": true, | ||||
|     "requireSpacesInFunctionExpression": { | ||||
|         "beforeOpeningCurlyBrace": true | ||||
|     }, | ||||
|     "disallowSpacesInsideArrayBrackets": true, | ||||
|     "disallowSpacesInsideParentheses": true, | ||||
|     "validateIndentation": 2 | ||||
| } | ||||
|  | @ -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 | ||||
|   } | ||||
| } | ||||
|  | @ -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']); | ||||
| }; | ||||
|  | @ -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" | ||||
| } | ||||
|  | @ -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. | ||||
|  | @ -1,3 +0,0 @@ | |||
| <h3> | ||||
| 	Nginx config! | ||||
| </h3> | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| export class NginxAppConfigCtrl { | ||||
| } | ||||
| NginxAppConfigCtrl.templateUrl = 'components/config.html'; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1,3 +0,0 @@ | |||
| <h3> | ||||
| 	Logs page! | ||||
| </h3> | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| export class LogsPageCtrl { | ||||
| } | ||||
| LogsPageCtrl.templateUrl = 'components/logs.html'; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1,3 +0,0 @@ | |||
| <h3> | ||||
| 	Stream page! | ||||
| </h3> | ||||
|  | @ -1,6 +0,0 @@ | |||
| 
 | ||||
| export class StreamPageCtrl { | ||||
| } | ||||
| StreamPageCtrl.templateUrl = 'components/stream.html'; | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1,17 +0,0 @@ | |||
| require([ | ||||
| ], function () { | ||||
| 
 | ||||
|   function Dashboard() { | ||||
| 
 | ||||
|     this.getInputs = function() { | ||||
| 
 | ||||
|     }; | ||||
| 
 | ||||
|     this.buildDashboard = function() { | ||||
| 
 | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   return Dashboard; | ||||
| }); | ||||
| 
 | ||||
|  | @ -1,12 +0,0 @@ | |||
| export default class NginxDatasource { | ||||
| 
 | ||||
|   constructor() {} | ||||
| 
 | ||||
|   query(options) { | ||||
|     return []; | ||||
|   } | ||||
| 
 | ||||
|   testDatasource() { | ||||
|     return false; | ||||
|   } | ||||
| } | ||||
|  | @ -1,5 +0,0 @@ | |||
| import {Datasource} from  './datasource'; | ||||
| 
 | ||||
| export { | ||||
|   Datasource | ||||
| }; | ||||
|  | @ -1,5 +0,0 @@ | |||
| { | ||||
|   "type": "datasource", | ||||
|   "name": "Nginx Datasource", | ||||
|   "id": "nginx-datasource" | ||||
| } | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 14 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 6.3 KiB | 
|  | @ -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 | ||||
| }; | ||||
|  | @ -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 | ||||
| }; | ||||
| 
 | ||||
|  | @ -1,5 +0,0 @@ | |||
| { | ||||
|   "type": "panel", | ||||
|   "name": "Nginx Panel", | ||||
|   "id": "nginx-panel" | ||||
| } | ||||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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"}, | ||||
|  |  | |||
|  | @ -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"` | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| } | ||||
|  | @ -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() | ||||
|  |  | |||
|  | @ -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") | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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", | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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 +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 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  | @ -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, "") | ||||
| 		}) | ||||
| 	}) | ||||
| 
 | ||||
| } | ||||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
|  | @ -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") | ||||
| 	}) | ||||
| 
 | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
|           }; | ||||
|         }); | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -13,7 +13,6 @@ define([ | |||
|   './timeSrv', | ||||
|   './unsavedChangesSrv', | ||||
|   './timepicker/timepicker', | ||||
|   './import_list/import_list', | ||||
|   './graphiteImportCtrl', | ||||
|   './dynamicDashboardSrv', | ||||
|   './importCtrl', | ||||
|  |  | |||
|  | @ -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); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -1,4 +0,0 @@ | |||
| define([ | ||||
|   './list_ctrl', | ||||
|   './edit_ctrl', | ||||
| ], function () {}); | ||||
|  | @ -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(); | ||||
|   }); | ||||
| }); | ||||
|  | @ -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-import-list plugin="current"></dashboard-import-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> | ||||
| 
 | ||||
|  | @ -4,5 +4,4 @@ define([ | |||
|   './userInviteCtrl', | ||||
|   './orgApiKeysCtrl', | ||||
|   './orgDetailsCtrl', | ||||
|   '../datasources/all', | ||||
| ], function () {}); | ||||
|  |  | |||
|  | @ -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'; | ||||
|  |  | |||
|  | @ -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 +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> | ||||
| 
 | ||||
|  | @ -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); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -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> | ||||
| 
 | ||||
|  | @ -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); | ||||
| 
 | ||||
|  | @ -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; | ||||
|     }); | ||||
|   } | ||||
|  | @ -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([]); | ||||
|     }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
|     } | ||||
|   }, | ||||
| 
 | ||||
|   "title": "Carbon stats", | ||||
|   "title": "Carbon Cache Stats", | ||||
|   "version": 1, | ||||
|   "rows": [ | ||||
|     { | ||||
|  |  | |||
|  | @ -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, | ||||
|  |  | |||
|  | @ -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); | ||||
|     } | ||||
|  |  | |||
|  | @ -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"} | ||||
|     ] | ||||
|   } | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| { | ||||
|   "title": "Nginx Connections", | ||||
|   "revision": "1.5", | ||||
|   "schemaVersion": 11 | ||||
| } | ||||
|  | @ -0,0 +1,5 @@ | |||
| { | ||||
|   "title": "Nginx Memory", | ||||
|   "revision": "2.0", | ||||
|   "schemaVersion": 11 | ||||
| } | ||||
|  | @ -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"} | ||||
|   ], | ||||
		Loading…
	
		Reference in New Issue