mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'alerting' into alerting_notifications
This commit is contained in:
commit
a0418da160
4
.floo
4
.floo
@ -1,3 +1,3 @@
|
||||
{
|
||||
"url": "https://floobits.com/raintank/grafana"
|
||||
}
|
||||
"url": "https://floobits.com/raintank/grafana"
|
||||
}
|
||||
|
@ -10,4 +10,3 @@ data/
|
||||
vendor/
|
||||
public_gen/
|
||||
dist/
|
||||
|
||||
|
@ -10,4 +10,4 @@
|
||||
"disallowSpacesInsideArrayBrackets": true,
|
||||
"disallowSpacesInsideParentheses": true,
|
||||
"validateIndentation": 2
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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%
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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` |
|
||||
|
@ -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)
|
||||
|
@ -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")
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
|
20
pkg/log/syslog_windows.go
Normal file
20
pkg/log/syslog_windows.go
Normal file
@ -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
|
||||
}
|
@ -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.
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
}))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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 = [
|
||||
|
@ -62,14 +62,16 @@
|
||||
</div>
|
||||
|
||||
<div class="search-button-row">
|
||||
<button class="btn btn-inverse pull-left" ng-click="ctrl.newDashboard()" ng-show="ctrl.contextSrv.isEditor">
|
||||
<a class="btn btn-inverse pull-left" href="dashboard/new" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
|
||||
<i class="fa fa-plus"></i>
|
||||
New
|
||||
</button>
|
||||
<a class="btn btn-inverse pull-left" href="import/dashboard" ng-show="ctrl.contextSrv.isEditor">
|
||||
<i class="fa fa-download"></i>
|
||||
Create New
|
||||
</a>
|
||||
|
||||
<a class="btn btn-inverse pull-left" href="dashboard/new/?editview=import" ng-show="ctrl.contextSrv.isEditor" ng-click="ctrl.isOpen = false;">
|
||||
<i class="fa fa-upload"></i>
|
||||
Import
|
||||
</a>
|
||||
<div class="clearfix"></div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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() {
|
||||
|
32
public/app/core/components/wizard/wizard.html
Normal file
32
public/app/core/components/wizard/wizard.html
Normal file
@ -0,0 +1,32 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-cog fa-spin"></i>
|
||||
<span class="p-l-1">{{model.name}}</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<div ng-if="activeStep">
|
||||
|
||||
</div>
|
||||
|
||||
<!-- <table class="filter-table"> -->
|
||||
<!-- <tbody> -->
|
||||
<!-- <tr ng-repeat="step in model.steps"> -->
|
||||
<!-- <td>{{step.name}}</td> -->
|
||||
<!-- <td>{{step.status}}</td> -->
|
||||
<!-- <td width="1%"> -->
|
||||
<!-- <i class="fa fa-check" style="color: #39A039"></i> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </tbody> -->
|
||||
<!-- </table> -->
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
73
public/app/core/components/wizard/wizard.ts
Normal file
73
public/app/core/components/wizard/wizard.ts
Normal file
@ -0,0 +1,73 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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);
|
@ -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,
|
||||
};
|
||||
|
@ -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: '<dash-import></dash-import>' }
|
||||
};
|
||||
|
||||
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 = $('<div class="tabbed-view" ng-include="' + src + '"></div>');
|
||||
if (editview === 'import') {
|
||||
var modalScope = $rootScope.$new();
|
||||
modalScope.$on("$destroy", function() {
|
||||
editorScope.dismiss();
|
||||
});
|
||||
|
||||
$rootScope.appEvent('show-modal', {
|
||||
templateHtml: '<dash-import></dash-import>',
|
||||
scope: modalScope,
|
||||
backdrop: 'static'
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var view = payload.src;
|
||||
if (view.indexOf('.html') > 0) {
|
||||
view = $('<div class="tabbed-view" ng-include="' + "'" + view + "'" + '"></div>');
|
||||
}
|
||||
|
||||
elem.append(view);
|
||||
$compile(elem.contents())(editorScope);
|
||||
|
@ -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');
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
@ -1,5 +1,5 @@
|
||||
///<reference path="../headers/common.d.ts" />
|
||||
//
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 },
|
||||
|
@ -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', {
|
||||
|
@ -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});
|
||||
};
|
||||
|
||||
});
|
||||
});
|
177
public/app/core/services/backend_srv.ts
Normal file
177
public/app/core/services/backend_srv.ts
Normal file
@ -0,0 +1,177 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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);
|
@ -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});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
43
public/app/core/services/util_srv.ts
Normal file
43
public/app/core/services/util_srv.ts
Normal file
@ -0,0 +1,43 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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);
|
@ -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 + ';';
|
||||
|
@ -4,12 +4,13 @@
|
||||
<div class="page-container" >
|
||||
<div class="page-header">
|
||||
<h1>Alerting</h1>
|
||||
<div class="gf-form-inline pull-right">
|
||||
<gf-form-switch class="gf-form" label="Ok" label-class="width-5" checked="ctrl.filter.ok" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Warn" label-class="width-5" checked="ctrl.filter.warn" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Critical" label-class="width-5" checked="ctrl.filter.critical" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Acknowleged" label-class="width-7" checked="ctrl.filter.acknowleged" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<gf-form-switch class="gf-form" label="Ok" label-class="width-5" checked="ctrl.filter.ok" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Warn" label-class="width-5" checked="ctrl.filter.warn" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Critical" label-class="width-5" checked="ctrl.filter.critical" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
<gf-form-switch class="gf-form" label="Acknowleged" label-class="width-7" checked="ctrl.filter.acknowleged" on-change="ctrl.updateFilter()"></gf-form-switch>
|
||||
</div>
|
||||
|
||||
<table class="grafana-options-table">
|
||||
|
@ -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 () {});
|
||||
|
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
11
public/app/features/dashboard/dash_list_ctrl.ts
Normal file
@ -0,0 +1,11 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
export class DashListCtrl {
|
||||
/** @ngInject */
|
||||
constructor() {
|
||||
}
|
||||
}
|
||||
|
||||
coreModule.controller('DashListCtrl', DashListCtrl);
|
@ -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');
|
||||
};
|
||||
|
||||
});
|
||||
|
||||
});
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
145
public/app/features/dashboard/dashboard_ctrl.ts
Normal file
@ -0,0 +1,145 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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);
|
@ -26,11 +26,19 @@
|
||||
<li>
|
||||
<a class="pointer" ng-click="shareDashboard(0)">
|
||||
<i class="fa fa-link"></i> Link to Dashboard
|
||||
<div class="dropdown-desc">Share an internal link to the current dashboard. Some configuration options available.</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="pointer" ng-click="shareDashboard(1)">
|
||||
<i class="icon-gf icon-gf-snapshot"></i>Snapshot sharing
|
||||
<i class="icon-gf icon-gf-snapshot"></i>Snapshot
|
||||
<div class="dropdown-desc">Interactive, publically accessible dashboard. Sensitive data is stripped out.</div>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="pointer" ng-click="shareDashboard(2)">
|
||||
<i class="fa fa-cloud-upload"></i>Export
|
||||
<div class="dropdown-desc">Export the dashboard to a JSON file for others and to share on Grafana.net</div>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
@ -44,8 +52,7 @@
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('settings');">Settings</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('annotations');">Annotations</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="openEditView('templating');">Templating</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="exportDashboard();">Export</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="editJson();">View JSON</a></li>
|
||||
<li ng-if="dashboardMeta.canEdit"><a class="pointer" ng-click="viewJson();">View JSON</a></li>
|
||||
<li ng-if="contextSrv.isEditor && !dashboard.editable"><a class="pointer" ng-click="makeEditable();">Make Editable</a></li>
|
||||
<li ng-if="contextSrv.isEditor"><a class="pointer" ng-click="saveDashboardAs();">Save As...</a></li>
|
||||
<li ng-if="dashboardMeta.canSave"><a class="pointer" ng-click="deleteDashboard();">Delete dashboard</a></li>
|
||||
|
@ -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);
|
||||
|
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
188
public/app/features/dashboard/dynamic_dashboard_srv.ts
Normal file
@ -0,0 +1,188 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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);
|
||||
|
29
public/app/features/dashboard/export/export_modal.html
Normal file
29
public/app/features/dashboard/export/export_modal.html
Normal file
@ -0,0 +1,29 @@
|
||||
|
||||
<!-- <p> -->
|
||||
<!-- Exporting will export a cleaned sharable dashboard that can be imported -->
|
||||
<!-- into another Grafana instance. -->
|
||||
<!-- </p> -->
|
||||
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
</div>
|
||||
<div>
|
||||
<p class="share-modal-info-text">
|
||||
Export the dashboard to a JSON file. The exporter will templatize the
|
||||
dashboard's data sources to make it easy for other's to to import and reuse.
|
||||
You can share dashboards on <a class="external-link" href="https://grafana.net">Grafana.net</a>
|
||||
</p>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.save()">
|
||||
<i class="fa fa-save"></i> Save to file
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn width-10 btn-secondary" ng-click="ctrl.saveJson()">
|
||||
<i class="fa fa-file-text-o"></i> View JSON
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
53
public/app/features/dashboard/export/export_modal.ts
Normal file
53
public/app/features/dashboard/export/export_modal.ts
Normal file
@ -0,0 +1,53 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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);
|
135
public/app/features/dashboard/export/exporter.ts
Normal file
135
public/app/features/dashboard/export/exporter.ts
Normal file
@ -0,0 +1,135 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
130
public/app/features/dashboard/import/dash_import.html
Normal file
130
public/app/features/dashboard/import/dash_import.html
Normal file
@ -0,0 +1,130 @@
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-upload"></i>
|
||||
<span class="p-l-1">Import Dashboard</span>
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content" ng-cloak>
|
||||
<div ng-if="ctrl.step === 1">
|
||||
|
||||
<form class="gf-form-group">
|
||||
<dash-upload on-upload="ctrl.onUpload(dash)"></dash-upload>
|
||||
</form>
|
||||
|
||||
<h5 class="section-heading">Grafana.net Dashboard</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.gnetUrl" placeholder="Paste Grafana.net dashboard url or id" ng-blur="ctrl.checkGnetDashboard()"></textarea>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.gnetError">
|
||||
<label class="gf-form-label text-warning">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.gnetError}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Or paste JSON</h5>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<textarea rows="7" data-share-panel-url="" class="gf-form-input" ng-ctrl="ctrl.jsonText"></textarea>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" ng-click="ctrl.loadJsonText()">
|
||||
<i class="fa fa-paste"></i>
|
||||
Load
|
||||
</button>
|
||||
<span ng-if="ctrl.parseError" class="text-error p-l-1">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.parseError}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.step === 2">
|
||||
<div class="gf-form-group" ng-if="ctrl.dash.gnetId">
|
||||
<h3 class="section-heading">
|
||||
Importing Dashboard from
|
||||
<a href="https://grafana.net/dashboards/{{ctrl.dash.gnetId}}" class="external-link" target="_blank">Grafana.net</a>
|
||||
</h3>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">Published by</label>
|
||||
<label class="gf-form-label width-15">{{ctrl.gnetInfo.orgName}}</label>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">Updated on</label>
|
||||
<label class="gf-form-label width-15">{{ctrl.gnetInfo.updatedAt | date : 'yyyy-MM-dd HH:mm:ss'}}</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="section-heading">
|
||||
Options
|
||||
</h3>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<label class="gf-form-label width-15">Name</label>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.dash.title" give-focus="true" ng-change="ctrl.titleChanged()" ng-class="{'validation-error': ctrl.nameExists}">
|
||||
<label class="gf-form-label text-success" ng-if="!ctrl.nameExists">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-inline" ng-if="ctrl.nameExists">
|
||||
<div class="gf-form offset-width-15 gf-form--grow">
|
||||
<label class="gf-form-label text-warning gf-form-label--grow">
|
||||
<i class="fa fa-warning"></i>
|
||||
A Dashboard with the same name already exists
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-repeat="input in ctrl.inputs">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-15">
|
||||
{{input.label}}
|
||||
<info-popover mode="right-normal">
|
||||
{{input.info}}
|
||||
</info-popover>
|
||||
</label>
|
||||
<!-- Data source input -->
|
||||
<div class="gf-form-select-wrapper" style="width: 100%" ng-if="input.type === 'datasource'">
|
||||
<select class="gf-form-input" ng-model="input.value" ng-options="v.value as v.text for v in input.options" ng-change="ctrl.inputValueChanged()">
|
||||
<option value="" ng-hide="input.value">{{input.info}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Constant input -->
|
||||
<input ng-if="input.type === 'constant'" type="text" class="gf-form-input" ng-model="input.value" placeholder="{{input.default}}" ng-change="ctrl.inputValueChanged()">
|
||||
<label class="gf-form-label text-success" ng-show="input.value">
|
||||
<i class="fa fa-check"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn gf-form-btn btn-success width-10" ng-click="ctrl.saveDashboard()" ng-hide="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||
<i class="fa fa-save"></i> Save & Open
|
||||
</button>
|
||||
<button type="button" class="btn gf-form-btn btn-danger width-10" ng-click="ctrl.saveDashboard()" ng-show="ctrl.nameExists" ng-disabled="!ctrl.inputsValid">
|
||||
<i class="fa fa-save"></i> Overwrite & Open
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||
<a class="btn btn-link" ng-click="ctrl.back()">Back</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
180
public/app/features/dashboard/import/dash_import.ts
Normal file
180
public/app/features/dashboard/import/dash_import.ts
Normal file
@ -0,0 +1,180 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
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);
|
@ -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 });
|
||||
|
10
public/app/features/dashboard/partials/dash_list.html
Normal file
10
public/app/features/dashboard/partials/dash_list.html
Normal file
@ -0,0 +1,10 @@
|
||||
<navbar title="Dashboards" title-url="dashboards" icon="icon-gf icon-gf-dashboard">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>Dashboards</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -1,23 +1,15 @@
|
||||
<navbar title="Import" title-url="import/dashboard" icon="fa fa-download">
|
||||
<navbar title="Migrate" title-url="dashboards/migrate" icon="fa fa-download">
|
||||
</navbar>
|
||||
|
||||
<div class="page-container">
|
||||
<div class="page-header">
|
||||
<h1>
|
||||
Import file
|
||||
<em style="font-size: 14px;padding-left: 10px;"> <i class="fa fa-info-circle"></i> Load dashboard from local .json file</em>
|
||||
Migrate dashboards
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<form class="gf-form">
|
||||
<input type="file" id="dashupload" dash-upload/><br>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">
|
||||
Migrate dashboards
|
||||
<em style="font-size: 14px;padding-left: 10px;"><i class="fa fa-info-circle"></i> Import dashboards from Elasticsearch or InfluxDB</em>
|
||||
Import dashboards from Elasticsearch or InfluxDB
|
||||
</h5>
|
||||
|
||||
<div class="gf-form-inline gf-form-group">
|
@ -22,10 +22,14 @@
|
||||
<div class="gf-form-group section">
|
||||
<h5 class="section-heading">Details</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Title</label>
|
||||
<input type="text" class="gf-form-input width-25" ng-model='dashboard.title'></input>
|
||||
<label class="gf-form-label width-7">Name</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='dashboard.title'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Description</label>
|
||||
<input type="text" class="gf-form-input width-30" ng-model='dashboard.description'></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">
|
||||
Tags
|
||||
<info-popover mode="right-normal">Press enter to add a tag</info-popover>
|
||||
@ -107,7 +111,7 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.updated)}}</span>
|
||||
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.updated)}}</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Last updated by:</span>
|
||||
@ -115,7 +119,7 @@
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created at:</span>
|
||||
<span class="gf-form-label width-18">{{formatDate(dashboardMeta.created)}} </span>
|
||||
<span class="gf-form-label width-18">{{dashboard.formatDate(dashboardMeta.created)}} </span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-10">Created by:</span>
|
||||
|
@ -25,28 +25,33 @@
|
||||
</div>
|
||||
|
||||
<script type="text/ng-template" id="shareEmbed.html">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-code"></i>
|
||||
</div>
|
||||
<div class="share-modal-content">
|
||||
<p class="share-modal-info-text">
|
||||
The html code below can be pasted and included in another web page. Unless anonymous access
|
||||
is enabled the user viewing that page need to be signed into grafana for the graph to load.
|
||||
</p>
|
||||
|
||||
<div class="share-snapshot-header">
|
||||
<p class="share-snapshot-info-text">
|
||||
The html code below can be pasted and included in another web page. Unless anonymous access
|
||||
is enabled the user viewing that page need to be signed into grafana for the graph to load.
|
||||
</p>
|
||||
</div>
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
|
||||
<div class="gf-form-group section">
|
||||
<div class="gf-form width-30">
|
||||
<textarea rows="5" data-share-panel-url class="gf-form-input width-30" ng-model='iframeHtml'></textarea>
|
||||
<div class="gf-form-group gf-form--grow">
|
||||
<div class="gf-form">
|
||||
<textarea rows="5" data-share-panel-url class="gf-form-input" ng-model='iframeHtml'></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareExport.html">
|
||||
<dash-export-modal></dash-export-modal>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareLinkOptions.html">
|
||||
<div class="gf-form-group section">
|
||||
<div class="gf-form-group">
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Current time range" label-class="width-12" switch-class="max-width-6"
|
||||
checked="options.forCurrent" on-change="buildUrl()">
|
||||
@ -65,91 +70,100 @@
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareLink.html">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-link"></i>
|
||||
</div>
|
||||
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
<div>
|
||||
<div class="gf-form-group section">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form width-30">
|
||||
<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
|
||||
</div>
|
||||
<div class="gf-form pull-right">
|
||||
<button class="btn btn-inverse pull-right" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-link"></i>
|
||||
</div>
|
||||
<div class="share-modal-content">
|
||||
<p class="share-modal-info-text">
|
||||
Create a direct link to this dashboard or panel, customized with the options below.
|
||||
</p>
|
||||
<div ng-include src="'shareLinkOptions.html'"></div>
|
||||
<div>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-form--grow">
|
||||
<input type="text" data-share-panel-url class="gf-form-input" ng-model="shareUrl"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<button class="btn btn-inverse" data-clipboard-text="{{shareUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="modeSharePanel">
|
||||
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form section" ng-show="modeSharePanel">
|
||||
<a href="{{imageUrl}}" target="_blank"><i class="fa fa-camera"></i> Direct link rendered image</a>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/ng-template" id="shareSnapshot.html">
|
||||
<div class="ng-cloak" ng-cloak ng-controller="ShareSnapshotCtrl" ng-init="init()">
|
||||
<div class="share-modal-big-icon">
|
||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
|
||||
</div>
|
||||
|
||||
<div class="share-snapshot-header" ng-if="step === 1">
|
||||
<p class="share-snapshot-info-text">
|
||||
A snapshot is an instant way to share an interactive dashboard publicly.
|
||||
When created, we <strong>strip sensitive data</strong> like queries (metric, template and annotation) and panel links,
|
||||
leaving only the visible metric data and series names embedded into your dashboard.
|
||||
</p>
|
||||
<p class="share-snapshot-info-text">
|
||||
Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
|
||||
Share wisely.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="share-snapshot-header" ng-if="step === 3">
|
||||
<p class="share-snapshot-info-text">
|
||||
The snapshot has now been deleted. If it you have already accessed it once, It might take up to an hour before it is removed from
|
||||
browser caches or CDN caches.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<div class="gf-form" ng-if="step === 1">
|
||||
<span class="gf-form-label width-12">Snapshot name</span>
|
||||
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
|
||||
<div class="share-modal-header">
|
||||
<div class="share-modal-big-icon">
|
||||
<i ng-if="loading" class="fa fa-spinner fa-spin"></i>
|
||||
<i ng-if="!loading" class="icon-gf icon-gf-snapshot"></i>
|
||||
</div>
|
||||
<div class="gf-form" ng-if="step === 1">
|
||||
<span class="gf-form-label width-12">Expire</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
||||
<div class="share-modal-content">
|
||||
<div ng-if="step === 1">
|
||||
<p class="share-modal-info-text">
|
||||
A snapshot is an instant way to share an interactive dashboard publicly.
|
||||
When created, we <strong>strip sensitive data</strong> like queries (metric, template and annotation) and panel links,
|
||||
leaving only the visible metric data and series names embedded into your dashboard.
|
||||
</p>
|
||||
<p class="share-modal-info-text">
|
||||
Keep in mind, your <strong>snapshot can be viewed by anyone</strong> that has the link and can reach the URL.
|
||||
Share wisely.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="share-modal-header" ng-if="step === 3">
|
||||
<p class="share-modal-info-text">
|
||||
The snapshot has now been deleted. If it you have already accessed it once, It might take up to an hour before it is removed from
|
||||
browser caches or CDN caches.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<div class="gf-form" ng-if="step === 1">
|
||||
<span class="gf-form-label width-12">Snapshot name</span>
|
||||
<input type="text" ng-model="snapshot.name" class="gf-form-input max-width-15" >
|
||||
</div>
|
||||
<div class="gf-form" ng-if="step === 1">
|
||||
<span class="gf-form-label width-12">Expire</span>
|
||||
<div class="gf-form-select-wrapper max-width-15">
|
||||
<select class="gf-form-input" ng-model="snapshot.expires" ng-options="f.value as f.text for f in expireOptions"></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
|
||||
<div class="gf-form-row">
|
||||
<a href="{{snapshotUrl}}" class="large share-modal-link" target="_blank">
|
||||
<i class="fa fa-external-link-square"></i>
|
||||
{{snapshotUrl}}
|
||||
</a>
|
||||
<br>
|
||||
<button class="btn btn-inverse" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="step === 1" class="gf-form-button-row">
|
||||
<button class="btn gf-form-btn width-10 btn-success" ng-click="createSnapshot()" ng-disabled="loading">
|
||||
<i class="fa fa-save"></i>
|
||||
Local Snapshot
|
||||
</button>
|
||||
<button class="btn gf-form-btn width-16 btn-secondary" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
{{sharingButtonText}}
|
||||
</button>
|
||||
<a class="btn btn-link" ng-click="dismiss()">Cancel</a>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
|
||||
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form" ng-if="step === 2" style="margin-top: 40px">
|
||||
<div class="gf-form-row">
|
||||
<a href="{{snapshotUrl}}" class="large share-snapshot-link" target="_blank">
|
||||
<i class="fa fa-external-link-square"></i>
|
||||
{{snapshotUrl}}
|
||||
</a>
|
||||
<br>
|
||||
<button class="btn btn-inverse btn-large" data-clipboard-text="{{snapshotUrl}}" clipboard-button><i class="fa fa-clipboard"></i> Copy Link</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-if="step === 1" class="gf-form-buttons-row">
|
||||
<button class="btn btn-success btn-large" ng-click="createSnapshot()" ng-disabled="loading">
|
||||
<i class="fa fa-save"></i>
|
||||
Local Snapshot
|
||||
</button>
|
||||
<button class="btn btn-primary btn-large" ng-if="externalEnabled" ng-click="createSnapshot(true)" ng-disabled="loading">
|
||||
<i class="fa fa-cloud-upload"></i>
|
||||
{{sharingButtonText}}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" ng-if="step === 2" style="padding: 5px">
|
||||
Did you make a mistake? <a class="pointer" ng-click="deleteSnapshot()" target="_blank">delete snapshot.</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -22,11 +22,15 @@ function (angular, _, require, config) {
|
||||
$scope.modalTitle = 'Share Panel';
|
||||
$scope.tabs.push({title: 'Embed', src: 'shareEmbed.html'});
|
||||
} else {
|
||||
$scope.modalTitle = 'Share Dashboard';
|
||||
$scope.modalTitle = 'Share';
|
||||
}
|
||||
|
||||
if (!$scope.dashboard.meta.isSnapshot) {
|
||||
$scope.tabs.push({title: 'Snapshot sharing', src: 'shareSnapshot.html'});
|
||||
$scope.tabs.push({title: 'Snapshot', src: 'shareSnapshot.html'});
|
||||
}
|
||||
|
||||
if (!$scope.dashboard.meta.isSnapshot) {
|
||||
$scope.tabs.push({title: 'Export', src: 'shareExport.html'});
|
||||
}
|
||||
|
||||
$scope.buildUrl();
|
||||
|
@ -0,0 +1,84 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import {DashImportCtrl} from 'app/features/dashboard/import/dash_import';
|
||||
import config from 'app/core/config';
|
||||
|
||||
describe('DashImportCtrl', function() {
|
||||
var ctx: any = {};
|
||||
var backendSrv = {
|
||||
search: sinon.stub().returns(Promise.resolve([])),
|
||||
get: sinon.stub()
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
|
||||
beforeEach(angularMocks.inject(($rootScope, $controller, $q) => {
|
||||
ctx.$q = $q;
|
||||
ctx.scope = $rootScope.$new();
|
||||
ctx.ctrl = $controller(DashImportCtrl, {
|
||||
$scope: ctx.scope,
|
||||
backendSrv: backendSrv,
|
||||
});
|
||||
}));
|
||||
|
||||
describe('when uploading json', function() {
|
||||
beforeEach(function() {
|
||||
config.datasources = {
|
||||
ds: {
|
||||
type: 'test-db',
|
||||
}
|
||||
};
|
||||
|
||||
ctx.ctrl.onUpload({
|
||||
'__inputs': [
|
||||
{name: 'ds', pluginId: 'test-db', type: 'datasource', pluginName: 'Test DB'}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should build input model', function() {
|
||||
expect(ctx.ctrl.inputs.length).to.eql(1);
|
||||
expect(ctx.ctrl.inputs[0].name).to.eql('ds');
|
||||
expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source');
|
||||
});
|
||||
|
||||
it('should set inputValid to false', function() {
|
||||
expect(ctx.ctrl.inputsValid).to.eql(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when specifing grafana.net url', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.gnetUrl = 'http://grafana.net/dashboards/123';
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when specifing dashbord id', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.gnetUrl = '2342';
|
||||
// setup api mock
|
||||
backendSrv.get = sinon.spy(() => {
|
||||
return Promise.resolve({
|
||||
});
|
||||
});
|
||||
ctx.ctrl.checkGnetDashboard();
|
||||
});
|
||||
|
||||
it('should call gnet api with correct dashboard id', function() {
|
||||
expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,264 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import 'app/features/dashboard/dashboardSrv';
|
||||
import {DynamicDashboardSrv} from '../dynamic_dashboard_srv';
|
||||
|
||||
function dynamicDashScenario(desc, func) {
|
||||
|
||||
describe(desc, function() {
|
||||
var ctx: any = {};
|
||||
|
||||
ctx.setup = function (setupFunc) {
|
||||
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module(function($provide) {
|
||||
$provide.value('contextSrv', {
|
||||
user: { timezone: 'utc'}
|
||||
});
|
||||
}));
|
||||
|
||||
beforeEach(angularMocks.inject(function(dashboardSrv) {
|
||||
ctx.dashboardSrv = dashboardSrv;
|
||||
var model = {
|
||||
rows: [],
|
||||
templating: { list: [] }
|
||||
};
|
||||
|
||||
setupFunc(model);
|
||||
ctx.dash = ctx.dashboardSrv.create(model);
|
||||
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
|
||||
ctx.dynamicDashboardSrv.init(ctx.dash);
|
||||
ctx.rows = ctx.dash.rows;
|
||||
}));
|
||||
};
|
||||
|
||||
func(ctx);
|
||||
});
|
||||
}
|
||||
|
||||
dynamicDashScenario('given dashboard with panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
panels: [{id: 2, repeat: 'apps'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'apps',
|
||||
current: {
|
||||
text: 'se1, se2, se3',
|
||||
value: ['se1', 'se2', 'se3']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
{text: 'se3', value: 'se3', selected: true},
|
||||
{text: 'se4', value: 'se4', selected: false}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat panel one time', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should mark panel repeated', function() {
|
||||
expect(ctx.rows[0].panels[0].repeat).to.be('apps');
|
||||
expect(ctx.rows[0].panels[1].repeatPanelId).to.be(2);
|
||||
});
|
||||
|
||||
it('should set scopedVars on panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.apps.value).to.be('se1');
|
||||
expect(ctx.rows[0].panels[1].scopedVars.apps.value).to.be('se2');
|
||||
expect(ctx.rows[0].panels[2].scopedVars.apps.value).to.be('se3');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedPanelAfterIteration1;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedPanelAfterIteration1 = ctx.rows[0].panels[1];
|
||||
ctx.rows[0].panels[0].fill = 10;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should have reused same panel instances', function() {
|
||||
expect(ctx.rows[0].panels[1]).to.be(repeatedPanelAfterIteration1);
|
||||
});
|
||||
|
||||
it('reused panel should copy properties from source', function() {
|
||||
expect(ctx.rows[0].panels[1].fill).to.be(10);
|
||||
});
|
||||
|
||||
it('should have same panel count', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(3);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and panel repeat is turned off', function() {
|
||||
beforeEach(function() {
|
||||
ctx.rows[0].panels[0].repeat = null;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should clean up repeated panel', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should remove scoped vars from reused panel', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars).to.be.empty();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2}]
|
||||
});
|
||||
dash.rows.push({panels: []});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: {
|
||||
text: 'se1, se2',
|
||||
value: ['se1', 'se2']
|
||||
},
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep first row as repeat', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should add scopedVars to rows', function() {
|
||||
expect(ctx.rows[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
describe('After a second iteration', function() {
|
||||
var repeatedRowAfterFirstIteration;
|
||||
|
||||
beforeEach(function() {
|
||||
repeatedRowAfterFirstIteration = ctx.rows[1];
|
||||
ctx.rows[0].height = 500;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should still only have 2 rows', function() {
|
||||
expect(ctx.rows.length).to.be(3);
|
||||
});
|
||||
|
||||
it.skip('should have updated props from source', function() {
|
||||
expect(ctx.rows[1].height).to.be(500);
|
||||
});
|
||||
|
||||
it('should reuse row instance', function() {
|
||||
expect(ctx.rows[1]).to.be(repeatedRowAfterFirstIteration);
|
||||
});
|
||||
});
|
||||
|
||||
describe('After a second iteration and selected values reduced', function() {
|
||||
beforeEach(function() {
|
||||
ctx.dash.templating.list[0].options[1].selected = false;
|
||||
ctx.dynamicDashboardSrv.update(ctx.dash);
|
||||
});
|
||||
|
||||
it('should remove repeated second row', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
dynamicDashScenario('given dashboard with row repeat and panel repeat', function(ctx) {
|
||||
ctx.setup(function(dash) {
|
||||
dash.rows.push({
|
||||
repeat: 'servers',
|
||||
panels: [{id: 2, repeat: 'metric'}]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'servers',
|
||||
current: { text: 'se1, se2', value: ['se1', 'se2'] },
|
||||
options: [
|
||||
{text: 'se1', value: 'se1', selected: true},
|
||||
{text: 'se2', value: 'se2', selected: true},
|
||||
]
|
||||
});
|
||||
dash.templating.list.push({
|
||||
name: 'metric',
|
||||
current: { text: 'm1, m2', value: ['m1', 'm2'] },
|
||||
options: [
|
||||
{text: 'm1', value: 'm1', selected: true},
|
||||
{text: 'm2', value: 'm2', selected: true},
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('should repeat row one time', function() {
|
||||
expect(ctx.rows.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should repeat panel on both rows', function() {
|
||||
expect(ctx.rows[0].panels.length).to.be(2);
|
||||
expect(ctx.rows[1].panels.length).to.be(2);
|
||||
});
|
||||
|
||||
it('should keep panel ids on first row', function() {
|
||||
expect(ctx.rows[0].panels[0].id).to.be(2);
|
||||
});
|
||||
|
||||
it('should mark second row as repeated', function() {
|
||||
expect(ctx.rows[0].repeat).to.be('servers');
|
||||
});
|
||||
|
||||
it('should clear repeat field on repeated row', function() {
|
||||
expect(ctx.rows[1].repeat).to.be(null);
|
||||
});
|
||||
|
||||
it('should generate a repeartRowId based on repeat row index', function() {
|
||||
expect(ctx.rows[1].repeatRowId).to.be(1);
|
||||
});
|
||||
|
||||
it('should set scopedVars on row panels', function() {
|
||||
expect(ctx.rows[0].panels[0].scopedVars.servers.value).to.be('se1');
|
||||
expect(ctx.rows[1].panels[0].scopedVars.servers.value).to.be('se2');
|
||||
});
|
||||
|
||||
});
|
||||
|
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
142
public/app/features/dashboard/specs/exporter_specs.ts
Normal file
@ -0,0 +1,142 @@
|
||||
import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
|
||||
|
||||
import _ from 'lodash';
|
||||
import config from 'app/core/config';
|
||||
import {DashboardExporter} from '../export/exporter';
|
||||
|
||||
describe('given dashboard with repeated panels', function() {
|
||||
var dash, exported;
|
||||
|
||||
beforeEach(done => {
|
||||
dash = {
|
||||
rows: [],
|
||||
templating: { list: [] },
|
||||
annotations: { list: [] },
|
||||
};
|
||||
|
||||
config.buildInfo = {
|
||||
version: "3.0.2"
|
||||
};
|
||||
|
||||
dash.templating.list.push({
|
||||
name: 'apps',
|
||||
type: 'query',
|
||||
datasource: 'gfdb',
|
||||
current: {value: 'Asd', text: 'Asd'},
|
||||
options: [{value: 'Asd', text: 'Asd'}]
|
||||
});
|
||||
|
||||
dash.templating.list.push({
|
||||
name: 'prefix',
|
||||
type: 'constant',
|
||||
current: {value: 'collectd', text: 'collectd'},
|
||||
options: []
|
||||
});
|
||||
|
||||
dash.annotations.list.push({
|
||||
name: 'logs',
|
||||
datasource: 'gfdb',
|
||||
});
|
||||
|
||||
dash.rows.push({
|
||||
repeat: 'test',
|
||||
panels: [
|
||||
{id: 2, repeat: 'apps', datasource: 'gfdb', type: 'graph'},
|
||||
{id: 2, repeat: null, repeatPanelId: 2},
|
||||
]
|
||||
});
|
||||
dash.rows.push({
|
||||
repeat: null,
|
||||
repeatRowId: 1
|
||||
});
|
||||
|
||||
var datasourceSrvStub = {
|
||||
get: sinon.stub().returns(Promise.resolve({
|
||||
name: 'gfdb',
|
||||
meta: {id: "testdb", info: {version: "1.2.1"}, name: "TestDB"}
|
||||
}))
|
||||
};
|
||||
|
||||
config.panels['graph'] = {
|
||||
id: "graph",
|
||||
name: "Graph",
|
||||
info: {version: "1.1.0"}
|
||||
};
|
||||
|
||||
var exporter = new DashboardExporter(datasourceSrvStub);
|
||||
exporter.makeExportable(dash).then(clean => {
|
||||
exported = clean;
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated panels', function() {
|
||||
expect(exported.rows[0].panels.length).to.be(1);
|
||||
});
|
||||
|
||||
it('exported dashboard should not contain repeated rows', function() {
|
||||
expect(exported.rows.length).to.be(1);
|
||||
});
|
||||
|
||||
it('should replace datasource refs', function() {
|
||||
var panel = exported.rows[0].panels[0];
|
||||
expect(panel.datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
it('should replace datasource in variable query', function() {
|
||||
expect(exported.templating.list[0].datasource).to.be("${DS_GFDB}");
|
||||
expect(exported.templating.list[0].options.length).to.be(0);
|
||||
expect(exported.templating.list[0].current.value).to.be(undefined);
|
||||
expect(exported.templating.list[0].current.text).to.be(undefined);
|
||||
});
|
||||
|
||||
it('should replace datasource in annotation query', function() {
|
||||
expect(exported.annotations.list[0].datasource).to.be("${DS_GFDB}");
|
||||
});
|
||||
|
||||
it('should add datasource as input', function() {
|
||||
expect(exported.__inputs[0].name).to.be("DS_GFDB");
|
||||
expect(exported.__inputs[0].pluginId).to.be("testdb");
|
||||
expect(exported.__inputs[0].type).to.be("datasource");
|
||||
});
|
||||
|
||||
it('should add datasource to required', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'TestDB'});
|
||||
expect(require.name).to.be("TestDB");
|
||||
expect(require.id).to.be("testdb");
|
||||
expect(require.type).to.be("datasource");
|
||||
expect(require.version).to.be("1.2.1");
|
||||
});
|
||||
|
||||
it('should add panel to required', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'Graph'});
|
||||
expect(require.name).to.be("Graph");
|
||||
expect(require.id).to.be("graph");
|
||||
expect(require.version).to.be("1.1.0");
|
||||
});
|
||||
|
||||
it('should add grafana version', function() {
|
||||
var require = _.findWhere(exported.__requires, {name: 'Grafana'});
|
||||
expect(require.type).to.be("grafana");
|
||||
expect(require.id).to.be("grafana");
|
||||
expect(require.version).to.be("3.0.2");
|
||||
});
|
||||
|
||||
it('should add constant template variables as inputs', function() {
|
||||
var input = _.findWhere(exported.__inputs, {name: 'VAR_PREFIX'});
|
||||
expect(input.type).to.be("constant");
|
||||
expect(input.label).to.be("prefix");
|
||||
expect(input.value).to.be("collectd");
|
||||
});
|
||||
|
||||
it('should templatize constant variables', function() {
|
||||
var variable = _.findWhere(exported.templating.list, {name: 'prefix'});
|
||||
expect(variable.query).to.be("${VAR_PREFIX}");
|
||||
expect(variable.current.text).to.be("${VAR_PREFIX}");
|
||||
expect(variable.current.value).to.be("${VAR_PREFIX}");
|
||||
expect(variable.options[0].text).to.be("${VAR_PREFIX}");
|
||||
expect(variable.options[0].value).to.be("${VAR_PREFIX}");
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -12,7 +12,6 @@ export class SubmenuCtrl {
|
||||
constructor(private $rootScope,
|
||||
private templateValuesSrv,
|
||||
private templateSrv,
|
||||
private dynamicDashboardSrv,
|
||||
private $location) {
|
||||
this.annotations = this.dashboard.templating.list;
|
||||
this.variables = this.dashboard.templating.list;
|
||||
@ -29,7 +28,6 @@ export class SubmenuCtrl {
|
||||
|
||||
variableUpdated(variable) {
|
||||
this.templateValuesSrv.variableUpdated(variable).then(() => {
|
||||
this.dynamicDashboardSrv.update(this.dashboard);
|
||||
this.$rootScope.$emit('template-variable-value-updated');
|
||||
this.$rootScope.$broadcast('refresh');
|
||||
});
|
||||
|
61
public/app/features/dashboard/upload.ts
Normal file
61
public/app/features/dashboard/upload.ts
Normal file
@ -0,0 +1,61 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
var template = `
|
||||
<input type="file" id="dashupload" name="dashupload" class="hide"/>
|
||||
<label class="btn btn-secondary" for="dashupload">
|
||||
<i class="fa fa-upload"></i>
|
||||
Upload .json File
|
||||
</label>
|
||||
`;
|
||||
|
||||
/** @ngInject */
|
||||
function uploadDashboardDirective(timer, alertSrv, $location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
template: template,
|
||||
scope: {
|
||||
onUpload: '&',
|
||||
},
|
||||
link: function(scope) {
|
||||
function file_selected(evt) {
|
||||
var files = evt.target.files; // FileList object
|
||||
var readerOnload = function() {
|
||||
return function(e) {
|
||||
var dash;
|
||||
try {
|
||||
dash = JSON.parse(e.target.result);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
scope.appEvent('alert-error', ['Import failed', 'JSON -> JS Serialization failed: ' + err.message]);
|
||||
return;
|
||||
}
|
||||
|
||||
scope.$apply(function() {
|
||||
scope.onUpload({dash: dash});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
for (var i = 0, f; f = files[i]; i++) {
|
||||
var reader = new FileReader();
|
||||
reader.onload = readerOnload();
|
||||
reader.readAsText(f);
|
||||
}
|
||||
}
|
||||
|
||||
var wnd: any = window;
|
||||
// Check for the various File API support.
|
||||
if (wnd.File && wnd.FileReader && wnd.FileList && wnd.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');
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
coreModule.directive('dashUpload', uploadDashboardDirective);
|
@ -12,7 +12,7 @@ import * as dateMath from 'app/core/utils/datemath';
|
||||
import {Subject} from 'vendor/npm/rxjs/Subject';
|
||||
|
||||
class MetricsPanelCtrl extends PanelCtrl {
|
||||
error: boolean;
|
||||
error: any;
|
||||
loading: boolean;
|
||||
datasource: any;
|
||||
datasourceName: any;
|
||||
@ -86,8 +86,14 @@ class MetricsPanelCtrl extends PanelCtrl {
|
||||
.then(this.issueQueries.bind(this))
|
||||
.then(this.handleQueryResult.bind(this))
|
||||
.catch(err => {
|
||||
// if cancelled keep loading set to true
|
||||
if (err.cancelled) {
|
||||
console.log('Panel request cancelled', err);
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = false;
|
||||
this.error = err.message || "Timeseries data request error";
|
||||
this.error = err.message || "Request Error";
|
||||
this.inspector = {error: err};
|
||||
this.events.emit('data-error', err);
|
||||
console.log('Panel data error:', err);
|
||||
|
@ -6,27 +6,27 @@
|
||||
<i class="icon-gf icon-gf-dashboard"></i>
|
||||
</td>
|
||||
<td>
|
||||
<a href="dashboard/{{dash.installedUri}}" ng-show="dash.installed">
|
||||
<a href="dashboard/{{dash.importedUri}}" ng-show="dash.imported">
|
||||
{{dash.title}}
|
||||
</a>
|
||||
<span ng-show="!dash.installed">
|
||||
<span ng-show="!dash.imported">
|
||||
{{dash.title}}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
v{{dash.revision}}
|
||||
<span ng-if="dash.installed">
|
||||
(Imported v{{dash.installedRevision}})
|
||||
(Imported v{{dash.importedRevision}})
|
||||
<span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.installed">
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, false)" ng-show="!dash.imported">
|
||||
Import
|
||||
</button>
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.installed">
|
||||
<button class="btn btn-secondary" ng-click="ctrl.import(dash, true)" ng-show="dash.imported">
|
||||
Update
|
||||
</button>
|
||||
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.installed">
|
||||
<button class="btn btn-danger" ng-click="ctrl.remove(dash)" ng-show="dash.imported">
|
||||
Delete
|
||||
</button>
|
||||
</td>
|
||||
|
@ -61,15 +61,15 @@ export class DashImportListCtrl {
|
||||
}
|
||||
|
||||
return this.backendSrv.post(`/api/dashboards/import`, installCmd).then(res => {
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard Installed', dash.title]);
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard Imported', dash.title]);
|
||||
_.extend(dash, res);
|
||||
});
|
||||
}
|
||||
|
||||
remove(dash) {
|
||||
this.backendSrv.delete('/api/dashboards/' + dash.installedUri).then(() => {
|
||||
this.backendSrv.delete('/api/dashboards/' + dash.importedUri).then(() => {
|
||||
this.$rootScope.appEvent('alert-success', ['Dashboard Deleted', dash.title]);
|
||||
dash.installed = false;
|
||||
dash.imported = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -89,7 +89,3 @@ export function dashboardImportList() {
|
||||
}
|
||||
|
||||
coreModule.directive('dashboardImportList', dashboardImportList);
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -88,7 +88,6 @@ export class PluginEditCtrl {
|
||||
jsonData: this.model.jsonData,
|
||||
secureJsonData: this.model.secureJsonData,
|
||||
}, {});
|
||||
|
||||
return this.backendSrv.post(`/api/plugins/${this.pluginId}/settings`, updateCmd);
|
||||
})
|
||||
.then(this.postUpdateHook)
|
||||
|
@ -40,10 +40,6 @@
|
||||
<td><span class="label label-info">CTRL+S</span></td>
|
||||
<td>Save dashboard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="label label-info">CTRL+E</span></td>
|
||||
<td>Export dashboard</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><span class="label label-info">CTRL+H</span></td>
|
||||
<td>Hide row controls</td>
|
||||
|
@ -283,42 +283,6 @@ function (angular, _, moment, kbn, ElasticQueryBuilder, IndexPattern, ElasticRes
|
||||
return this.getTerms(query);
|
||||
}
|
||||
};
|
||||
|
||||
this.getDashboard = function(id) {
|
||||
return this._get('/dashboard/' + id)
|
||||
.then(function(result) {
|
||||
return angular.fromJson(result._source.dashboard);
|
||||
});
|
||||
};
|
||||
|
||||
this.searchDashboards = function() {
|
||||
var query = {
|
||||
query: { query_string: { query: '*' } },
|
||||
size: 10000,
|
||||
sort: ["_uid"],
|
||||
};
|
||||
|
||||
return this._post(this.index + '/dashboard/_search', query)
|
||||
.then(function(results) {
|
||||
if(_.isUndefined(results.hits)) {
|
||||
return { dashboards: [], tags: [] };
|
||||
}
|
||||
|
||||
var resultsHits = results.hits.hits;
|
||||
var displayHits = { dashboards: [] };
|
||||
|
||||
for (var i = 0, len = resultsHits.length; i < len; i++) {
|
||||
var hit = resultsHits[i];
|
||||
displayHits.dashboards.push({
|
||||
id: hit._id,
|
||||
title: hit._source.title,
|
||||
tags: hit._source.tags
|
||||
});
|
||||
}
|
||||
|
||||
return displayHits;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -30,17 +30,17 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
return $q.when({data: []});
|
||||
}
|
||||
|
||||
if (options.format === 'png') {
|
||||
return $q.when({data: this.url + '/render' + '?' + params.join('&')});
|
||||
}
|
||||
var httpOptions: any = {
|
||||
method: 'POST',
|
||||
url: '/render',
|
||||
data: params.join('&'),
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
};
|
||||
|
||||
var httpOptions: any = {method: this.render_method, url: '/render'};
|
||||
|
||||
if (httpOptions.method === 'GET') {
|
||||
httpOptions.url = httpOptions.url + '?' + params.join('&');
|
||||
} else {
|
||||
httpOptions.data = params.join('&');
|
||||
httpOptions.headers = { 'Content-Type': 'application/x-www-form-urlencoded' };
|
||||
if (options.panelId) {
|
||||
httpOptions.requestId = 'panel' + options.panelId;
|
||||
}
|
||||
|
||||
return this.doGraphiteRequest(httpOptions).then(this.convertDataPointsToMs);
|
||||
@ -181,17 +181,6 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
});
|
||||
};
|
||||
|
||||
this.listDashboards = function(query) {
|
||||
return this.doGraphiteRequest({ method: 'GET', url: '/dashboard/find/', params: {query: query || ''} })
|
||||
.then(function(results) {
|
||||
return results.data.dashboards;
|
||||
});
|
||||
};
|
||||
|
||||
this.loadDashboard = function(dashName) {
|
||||
return this.doGraphiteRequest({method: 'GET', url: '/dashboard/load/' + encodeURIComponent(dashName) });
|
||||
};
|
||||
|
||||
this.doGraphiteRequest = function(options) {
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
options.withCredentials = true;
|
||||
@ -202,7 +191,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
}
|
||||
|
||||
options.url = this.url + options.url;
|
||||
options.inspect = { type: 'graphite' };
|
||||
options.inspect = {type: 'graphite'};
|
||||
|
||||
return backendSrv.datasourceRequest(options);
|
||||
};
|
||||
@ -217,9 +206,7 @@ export function GraphiteDatasource(instanceSettings, $q, backendSrv, templateSrv
|
||||
var intervalFormatFixRegex = /'(\d+)m'/gi;
|
||||
var hasTargets = false;
|
||||
|
||||
if (options.format !== 'png') {
|
||||
options['format'] = 'json';
|
||||
}
|
||||
options['format'] = 'json';
|
||||
|
||||
function fixIntervalFormat(match) {
|
||||
return match.replace('m', 'min').replace('M', 'mon');
|
||||
|
@ -162,12 +162,23 @@ function (angular, _, dateMath) {
|
||||
});
|
||||
};
|
||||
|
||||
this._performMetricKeyValueLookup = function(metric, key) {
|
||||
if(!metric || !key) {
|
||||
this._performMetricKeyValueLookup = function(metric, keys) {
|
||||
|
||||
if(!metric || !keys) {
|
||||
return $q.when([]);
|
||||
}
|
||||
|
||||
var m = metric + "{" + key + "=*}";
|
||||
var keysArray = keys.split(",").map(function(key) {
|
||||
return key.trim();
|
||||
});
|
||||
var key = keysArray[0];
|
||||
var keysQuery = key + "=*";
|
||||
|
||||
if (keysArray.length > 1) {
|
||||
keysQuery += "," + keysArray.splice(1).join(",");
|
||||
}
|
||||
|
||||
var m = metric + "{" + keysQuery + "}";
|
||||
|
||||
return this._get('/api/search/lookup', {m: m, limit: 3000}).then(function(result) {
|
||||
result = result.data.results;
|
||||
@ -225,7 +236,7 @@ function (angular, _, dateMath) {
|
||||
|
||||
var metrics_regex = /metrics\((.*)\)/;
|
||||
var tag_names_regex = /tag_names\((.*)\)/;
|
||||
var tag_values_regex = /tag_values\((.*),\s?(.*)\)/;
|
||||
var tag_values_regex = /tag_values\((.*?),\s?(.*)\)/;
|
||||
var tag_names_suggest_regex = /suggest_tagk\((.*)\)/;
|
||||
var tag_values_suggest_regex = /suggest_tagv\((.*)\)/;
|
||||
|
||||
|
@ -51,6 +51,20 @@ describe('opentsdb', function() {
|
||||
expect(requestOptions.params.m).to.be('cpu{hostname=*}');
|
||||
});
|
||||
|
||||
it('tag_values(cpu, test) should generate lookup query', function() {
|
||||
ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env)').then(function(data) { results = data; });
|
||||
ctx.$rootScope.$apply();
|
||||
expect(requestOptions.url).to.be('/api/search/lookup');
|
||||
expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env}');
|
||||
});
|
||||
|
||||
it('tag_values(cpu, test) should generate lookup query', function() {
|
||||
ctx.ds.metricFindQuery('tag_values(cpu, hostname, env=$env, region=$region)').then(function(data) { results = data; });
|
||||
ctx.$rootScope.$apply();
|
||||
expect(requestOptions.url).to.be('/api/search/lookup');
|
||||
expect(requestOptions.params.m).to.be('cpu{hostname=*,env=$env,region=$region}');
|
||||
});
|
||||
|
||||
it('suggest_tagk() should generate api suggest query', function() {
|
||||
ctx.ds.metricFindQuery('suggest_tagk(foo)').then(function(data) { results = data; });
|
||||
ctx.$rootScope.$apply();
|
||||
|
@ -21,10 +21,11 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
this.withCredentials = instanceSettings.withCredentials;
|
||||
this.lastErrors = {};
|
||||
|
||||
this._request = function(method, url) {
|
||||
this._request = function(method, url, requestId) {
|
||||
var options: any = {
|
||||
url: this.url + url,
|
||||
method: method
|
||||
method: method,
|
||||
requestId: requestId,
|
||||
};
|
||||
|
||||
if (this.basicAuth || this.withCredentials) {
|
||||
@ -57,6 +58,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
return escapedValues.join('|');
|
||||
};
|
||||
|
||||
var HTTP_REQUEST_ABORTED = -1;
|
||||
// Called once per panel (graph)
|
||||
this.query = function(options) {
|
||||
var self = this;
|
||||
@ -75,6 +77,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
var query: any = {};
|
||||
query.expr = templateSrv.replace(target.expr, options.scopedVars, self.interpolateQueryExpr);
|
||||
query.requestId = target.expr;
|
||||
|
||||
var interval = target.interval || options.interval;
|
||||
var intervalFactor = target.intervalFactor || 1;
|
||||
@ -100,8 +103,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
return this.performTimeSeriesQuery(query, start, end);
|
||||
}, this));
|
||||
|
||||
return $q.all(allQueryPromise)
|
||||
.then(function(allResponse) {
|
||||
return $q.all(allQueryPromise).then(function(allResponse) {
|
||||
var result = [];
|
||||
|
||||
_.each(allResponse, function(response, index) {
|
||||
@ -122,7 +124,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
|
||||
this.performTimeSeriesQuery = function(query, start, end) {
|
||||
var url = '/api/v1/query_range?query=' + encodeURIComponent(query.expr) + '&start=' + start + '&end=' + end + '&step=' + query.step;
|
||||
return this._request('GET', url);
|
||||
return this._request('GET', url, query.requestId);
|
||||
};
|
||||
|
||||
this.performSuggestQuery = function(query) {
|
||||
@ -169,9 +171,11 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS
|
||||
expr: interpolated,
|
||||
step: '60s'
|
||||
};
|
||||
|
||||
var start = getPrometheusTime(options.range.from, false);
|
||||
var end = getPrometheusTime(options.range.to, true);
|
||||
var self = this;
|
||||
|
||||
return this.performTimeSeriesQuery(query, start, end).then(function(results) {
|
||||
var eventList = [];
|
||||
tagKeys = tagKeys.split(',');
|
||||
|
@ -135,13 +135,13 @@ function ($) {
|
||||
|
||||
// Dynamically reorder the hovercard for the current time point if the
|
||||
// option is enabled.
|
||||
if (panel.tooltip.ordering === 'decreasing') {
|
||||
if (panel.tooltip.sort === 2) {
|
||||
seriesHoverInfo.sort(function(a, b) {
|
||||
return parseFloat(b.value) - parseFloat(a.value);
|
||||
return b.value - a.value;
|
||||
});
|
||||
} else if (panel.tooltip.ordering === 'increasing') {
|
||||
} else if (panel.tooltip.sort === 1) {
|
||||
seriesHoverInfo.sort(function(a, b) {
|
||||
return parseFloat(a.value) - parseFloat(b.value);
|
||||
return a.value - b.value;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
tooltip : {
|
||||
value_type: 'cumulative',
|
||||
shared: true,
|
||||
ordering: 'alphabetical',
|
||||
sort: 0,
|
||||
msResolution: false,
|
||||
},
|
||||
// time overrides
|
||||
@ -176,12 +176,6 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
}
|
||||
|
||||
onDataReceived(dataList) {
|
||||
// png renderer returns just a url
|
||||
if (_.isString(dataList)) {
|
||||
this.render(dataList);
|
||||
return;
|
||||
}
|
||||
|
||||
this.datapointsWarning = false;
|
||||
this.datapointsCount = 0;
|
||||
this.datapointsOutside = false;
|
||||
|
@ -28,47 +28,39 @@
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.linewidth" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch ng-show="ctrl.panel.lines" class="gf-form" label="Staircase" label-class="width-8" checked="ctrl.panel.steppedLine" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form" ng-show="ctrl.panel.points">
|
||||
<label class="gf-form-label width-8">Point Radius</label>
|
||||
<div class="gf-form-select-wrapper max-width-5">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.pointradius" ng-options="f for f in [1,2,3,4,5,6,7,8,9,10]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Staircase" label-class="width-8"
|
||||
checked="ctrl.panel.steppedLine" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Misc options</h5>
|
||||
<h5 class="section-heading">Hover info</h5>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Null value</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input max-width-8" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Renderer</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.renderer" ng-options="f for f in ['flot', 'png']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Tooltip mode</label>
|
||||
<label class="gf-form-label width-9">Mode</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.shared" ng-options="f.value as f.text for f in [{text: 'All series', value: true}, {text: 'Single', value: false}]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-9">Tooltip ordering<tip>The ordering from top to bottom</tip></label>
|
||||
<label class="gf-form-label width-9">Sort order</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.ordering" ng-options="f.value as f.text for f in [{text: 'Alphabetical', value: 'alphabetical'}, {text: 'Increasing', value: 'increasing'}, {text: 'Decreasing', value: 'decreasing'}]" ng-change="ctrl.render()"></select>
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.sort" ng-options="f.value as f.text for f in [{text: 'None', value: 0}, {text: 'Increasing', value: 1}, {text: 'Decreasing', value: 2}]" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.panel.stack">
|
||||
<label class="gf-form-label width-9">Stacked value</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section gf-form-group">
|
||||
<h5 class="section-heading">Multiple Series</h5>
|
||||
<h5 class="section-heading">Stacking & Null value</h5>
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Stack" label-class="width-7"
|
||||
checked="ctrl.panel.stack" on-change="ctrl.render()">
|
||||
@ -77,10 +69,10 @@
|
||||
label="Percent" label-class="width-7"
|
||||
checked="ctrl.panel.percentage" on-change="ctrl.render()">
|
||||
</gf-form-switch>
|
||||
<div class="gf-form" ng-show="ctrl.panel.stack">
|
||||
<label class="gf-form-label width-7">Tooltip value</label>
|
||||
<div class="gf-form-select-wrapper max-width-8">
|
||||
<select class="gf-form-input" ng-model="ctrl.panel.tooltip.value_type" ng-options="f for f in ['cumulative','individual']" ng-change="ctrl.render()"></select>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Null value</label>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input max-width-8" ng-model="ctrl.panel.nullPointMode" ng-options="f for f in ['connected', 'null', 'null as zero']" ng-change="ctrl.render()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"id": null,
|
||||
"title": "Home",
|
||||
"originalTitle": "Home",
|
||||
"tags": [],
|
||||
"style": "dark",
|
||||
"timezone": "browser",
|
||||
|
@ -1,7 +1,6 @@
|
||||
{
|
||||
"id": null,
|
||||
"title": "Templated Graphs Nested",
|
||||
"originalTitle": "Templated Graphs Nested",
|
||||
"tags": [
|
||||
"showcase",
|
||||
"templated"
|
||||
|
@ -233,13 +233,13 @@ $paginationActiveBackground: $blue;
|
||||
|
||||
// Form states and alerts
|
||||
// -------------------------
|
||||
$state-warning-text: darken(#c09853, 10%);
|
||||
$state-warning-text: $warn;
|
||||
$state-warning-bg: $brand-warning;
|
||||
|
||||
$errorText: #b94a48;
|
||||
$errorText: #E84D4D;
|
||||
$errorBackground: $btn-danger-bg;
|
||||
|
||||
$successText: #468847;
|
||||
$successText: #12D95A;
|
||||
$successBackground: $btn-success-bg;
|
||||
|
||||
$infoText: $blue-dark;
|
||||
|
@ -17,6 +17,16 @@
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.dropdown-desc {
|
||||
position: relative;
|
||||
top: -3px;
|
||||
width: 250px;
|
||||
font-size: 80%;
|
||||
margin-left: 22px;
|
||||
color: $gray-2;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
// Dropdown arrow/caret
|
||||
// --------------------
|
||||
.caret {
|
||||
|
@ -158,6 +158,10 @@ $gf-form-margin: 0.25rem;
|
||||
color: transparent;
|
||||
text-shadow: 0 0 0 $text-color;
|
||||
}
|
||||
|
||||
&.ng-empty {
|
||||
color: $text-color-weak;
|
||||
}
|
||||
}
|
||||
|
||||
&:after {
|
||||
|
@ -125,7 +125,6 @@
|
||||
}
|
||||
|
||||
.share-modal-body {
|
||||
text-align: center;
|
||||
padding: 10px 0;
|
||||
|
||||
.tight-form {
|
||||
@ -133,35 +132,40 @@
|
||||
}
|
||||
|
||||
.share-modal-options {
|
||||
margin: 11px 20px 33px 20px;
|
||||
margin: 11px 0px 33px 0px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.share-modal-big-icon {
|
||||
margin-bottom: 2rem;
|
||||
|
||||
margin-bottom: 10px;
|
||||
margin-right: 2rem;
|
||||
.fa, .icon-gf {
|
||||
font-size: 70px;
|
||||
font-size: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.share-snapshot-info-text {
|
||||
margin: 10px 105px;
|
||||
.share-modal-info-text {
|
||||
margin-top: 5px;
|
||||
strong {
|
||||
color: $headings-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.share-snapshot-header {
|
||||
margin: 20px 0 22px 0;
|
||||
.share-modal-header {
|
||||
display: flex;
|
||||
margin: 0px 0 22px 0;
|
||||
}
|
||||
|
||||
.share-modal-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.tight-form {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.share-snapshot-link {
|
||||
.share-modal-link {
|
||||
max-width: 716px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-right: 20px;
|
||||
margin-right: 3rem;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
input[type=text].ng-dirty.ng-invalid {
|
||||
}
|
||||
|
||||
input.validation-error,
|
||||
input.ng-dirty.ng-invalid {
|
||||
box-shadow: inset 0 0px 5px $red;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
11
public/vendor/angular-other/angular-strap.js
vendored
11
public/vendor/angular-other/angular-strap.js
vendored
@ -25,11 +25,16 @@ angular.module('$strap.directives').factory('$modal', [
|
||||
function ($rootScope, $compile, $http, $timeout, $q, $templateCache, $strapConfig) {
|
||||
var ModalFactory = function ModalFactory(config) {
|
||||
function Modal(config) {
|
||||
var options = angular.extend({ show: true }, $strapConfig.modal, config), scope = options.scope ? options.scope : $rootScope.$new(), templateUrl = options.template;
|
||||
return $q.when($templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
|
||||
var options = angular.extend({ show: true }, $strapConfig.modal, config);
|
||||
var scope = options.scope ? options.scope : $rootScope.$new()
|
||||
var templateUrl = options.template;
|
||||
return $q.when(options.templateHtml || $templateCache.get(templateUrl) || $http.get(templateUrl, { cache: true }).then(function (res) {
|
||||
return res.data;
|
||||
})).then(function onSuccess(template) {
|
||||
var id = templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-') + '-' + scope.$id;
|
||||
var id = scope.$id;
|
||||
if (templateUrl) {
|
||||
id += templateUrl.replace('.html', '').replace(/[\/|\.|:]/g, '-');
|
||||
}
|
||||
// grafana change, removed fade
|
||||
var $modal = $('<div class="modal hide" tabindex="-1"></div>').attr('id', id).html(template);
|
||||
if (options.modalClass)
|
||||
|
Loading…
Reference in New Issue
Block a user