diff --git a/docs/sources/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index 4d9d8b38c1c..351b08eb2aa 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -64,9 +64,19 @@ Name | Description `metrics(namespace)` | Returns a list of metrics in the namespace. `dimension_keys(namespace)` | Returns a list of dimension keys in the namespace. `dimension_values(region, namespace, metric, dimension_key)` | Returns a list of dimension values matching the specified `region`, `namespace`, `metric` and `dimension_key`. +`ebs_volume_ids(region, instance_id)` | Returns a list of volume id matching the specified `region`, `instance_id`. +`ec2_instance_attribute(region, attribute_name, filters)` | Returns a list of attribute matching the specified `region`, `attribute_name`, `filters`. For details about the metrics CloudWatch provides, please refer to the [CloudWatch documentation](https://docs.aws.amazon.com/AmazonCloudWatch/latest/DeveloperGuide/CW_Support_For_AWS.html). +The `ec2_instance_attribute` query take `filters` in JSON format. +You can specify [pre-defined filters of ec2:DescribeInstances](http://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html). +Specify like `{ filter_name1: [ filter_value1 ], filter_name2: [ filter_value2 ] }` + +Example `ec2_instance_attribute()` query + + ec2_instance_attribute(us-east-1, InstanceId, { "tag:Environment": [ "production" ] }) + ![](/img/v2/cloudwatch_templating.png) ## Cost diff --git a/examples/README.md b/examples/README.md index 2e8a46f2aa3..64341a7fdbc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,3 +1,4 @@ ## Example plugin implementations -[datasource-plugin-genericdatsource](https://github.com/grafana/datasource-plugin-genericdatasource/tree/3.0) \ No newline at end of file +datasource:[simple-json-datasource](https://github.com/grafana/simple-json-datasource) +app: [example-app](https://github.com/grafana/example-app) \ No newline at end of file diff --git a/examples/panel-boilerplate-es5/css/styles.css b/examples/boilerplate-es5-panel/css/styles.css similarity index 100% rename from examples/panel-boilerplate-es5/css/styles.css rename to examples/boilerplate-es5-panel/css/styles.css diff --git a/examples/panel-boilerplate-es5/module.js b/examples/boilerplate-es5-panel/module.js similarity index 100% rename from examples/panel-boilerplate-es5/module.js rename to examples/boilerplate-es5-panel/module.js diff --git a/examples/panel-boilerplate-es5/panel.html b/examples/boilerplate-es5-panel/panel.html similarity index 100% rename from examples/panel-boilerplate-es5/panel.html rename to examples/boilerplate-es5-panel/panel.html diff --git a/examples/panel-boilerplate-es5/plugin.json b/examples/boilerplate-es5-panel/plugin.json similarity index 100% rename from examples/panel-boilerplate-es5/plugin.json rename to examples/boilerplate-es5-panel/plugin.json diff --git a/examples/nginx-app/.gitignore b/examples/nginx-app/.gitignore deleted file mode 100644 index 8c2c350441b..00000000000 --- a/examples/nginx-app/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -.DS_Store - -node_modules -tmp/* -npm-debug.log -dist/* - diff --git a/examples/nginx-app/.jscs.json b/examples/nginx-app/.jscs.json deleted file mode 100644 index dcf694dcc63..00000000000 --- a/examples/nginx-app/.jscs.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "disallowImplicitTypeConversion": ["string"], - "disallowKeywords": ["with"], - "disallowMultipleLineBreaks": true, - "disallowMixedSpacesAndTabs": true, - "disallowTrailingWhitespace": true, - "requireSpacesInFunctionExpression": { - "beforeOpeningCurlyBrace": true - }, - "disallowSpacesInsideArrayBrackets": true, - "disallowSpacesInsideParentheses": true, - "validateIndentation": 2 -} \ No newline at end of file diff --git a/examples/nginx-app/.jshintrc b/examples/nginx-app/.jshintrc deleted file mode 100644 index 3725af83afc..00000000000 --- a/examples/nginx-app/.jshintrc +++ /dev/null @@ -1,36 +0,0 @@ -{ - "browser": true, - "esnext": true, - - "bitwise":false, - "curly": true, - "eqnull": true, - "devel": true, - "eqeqeq": true, - "forin": false, - "immed": true, - "supernew": true, - "expr": true, - "indent": 2, - "latedef": true, - "newcap": true, - "noarg": true, - "noempty": true, - "undef": true, - "boss": true, - "trailing": true, - "laxbreak": true, - "laxcomma": true, - "sub": true, - "unused": true, - "maxdepth": 6, - "maxlen": 140, - - "globals": { - "System": true, - "define": true, - "require": true, - "Chromath": false, - "setImmediate": true - } -} diff --git a/examples/nginx-app/Gruntfile.js b/examples/nginx-app/Gruntfile.js deleted file mode 100644 index b88d857cde3..00000000000 --- a/examples/nginx-app/Gruntfile.js +++ /dev/null @@ -1,54 +0,0 @@ -module.exports = function(grunt) { - - require('load-grunt-tasks')(grunt); - - grunt.loadNpmTasks('grunt-execute'); - grunt.loadNpmTasks('grunt-contrib-clean'); - - grunt.initConfig({ - - clean: ["dist"], - - copy: { - src_to_dist: { - cwd: 'src', - expand: true, - src: ['**/*', '!**/*.js', '!**/*.scss'], - dest: 'dist' - }, - pluginDef: { - expand: true, - src: ['plugin.json', 'readme.md'], - dest: 'dist', - } - }, - - watch: { - rebuild_all: { - files: ['src/**/*', 'plugin.json', 'readme.md'], - tasks: ['default'], - options: {spawn: false} - }, - }, - - babel: { - options: { - sourceMap: true, - presets: ["es2015"], - plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], - }, - dist: { - files: [{ - cwd: 'src', - expand: true, - src: ['**/*.js'], - dest: 'dist', - ext:'.js' - }] - }, - }, - - }); - - grunt.registerTask('default', ['clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel']); -}; diff --git a/examples/nginx-app/package.json b/examples/nginx-app/package.json deleted file mode 100644 index 32c070f96b0..00000000000 --- a/examples/nginx-app/package.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "name": "kentik-app", - "private": true, - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/raintank/kentik-app-poc.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/raintank/kentik-app-poc/issues" - }, - "devDependencies": { - "grunt": "~0.4.5", - "babel": "~6.5.1", - "grunt-babel": "~6.0.0", - "grunt-contrib-copy": "~0.8.2", - "grunt-contrib-watch": "^0.6.1", - "grunt-contrib-uglify": "~0.11.0", - "grunt-systemjs-builder": "^0.2.5", - "load-grunt-tasks": "~3.2.0", - "grunt-execute": "~0.2.2", - "grunt-contrib-clean": "~0.6.0" - }, - "dependencies": { - "babel-plugin-transform-es2015-modules-systemjs": "^6.5.0", - "babel-preset-es2015": "^6.5.0", - "lodash": "~4.0.0", - }, - "homepage": "https://github.com/raintank/kentik-app-poc#readme" -} diff --git a/examples/nginx-app/readme.md b/examples/nginx-app/readme.md deleted file mode 100644 index c267cfbb1fc..00000000000 --- a/examples/nginx-app/readme.md +++ /dev/null @@ -1,7 +0,0 @@ -## Overview - -This application is an example app. - -### Awesome - -Even though it does not have any features it is still pretty awesome. diff --git a/examples/nginx-app/src/components/config.html b/examples/nginx-app/src/components/config.html deleted file mode 100644 index c531ec36d76..00000000000 --- a/examples/nginx-app/src/components/config.html +++ /dev/null @@ -1,3 +0,0 @@ -

- Nginx config! -

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

- Logs page! -

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

- Stream page! -

diff --git a/examples/nginx-app/src/components/stream.js b/examples/nginx-app/src/components/stream.js deleted file mode 100644 index 8684b36c64d..00000000000 --- a/examples/nginx-app/src/components/stream.js +++ /dev/null @@ -1,6 +0,0 @@ - -export class StreamPageCtrl { -} -StreamPageCtrl.templateUrl = 'components/stream.html'; - - diff --git a/examples/nginx-app/src/css/dark.css b/examples/nginx-app/src/css/dark.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/css/light.css b/examples/nginx-app/src/css/light.css deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/dashboards/dashboard.js b/examples/nginx-app/src/dashboards/dashboard.js deleted file mode 100644 index 794e2c5217b..00000000000 --- a/examples/nginx-app/src/dashboards/dashboard.js +++ /dev/null @@ -1,17 +0,0 @@ -require([ -], function () { - - function Dashboard() { - - this.getInputs = function() { - - }; - - this.buildDashboard = function() { - - }; - } - - return Dashboard; -}); - diff --git a/examples/nginx-app/src/dashboards/nginx_connection_stats.json b/examples/nginx-app/src/dashboards/nginx_connection_stats.json deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/examples/nginx-app/src/datasource/datasource.js b/examples/nginx-app/src/datasource/datasource.js deleted file mode 100644 index 7f4ed363707..00000000000 --- a/examples/nginx-app/src/datasource/datasource.js +++ /dev/null @@ -1,12 +0,0 @@ -export default class NginxDatasource { - - constructor() {} - - query(options) { - return []; - } - - testDatasource() { - return false; - } -} diff --git a/examples/nginx-app/src/datasource/module.js b/examples/nginx-app/src/datasource/module.js deleted file mode 100644 index a3473e59889..00000000000 --- a/examples/nginx-app/src/datasource/module.js +++ /dev/null @@ -1,5 +0,0 @@ -import {Datasource} from './datasource'; - -export { - Datasource -}; \ No newline at end of file diff --git a/examples/nginx-app/src/datasource/plugin.json b/examples/nginx-app/src/datasource/plugin.json deleted file mode 100644 index ffce9492418..00000000000 --- a/examples/nginx-app/src/datasource/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "datasource", - "name": "Nginx Datasource", - "id": "nginx-datasource" -} diff --git a/examples/nginx-app/src/img/logo_large.png b/examples/nginx-app/src/img/logo_large.png deleted file mode 100644 index c28955960e4..00000000000 Binary files a/examples/nginx-app/src/img/logo_large.png and /dev/null differ diff --git a/examples/nginx-app/src/img/logo_small.png b/examples/nginx-app/src/img/logo_small.png deleted file mode 100644 index a6040f66f3d..00000000000 Binary files a/examples/nginx-app/src/img/logo_small.png and /dev/null differ diff --git a/examples/nginx-app/src/module.js b/examples/nginx-app/src/module.js deleted file mode 100644 index b5aeecc6ccf..00000000000 --- a/examples/nginx-app/src/module.js +++ /dev/null @@ -1,9 +0,0 @@ -import {LogsPageCtrl} from './components/logs'; -import {StreamPageCtrl} from './components/stream'; -import {NginxAppConfigCtrl} from './components/config'; - -export { - NginxAppConfigCtrl as ConfigCtrl, - StreamPageCtrl, - LogsPageCtrl -}; diff --git a/examples/nginx-app/src/panel/module.js b/examples/nginx-app/src/panel/module.js deleted file mode 100644 index 899586da81b..00000000000 --- a/examples/nginx-app/src/panel/module.js +++ /dev/null @@ -1,15 +0,0 @@ -import {PanelCtrl} from 'app/plugins/sdk'; - -class NginxPanelCtrl extends PanelCtrl { - - constructor($scope, $injector) { - super($scope, $injector); - } - -} -NginxPanelCtrl.template = '

nginx!

'; - -export { - NginxPanelCtrl as PanelCtrl -}; - diff --git a/examples/nginx-app/src/panel/plugin.json b/examples/nginx-app/src/panel/plugin.json deleted file mode 100644 index f3548c987f3..00000000000 --- a/examples/nginx-app/src/panel/plugin.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "panel", - "name": "Nginx Panel", - "id": "nginx-panel" -} diff --git a/pkg/api/api.go b/pkg/api/api.go index 793703153e1..d4bb22149f8 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -126,10 +126,6 @@ func Register(r *macaron.Macaron) { r.Post("/invites", quota("user"), bind(dtos.AddInviteForm{}), wrap(AddOrgInvite)) r.Patch("/invites/:code/revoke", wrap(RevokeInvite)) - // apps - r.Get("/plugins", wrap(GetPluginList)) - r.Get("/plugins/:pluginId/settings", wrap(GetPluginSettingById)) - r.Post("/plugins/:pluginId/settings", bind(m.UpdatePluginSettingCmd{}), wrap(UpdatePluginSetting)) }, reqOrgAdmin) // create new org @@ -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) diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index a4e97cdb347..54ab565bede 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/pkg/api/cloudwatch/metrics.go @@ -55,8 +55,10 @@ func init() { "S3BytesWritten", "S3BytesRead", "HDFSUtilization", "HDFSBytesRead", "HDFSBytesWritten", "MissingBlocks", "CorruptBlocks", "TotalLoad", "MemoryTotalMB", "MemoryReservedMB", "MemoryAvailableMB", "MemoryAllocatedMB", "PendingDeletionBlocks", "UnderReplicatedBlocks", "DfsPendingReplicationBlocks", "CapacityRemainingGB", "HbaseBackupFailed", "MostRecentBackupDuration", "TimeSinceLastSuccessfulBackup"}, "AWS/ES": {"ClusterStatus.green", "ClusterStatus.yellow", "ClusterStatus.red", "Nodes", "SearchableDocuments", "DeletedDocuments", "CPUUtilization", "FreeStorageSpace", "JVMMemoryPressure", "AutomatedSnapshotFailure", "MasterCPUUtilization", "MasterFreeStorageSpace", "MasterJVMMemoryPressure", "ReadLatency", "WriteLatency", "ReadThroughput", "WriteThroughput", "DiskQueueLength", "ReadIOPS", "WriteIOPS"}, + "AWS/Events": {"Invocations", "FailedInvocations", "TriggeredRules", "MatchedEvents", "ThrottledRules"}, "AWS/Kinesis": {"PutRecord.Bytes", "PutRecord.Latency", "PutRecord.Success", "PutRecords.Bytes", "PutRecords.Latency", "PutRecords.Records", "PutRecords.Success", "IncomingBytes", "IncomingRecords", "GetRecords.Bytes", "GetRecords.IteratorAgeMilliseconds", "GetRecords.Latency", "GetRecords.Success"}, "AWS/Lambda": {"Invocations", "Errors", "Duration", "Throttles"}, + "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "ReadIOPS", "ReadLatency", "ReadThroughput", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -85,8 +87,10 @@ func init() { "AWS/ELB": {"LoadBalancerName", "AvailabilityZone"}, "AWS/ElasticMapReduce": {"ClusterId", "JobFlowId", "JobId"}, "AWS/ES": {}, + "AWS/Events": {"RuleName"}, "AWS/Kinesis": {"StreamName"}, "AWS/Lambda": {"FunctionName"}, + "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier"}, diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index 61b149d73e4..5f3dac76296 100644 --- a/pkg/api/dtos/plugins.go +++ b/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"` +} diff --git a/pkg/api/plugin_setting.go b/pkg/api/plugins.go similarity index 77% rename from pkg/api/plugin_setting.go rename to pkg/api/plugins.go index 1ceecdfac18..1ba0dde6c9f 100644 --- a/pkg/api/plugin_setting.go +++ b/pkg/api/plugins.go @@ -107,3 +107,34 @@ func UpdatePluginSetting(c *middleware.Context, cmd m.UpdatePluginSettingCmd) Re return ApiSuccess("Plugin settings updated") } + +func GetPluginDashboards(c *middleware.Context) Response { + pluginId := c.Params(":pluginId") + + if list, err := plugins.GetPluginDashboards(c.OrgId, pluginId); err != nil { + if notfound, ok := err.(plugins.PluginNotFoundError); ok { + return ApiError(404, notfound.Error(), nil) + } + + return ApiError(500, "Failed to get plugin dashboards", err) + } else { + return Json(200, list) + } +} + +func InstallPluginDashboard(c *middleware.Context, apiCmd dtos.InstallPluginDashboardCmd) Response { + + cmd := plugins.InstallPluginDashboardCommand{ + OrgId: c.OrgId, + UserId: c.UserId, + PluginId: apiCmd.PluginId, + Path: apiCmd.Path, + Inputs: apiCmd.Inputs, + } + + if err := bus.Dispatch(&cmd); err != nil { + return ApiError(500, "Failed to install dashboard", err) + } + + return Json(200, cmd.Result) +} diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index 4993d8aa045..f1b36c90ef2 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -11,6 +11,7 @@ func runCommand(command func(commandLine CommandLine) error) func(context *cli.C cmd := &contextCommandLine{context} if err := command(cmd); err != nil { + log.Error("\nError: ") log.Errorf("%s\n\n", err) cmd.ShowHelp() diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index f98ff7b8a94..7ed93e825d3 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/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") } diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 4e74578c604..b277714fe9b 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -41,7 +41,7 @@ func main() { cli.StringFlag{ Name: "repo", Usage: "url to the plugin repository", - Value: "", + Value: "https://grafana-net.raintank.io/api/plugins", }, cli.BoolFlag{ Name: "debug, d", diff --git a/pkg/cmd/grafana-cli/services/services.go b/pkg/cmd/grafana-cli/services/services.go index fc77570b77c..cd03f755075 100644 --- a/pkg/cmd/grafana-cli/services/services.go +++ b/pkg/cmd/grafana-cli/services/services.go @@ -3,6 +3,7 @@ package services import ( "encoding/json" "errors" + "fmt" "github.com/franela/goreq" "github.com/grafana/grafana/pkg/cmd/grafana-cli/log" m "github.com/grafana/grafana/pkg/cmd/grafana-cli/models" @@ -12,8 +13,12 @@ import ( var IoHelper m.IoUtil = IoUtilImp{} func ListAllPlugins(repoUrl string) (m.PluginRepo, error) { + fullUrl := repoUrl + "/repo" + res, _ := goreq.Request{Uri: fullUrl, MaxRedirects: 3}.Do() - res, _ := goreq.Request{Uri: repoUrl + "/repo", MaxRedirects: 3}.Do() + if res.StatusCode != 200 { + return m.PluginRepo{}, fmt.Errorf("Could not access %s statuscode %v", fullUrl, res.StatusCode) + } var resp m.PluginRepo err := res.Body.FromJsonTo(&resp) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 1015dfbe28c..3e87f504a77 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -102,8 +102,11 @@ func (cmd *SaveDashboardCommand) GetDashboardModel() *Dashboard { } // GetString a -func (dash *Dashboard) GetString(prop string) string { - return dash.Data[prop].(string) +func (dash *Dashboard) GetString(prop string, defaultValue string) string { + if val, exists := dash.Data[prop]; exists { + return val.(string) + } + return defaultValue } // UpdateSlug updates the slug diff --git a/pkg/models/plugin_setting.go b/pkg/models/plugin_settings.go similarity index 100% rename from pkg/models/plugin_setting.go rename to pkg/models/plugin_settings.go diff --git a/pkg/plugins/dashboard_installer.go b/pkg/plugins/dashboard_installer.go new file mode 100644 index 00000000000..279ab209989 --- /dev/null +++ b/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 +} diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go new file mode 100644 index 00000000000..1697ad808f2 --- /dev/null +++ b/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 +} diff --git a/pkg/plugins/dashboards_test.go b/pkg/plugins/dashboards_test.go new file mode 100644 index 00000000000..58cbe2f4920 --- /dev/null +++ b/pkg/plugins/dashboards_test.go @@ -0,0 +1,53 @@ +package plugins + +import ( + "testing" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + . "github.com/smartystreets/goconvey/convey" + "gopkg.in/ini.v1" +) + +func TestPluginDashboards(t *testing.T) { + + Convey("When asking plugin dashboard info", t, func() { + setting.Cfg = ini.Empty() + sec, _ := setting.Cfg.NewSection("plugin.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, "") + }) + }) + +} diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 0addaac9bb7..502c355eda8 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -3,12 +3,28 @@ package plugins import ( "encoding/json" "errors" + "fmt" "strings" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" ) +var ( + PluginTypeApp = "app" + PluginTypeDatasource = "datasource" + PluginTypePanel = "panel" + PluginTypeDashboard = "dashboard" +) + +type PluginNotFoundError struct { + PluginId string +} + +func (e PluginNotFoundError) Error() string { + return fmt.Sprintf("Plugin with id %s not found", e.PluginId) +} + type PluginLoader interface { Load(decoder *json.Decoder, pluginDir string) error } diff --git a/pkg/plugins/plugins_test.go b/pkg/plugins/plugins_test.go index 08ff3cbdfdd..f2dbc1e2e82 100644 --- a/pkg/plugins/plugins_test.go +++ b/pkg/plugins/plugins_test.go @@ -27,14 +27,15 @@ func TestPluginScans(t *testing.T) { Convey("When reading app plugin definition", t, func() { setting.Cfg = ini.Empty() - sec, _ := setting.Cfg.NewSection("plugin.app-test") - sec.NewKey("path", "../../tests/app-plugin-json") + sec, _ := setting.Cfg.NewSection("plugin.nginx-app") + sec.NewKey("path", "../../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") }) } diff --git a/public/app/core/directives/plugin_component.ts b/public/app/core/directives/plugin_component.ts index 858aab02035..69535327cc8 100644 --- a/public/app/core/directives/plugin_component.ts +++ b/public/app/core/directives/plugin_component.ts @@ -148,12 +148,13 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ } // ConfigCtrl case 'datasource-config-ctrl': { - return System.import(scope.datasourceMeta.module).then(function(dsModule) { + var dsMeta = scope.ctrl.datasourceMeta; + return System.import(dsMeta.module).then(function(dsModule) { return { - baseUrl: scope.datasourceMeta.baseUrl, - name: 'ds-config-' + scope.datasourceMeta.id, + baseUrl: dsMeta.baseUrl, + name: 'ds-config-' + dsMeta.id, bindings: {meta: "=", current: "="}, - attrs: {meta: "datasourceMeta", current: "current"}, + attrs: {meta: "ctrl.datasourceMeta", current: "ctrl.current"}, Component: dsModule.ConfigCtrl, }; }); diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 6e3af683c82..92cf32448a1 100644 --- a/public/app/core/routes/routes.ts +++ b/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, diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index b96f6693324..d110019add6 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -13,7 +13,6 @@ define([ './timeSrv', './unsavedChangesSrv', './timepicker/timepicker', - './import_list/import_list', './graphiteImportCtrl', './dynamicDashboardSrv', './importCtrl', diff --git a/public/app/features/dashboard/import_list/import_list.ts b/public/app/features/dashboard/import_list/import_list.ts deleted file mode 100644 index 1d3aa42611b..00000000000 --- a/public/app/features/dashboard/import_list/import_list.ts +++ /dev/null @@ -1,53 +0,0 @@ -/// - -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 = ` -

Dashboards

-
- -
-`; - -export function dashboardImportList() { - return { - restrict: 'E', - template: template, - controller: DashImportListCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - plugin: "=" - } - }; -} - - -coreModule.directive('dashboardImportList', dashboardImportList); - - - - diff --git a/public/app/features/datasources/all.js b/public/app/features/datasources/all.js deleted file mode 100644 index b181fd475c2..00000000000 --- a/public/app/features/datasources/all.js +++ /dev/null @@ -1,4 +0,0 @@ -define([ - './list_ctrl', - './edit_ctrl', -], function () {}); diff --git a/public/app/features/datasources/edit_ctrl.js b/public/app/features/datasources/edit_ctrl.js deleted file mode 100644 index 062cec6e5e9..00000000000 --- a/public/app/features/datasources/edit_ctrl.js +++ /dev/null @@ -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(); - }); -}); diff --git a/public/app/features/datasources/partials/edit.html b/public/app/features/datasources/partials/edit.html deleted file mode 100644 index 39639206c23..00000000000 --- a/public/app/features/datasources/partials/edit.html +++ /dev/null @@ -1,63 +0,0 @@ - - - -
- - -
-
-
- Name - - - The name is used when you select the data source in panels. - The Default data source is preselected in new - panels. - - - -
- -
- Type -
- -
-
-
- - - - - - - -
-
Testing....
-
Test results
-
-
{{testing.title}}
-
-
-
- - - -
- - - - Cancel -
- -
-
- diff --git a/public/app/features/org/all.js b/public/app/features/org/all.js index cebd0dd1def..e04634d709a 100644 --- a/public/app/features/org/all.js +++ b/public/app/features/org/all.js @@ -4,5 +4,4 @@ define([ './userInviteCtrl', './orgApiKeysCtrl', './orgDetailsCtrl', - '../datasources/all', ], function () {}); diff --git a/public/app/features/plugins/all.ts b/public/app/features/plugins/all.ts index 9d54165e56b..346fb2b30ef 100644 --- a/public/app/features/plugins/all.ts +++ b/public/app/features/plugins/all.ts @@ -1,3 +1,6 @@ -import './edit_ctrl'; -import './page_ctrl'; -import './list_ctrl'; +import './plugin_edit_ctrl'; +import './plugin_page_ctrl'; +import './plugin_list_ctrl'; +import './import_list/import_list'; +import './ds_edit_ctrl'; +import './ds_list_ctrl'; diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts new file mode 100644 index 00000000000..1e07b3a814a --- /dev/null +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -0,0 +1,145 @@ +/// + +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' + }; +}); diff --git a/public/app/features/datasources/list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts similarity index 100% rename from public/app/features/datasources/list_ctrl.ts rename to public/app/features/plugins/ds_list_ctrl.ts diff --git a/public/app/features/plugins/import_list/import_list.html b/public/app/features/plugins/import_list/import_list.html new file mode 100644 index 00000000000..d6c1bb914ce --- /dev/null +++ b/public/app/features/plugins/import_list/import_list.html @@ -0,0 +1,37 @@ +
+ + + + + + + + + + +
+ + + + {{dash.title}} + + + {{dash.title}} + + + v{{dash.revision}} + + Installed v{{dash.installedRevision}} + + + + +
+
+ diff --git a/public/app/features/plugins/import_list/import_list.ts b/public/app/features/plugins/import_list/import_list.ts new file mode 100644 index 00000000000..62145623fa2 --- /dev/null +++ b/public/app/features/plugins/import_list/import_list.ts @@ -0,0 +1,58 @@ +/// + +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); + + + + diff --git a/public/app/features/plugins/partials/ds_edit.html b/public/app/features/plugins/partials/ds_edit.html new file mode 100644 index 00000000000..46177d998cc --- /dev/null +++ b/public/app/features/plugins/partials/ds_edit.html @@ -0,0 +1,81 @@ + + + +
+ + + +
+ +
+
+
+ Name + + + The name is used when you select the data source in panels. + The Default data source is preselected in new + panels. + + + +
+ +
+ Type +
+ +
+
+
+ + + + + + +
+
Testing....
+
Test results
+
+
{{testing.title}}
+
+
+
+ +
+ + + + Cancel +
+ +
+
+ +
+ +
+ +
+ diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/plugins/partials/ds_http_settings.html similarity index 100% rename from public/app/features/datasources/partials/http_settings.html rename to public/app/features/plugins/partials/ds_http_settings.html diff --git a/public/app/features/datasources/partials/list.html b/public/app/features/plugins/partials/ds_list.html similarity index 100% rename from public/app/features/datasources/partials/list.html rename to public/app/features/plugins/partials/ds_list.html diff --git a/public/app/features/plugins/partials/edit.html b/public/app/features/plugins/partials/plugin_edit.html similarity index 100% rename from public/app/features/plugins/partials/edit.html rename to public/app/features/plugins/partials/plugin_edit.html diff --git a/public/app/features/plugins/partials/list.html b/public/app/features/plugins/partials/plugin_list.html similarity index 100% rename from public/app/features/plugins/partials/list.html rename to public/app/features/plugins/partials/plugin_list.html diff --git a/public/app/features/plugins/partials/page.html b/public/app/features/plugins/partials/plugin_page.html similarity index 100% rename from public/app/features/plugins/partials/page.html rename to public/app/features/plugins/partials/plugin_page.html diff --git a/public/app/features/plugins/edit_ctrl.ts b/public/app/features/plugins/plugin_edit_ctrl.ts similarity index 97% rename from public/app/features/plugins/edit_ctrl.ts rename to public/app/features/plugins/plugin_edit_ctrl.ts index f8e7c96792c..2d4362835d4 100644 --- a/public/app/features/plugins/edit_ctrl.ts +++ b/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); diff --git a/public/app/features/plugins/list_ctrl.ts b/public/app/features/plugins/plugin_list_ctrl.ts similarity index 81% rename from public/app/features/plugins/list_ctrl.ts rename to public/app/features/plugins/plugin_list_ctrl.ts index a1bacc09c14..2693a4d8755 100644 --- a/public/app/features/plugins/list_ctrl.ts +++ b/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; }); } diff --git a/public/app/features/plugins/page_ctrl.ts b/public/app/features/plugins/plugin_page_ctrl.ts similarity index 100% rename from public/app/features/plugins/page_ctrl.ts rename to public/app/features/plugins/plugin_page_ctrl.ts diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 06c723e416a..704cbd41e72 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -143,7 +143,7 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { return this.awsRequest({ region: region, action: 'DescribeInstances', - parameters: { filter: filters, instanceIds: instanceIds } + parameters: { filters: filters, instanceIds: instanceIds } }); }; @@ -205,6 +205,28 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { }); } + var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/); + if (ec2InstanceAttributeQuery) { + region = templateSrv.replace(ec2InstanceAttributeQuery[1]); + var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3])); + var 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([]); }; diff --git a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json index 642f32b6e6a..ad2c32be759 100644 --- a/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json +++ b/public/app/plugins/datasource/graphite/dashboards/carbon_stats.json @@ -6,7 +6,7 @@ } }, - "title": "Carbon stats", + "title": "Carbon Cache Stats", "version": 1, "rows": [ { diff --git a/public/app/plugins/datasource/graphite/plugin.json b/public/app/plugins/datasource/graphite/plugin.json index b079ec320f2..f7715f9c5b9 100644 --- a/public/app/plugins/datasource/graphite/plugin.json +++ b/public/app/plugins/datasource/graphite/plugin.json @@ -4,7 +4,7 @@ "id": "graphite", "includes": [ - {"type": "dashboard", "name": "Carbon Stats", "path": "dashboards/carbon_stats.json"} + {"type": "dashboard", "name": "Carbon Cache Stats", "path": "dashboards/carbon_stats.json"} ], "metrics": true, diff --git a/public/app/plugins/datasource/influxdb/datasource.ts b/public/app/plugins/datasource/influxdb/datasource.ts index 84c7a83394c..1136c9709bd 100644 --- a/public/app/plugins/datasource/influxdb/datasource.ts +++ b/public/app/plugins/datasource/influxdb/datasource.ts @@ -104,7 +104,7 @@ export function InfluxDatasource(instanceSettings, $q, backendSrv, templateSrv) this.metricFindQuery = function (query) { var interpolated; try { - interpolated = templateSrv.replace(query); + interpolated = templateSrv.replace(query, null, 'regex'); } catch (err) { return $q.reject(err); } diff --git a/tests/app-plugin-json/plugin.json b/tests/app-plugin-json/plugin.json deleted file mode 100644 index 59f41766eda..00000000000 --- a/tests/app-plugin-json/plugin.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "type": "app", - "name": "App Example", - "id": "app-example", - - "staticRoot": ".", - "module": "app", - - "pages": [ - {"name": "Example1", "url": "/app-example", "reqRole": "Editor"} - ], - - "css": { - "light": "css/plugin.dark.css", - "dark": "css/plugin.light.css" - }, - - "info": { - "description": "Example Grafana App", - "author": { - "name": "Raintank Inc.", - "url": "http://raintank.io" - }, - "keywords": ["example"], - "logos": { - "small": "img/logo_small.png", - "large": "img/logo_large.png" - }, - "screenshots": [ - {"name": "img1", "path": "img/screenshot1.png"}, - {"name": "img2", "path": "img/screenshot2.png"} - ], - "links": [ - {"name": "Project site", "url": "http://project.com"}, - {"name": "License & Terms", "url": "http://license.com"} - ], - "version": "1.0.0", - "updated": "2015-02-10" - }, - - "dependencies": { - "grafanaVersion": "2.6.x", - "plugins": [ - {"type": "datasource", "id": "graphite", "name": "Graphite", "version": "1.0.0"}, - {"type": "panel", "id": "graph", "name": "Graph", "version": "1.0.0"} - ] - } -} diff --git a/tests/test-app/dashboards/connections.json b/tests/test-app/dashboards/connections.json new file mode 100644 index 00000000000..f1f62826f73 --- /dev/null +++ b/tests/test-app/dashboards/connections.json @@ -0,0 +1,5 @@ +{ + "title": "Nginx Connections", + "revision": "1.5", + "schemaVersion": 11 +} diff --git a/tests/test-app/dashboards/memory.json b/tests/test-app/dashboards/memory.json new file mode 100644 index 00000000000..b79cb4d7dba --- /dev/null +++ b/tests/test-app/dashboards/memory.json @@ -0,0 +1,5 @@ +{ + "title": "Nginx Memory", + "revision": "2.0", + "schemaVersion": 11 +} diff --git a/examples/nginx-app/plugin.json b/tests/test-app/plugin.json similarity index 66% rename from examples/nginx-app/plugin.json rename to tests/test-app/plugin.json index 65f01ad62fc..cc82141690c 100644 --- a/examples/nginx-app/plugin.json +++ b/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"} ],