diff --git a/.floo b/.floo index 1201c5e93b9..1c2038f98cc 100644 --- a/.floo +++ b/.floo @@ -1,3 +1,3 @@ { - "url": "https://floobits.com/raintank/grafana" -} \ No newline at end of file + "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/CHANGELOG.md b/CHANGELOG.md index eb1cb5c55ab..5010b572c98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # 3.1.0 (unreleased) ### Enhancements +* **Dashboard Export/Import**: Dashboard export now templetize data sources and constant variables, users pick these on import, closes [#5084](https://github.com/grafana/grafana/issues/5084) * **Dashboard Url**: Time range changes updates url, closes [#458](https://github.com/grafana/grafana/issues/458) * **Dashboard Url**: Template variable change updates url, closes [#5002](https://github.com/grafana/grafana/issues/5002) * **Singlestat**: Add support for range to text mappings, closes [#1319](https://github.com/grafana/grafana/issues/1319) @@ -11,9 +12,12 @@ * **Scripts**: Use restart instead of start for deb package script, closes [#5282](https://github.com/grafana/grafana/pull/5282) * **Logging**: Moved to structured logging lib, and moved to component specific level filters via config file, closes [#4590](https://github.com/grafana/grafana/issues/4590) * **Search**: Add search limit query parameter, closes [#5292](https://github.com/grafana/grafana/pull/5292) +* **OpenTSDB**: Support nested template variables in tag_values function, closes [4398](https://github.com/grafana/grafana/issues/4398) +* **Datasource**: Pending data source requests are cancelled before new ones are issues (Graphite & Prometheus), closes [5321](https://github.com/grafana/grafana/issues/5321) ## Breaking changes * **Logging** : Changed default logging output format (now structured into message, and key value pairs, with logger key acting as component). You can also no change in config to json log ouput. +* **Graphite** : The Graph panel no longer have a Graphite PNG option. closes #[5367](https://github.com/grafana/grafana/issues/5367) # 3.0.4 Patch release (2016-05-25) * **Panel**: Fixed blank dashboard issue when switching to other dashboard while in fullscreen edit mode, fixes [#5163](https://github.com/grafana/grafana/pull/5163) diff --git a/appveyor.yml b/appveyor.yml index 7d84bafc148..766fbed9855 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,7 +14,7 @@ install: - npm install - npm install -g grunt-cli # install gcc (needed for sqlite3) - - choco install -y mingw + - choco install -y mingw -limitoutput - set PATH=C:\tools\mingw64\bin;%PATH% - echo %PATH% - echo %GOPATH% diff --git a/conf/defaults.ini b/conf/defaults.ini index 8845abeb439..7543418e946 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -367,10 +367,15 @@ global_session = -1 enabled = false #################################### 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/datasources/cloudwatch.md b/docs/sources/datasources/cloudwatch.md index a3b6a3ba61f..92f4367d9ae 100644 --- a/docs/sources/datasources/cloudwatch.md +++ b/docs/sources/datasources/cloudwatch.md @@ -97,8 +97,8 @@ Example `ec2_instance_attribute()` query ## Cost -It's worth to mention that Amazon will charge you for CloudWatch API usage. CloudWatch costs -$0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will +Amazon provides 1 million CloudWatch API requests each month at no additional charge. Past this, +it costs $0.01 per 1,000 GetMetricStatistics or ListMetrics requests. For each query Grafana will issue a GetMetricStatistics request and every time you pick a dimension in the query editor Grafana will issue a ListMetrics request. diff --git a/docs/sources/datasources/opentsdb.md b/docs/sources/datasources/opentsdb.md index 6ef930c0cc0..9294b6b68ee 100644 --- a/docs/sources/datasources/opentsdb.md +++ b/docs/sources/datasources/opentsdb.md @@ -51,6 +51,13 @@ When using OpenTSDB with a template variable of `query` type you can use followi If you do not see template variables being populated in `Preview of values` section, you need to enable `tsd.core.meta.enable_realtime_ts` in the OpenTSDB server settings. Also, to populate metadata of the existing time series data in OpenTSDB, you need to run `tsdb uid metasync` on the OpenTSDB server. +### Nested Templating + +One template variable can be used to filter tag values for another template varible. Very importantly, the order of the parameters matter in tag_values function. First parameter is the metric name, second parameter is the tag key for which you need to find tag values, and after that all other dependent template variables. Some examples are mentioned below to make nested template queries work successfully. + + tag_values(cpu, hostname, env=$env) // return tag values for cpu metric, selected env tag value and tag key hostname + tag_values(cpu, hostanme, env=$env, region=$region) // return tag values for cpu metric, selected env tag value, selected region tag value and tag key hostname + > Note: This is required for the OpenTSDB `lookup` api to work. For details on opentsdb metric queries checkout the official [OpenTSDB documentation](http://opentsdb.net/docs/build/html/index.html) 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 022edb9c74c..708fcd0ceeb 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/dataproxy.go b/pkg/api/dataproxy.go index 8c2e134f3af..f2dee2b3479 100644 --- a/pkg/api/dataproxy.go +++ b/pkg/api/dataproxy.go @@ -63,6 +63,8 @@ func NewReverseProxy(ds *m.DataSource, proxyPath string, targetUrl *url.URL) *ht req.Header.Add("Authorization", dsAuth) } + time.Sleep(time.Second * 5) + // clear cookie headers req.Header.Del("Cookie") req.Header.Del("Set-Cookie") 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..8c21a0f03a7 100644 --- a/pkg/api/gnetproxy.go +++ b/pkg/api/gnetproxy.go @@ -5,9 +5,11 @@ import ( "net" "net/http" "net/http/httputil" + "net/url" "time" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -22,12 +24,14 @@ 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) // clear cookie headers req.Header.Del("Cookie") diff --git a/pkg/api/index.go b/pkg/api/index.go index 4cfdad5cca9..9300a980502 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/log/syslog_windows.go b/pkg/log/syslog_windows.go new file mode 100644 index 00000000000..c9b79a38426 --- /dev/null +++ b/pkg/log/syslog_windows.go @@ -0,0 +1,20 @@ +//+build windows + +package log + +import "github.com/inconshreveable/log15" + +type SysLogHandler struct { +} + +func NewSyslog() *SysLogHandler { + return &SysLogHandler{} +} + +func (sw *SysLogHandler) Init() error { + return nil +} + +func (sw *SysLogHandler) Log(r *log15.Record) error { + return nil +} diff --git a/pkg/metrics/counter.go b/pkg/metrics/counter.go index 3cce9b7a0a7..8322d370a36 100644 --- a/pkg/metrics/counter.go +++ b/pkg/metrics/counter.go @@ -29,9 +29,8 @@ func RegCounter(name string, tagStrings ...string) Counter { // StandardCounter is the standard implementation of a Counter and uses the // sync/atomic package to manage a single int64 value. type StandardCounter struct { + count int64 //Due to a bug in golang the 64bit variable need to come first to be 64bit aligned. https://golang.org/pkg/sync/atomic/#pkg-note-BUG *MetricMeta - - count int64 } // Clear sets the counter to zero. 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 be2ec65a889..4bc43206621 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -144,6 +144,9 @@ var ( // logger logger log.Logger + + // Grafana.NET URL + GrafanaNetUrl string ) type CommandLineArgs struct { @@ -526,6 +529,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..6630fbe9517 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 { @@ -27,6 +29,7 @@ export class GrafanaCtrl { }; $scope.initDashboard = function(dashboardData, viewScope) { + $scope.appEvent("dashboard-fetch-end", dashboardData); $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); }; @@ -44,6 +47,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..08e5ea457ab 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'; @@ -7,7 +7,6 @@ import angular from 'angular'; export class Profiler { panelsRendered: number; enabled: boolean; - panels: any[]; panelsInitCount: any; timings: any; digestCounter: any; @@ -29,28 +28,21 @@ export class Profiler { return false; }, () => {}); - $rootScope.$on('refresh', this.refresh.bind(this)); - $rootScope.onAppEvent('dashboard-fetched', this.dashboardFetched.bind(this)); - $rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this)); - $rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this)); + $rootScope.onAppEvent('refresh', this.refresh.bind(this), $rootScope); + $rootScope.onAppEvent('dashboard-fetch-end', this.dashboardFetched.bind(this), $rootScope); + $rootScope.onAppEvent('dashboard-initialized', this.dashboardInitialized.bind(this), $rootScope); + $rootScope.onAppEvent('panel-initialized', this.panelInitialized.bind(this), $rootScope); } refresh() { - this.panels = []; + this.timings.query = 0; + this.timings.render = 0; setTimeout(() => { - var totalRender = 0; - var totalQuery = 0; - - for (let panelTiming of this.panels) { - totalRender += panelTiming.render; - totalQuery += panelTiming.query; - } - - console.log('panel count: ' + this.panels.length); - console.log('total query: ' + totalQuery); - console.log('total render: ' + totalRender); - console.log('avg render: ' + totalRender / this.panels.length); + console.log('panel count: ' + this.panelsInitCount); + console.log('total query: ' + this.timings.query); + console.log('total render: ' + this.timings.render); + console.log('avg render: ' + this.timings.render / this.panelsInitCount); }, 5000); } @@ -60,7 +52,8 @@ export class Profiler { this.digestCounter = 0; this.panelsInitCount = 0; this.panelsRendered = 0; - this.panels = []; + this.timings.query = 0; + this.timings.render = 0; } dashboardInitialized() { @@ -110,11 +103,8 @@ export class Profiler { if (this.enabled) { panelTimings.renderEnd = new Date().getTime(); - this.panels.push({ - panelId: panelId, - query: panelTimings.queryEnd - panelTimings.queryStart, - render: panelTimings.renderEnd - panelTimings.renderStart, - }); + this.timings.query += panelTimings.queryEnd - panelTimings.queryStart; + this.timings.render += panelTimings.renderEnd - panelTimings.renderStart; } } diff --git a/public/app/core/routes/dashboard_loaders.js b/public/app/core/routes/dashboard_loaders.js index 61cdf32c128..49e81c23a7e 100644 --- a/public/app/core/routes/dashboard_loaders.js +++ b/public/app/core/routes/dashboard_loaders.js @@ -5,6 +5,7 @@ function (coreModule) { "use strict"; coreModule.default.controller('LoadDashboardCtrl', function($scope, $routeParams, dashboardLoaderSrv, backendSrv, $location) { + $scope.appEvent("dashboard-fetch-start"); if (!$routeParams.slug) { backendSrv.get('/api/dashboards/home').then(function(homeDash) { @@ -25,18 +26,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 5f83013433b..ba79398cb72 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -33,20 +33,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/backend_srv.js b/public/app/core/services/backend_srv.js deleted file mode 100644 index f27e427c70b..00000000000 --- a/public/app/core/services/backend_srv.js +++ /dev/null @@ -1,147 +0,0 @@ -define([ - 'angular', - 'lodash', - '../core_module', - 'app/core/config', -], -function (angular, _, coreModule, config) { - 'use strict'; - - coreModule.default.service('backendSrv', function($http, alertSrv, $timeout) { - var self = this; - - this.get = function(url, params) { - return this.request({ method: 'GET', url: url, params: params }); - }; - - this.delete = function(url) { - return this.request({ method: 'DELETE', url: url }); - }; - - this.post = function(url, data) { - return this.request({ method: 'POST', url: url, data: data }); - }; - - this.patch = function(url, data) { - return this.request({ method: 'PATCH', url: url, data: data }); - }; - - this.put = function(url, data) { - return this.request({ method: 'PUT', url: url, data: data }); - }; - - this._handleError = function(err) { - return function() { - if (err.isHandled) { - return; - } - - var data = err.data || { message: 'Unexpected error' }; - if (_.isString(data)) { - data = { message: data }; - } - - if (err.status === 422) { - alertSrv.set("Validation failed", data.message, "warning", 4000); - throw data; - } - - data.severity = 'error'; - - if (err.status < 500) { - data.severity = "warning"; - } - - if (data.message) { - alertSrv.set("Problem!", data.message, data.severity, 10000); - } - - throw data; - }; - }; - - this.request = function(options) { - options.retry = options.retry || 0; - var requestIsLocal = options.url.indexOf('/') === 0; - var firstAttempt = options.retry === 0; - - if (requestIsLocal && !options.hasSubUrl) { - options.url = config.appSubUrl + options.url; - options.hasSubUrl = true; - } - - return $http(options).then(function(results) { - if (options.method !== 'GET') { - if (results && results.data.message) { - alertSrv.set(results.data.message, '', 'success', 3000); - } - } - return results.data; - }, function(err) { - // handle unauthorized - if (err.status === 401 && firstAttempt) { - return self.loginPing().then(function() { - options.retry = 1; - return self.request(options); - }); - } - - $timeout(self._handleError(err), 50); - throw err; - }); - }; - - this.datasourceRequest = function(options) { - options.retry = options.retry || 0; - var requestIsLocal = options.url.indexOf('/') === 0; - var firstAttempt = options.retry === 0; - - if (requestIsLocal && options.headers && options.headers.Authorization) { - options.headers['X-DS-Authorization'] = options.headers.Authorization; - delete options.headers.Authorization; - } - - return $http(options).then(null, function(err) { - // handle unauthorized for backend requests - if (requestIsLocal && firstAttempt && err.status === 401) { - return self.loginPing().then(function() { - options.retry = 1; - return self.datasourceRequest(options); - }); - } - - //populate error obj on Internal Error - if (_.isString(err.data) && err.status === 500) { - err.data = { - error: err.statusText - }; - } - - // for Prometheus - if (!err.data.message && _.isString(err.data.error)) { - err.data.message = err.data.error; - } - - throw err; - }); - }; - - this.loginPing = function() { - return this.request({url: '/api/login/ping', method: 'GET', retry: 1 }); - }; - - this.search = function(query) { - return this.get('/api/search', query); - }; - - this.getDashboard = function(type, slug) { - return this.get('/api/dashboards/' + type + '/' + slug); - }; - - this.saveDashboard = function(dash, options) { - options = (options || {}); - return this.post('/api/dashboards/db/', {dashboard: dash, overwrite: options.overwrite === true}); - }; - - }); -}); diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts new file mode 100644 index 00000000000..412873f3677 --- /dev/null +++ b/public/app/core/services/backend_srv.ts @@ -0,0 +1,177 @@ +/// + +import angular from 'angular'; +import _ from 'lodash'; +import config from 'app/core/config'; +import coreModule from 'app/core/core_module'; + +export class BackendSrv { + inFlightRequests = {}; + HTTP_REQUEST_CANCELLED = -1; + + /** @ngInject */ + constructor(private $http, private alertSrv, private $rootScope, private $q, private $timeout) { + } + + get(url, params?) { + return this.request({ method: 'GET', url: url, params: params }); + } + + delete(url) { + return this.request({ method: 'DELETE', url: url }); + } + + post(url, data) { + return this.request({ method: 'POST', url: url, data: data }); + }; + + patch(url, data) { + return this.request({ method: 'PATCH', url: url, data: data }); + } + + put(url, data) { + return this.request({ method: 'PUT', url: url, data: data }); + } + + requestErrorHandler(err) { + if (err.isHandled) { + return; + } + + var data = err.data || { message: 'Unexpected error' }; + if (_.isString(data)) { + data = { message: data }; + } + + if (err.status === 422) { + this.alertSrv.set("Validation failed", data.message, "warning", 4000); + throw data; + } + + data.severity = 'error'; + + if (err.status < 500) { + data.severity = "warning"; + } + + if (data.message) { + this.alertSrv.set("Problem!", data.message, data.severity, 10000); + } + + throw data; + } + + request(options) { + options.retry = options.retry || 0; + var requestIsLocal = options.url.indexOf('/') === 0; + var firstAttempt = options.retry === 0; + + if (requestIsLocal && !options.hasSubUrl) { + options.url = config.appSubUrl + options.url; + options.hasSubUrl = true; + } + + return this.$http(options).then(results => { + if (options.method !== 'GET') { + if (results && results.data.message) { + this.alertSrv.set(results.data.message, '', 'success', 3000); + } + } + return results.data; + }, err => { + // handle unauthorized + if (err.status === 401 && firstAttempt) { + return this.loginPing().then(() => { + options.retry = 1; + return this.request(options); + }); + } + + this.$timeout(this.requestErrorHandler.bind(this), 50); + throw err; + }); + }; + + datasourceRequest(options) { + options.retry = options.retry || 0; + + // A requestID is provided by the datasource as a unique identifier for a + // particular query. If the requestID exists, the promise it is keyed to + // is canceled, canceling the previous datasource request if it is still + // in-flight. + var canceler; + if (options.requestId) { + canceler = this.inFlightRequests[options.requestId]; + if (canceler) { + canceler.resolve(); + } + // create new canceler + canceler = this.$q.defer(); + options.timeout = canceler.promise; + this.inFlightRequests[options.requestId] = canceler; + } + + var requestIsLocal = options.url.indexOf('/') === 0; + var firstAttempt = options.retry === 0; + + if (requestIsLocal && options.headers && options.headers.Authorization) { + options.headers['X-DS-Authorization'] = options.headers.Authorization; + delete options.headers.Authorization; + } + + return this.$http(options).catch(err => { + if (err.status === this.HTTP_REQUEST_CANCELLED) { + throw {err, cancelled: true}; + } + + // handle unauthorized for backend requests + if (requestIsLocal && firstAttempt && err.status === 401) { + return this.loginPing().then(() => { + options.retry = 1; + if (canceler) { + canceler.resolve(); + } + return this.datasourceRequest(options); + }); + } + + //populate error obj on Internal Error + if (_.isString(err.data) && err.status === 500) { + err.data = { + error: err.statusText + }; + } + + // for Prometheus + if (!err.data.message && _.isString(err.data.error)) { + err.data.message = err.data.error; + } + + throw err; + }).finally(() => { + // clean up + if (options.requestId) { + delete this.inFlightRequests[options.requestId]; + } + }); + }; + + loginPing() { + return this.request({url: '/api/login/ping', method: 'GET', retry: 1 }); + } + + search(query) { + return this.get('/api/search', query); + } + + getDashboard(type, slug) { + return this.get('/api/dashboards/' + type + '/' + slug); + } + + saveDashboard(dash, options) { + options = (options || {}); + return this.post('/api/dashboards/db/', {dashboard: dash, overwrite: options.overwrite === true}); + } +} + +coreModule.service('backendSrv', BackendSrv); 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/core/utils/file_export.ts b/public/app/core/utils/file_export.ts index 944b6ae8a80..0366a76acc2 100644 --- a/public/app/core/utils/file_export.ts +++ b/public/app/core/utils/file_export.ts @@ -5,7 +5,7 @@ import _ from 'lodash'; declare var window: any; export function exportSeriesListToCsv(seriesList) { - var text = 'Series;Time;Value\n'; + var text = 'sep=;\nSeries;Time;Value\n'; _.each(seriesList, function(series) { _.each(series.datapoints, function(dp) { text += series.alias + ';' + new Date(dp[1]).toISOString() + ';' + dp[0] + '\n'; @@ -15,7 +15,7 @@ export function exportSeriesListToCsv(seriesList) { }; export function exportSeriesListToCsvColumns(seriesList) { - var text = 'Time;'; + var text = 'sep=;\nTime;'; // add header _.each(seriesList, function(series) { text += series.alias + ';'; diff --git a/public/app/features/alerting/partials/alert_list.html b/public/app/features/alerting/partials/alert_list.html index ce0b6c4b6bd..29641aa6c21 100644 --- a/public/app/features/alerting/partials/alert_list.html +++ b/public/app/features/alerting/partials/alert_list.html @@ -4,12 +4,13 @@
+ +
+ + + +
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/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index 70c49967ea5..c6df45f53b2 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -47,7 +47,6 @@ function (angular, moment, _, $, kbn, dateMath, impressionStore) { } promise.then(function(result) { - $rootScope.appEvent("dashboard-fetched", result.dashboard); if (result.meta.dashboardNotFound !== true) { impressionStore.impressions.addDashboardImpression(result.dashboard.id); 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..3f3c9eb4c0b --- /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-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.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 d980e3ed35b..a11ec17846a 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; @@ -186,11 +187,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() { @@ -198,7 +199,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
    - - + +
    -
    +
    + + +
    +