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 @@
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
+ Share an internal link to the current dashboard. Some configuration options available.
- Snapshot sharing
+ Snapshot
+ Interactive, publically accessible dashboard. Sensitive data is stripped out.
+
+
+
+
+ Export
+ Export the dashboard to a JSON file for others and to share on Grafana.net
@@ -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 @@
+
+
+
+
+
+
+
+
+
+
Grafana.net Dashboard
+
+
+
+
Or paste JSON
+
+
+
+
+
+
+
+
+ Options
+
+
+
+
+
+
+
+
+
+
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