diff --git a/.floo b/.floo index a8fb87025ec..1c2038f98cc 100644 --- a/.floo +++ b/.floo @@ -1,4 +1,3 @@ { "url": "https://floobits.com/raintank/grafana" } - diff --git a/.flooignore b/.flooignore index b9bb5ef4fe2..43cddf93bdf 100644 --- a/.flooignore +++ b/.flooignore @@ -10,4 +10,3 @@ data/ vendor/ public_gen/ dist/ - diff --git a/.jscs.json b/.jscs.json index dcf694dcc63..8fdad332de5 100644 --- a/.jscs.json +++ b/.jscs.json @@ -10,4 +10,4 @@ "disallowSpacesInsideArrayBrackets": true, "disallowSpacesInsideParentheses": true, "validateIndentation": 2 -} \ No newline at end of file +} diff --git a/conf/defaults.ini b/conf/defaults.ini index 068c038373c..ea70f095ee1 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -347,12 +347,15 @@ global_api_key = -1 global_session = -1 #################################### Internal Grafana Metrics ########################## +# Metrics available at HTTP API Url /api/metrics [metrics] enabled = true interval_seconds = 60 +# Send internal Grafana metrics to graphite ; [metrics.graphite] ; address = localhost:2003 ; prefix = prod.grafana.%(instance_name)s. - +[grafana_net] +url = https://grafana.net diff --git a/conf/sample.ini b/conf/sample.ini index dbb761c4613..811f4bbcc51 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -294,6 +294,7 @@ check_for_updates = true ;path = /var/lib/grafana/dashboards #################################### Internal Grafana Metrics ########################## +# Metrics available at HTTP API Url /api/metrics [metrics] # Disable / Enable internal metrics ;enabled = true @@ -306,4 +307,7 @@ check_for_updates = true ; address = localhost:2003 ; prefix = prod.grafana.%(instance_name)s. - +#################################### Internal Grafana Metrics ########################## +# Url used to to import dashboards directly from Grafana.net +[grafana_net] +url = https://grafana.net diff --git a/docs/sources/reference/dashboard.md b/docs/sources/reference/dashboard.md index 93adf5cd789..831dbe3abdc 100644 --- a/docs/sources/reference/dashboard.md +++ b/docs/sources/reference/dashboard.md @@ -26,7 +26,6 @@ When a user creates a new dashboard, a new dashboard JSON object is initialized { "id": null, "title": "New dashboard", - "originalTitle": "New dashboard", "tags": [], "style": "dark", "timezone": "browser", @@ -59,7 +58,6 @@ Each field in the dashboard JSON is explained below with its usage: | ---- | ----- | | **id** | unique dashboard id, an integer | | **title** | current title of dashboard | -| **originalTitle** | title of dashboard when saved for the first time | | **tags** | tags associated with dashboard, an array of strings | | **style** | theme of dashboard, i.e. `dark` or `light` | | **timezone** | timezone of dashboard, i.e. `utc` or `browser` | diff --git a/pkg/api/api.go b/pkg/api/api.go index ccbb59a976f..430b81f2380 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -55,6 +55,8 @@ func Register(r *macaron.Macaron) { r.Get("/dashboard/*", reqSignedIn, Index) r.Get("/dashboard-solo/*", reqSignedIn, Index) + r.Get("/import/dashboard", reqSignedIn, Index) + r.Get("/dashboards/*", reqSignedIn, Index) r.Get("/playlists/", reqSignedIn, Index) r.Get("/playlists/*", reqSignedIn, Index) diff --git a/pkg/api/dtos/plugins.go b/pkg/api/dtos/plugins.go index fccb7c36849..70e732424ab 100644 --- a/pkg/api/dtos/plugins.go +++ b/pkg/api/dtos/plugins.go @@ -1,6 +1,9 @@ package dtos -import "github.com/grafana/grafana/pkg/plugins" +import ( + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/plugins" +) type PluginSetting struct { Name string `json:"name"` @@ -50,5 +53,6 @@ type ImportDashboardCommand struct { PluginId string `json:"pluginId"` Path string `json:"path"` Overwrite bool `json:"overwrite"` + Dashboard *simplejson.Json `json:"dashboard"` Inputs []plugins.ImportDashboardInput `json:"inputs"` } diff --git a/pkg/api/gnetproxy.go b/pkg/api/gnetproxy.go index 6511afd39b7..8425f0e41b7 100644 --- a/pkg/api/gnetproxy.go +++ b/pkg/api/gnetproxy.go @@ -5,9 +5,12 @@ import ( "net" "net/http" "net/http/httputil" + "net/url" "time" + "github.com/Unknwon/log" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -22,12 +25,15 @@ var gNetProxyTransport = &http.Transport{ } func ReverseProxyGnetReq(proxyPath string) *httputil.ReverseProxy { - director := func(req *http.Request) { - req.URL.Scheme = "https" - req.URL.Host = "grafana.net" - req.Host = "grafana.net" + url, _ := url.Parse(setting.GrafanaNetUrl) - req.URL.Path = util.JoinUrlFragments("https://grafana.net/api", proxyPath) + director := func(req *http.Request) { + req.URL.Scheme = url.Scheme + req.URL.Host = url.Host + req.Host = url.Host + + req.URL.Path = util.JoinUrlFragments(url.Path+"/api", proxyPath) + log.Info("Url: %v", req.URL.Path) // clear cookie headers req.Header.Del("Cookie") diff --git a/pkg/api/index.go b/pkg/api/index.go index 4e4f4fadfeb..9108de5e944 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -69,7 +69,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Divider: true}) dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "New", Icon: "fa fa-plus", Url: setting.AppSubUrl + "/dashboard/new"}) - dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/import/dashboard"}) + dashboardChildNavs = append(dashboardChildNavs, &dtos.NavLink{Text: "Import", Icon: "fa fa-download", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}) } data.MainNavLinks = append(data.MainNavLinks, &dtos.NavLink{ diff --git a/pkg/api/plugins.go b/pkg/api/plugins.go index 7d6d5906913..9d25b9c331e 100644 --- a/pkg/api/plugins.go +++ b/pkg/api/plugins.go @@ -168,10 +168,11 @@ func ImportDashboard(c *middleware.Context, apiCmd dtos.ImportDashboardCommand) Path: apiCmd.Path, Inputs: apiCmd.Inputs, Overwrite: apiCmd.Overwrite, + Dashboard: apiCmd.Dashboard, } if err := bus.Dispatch(&cmd); err != nil { - return ApiError(500, "Failed to install dashboard", err) + return ApiError(500, "Failed to import dashboard", err) } return Json(200, cmd.Result) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index 6b19224f934..610f29e70aa 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -29,6 +29,7 @@ type Dashboard struct { Id int64 Slug string OrgId int64 + GnetId int64 Version int Created time.Time @@ -77,6 +78,10 @@ func NewDashboardFromJson(data *simplejson.Json) *Dashboard { dash.Updated = time.Now() } + if gnetId, err := dash.Data.Get("gnetId").Float64(); err == nil { + dash.GnetId = int64(gnetId) + } + return dash } diff --git a/pkg/plugins/dashboard_importer.go b/pkg/plugins/dashboard_importer.go index 4d2757b9a0e..8f6998d344d 100644 --- a/pkg/plugins/dashboard_importer.go +++ b/pkg/plugins/dashboard_importer.go @@ -11,6 +11,7 @@ import ( ) type ImportDashboardCommand struct { + Dashboard *simplejson.Json Path string Inputs []ImportDashboardInput Overwrite bool @@ -41,17 +42,15 @@ func init() { } func ImportDashboard(cmd *ImportDashboardCommand) error { - plugin, exists := Plugins[cmd.PluginId] - - if !exists { - return PluginNotFoundError{cmd.PluginId} - } - var dashboard *m.Dashboard var err error - if dashboard, err = loadPluginDashboard(plugin, cmd.Path); err != nil { - return err + if cmd.PluginId != "" { + if dashboard, err = loadPluginDashboard(cmd.PluginId, cmd.Path); err != nil { + return err + } + } else { + dashboard = m.NewDashboardFromJson(cmd.Dashboard) } evaluator := &DashTemplateEvaluator{ @@ -76,13 +75,13 @@ func ImportDashboard(cmd *ImportDashboardCommand) error { } 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, + PluginId: cmd.PluginId, + Title: dashboard.Title, + Path: cmd.Path, + Revision: dashboard.Data.Get("revision").MustInt64(1), + ImportedUri: "db/" + saveCmd.Result.Slug, + ImportedRevision: dashboard.Data.Get("revision").MustInt64(1), + Imported: true, } return nil @@ -110,7 +109,7 @@ func (this *DashTemplateEvaluator) findInput(varName string, varType string) *Im func (this *DashTemplateEvaluator) Eval() (*simplejson.Json, error) { this.result = simplejson.New() this.variables = make(map[string]string) - this.varRegex, _ = regexp.Compile(`(\$\{\w+\})`) + this.varRegex, _ = regexp.Compile(`(\$\{.+\})`) // check that we have all inputs we need for _, inputDef := range this.template.Get("__inputs").MustArray() { diff --git a/pkg/plugins/dashboards.go b/pkg/plugins/dashboards.go index 932196a42a9..1a160fe6632 100644 --- a/pkg/plugins/dashboards.go +++ b/pkg/plugins/dashboards.go @@ -10,14 +10,14 @@ import ( ) 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"` + PluginId string `json:"pluginId"` + Title string `json:"title"` + Imported bool `json:"imported"` + ImportedUri string `json:"importedUri"` + ImportedRevision int64 `json:"importedRevision"` + Revision int64 `json:"revision"` + Description string `json:"description"` + Path string `json:"path"` } func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDTO, error) { @@ -42,7 +42,12 @@ func GetPluginDashboards(orgId int64, pluginId string) ([]*PluginDashboardInfoDT return result, nil } -func loadPluginDashboard(plugin *PluginBase, path string) (*m.Dashboard, error) { +func loadPluginDashboard(pluginId, path string) (*m.Dashboard, error) { + plugin, exists := Plugins[pluginId] + + if !exists { + return nil, PluginNotFoundError{pluginId} + } dashboardFilePath := filepath.Join(plugin.PluginDir, path) reader, err := os.Open(dashboardFilePath) @@ -66,14 +71,14 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl var dashboard *m.Dashboard var err error - if dashboard, err = loadPluginDashboard(plugin, path); err != nil { + if dashboard, err = loadPluginDashboard(plugin.Id, path); err != nil { return nil, err } res.Path = path res.PluginId = plugin.Id res.Title = dashboard.Title - res.Revision = dashboard.GetString("revision", "1.0") + res.Revision = dashboard.Data.Get("revision").MustInt64(1) query := m.GetDashboardQuery{OrgId: orgId, Slug: dashboard.Slug} @@ -82,9 +87,9 @@ func getDashboardImportStatus(orgId int64, plugin *PluginBase, path string) (*Pl return nil, err } } else { - res.Installed = true - res.InstalledUri = "db/" + query.Result.Slug - res.InstalledRevision = query.Result.GetString("revision", "1.0") + res.Imported = true + res.ImportedUri = "db/" + query.Result.Slug + res.ImportedRevision = query.Result.Data.Get("revision").MustInt64(1) } return res, nil diff --git a/pkg/services/sqlstore/migrations/dashboard_mig.go b/pkg/services/sqlstore/migrations/dashboard_mig.go index a4a8629b331..536b153c6c9 100644 --- a/pkg/services/sqlstore/migrations/dashboard_mig.go +++ b/pkg/services/sqlstore/migrations/dashboard_mig.go @@ -102,4 +102,9 @@ func addDashboardMigration(mg *Migrator) { mg.AddMigration("Add column created_by in dashboard - v2", NewAddColumnMigration(dashboardV2, &Column{ Name: "created_by", Type: DB_Int, Nullable: true, })) + + // add column to store gnetId + mg.AddMigration("Add column gnetId in dashboard", NewAddColumnMigration(dashboardV2, &Column{ + Name: "gnet_id", Type: DB_BigInt, Nullable: true, + })) } diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 507bca272f4..6c425505289 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -141,6 +141,9 @@ var ( // logger logger log.Logger + + // Grafana.NET URL + GrafanaNetUrl string ) type CommandLineArgs struct { @@ -520,6 +523,8 @@ func NewConfigContext(args *CommandLineArgs) error { log.Warn("require_email_validation is enabled but smpt is disabled") } + GrafanaNetUrl = Cfg.Section("grafana.net").Key("url").MustString("https://grafana.net") + return nil } diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index a871e06ad30..91eebe09915 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -5,8 +5,10 @@ import store from 'app/core/store'; import _ from 'lodash'; import angular from 'angular'; import $ from 'jquery'; + import coreModule from 'app/core/core_module'; import {profiler} from 'app/core/profiler'; +import appEvents from 'app/core/app_events'; export class GrafanaCtrl { @@ -44,6 +46,7 @@ export class GrafanaCtrl { $rootScope.appEvent = function(name, payload) { $rootScope.$emit(name, payload); + appEvents.emit(name, payload); }; $rootScope.colors = [ diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 35c4431d80c..6344a26c886 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -62,14 +62,16 @@
- - - + Create New + + + + Import -
+ +
diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index e296acf56e1..a581afe3fd3 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -5,6 +5,7 @@ import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from '../../core_module'; +import appEvents from 'app/core/app_events'; export class SearchCtrl { isOpen: boolean; @@ -148,9 +149,6 @@ export class SearchCtrl { this.searchDashboards(); }; - newDashboard() { - this.$location.url('dashboard/new'); - }; } export function searchDirective() { diff --git a/public/app/core/components/wizard/wizard.html b/public/app/core/components/wizard/wizard.html new file mode 100644 index 00000000000..9d3f680649a --- /dev/null +++ b/public/app/core/components/wizard/wizard.html @@ -0,0 +1,32 @@ + + diff --git a/public/app/core/components/wizard/wizard.ts b/public/app/core/components/wizard/wizard.ts new file mode 100644 index 00000000000..2ae38cf9e03 --- /dev/null +++ b/public/app/core/components/wizard/wizard.ts @@ -0,0 +1,73 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; + +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class WizardSrv { + /** @ngInject */ + constructor() { + } +} + +export interface WizardStep { + name: string; + type: string; + process: any; +} + +export class SelectOptionStep { + type: string; + name: string; + fulfill: any; + + constructor() { + this.type = 'select'; + } + + process() { + return new Promise((fulfill, reject) => { + + }); + } +} + +export class WizardFlow { + name: string; + steps: WizardStep[]; + + constructor(name) { + this.name = name; + this.steps = []; + } + + addStep(step) { + this.steps.push(step); + } + + next(index) { + var step = this.steps[0]; + + return step.process().then(() => { + if (this.steps.length === index+1) { + return; + } + + return this.next(index+1); + }); + } + + start() { + appEvents.emit('show-modal', { + src: 'public/app/core/components/wizard/wizard.html', + model: this + }); + + return this.next(0); + } +} + +coreModule.service('wizardSrv', WizardSrv); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 9c8ae9cdad2..4dae139b66d 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -5,7 +5,6 @@ import "./directives/annotation_tooltip"; import "./directives/dash_class"; import "./directives/confirm_click"; import "./directives/dash_edit_link"; -import "./directives/dash_upload"; import "./directives/dropdown_typeahead"; import "./directives/grafana_version_check"; import "./directives/metric_segment"; @@ -34,6 +33,7 @@ import {layoutSelector} from './components/layout_selector/layout_selector'; import {switchDirective} from './components/switch'; import {dashboardSelector} from './components/dashboard_selector'; import {queryPartEditorDirective} from './components/query_part/query_part_editor'; +import {WizardFlow} from './components/wizard/wizard'; import 'app/core/controllers/all'; import 'app/core/services/all'; import 'app/core/routes/routes'; @@ -58,4 +58,5 @@ export { appEvents, dashboardSelector, queryPartEditorDirective, + WizardFlow, }; diff --git a/public/app/core/directives/dash_edit_link.js b/public/app/core/directives/dash_edit_link.js index 23855925541..7486d0ada18 100644 --- a/public/app/core/directives/dash_edit_link.js +++ b/public/app/core/directives/dash_edit_link.js @@ -6,28 +6,13 @@ function ($, coreModule) { 'use strict'; var editViewMap = { - 'settings': { src: 'public/app/features/dashboard/partials/settings.html', title: "Settings" }, - 'annotations': { src: 'public/app/features/annotations/partials/editor.html', title: "Annotations" }, - 'templating': { src: 'public/app/features/templating/partials/editor.html', title: "Templating" } + 'settings': { src: 'public/app/features/dashboard/partials/settings.html'}, + 'annotations': { src: 'public/app/features/annotations/partials/editor.html'}, + 'templating': { src: 'public/app/features/templating/partials/editor.html'}, + 'import': { src: '' } }; - coreModule.default.directive('dashEditorLink', function($timeout) { - return { - restrict: 'A', - link: function(scope, elem, attrs) { - var partial = attrs.dashEditorLink; - - elem.bind('click',function() { - $timeout(function() { - var editorScope = attrs.editorScope === 'isolated' ? null : scope; - scope.appEvent('show-dash-editor', { src: partial, scope: editorScope }); - }); - }); - } - }; - }); - - coreModule.default.directive('dashEditorView', function($compile, $location) { + coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) { return { restrict: 'A', link: function(scope, elem) { @@ -72,8 +57,25 @@ function ($, coreModule) { } }; - var src = "'" + payload.src + "'"; - var view = $('
'); + if (editview === 'import') { + var modalScope = $rootScope.$new(); + modalScope.$on("$destroy", function() { + editorScope.dismiss(); + }); + + $rootScope.appEvent('show-modal', { + templateHtml: '', + scope: modalScope, + backdrop: 'static' + }); + + return; + } + + var view = payload.src; + if (view.indexOf('.html') > 0) { + view = $('
'); + } elem.append(view); $compile(elem.contents())(editorScope); diff --git a/public/app/core/directives/dash_upload.js b/public/app/core/directives/dash_upload.js deleted file mode 100644 index b03bc201e83..00000000000 --- a/public/app/core/directives/dash_upload.js +++ /dev/null @@ -1,46 +0,0 @@ -define([ - '../core_module', - 'app/core/utils/kbn', -], -function (coreModule, kbn) { - 'use strict'; - - coreModule.default.directive('dashUpload', function(timer, alertSrv, $location) { - return { - restrict: 'A', - link: function(scope) { - function file_selected(evt) { - var files = evt.target.files; // FileList object - var readerOnload = function() { - return function(e) { - scope.$apply(function() { - try { - window.grafanaImportDashboard = JSON.parse(e.target.result); - } catch (err) { - console.log(err); - scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]); - return; - } - var title = kbn.slugifyForUrl(window.grafanaImportDashboard.title); - window.grafanaImportDashboard.id = null; - $location.path('/dashboard-import/' + title); - }); - }; - }; - for (var i = 0, f; f = files[i]; i++) { - var reader = new FileReader(); - reader.onload = (readerOnload)(f); - reader.readAsText(f); - } - } - // Check for the various File API support. - if (window.File && window.FileReader && window.FileList && window.Blob) { - // Something - document.getElementById('dashupload').addEventListener('change', file_selected, false); - } else { - alertSrv.set('Oops','Sorry, the HTML5 File APIs are not fully supported in this browser.','error'); - } - } - }; - }); -}); diff --git a/public/app/core/profiler.ts b/public/app/core/profiler.ts index 8684a5d3531..3e66916ced7 100644 --- a/public/app/core/profiler.ts +++ b/public/app/core/profiler.ts @@ -1,5 +1,5 @@ /// -// + import $ from 'jquery'; import _ from 'lodash'; import angular from 'angular'; diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index 61cdf32c128..97f7213850d 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -25,18 +25,6 @@ function (coreModule) { }); - coreModule.default.controller('DashFromImportCtrl', function($scope, $location, alertSrv) { - if (!window.grafanaImportDashboard) { - alertSrv.set('Not found', 'Cannot reload page with unsaved imported dashboard', 'warning', 7000); - $location.path(''); - return; - } - $scope.initDashboard({ - meta: { canShare: false, canStar: false }, - dashboard: window.grafanaImportDashboard - }, $scope); - }); - coreModule.default.controller('NewDashboardCtrl', function($scope) { $scope.initDashboard({ meta: { canStar: false, canShare: false }, diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 1608a772e87..487e9deef27 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -32,20 +32,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'SoloPanelCtrl', pageClass: 'page-dashboard', }) - .when('/dashboard-import/:file', { - templateUrl: 'public/app/partials/dashboard.html', - controller : 'DashFromImportCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) .when('/dashboard/new', { templateUrl: 'public/app/partials/dashboard.html', controller : 'NewDashboardCtrl', reloadOnSearch: false, pageClass: 'page-dashboard', }) - .when('/import/dashboard', { - templateUrl: 'public/app/features/dashboard/partials/import.html', + .when('/dashboards/list', { + templateUrl: 'public/app/features/dashboard/partials/dash_list.html', + controller : 'DashListCtrl', + }) + .when('/dashboards/migrate', { + templateUrl: 'public/app/features/dashboard/partials/migrate.html', controller : 'DashboardImportCtrl', }) .when('/datasources', { diff --git a/public/app/core/services/datasource_srv.js b/public/app/core/services/datasource_srv.js index 378cfa2d9d5..b8b94cd286d 100644 --- a/public/app/core/services/datasource_srv.js +++ b/public/app/core/services/datasource_srv.js @@ -84,11 +84,11 @@ function (angular, _, coreModule, config) { _.each(config.datasources, function(value, key) { if (value.meta && value.meta.metrics) { - metricSources.push({ - value: key === config.defaultDatasource ? null : key, - name: key, - meta: value.meta, - }); + metricSources.push({value: key, name: key, meta: value.meta}); + + if (key === config.defaultDatasource) { + metricSources.push({value: null, name: 'default', meta: value.meta}); + } } }); diff --git a/public/app/core/services/util_srv.js b/public/app/core/services/util_srv.js deleted file mode 100644 index e6bf3ae08bf..00000000000 --- a/public/app/core/services/util_srv.js +++ /dev/null @@ -1,31 +0,0 @@ -define([ - 'angular', - '../core_module', -], -function (angular, coreModule) { - 'use strict'; - - coreModule.default.service('utilSrv', function($rootScope, $modal, $q) { - - this.init = function() { - $rootScope.onAppEvent('show-modal', this.showModal, $rootScope); - }; - - this.showModal = function(e, options) { - var modal = $modal({ - modalClass: options.modalClass, - template: options.src, - persist: false, - show: false, - scope: options.scope, - keyboard: false - }); - - $q.when(modal).then(function(modalEl) { - modalEl.modal('show'); - }); - }; - - }); - -}); diff --git a/public/app/core/services/util_srv.ts b/public/app/core/services/util_srv.ts new file mode 100644 index 00000000000..8ca7bf8be72 --- /dev/null +++ b/public/app/core/services/util_srv.ts @@ -0,0 +1,43 @@ +/// + +import config from 'app/core/config'; +import _ from 'lodash'; +import $ from 'jquery'; + +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +export class UtilSrv { + + /** @ngInject */ + constructor(private $rootScope, private $modal) { + } + + init() { + appEvents.on('show-modal', this.showModal.bind(this), this.$rootScope); + } + + showModal(options) { + if (options.model) { + options.scope = this.$rootScope.$new(); + options.scope.model = options.model; + } + + var modal = this.$modal({ + modalClass: options.modalClass, + template: options.src, + templateHtml: options.templateHtml, + persist: false, + show: false, + scope: options.scope, + keyboard: false, + backdrop: options.backdrop + }); + + Promise.resolve(modal).then(function(modalEl) { + modalEl.modal('show'); + }); + } +} + +coreModule.service('utilSrv', UtilSrv); diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js index d110019add6..6aea2efa9f1 100644 --- a/public/app/features/dashboard/all.js +++ b/public/app/features/dashboard/all.js @@ -1,5 +1,5 @@ define([ - './dashboardCtrl', + './dashboard_ctrl', './dashboardLoaderSrv', './dashnav/dashnav', './submenu/submenu', @@ -14,7 +14,10 @@ define([ './unsavedChangesSrv', './timepicker/timepicker', './graphiteImportCtrl', - './dynamicDashboardSrv', './importCtrl', './impression_store', + './upload', + './import/dash_import', + './export/export_modal', + './dash_list_ctrl', ], function () {}); diff --git a/public/app/features/dashboard/dash_list_ctrl.ts b/public/app/features/dashboard/dash_list_ctrl.ts new file mode 100644 index 00000000000..f08d7507e65 --- /dev/null +++ b/public/app/features/dashboard/dash_list_ctrl.ts @@ -0,0 +1,11 @@ +/// + +import coreModule from 'app/core/core_module'; + +export class DashListCtrl { + /** @ngInject */ + constructor() { + } +} + +coreModule.controller('DashListCtrl', DashListCtrl); diff --git a/public/app/features/dashboard/dashboardCtrl.js b/public/app/features/dashboard/dashboardCtrl.js deleted file mode 100644 index 0a9c0fd7e92..00000000000 --- a/public/app/features/dashboard/dashboardCtrl.js +++ /dev/null @@ -1,150 +0,0 @@ -define([ - 'angular', - 'jquery', - 'app/core/config', - 'moment', -], -function (angular, $, config, moment) { - "use strict"; - - var module = angular.module('grafana.controllers'); - - module.controller('DashboardCtrl', function( - $scope, - $rootScope, - dashboardKeybindings, - timeSrv, - templateValuesSrv, - dynamicDashboardSrv, - dashboardSrv, - unsavedChangesSrv, - dashboardViewStateSrv, - contextSrv, - $timeout) { - - $scope.editor = { index: 0 }; - $scope.panels = config.panels; - - var resizeEventTimeout; - - this.init = function(dashboard) { - $scope.resetRow(); - $scope.registerWindowResizeEvent(); - $scope.onAppEvent('show-json-editor', $scope.showJsonEditor); - $scope.setupDashboard(dashboard); - }; - - $scope.setupDashboard = function(data) { - var dashboard = dashboardSrv.create(data.dashboard, data.meta); - dashboardSrv.setCurrent(dashboard); - - // init services - timeSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - templateValuesSrv.init(dashboard).finally(function() { - dynamicDashboardSrv.init(dashboard); - unsavedChangesSrv.init(dashboard, $scope); - - $scope.dashboard = dashboard; - $scope.dashboardMeta = dashboard.meta; - $scope.dashboardViewState = dashboardViewStateSrv.create($scope); - - dashboardKeybindings.shortcuts($scope); - - $scope.updateSubmenuVisibility(); - $scope.setWindowTitleAndTheme(); - - if ($scope.profilingEnabled) { - $scope.performance.panels = []; - $scope.performance.panelCount = 0; - $scope.dashboard.rows.forEach(function(row) { - $scope.performance.panelCount += row.panels.length; - }); - } - - $scope.appEvent("dashboard-initialized", $scope.dashboard); - }).catch(function(err) { - if (err.data && err.data.message) { err.message = err.data.message; } - $scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]); - }); - }; - - $scope.updateSubmenuVisibility = function() { - $scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); - }; - - $scope.setWindowTitleAndTheme = function() { - window.document.title = config.window_title_prefix + $scope.dashboard.title; - }; - - $scope.broadcastRefresh = function() { - $rootScope.$broadcast('refresh'); - }; - - $scope.addRow = function(dash, row) { - dash.rows.push(row); - }; - - $scope.addRowDefault = function() { - $scope.resetRow(); - $scope.row.title = 'New row'; - $scope.addRow($scope.dashboard, $scope.row); - }; - - $scope.resetRow = function() { - $scope.row = { - title: '', - height: '250px', - editable: true, - }; - }; - - $scope.showJsonEditor = function(evt, options) { - var editScope = $rootScope.$new(); - editScope.object = options.object; - editScope.updateHandler = options.updateHandler; - $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); - }; - - $scope.onDrop = function(panelId, row, dropTarget) { - var info = $scope.dashboard.getPanelInfoById(panelId); - if (dropTarget) { - var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id); - dropInfo.row.panels[dropInfo.index] = info.panel; - info.row.panels[info.index] = dropTarget; - var dragSpan = info.panel.span; - info.panel.span = dropTarget.span; - dropTarget.span = dragSpan; - } - else { - info.row.panels.splice(info.index, 1); - info.panel.span = 12 - $scope.dashboard.rowSpan(row); - row.panels.push(info.panel); - } - - $rootScope.$broadcast('render'); - }; - - $scope.registerWindowResizeEvent = function() { - angular.element(window).bind('resize', function() { - $timeout.cancel(resizeEventTimeout); - resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); - }); - $scope.$on('$destroy', function() { - angular.element(window).unbind('resize'); - }); - }; - - $scope.timezoneChanged = function() { - $rootScope.$broadcast("refresh"); - }; - - $scope.formatDate = function(date) { - return moment(date).format('MMM Do YYYY, h:mm:ss a'); - }; - - }); - -}); diff --git a/public/app/features/dashboard/dashboardSrv.js b/public/app/features/dashboard/dashboardSrv.js index a4ae7b31468..0a3c1b95f95 100644 --- a/public/app/features/dashboard/dashboardSrv.js +++ b/public/app/features/dashboard/dashboardSrv.js @@ -22,7 +22,7 @@ function (angular, $, _, moment) { this.id = data.id || null; this.title = data.title || 'No Title'; - this.originalTitle = this.title; + this.description = data.description; this.tags = data.tags || []; this.style = data.style || "dark"; this.timezone = data.timezone || ''; @@ -39,6 +39,7 @@ function (angular, $, _, moment) { this.schemaVersion = data.schemaVersion || 0; this.version = data.version || 0; this.links = data.links || []; + this.gnetId = data.gnetId || null; this._updateSchema(data); this._initMeta(meta); } diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts new file mode 100644 index 00000000000..bd90cd08f00 --- /dev/null +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -0,0 +1,145 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import moment from 'moment'; +import _ from 'lodash'; + +import coreModule from 'app/core/core_module'; + +export class DashboardCtrl { + + /** @ngInject */ + constructor( + private $scope, + private $rootScope, + dashboardKeybindings, + timeSrv, + templateValuesSrv, + dashboardSrv, + unsavedChangesSrv, + dynamicDashboardSrv, + dashboardViewStateSrv, + contextSrv, + $timeout) { + + $scope.editor = { index: 0 }; + $scope.panels = config.panels; + + var resizeEventTimeout; + + $scope.setupDashboard = function(data) { + var dashboard = dashboardSrv.create(data.dashboard, data.meta); + dashboardSrv.setCurrent(dashboard); + + // init services + timeSrv.init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + templateValuesSrv.init(dashboard).finally(function() { + dynamicDashboardSrv.init(dashboard); + + unsavedChangesSrv.init(dashboard, $scope); + + $scope.dashboard = dashboard; + $scope.dashboardMeta = dashboard.meta; + $scope.dashboardViewState = dashboardViewStateSrv.create($scope); + + dashboardKeybindings.shortcuts($scope); + + $scope.updateSubmenuVisibility(); + $scope.setWindowTitleAndTheme(); + + $scope.appEvent("dashboard-loaded", $scope.dashboard); + }).catch(function(err) { + if (err.data && err.data.message) { err.message = err.data.message; } + $scope.appEvent("alert-error", ['Dashboard init failed', 'Template variables could not be initialized: ' + err.message]); + }); + }; + + $scope.templateVariableUpdated = function() { + dynamicDashboardSrv.update($scope.dashboard); + }; + + $scope.updateSubmenuVisibility = function() { + $scope.submenuEnabled = $scope.dashboard.isSubmenuFeaturesEnabled(); + }; + + $scope.setWindowTitleAndTheme = function() { + window.document.title = config.window_title_prefix + $scope.dashboard.title; + }; + + $scope.broadcastRefresh = function() { + $rootScope.performance.panelsRendered = 0; + $rootScope.$broadcast('refresh'); + }; + + $scope.addRow = function(dash, row) { + dash.rows.push(row); + }; + + $scope.addRowDefault = function() { + $scope.resetRow(); + $scope.row.title = 'New row'; + $scope.addRow($scope.dashboard, $scope.row); + }; + + $scope.resetRow = function() { + $scope.row = { + title: '', + height: '250px', + editable: true, + }; + }; + + $scope.showJsonEditor = function(evt, options) { + var editScope = $rootScope.$new(); + editScope.object = options.object; + editScope.updateHandler = options.updateHandler; + $scope.appEvent('show-dash-editor', { src: 'public/app/partials/edit_json.html', scope: editScope }); + }; + + $scope.onDrop = function(panelId, row, dropTarget) { + var info = $scope.dashboard.getPanelInfoById(panelId); + if (dropTarget) { + var dropInfo = $scope.dashboard.getPanelInfoById(dropTarget.id); + dropInfo.row.panels[dropInfo.index] = info.panel; + info.row.panels[info.index] = dropTarget; + var dragSpan = info.panel.span; + info.panel.span = dropTarget.span; + dropTarget.span = dragSpan; + } else { + info.row.panels.splice(info.index, 1); + info.panel.span = 12 - $scope.dashboard.rowSpan(row); + row.panels.push(info.panel); + } + + $rootScope.$broadcast('render'); + }; + + $scope.registerWindowResizeEvent = function() { + angular.element(window).bind('resize', function() { + $timeout.cancel(resizeEventTimeout); + resizeEventTimeout = $timeout(function() { $scope.$broadcast('render'); }, 200); + }); + $scope.$on('$destroy', function() { + angular.element(window).unbind('resize'); + }); + }; + + $scope.timezoneChanged = function() { + $rootScope.$broadcast("refresh"); + }; + } + + init(dashboard) { + this.$scope.resetRow(); + this.$scope.registerWindowResizeEvent(); + this.$scope.onAppEvent('show-json-editor', this.$scope.showJsonEditor); + this.$scope.onAppEvent('template-variable-value-updated', this.$scope.templateVariableUpdated); + this.$scope.setupDashboard(dashboard); + } +} + +coreModule.controller('DashboardCtrl', DashboardCtrl); diff --git a/public/app/features/dashboard/dashnav/dashnav.html b/public/app/features/dashboard/dashnav/dashnav.html index 9afd152d8aa..cc8878163af 100644 --- a/public/app/features/dashboard/dashnav/dashnav.html +++ b/public/app/features/dashboard/dashnav/dashnav.html @@ -26,11 +26,19 @@
  • Link to Dashboard +
  • - Snapshot sharing + Snapshot + + +
  • +
  • + + Export +
  • @@ -44,8 +52,7 @@
  • Settings
  • Annotations
  • Templating
  • -
  • Export
  • -
  • View JSON
  • +
  • View JSON
  • Make Editable
  • Save As...
  • Delete dashboard
  • diff --git a/public/app/features/dashboard/dashnav/dashnav.ts b/public/app/features/dashboard/dashnav/dashnav.ts index 99b113b88c5..62f598e1cbd 100644 --- a/public/app/features/dashboard/dashnav/dashnav.ts +++ b/public/app/features/dashboard/dashnav/dashnav.ts @@ -4,15 +4,16 @@ import _ from 'lodash'; import moment from 'moment'; import angular from 'angular'; +import {DashboardExporter} from '../export/exporter'; + export class DashNavCtrl { /** @ngInject */ - constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout) { + constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) { $scope.init = function() { $scope.onAppEvent('save-dashboard', $scope.saveDashboard); $scope.onAppEvent('delete-dashboard', $scope.deleteDashboard); - $scope.onAppEvent('export-dashboard', $scope.snapshot); $scope.onAppEvent('quick-snapshot', $scope.quickSnapshot); $scope.showSettingsMenu = $scope.dashboardMeta.canEdit || $scope.contextSrv.isEditor; @@ -168,11 +169,11 @@ export class DashNavCtrl { }); }; - $scope.exportDashboard = function() { + $scope.viewJson = function() { var clone = $scope.dashboard.getSaveModelClone(); - var blob = new Blob([angular.toJson(clone, true)], { type: "application/json;charset=utf-8" }); - var wnd: any = window; - wnd.saveAs(blob, $scope.dashboard.title + '-' + new Date().getTime() + '.json'); + var html = angular.toJson(clone, true); + var uri = "data:application/json," + encodeURIComponent(html); + var newWindow = window.open(uri); }; $scope.snapshot = function() { @@ -180,7 +181,6 @@ export class DashNavCtrl { $rootScope.$broadcast('refresh'); $timeout(function() { - $scope.exportDashboard(); $scope.dashboard.snapshot = false; $scope.appEvent('dashboard-snapshot-cleanup'); }, 1000); diff --git a/public/app/features/dashboard/dynamic_dashboard_srv.ts b/public/app/features/dashboard/dynamic_dashboard_srv.ts new file mode 100644 index 00000000000..6b58f448085 --- /dev/null +++ b/public/app/features/dashboard/dynamic_dashboard_srv.ts @@ -0,0 +1,188 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import _ from 'lodash'; + +import coreModule from 'app/core/core_module'; + +export class DynamicDashboardSrv { + iteration: number; + dashboard: any; + + constructor() { + this.iteration = new Date().getTime(); + } + + init(dashboard) { + if (dashboard.snapshot) { return; } + this.process(dashboard, {}); + } + + update(dashboard) { + if (dashboard.snapshot) { return; } + + this.iteration = this.iteration + 1; + this.process(dashboard, {}); + } + + process(dashboard, options) { + if (dashboard.templating.list.length === 0) { return; } + this.dashboard = dashboard; + + var cleanUpOnly = options.cleanUpOnly; + + var i, j, row, panel; + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; + // handle row repeats + if (row.repeat) { + if (!cleanUpOnly) { + this.repeatRow(row, i); + } + } else if (row.repeatRowId && row.repeatIteration !== this.iteration) { + // clean up old left overs + this.dashboard.rows.splice(i, 1); + i = i - 1; + continue; + } + + // repeat panels + for (j = 0; j < row.panels.length; j++) { + panel = row.panels[j]; + if (panel.repeat) { + if (!cleanUpOnly) { + this.repeatPanel(panel, row); + } + } else if (panel.repeatPanelId && panel.repeatIteration !== this.iteration) { + // clean up old left overs + row.panels = _.without(row.panels, panel); + j = j - 1; + } else if (!_.isEmpty(panel.scopedVars) && panel.repeatIteration !== this.iteration) { + panel.scopedVars = {}; + } + } + } + } + + // returns a new row clone or reuses a clone from previous iteration + getRowClone(sourceRow, repeatIndex, sourceRowIndex) { + if (repeatIndex === 0) { + return sourceRow; + } + + var i, panel, row, copy; + var sourceRowId = sourceRowIndex + 1; + + // look for row to reuse + for (i = 0; i < this.dashboard.rows.length; i++) { + row = this.dashboard.rows[i]; + if (row.repeatRowId === sourceRowId && row.repeatIteration !== this.iteration) { + copy = row; + break; + } + } + + if (!copy) { + copy = angular.copy(sourceRow); + this.dashboard.rows.splice(sourceRowIndex + repeatIndex, 0, copy); + + // set new panel ids + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.id = this.dashboard.getNextPanelId(); + } + } + + copy.repeat = null; + copy.repeatRowId = sourceRowId; + copy.repeatIteration = this.iteration; + return copy; + } + + // returns a new row clone or reuses a clone from previous iteration + repeatRow(row, rowIndex) { + var variables = this.dashboard.templating.list; + var variable = _.findWhere(variables, {name: row.repeat}); + if (!variable) { + return; + } + + var selected, copy, i, panel; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, (option, index) => { + copy = this.getRowClone(row, index, rowIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + + for (i = 0; i < copy.panels.length; i++) { + panel = copy.panels[i]; + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + panel.repeatIteration = this.iteration; + } + }); + } + + getPanelClone(sourcePanel, row, index) { + // if first clone return source + if (index === 0) { + return sourcePanel; + } + + var i, tmpId, panel, clone; + + // first try finding an existing clone to use + for (i = 0; i < row.panels.length; i++) { + panel = row.panels[i]; + if (panel.repeatIteration !== this.iteration && panel.repeatPanelId === sourcePanel.id) { + clone = panel; + break; + } + } + + if (!clone) { + clone = { id: this.dashboard.getNextPanelId() }; + row.panels.push(clone); + } + + // save id + tmpId = clone.id; + // copy properties from source + angular.copy(sourcePanel, clone); + // restore id + clone.id = tmpId; + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + return clone; + } + + repeatPanel(panel, row) { + var variables = this.dashboard.templating.list; + var variable = _.findWhere(variables, {name: panel.repeat}); + if (!variable) { return; } + + var selected; + if (variable.current.text === 'All') { + selected = variable.options.slice(1, variable.options.length); + } else { + selected = _.filter(variable.options, {selected: true}); + } + + _.each(selected, (option, index) => { + var copy = this.getPanelClone(panel, row, index); + copy.span = Math.max(12 / selected.length, panel.minSpan); + copy.scopedVars = copy.scopedVars || {}; + copy.scopedVars[variable.name] = option; + }); + } +} + +coreModule.service('dynamicDashboardSrv', DynamicDashboardSrv); + diff --git a/public/app/features/dashboard/export/export_modal.html b/public/app/features/dashboard/export/export_modal.html new file mode 100644 index 00000000000..e890e405020 --- /dev/null +++ b/public/app/features/dashboard/export/export_modal.html @@ -0,0 +1,29 @@ + + + + + + + diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts new file mode 100644 index 00000000000..57af9d9caf8 --- /dev/null +++ b/public/app/features/dashboard/export/export_modal.ts @@ -0,0 +1,53 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import angular from 'angular'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; +import _ from 'lodash'; + +import {DashboardExporter} from './exporter'; + +export class DashExportCtrl { + dash: any; + exporter: DashboardExporter; + + /** @ngInject */ + constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) { + this.exporter = new DashboardExporter(datasourceSrv); + + var current = dashboardSrv.getCurrent().getSaveModelClone(); + + this.exporter.makeExportable(current).then(dash => { + $scope.$apply(() => { + this.dash = dash; + }); + }); + } + + save() { + var blob = new Blob([angular.toJson(this.dash, true)], { type: "application/json;charset=utf-8" }); + var wnd: any = window; + wnd.saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json'); + } + + saveJson() { + var html = angular.toJson(this.dash, true); + var uri = "data:application/json," + encodeURIComponent(html); + var newWindow = window.open(uri); + } + +} + +export function dashExportDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/export/export_modal.html', + controller: DashExportCtrl, + bindToController: true, + controllerAs: 'ctrl', + }; +} + +coreModule.directive('dashExportModal', dashExportDirective); diff --git a/public/app/features/dashboard/export/exporter.ts b/public/app/features/dashboard/export/exporter.ts new file mode 100644 index 00000000000..958715b5cbb --- /dev/null +++ b/public/app/features/dashboard/export/exporter.ts @@ -0,0 +1,135 @@ +/// + +import config from 'app/core/config'; +import angular from 'angular'; +import _ from 'lodash'; + +import {DynamicDashboardSrv} from '../dynamic_dashboard_srv'; + +export class DashboardExporter { + + constructor(private datasourceSrv) { + } + + makeExportable(dash) { + var dynSrv = new DynamicDashboardSrv(); + dynSrv.process(dash, {cleanUpOnly: true}); + + dash.id = null; + + var inputs = []; + var requires = {}; + var datasources = {}; + var promises = []; + + var templateizeDatasourceUsage = obj => { + promises.push(this.datasourceSrv.get(obj.datasource).then(ds => { + var refName = 'DS_' + ds.name.replace(' ', '_').toUpperCase(); + datasources[refName] = { + name: refName, + label: ds.name, + description: '', + type: 'datasource', + pluginId: ds.meta.id, + pluginName: ds.meta.name, + }; + obj.datasource = '${' + refName +'}'; + + requires['datasource' + ds.meta.id] = { + type: 'datasource', + id: ds.meta.id, + name: ds.meta.name, + version: ds.meta.info.version || "1.0.0", + }; + })); + }; + + // check up panel data sources + for (let row of dash.rows) { + _.each(row.panels, (panel) => { + if (panel.datasource !== undefined) { + templateizeDatasourceUsage(panel); + } + + var panelDef = config.panels[panel.type]; + if (panelDef) { + requires['panel' + panelDef.id] = { + type: 'panel', + id: panelDef.id, + name: panelDef.name, + version: panelDef.info.version, + }; + } + }); + } + + // templatize template vars + for (let variable of dash.templating.list) { + if (variable.type === 'query') { + templateizeDatasourceUsage(variable); + variable.options = []; + variable.current = {}; + variable.refresh = 1; + } + } + + // templatize annotations vars + for (let annotationDef of dash.annotations.list) { + templateizeDatasourceUsage(annotationDef); + } + + // add grafana version + requires['grafana'] = { + type: 'grafana', + id: 'grafana', + name: 'Grafana', + version: config.buildInfo.version + }; + + return Promise.all(promises).then(() => { + _.each(datasources, (value, key) => { + inputs.push(value); + }); + + // templatize constants + for (let variable of dash.templating.list) { + if (variable.type === 'constant') { + var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase(); + inputs.push({ + name: refName, + type: 'constant', + label: variable.label || variable.name, + value: variable.current.value, + description: '', + }); + // update current and option + variable.query = '${' + refName + '}'; + variable.options[0] = variable.current = { + value: variable.query, + text: variable.query, + }; + } + } + + requires = _.map(requires, req => { + return req; + }); + + // make inputs and requires a top thing + var newObj = {}; + newObj["__inputs"] = inputs; + newObj["__requires"] = requires; + + _.defaults(newObj, dash); + + return newObj; + }).catch(err => { + console.log('Export failed:', err); + return { + error: err + }; + }); + } + +} + diff --git a/public/app/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html new file mode 100644 index 00000000000..64a4a91a3c8 --- /dev/null +++ b/public/app/features/dashboard/import/dash_import.html @@ -0,0 +1,130 @@ + + diff --git a/public/app/features/dashboard/import/dash_import.ts b/public/app/features/dashboard/import/dash_import.ts new file mode 100644 index 00000000000..264df3cd5a9 --- /dev/null +++ b/public/app/features/dashboard/import/dash_import.ts @@ -0,0 +1,180 @@ +/// + +import kbn from 'app/core/utils/kbn'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; +import _ from 'lodash'; + +export class DashImportCtrl { + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + inputs: any[]; + inputsValid: boolean; + gnetUrl: string; + gnetError: string; + gnetInfo: any; + + /** @ngInject */ + constructor(private backendSrv, private $location, private $scope, private $routeParams) { + this.step = 1; + this.nameExists = false; + + // check gnetId in url + if ($routeParams.gnetId) { + this.gnetUrl = $routeParams.gnetId ; + this.checkGnetDashboard(); + } + } + + onUpload(dash) { + this.dash = dash; + this.dash.id = null; + this.step = 2; + this.inputs = []; + + if (this.dash.__inputs) { + for (let input of this.dash.__inputs) { + var inputModel = { + name: input.name, + label: input.label, + info: input.description, + value: input.value, + type: input.type, + pluginId: input.pluginId, + options: [] + }; + + if (input.type === 'datasource') { + this.setDatasourceOptions(input, inputModel); + } else if (!inputModel.info) { + inputModel.info = 'Specify a string constant'; + } + + this.inputs.push(inputModel); + } + } + + this.inputsValid = this.inputs.length === 0; + this.titleChanged(); + } + + setDatasourceOptions(input, inputModel) { + var sources = _.filter(config.datasources, val => { + return val.type === input.pluginId; + }); + + if (sources.length === 0) { + inputModel.info = "No data sources of type " + input.pluginName + " found"; + } else if (inputModel.description) { + inputModel.info = inputModel.description; + } else { + inputModel.info = "Select a " + input.pluginName + " data source"; + } + + inputModel.options = sources.map(val => { + return {text: val.name, value: val.name}; + }); + } + + inputValueChanged() { + this.inputsValid = true; + for (let input of this.inputs) { + if (!input.value) { + this.inputsValid = false; + } + } + } + + titleChanged() { + this.backendSrv.search({query: this.dash.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.dash.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } + + saveDashboard() { + var inputs = this.inputs.map(input => { + return { + name: input.name, + type: input.type, + pluginId: input.pluginId, + value: input.value + }; + }); + + return this.backendSrv.post('api/dashboards/import', { + dashboard: this.dash, + overwrite: true, + inputs: inputs + }).then(res => { + this.$location.url('dashboard/' + res.importedUri); + this.$scope.dismiss(); + }); + } + + loadJsonText() { + try { + this.parseError = ''; + var dash = JSON.parse(this.jsonText); + this.onUpload(dash); + } catch (err) { + console.log(err); + this.parseError = err.message; + return; + } + } + + checkGnetDashboard() { + this.gnetError = ''; + + var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); + var dashboardId; + + if (match && match[1]) { + dashboardId = match[1]; + } else if (match && match[2]) { + dashboardId = match[2]; + } else { + this.gnetError = 'Could not find dashboard'; + } + + return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { + this.gnetInfo = res; + // store reference to grafana.net + res.json.gnetId = res.id; + this.onUpload(res.json); + }).catch(err => { + err.isHandled = true; + this.gnetError = err.data.message || err; + }); + } + + back() { + this.gnetUrl = ''; + this.step = 1; + this.gnetError = ''; + this.gnetInfo = ''; + } + +} + +export function dashImportDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/import/dash_import.html', + controller: DashImportCtrl, + bindToController: true, + controllerAs: 'ctrl', + }; +} + +coreModule.directive('dashImport', dashImportDirective); diff --git a/public/app/features/dashboard/keybindings.js b/public/app/features/dashboard/keybindings.js index b07dd2fd848..429e5f5d667 100644 --- a/public/app/features/dashboard/keybindings.js +++ b/public/app/features/dashboard/keybindings.js @@ -68,10 +68,6 @@ function(angular, $) { scope.appEvent('shift-time-forward', evt); }, { inputDisabled: true }); - keyboardManager.bind('ctrl+e', function(evt) { - scope.appEvent('export-dashboard', evt); - }, { inputDisabled: true }); - keyboardManager.bind('ctrl+i', function(evt) { scope.appEvent('quick-snapshot', evt); }, { inputDisabled: true }); diff --git a/public/app/features/dashboard/partials/dash_list.html b/public/app/features/dashboard/partials/dash_list.html new file mode 100644 index 00000000000..3058715e0e6 --- /dev/null +++ b/public/app/features/dashboard/partials/dash_list.html @@ -0,0 +1,10 @@ + + + +
    + +
    + + diff --git a/public/app/features/dashboard/partials/import.html b/public/app/features/dashboard/partials/migrate.html similarity index 65% rename from public/app/features/dashboard/partials/import.html rename to public/app/features/dashboard/partials/migrate.html index 3f23e4cb682..098dbd033cf 100644 --- a/public/app/features/dashboard/partials/import.html +++ b/public/app/features/dashboard/partials/migrate.html @@ -1,23 +1,15 @@ - +
    -
    -
    -
    -
    -
    -
    - Migrate dashboards - Import dashboards from Elasticsearch or InfluxDB + Import dashboards from Elasticsearch or InfluxDB
    diff --git a/public/app/features/dashboard/partials/settings.html b/public/app/features/dashboard/partials/settings.html index 7103cdcc49d..af64f8b6cc1 100644 --- a/public/app/features/dashboard/partials/settings.html +++ b/public/app/features/dashboard/partials/settings.html @@ -22,10 +22,14 @@
    Details
    - - + +
    -
    +
    + + +
    +