diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c98ec2c091..167c6ad829a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,7 @@ * **Panels**: Delay loading & Lazy load panels as they become visible (scrolled into view) [#5216](https://github.com/grafana/grafana/issues/5216) thx [@jifwin](https://github.com/jifwin) * **Graph**: Support auto grid min/max when using log scale [#3090](https://github.com/grafana/grafana/issues/3090), thx [@bigbenhur](https://github.com/bigbenhur) * **Graph**: Support for histograms [#600](https://github.com/grafana/grafana/issues/600) -* **Elasticsearch**: Support histogram aggregations [#3164](https://github.com/grafana/grafana/issues/3164) - +* **Prometheus**: Support table response formats (column per label) [#6140](https://github.com/grafana/grafana/issues/6140), thx [@mtanda](https://github.com/mtanda) ## Minor Enchancements @@ -20,10 +19,12 @@ * **Oauth**: Add common type for oauth authorization errors [#6428](https://github.com/grafana/grafana/issues/6428) thx [@amenzhinsky](https://github.com/amenzhinsky) * **Templating**: Data source variable now supports multi value and panel repeats [#7030](https://github.com/grafana/grafana/issues/7030) thx [@mtanda](https://github.com/mtanda) * **Telegram**: Telegram alert is not sending metric and legend. [#8110](https://github.com/grafana/grafana/issues/8110), thx [@bashgeek](https://github.com/bashgeek) +* **Graph**: Support dashed lines [#514](https://github.com/grafana/grafana/issues/514), thx [@smalik03](https://github.com/smalik03) ## Fixes * **Table Panel**: Fixed annotation display in table panel, [#8023](https://github.com/grafana/grafana/issues/8023) * **Dashboard**: If refresh is blocked due to tab not visible, then refresh when it becomes visible [#8076](https://github.com/grafana/grafana/issues/8076) thanks [@SimenB](https://github.com/SimenB) +* **Snapshots**: Fixed problem with annotations & snapshots [#7659](https://github.com/grafana/grafana/issues/7659) # 4.2.0 (2017-03-22) ## Minor Enhancements diff --git a/conf/defaults.ini b/conf/defaults.ini index 9955c22ca17..8eca55fc67c 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -25,7 +25,7 @@ plugins = data/plugins #################################### Server ############################## [server] -# Protocol (http or https) +# Protocol (http, https, socket) protocol = http # The ip address to bind to, empty will bind to all interfaces @@ -57,6 +57,9 @@ enable_gzip = false cert_file = cert_key = +# Unix socket path +socket = /tmp/grafana.sock + #################################### Database ############################ [database] # You can configure the database connection by specifying type, host, name, user and password @@ -444,3 +447,4 @@ secret_key = url = username = password = +public_url = diff --git a/conf/sample.ini b/conf/sample.ini index 87505221790..84328e7d537 100644 --- a/conf/sample.ini +++ b/conf/sample.ini @@ -26,7 +26,7 @@ # #################################### Server #################################### [server] -# Protocol (http or https) +# Protocol (http, https, socket) ;protocol = http # The ip address to bind to, empty will bind to all interfaces @@ -59,6 +59,9 @@ ;cert_file = ;cert_key = +# Unix socket path +;socket = + #################################### Database #################################### [database] # You can configure the database connection by specifying type, host, name, user and password @@ -399,5 +402,6 @@ [external_image_storage.webdav] ;url = +;public_url = ;username = ;password = diff --git a/docs/Makefile b/docs/Makefile index e5c555f8019..c79d3087492 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,4 +1,4 @@ -.PHONY: all default docs docs-build docs-shell shell test +.PHONY: all default docs docs-build docs-shell shell checkvars # to allow `make DOCSPORT=9000 docs` DOCSPORT := 3004 @@ -11,23 +11,24 @@ DOCS_MOUNT := -v $(SOURCES_HOST_DIR):/site/content DOCKER_RUN_DOCS := docker run --rm -it $(DOCS_MOUNT) -e NOCACHE -p 3004:3004 -p 3005:3005 +VERSION := $(shell head -n 1 VERSION) default: docs +checkvars: +ifndef ENV + $(error ENV is undefined set via ENV=staging or ENV=prod as argument to make) +endif + docs: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004" -test: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "ls -la /site/content" - -docs-watch: docs-build +watch: docs-build $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "grunt --env=dev-docs && grunt connect --port=3004 & grunt watch --port=3004 --env=dev-docs" -publish: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh staging-docs root" - -publish-prod: docs-build - $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh prod-docs root" +publish: checkvars docs-build + $(info Publishing ENV=${ENV} and VERSION=${VERSION}) + $(DOCKER_RUN_DOCS) $(DOCS_MOUNT) -e DOCKERHOST "$(DOCKER_DOCS_IMAGE)" /bin/bash -c "./publish.sh ${ENV}-docs ${VERSION}" docs-build: docker build -t "$(DOCKER_DOCS_IMAGE)" --no-cache . diff --git a/docs/VERSION b/docs/VERSION index fd2a01863fd..2fa2c5705ad 100644 --- a/docs/VERSION +++ b/docs/VERSION @@ -1 +1 @@ -3.1.0 +v4.2 diff --git a/docs/sources/archive.md b/docs/sources/archive.md index af4ab5e7a8f..7102395f4c7 100644 --- a/docs/sources/archive.md +++ b/docs/sources/archive.md @@ -12,10 +12,7 @@ weight = 200 Here you can find links to older versions of the documentation that might be better suited for your version of Grafana. -- [Latest](/) -- [Version 3.1](/v3.1) -- [Version 3.0](/v3.0) -- [Version 2.6](/v2.6) -- [Version 2.5](/v2.5) -- [Version 2.1](/v2.1) -- [Version 2.0](/v2.0) +- [Latest](http://docs.grafana.org) +- [Version 4.2](http://docs.grafana.org/v4.2) +- [Version 3.1](http://docs.grafana.org/v3.1) +- [Version 3.0](http://docs.grafana.org/v3.0) diff --git a/docs/sources/features/panels/dashlist.md b/docs/sources/features/panels/dashlist.md index 5377c7c1941..b6f7d21c1c8 100644 --- a/docs/sources/features/panels/dashlist.md +++ b/docs/sources/features/panels/dashlist.md @@ -2,6 +2,7 @@ title = "Dashboard List" keywords = ["grafana", "dashboard list", "documentation", "panel", "dashlist"] type = "docs" +aliases = ["/reference/dashlist/"] [menu.docs] name = "Dashboard list" parent = "panels" diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md index 7fbebcfeb97..fdd3f271184 100644 --- a/docs/sources/features/panels/graph.md +++ b/docs/sources/features/panels/graph.md @@ -2,6 +2,7 @@ title = "Graph Panel" keywords = ["grafana", "graph panel", "documentation", "guide", "graph"] type = "docs" +aliases = ["/reference/graph/"] [menu.docs] name = "Graph" parent = "panels" @@ -18,7 +19,8 @@ Clicking the title for a panel exposes a menu. The `edit` option opens addition options for the panel. ## General -![](/img/docs/v2/graph_general.png) + +![](/img/docs/v43/graph_general.png) The general tab allows customization of a panel's appearance and menu options. @@ -31,14 +33,14 @@ The general tab allows customization of a panel's appearance and menu options. ### Drilldown / detail link The drilldown section allows adding dynamic links to the panel that can link to other dashboards -or URLs +or URLs. Each link has a title, a type and params. A link can be either a ``dashboard`` or ``absolute`` links. -If it is a dashboard links, the `dashboard` value must be the name of a dashboard. If it's an -`absolute` link, the URL is the URL to link. +If it is a dashboard link, the `dashboard` value must be the name of a dashboard. If it is an +`absolute` link, the URL is the URL to the link. ``params`` allows adding additional URL params to the links. The format is the ``name=value`` with -multiple params separate by ``&``. Template variables can be added as values using ``$myvar``. +multiple params separated by ``&``. Template variables can be added as values using ``$myvar``. When linking to another dashboard that uses template variables, you can use ``var-myvar=value`` to populate the template variable to a desired value from the link. @@ -50,7 +52,7 @@ options. ## Axes & Grid -![](/img/docs/v2/graph_axes_grid_options.png) +![](/img/docs/v43/graph_axes_grid_options.png) The Axes & Grid tab controls the display of axes, grids and legend. @@ -74,7 +76,6 @@ values can be hidden from the legend using the ``Hide empty`` checkbox. ### Legend Values Additional values can be shown along-side the legend names: - - ``Total`` - Sum of all values returned from metric query - ``Current`` - Last value returned from the metric query - ``Min`` - Minimum of all values returned from metric query @@ -83,16 +84,16 @@ Additional values can be shown along-side the legend names: - ``Decimals`` - Controls how many decimals are displayed for legend values (and graph hover tooltips) The legend values are calculated client side by Grafana and depend on what type of -aggregation or point consolidation you metric query is using. All the above legend values cannot +aggregation or point consolidation your metric query is using. All the above legend values cannot be correct at the same time. For example if you plot a rate like requests/second, this is probably using average as aggregator, then the Total in the legend will not represent the total number of requests. It is just the sum of all data points received by Grafana. ## Display styles -![](/img/docs/v2/graph_display_styles.png) +![](/img/docs/v43/graph_display_styles.png) -Display styles controls properties of the graph. +Display styles control visual properties of the graph. ### Thresholds @@ -108,19 +109,19 @@ the graph crosses a particular threshold. ### Line Options -- ``Line Fill`` - Amount of color fill for a series. 0 is none. +- ``Line Fill`` - Amount of color fill for a series. 0 is none. - ``Line Width`` - The width of the line for a series. - ``Null point mode`` - How null values are displayed - ``Staircase line`` - Draws adjacent points as staircase ### Multiple Series -If there are multiple series, they can be display as a group. +If there are multiple series, they can be displayed as a group. - ``Stack`` - Each series is stacked on top of another -- ``Percent`` - Each series is draw as a percent of the total of all series +- ``Percent`` - Each series is drawn as a percentage of the total of all series -If you have stack enabled you can select what the mouse hover feature should show. +If you have stack enabled, you can select what the mouse hover feature should show. - Cumulative - Sum of series below plus the series you hover over - Individual - Just the value for the series you hover over @@ -134,11 +135,15 @@ If you have stack enabled you can select what the mouse hover feature should sho - ``All series`` - Show all series on the same tooltip and a x crosshairs to help follow all series -### Series specific overrides +### Series Specific Overrides -The section allows a series to be render different from the rest. For example, one series can be given -a thicker line width to make it standout. +The section allows a series to be rendered differently from the others. For example, one series can be given +a thicker line width to make it stand out. -## Time range +#### Dashes Drawing Style + +There is an option under Series overrides to draw lines as dashes. Set Dashes to the value True to override the line draw setting for a specific series. + +## Time Range ![](/img/docs/v2/graph_time_range.png) diff --git a/docs/sources/features/panels/singlestat.md b/docs/sources/features/panels/singlestat.md index 9f2edb7df05..eb9b2f26ea5 100644 --- a/docs/sources/features/panels/singlestat.md +++ b/docs/sources/features/panels/singlestat.md @@ -2,6 +2,7 @@ title = "Singlestat Panel" keywords = ["grafana", "dashboard", "documentation", "panels", "singlestat"] type = "docs" +aliases = ["/reference/singlestat/"] [menu.docs] name = "Singlestat" parent = "panels" diff --git a/docs/sources/features/panels/table_panel.md b/docs/sources/features/panels/table_panel.md index 249886e1f57..9ddc2cddaee 100644 --- a/docs/sources/features/panels/table_panel.md +++ b/docs/sources/features/panels/table_panel.md @@ -2,6 +2,7 @@ title = "Table Panel" keywords = ["grafana", "dashboard", "documentation", "panels", "table panel"] type = "docs" +aliases = ["/reference/table/"] [menu.docs] name = "Table" parent = "panels" diff --git a/docs/sources/http_api/dashboard.md b/docs/sources/http_api/dashboard.md index 12d46dc6aa4..053d4a8d329 100644 --- a/docs/sources/http_api/dashboard.md +++ b/docs/sources/http_api/dashboard.md @@ -221,10 +221,6 @@ Get all tags of dashboards } ] -## Dashboard from JSON file - -`GET /file/:file` - ## Search Dashboards `GET /api/search/` diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 176f843e86b..9cfee7140c4 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -313,7 +313,6 @@ example: auth_url = https://github.com/login/oauth/authorize token_url = https://github.com/login/oauth/access_token api_url = https://api.github.com/user - allow_sign_up = false team_ids = allowed_organizations = @@ -644,6 +643,9 @@ Secret key. e.g. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA ### url Url to where Grafana will send PUT request with images +### public_url +Optional parameter. Url to send to users in notifications, directly appended with the resulting uploaded file name. + ### username basic auth username diff --git a/emails/templates/layouts/default.html b/emails/templates/layouts/default.html index f6d729054cd..07eb32874c7 100644 --- a/emails/templates/layouts/default.html +++ b/emails/templates/layouts/default.html @@ -94,8 +94,7 @@ td[class="stack-column-center"] {
-
- +
diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 5652196b58c..58ea56cc8a7 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -255,6 +255,9 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R } if err := bus.Dispatch(cmd); err != nil { + if err == models.ErrSmtpNotEnabled { + return ApiError(412, err.Error(), err) + } return ApiError(500, "Failed to send alert notifications", err) } diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index ddf8681c3c8..1d99dac3d40 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -20,7 +20,7 @@ func GetDataSources(c *middleware.Context) Response { result := make(dtos.DataSourceList, 0) for _, ds := range query.Result { - dsItem := dtos.DataSource{ + dsItem := dtos.DataSourceListItemDTO{ Id: ds.Id, OrgId: ds.OrgId, Name: ds.Name, diff --git a/pkg/api/dtos/models.go b/pkg/api/dtos/models.go index 564d86a25bf..2d8bdcbae03 100644 --- a/pkg/api/dtos/models.go +++ b/pkg/api/dtos/models.go @@ -84,7 +84,23 @@ type DataSource struct { SecureJsonFields map[string]bool `json:"secureJsonFields"` } -type DataSourceList []DataSource +type DataSourceListItemDTO struct { + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + Name string `json:"name"` + Type string `json:"type"` + TypeLogoUrl string `json:"typeLogoUrl"` + Access m.DsAccess `json:"access"` + Url string `json:"url"` + Password string `json:"password"` + User string `json:"user"` + Database string `json:"database"` + BasicAuth bool `json:"basicAuth"` + IsDefault bool `json:"isDefault"` + JsonData *simplejson.Json `json:"jsonData,omitempty"` +} + +type DataSourceList []DataSourceListItemDTO func (slice DataSourceList) Len() int { return len(slice) diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index fe7ca21f402..06ef5a22125 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -5,6 +5,7 @@ import ( "crypto/tls" "errors" "fmt" + "net" "net/http" "os" "path" @@ -13,9 +14,12 @@ import ( "github.com/grafana/grafana/pkg/api/live" httpstatic "github.com/grafana/grafana/pkg/api/static" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/cmd/grafana-cli/logger" + "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/middleware" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/setting" ) @@ -46,7 +50,7 @@ func (hs *HttpServer) Start(ctx context.Context) error { hs.streamManager.Run(ctx) listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) - hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl) + hs.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl, "socket", setting.SocketPath) hs.httpSrv = &http.Server{Addr: listenAddr, Handler: hs.macaron} switch setting.Protocol { @@ -62,6 +66,18 @@ func (hs *HttpServer) Start(ctx context.Context) error { hs.log.Debug("server was shutdown gracefully") return nil } + case setting.SOCKET: + ln, err := net.Listen("unix", setting.SocketPath) + if err != nil { + hs.log.Debug("server was shutdown gracefully") + return nil + } + + err = hs.httpSrv.Serve(ln) + if err != nil { + hs.log.Debug("server was shutdown gracefully") + return nil + } default: hs.log.Error("Invalid protocol", "protocol", setting.Protocol) err = errors.New("Invalid Protocol") @@ -147,6 +163,7 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { Delims: macaron.Delims{Left: "[[", Right: "]]"}, })) + m.Use(hs.healthHandler) m.Use(middleware.GetContextHandler()) m.Use(middleware.Sessioner(&setting.SessionOptions)) m.Use(middleware.RequestMetrics()) @@ -160,6 +177,27 @@ func (hs *HttpServer) newMacaron() *macaron.Macaron { return m } +func (hs *HttpServer) healthHandler(ctx *macaron.Context) { + if ctx.Req.Method != "GET" || ctx.Req.URL.Path != "/api/health" { + return + } + + data := simplejson.New() + data.Set("database", "ok") + data.Set("version", setting.BuildVersion) + data.Set("commit", setting.BuildCommit) + + if err := bus.Dispatch(&models.GetDBHealthQuery{}); err != nil { + data.Set("database", "failing") + } + + ctx.Resp.Header().Set("Content-Type", "application/json; charset=UTF-8") + ctx.Resp.WriteHeader(200) + + dataBytes, _ := data.EncodePretty() + ctx.Resp.Write(dataBytes) +} + func (hs *HttpServer) mapStatic(m *macaron.Macaron, rootDir string, dir string, prefix string) { headers := func(c *macaron.Context) { c.Resp.Header().Set("Cache-Control", "public, max-age=3600") diff --git a/pkg/api/login.go b/pkg/api/login.go index d2bbf4e81b9..baec5f5f6c0 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -11,7 +11,6 @@ import ( "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" ) const ( @@ -79,8 +78,7 @@ func tryLoginUsingRememberCookie(c *middleware.Context) bool { user := userQuery.Result // validate remember me cookie - if val, _ := c.GetSuperSecureCookie( - util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName); val != user.Login { + if val, _ := c.GetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName); val != user.Login { return false } @@ -142,7 +140,7 @@ func loginUserWithUser(user *m.User, c *middleware.Context) { days := 86400 * setting.LogInRememberDays if days > 0 { c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/") - c.SetSuperSecureCookie(util.EncodeMd5(user.Rands+user.Password), setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") + c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") } c.Session.Set(middleware.SESS_KEY_USERID, user.Id) diff --git a/pkg/api/pluginproxy/pluginproxy_test.go b/pkg/api/pluginproxy/pluginproxy_test.go index f14ca6975e6..424c3fd670c 100644 --- a/pkg/api/pluginproxy/pluginproxy_test.go +++ b/pkg/api/pluginproxy/pluginproxy_test.go @@ -23,9 +23,14 @@ func TestPluginProxy(t *testing.T) { setting.SecretKey = "password" bus.AddHandler("test", func(query *m.GetPluginSettingByIdQuery) error { + key, err := util.Encrypt([]byte("123"), "password") + if err != nil { + return err + } + query.Result = &m.PluginSetting{ SecureJsonData: map[string][]byte{ - "key": util.Encrypt([]byte("123"), "password"), + "key": key, }, } return nil diff --git a/pkg/components/imguploader/imguploader.go b/pkg/components/imguploader/imguploader.go index ae5c89a0049..883ef8eefda 100644 --- a/pkg/components/imguploader/imguploader.go +++ b/pkg/components/imguploader/imguploader.go @@ -47,10 +47,11 @@ func NewImageUploader() (ImageUploader, error) { return nil, fmt.Errorf("Could not find url key for image.uploader.webdav") } + public_url := webdavSec.Key("public_url").String() username := webdavSec.Key("username").String() password := webdavSec.Key("password").String() - return NewWebdavImageUploader(url, username, password) + return NewWebdavImageUploader(url, username, password, public_url) } return NopImageUploader{}, nil diff --git a/pkg/components/imguploader/s3uploader_test.go b/pkg/components/imguploader/s3uploader_test.go index 1204a2db4e2..f75ad05af64 100644 --- a/pkg/components/imguploader/s3uploader_test.go +++ b/pkg/components/imguploader/s3uploader_test.go @@ -8,7 +8,7 @@ import ( ) func TestUploadToS3(t *testing.T) { - SkipConvey("[Integration test] for external_image_store.webdav", t, func() { + SkipConvey("[Integration test] for external_image_store.s3", t, func() { setting.NewConfigContext(&setting.CommandLineArgs{ HomePath: "../../../", }) diff --git a/pkg/components/imguploader/webdavuploader.go b/pkg/components/imguploader/webdavuploader.go index 93b3f9d2efa..ae3c5b5e194 100644 --- a/pkg/components/imguploader/webdavuploader.go +++ b/pkg/components/imguploader/webdavuploader.go @@ -14,9 +14,10 @@ import ( ) type WebdavUploader struct { - url string - username string - password string + url string + username string + password string + public_url string } var netTransport = &http.Transport{ @@ -33,7 +34,8 @@ var netClient = &http.Client{ func (u *WebdavUploader) Upload(pa string) (string, error) { url, _ := url.Parse(u.url) - url.Path = path.Join(url.Path, util.GetRandomString(20)+".png") + filename := util.GetRandomString(20) + ".png" + url.Path = path.Join(url.Path, filename) imgData, err := ioutil.ReadFile(pa) req, err := http.NewRequest("PUT", url.String(), bytes.NewReader(imgData)) @@ -53,13 +55,20 @@ func (u *WebdavUploader) Upload(pa string) (string, error) { return "", fmt.Errorf("Failed to upload image. Returned statuscode %v body %s", res.StatusCode, body) } + if u.public_url != "" { + publicURL, _ := url.Parse(u.public_url) + publicURL.Path = path.Join(publicURL.Path, filename) + return publicURL.String(), nil + } + return url.String(), nil } -func NewWebdavImageUploader(url, username, passwrod string) (*WebdavUploader, error) { +func NewWebdavImageUploader(url, username, password, public_url string) (*WebdavUploader, error) { return &WebdavUploader{ - url: url, - username: username, - password: passwrod, + url: url, + username: username, + password: password, + public_url: public_url, }, nil } diff --git a/pkg/components/imguploader/webdavuploader_test.go b/pkg/components/imguploader/webdavuploader_test.go index 273cd1c2a86..e88e28bd712 100644 --- a/pkg/components/imguploader/webdavuploader_test.go +++ b/pkg/components/imguploader/webdavuploader_test.go @@ -7,12 +7,21 @@ import ( ) func TestUploadToWebdav(t *testing.T) { - webdavUploader, _ := NewWebdavImageUploader("http://localhost:9998/dav/", "username", "password") + // Can be tested with this docker container: https://hub.docker.com/r/morrisjobke/webdav/ SkipConvey("[Integration test] for external_image_store.webdav", t, func() { + webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "") path, err := webdavUploader.Upload("../../../public/img/logo_transparent_400x.png") So(err, ShouldBeNil) - So(path, ShouldNotEqual, "") + So(path, ShouldStartWith, "http://localhost:8888/webdav/") + }) + + SkipConvey("[Integration test] for external_image_store.webdav with public url", t, func() { + webdavUploader, _ := NewWebdavImageUploader("http://localhost:8888/webdav/", "test", "test", "http://publicurl:8888/webdav") + path, err := webdavUploader.Upload("../../../public/img/logo_transparent_400x.png") + + So(err, ShouldBeNil) + So(path, ShouldStartWith, "http://publicurl:8888/webdav/") }) } diff --git a/pkg/components/securejsondata/securejsondata.go b/pkg/components/securejsondata/securejsondata.go index d7bec6894b0..caf4b369483 100644 --- a/pkg/components/securejsondata/securejsondata.go +++ b/pkg/components/securejsondata/securejsondata.go @@ -1,6 +1,7 @@ package securejsondata import ( + "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" ) @@ -10,7 +11,12 @@ type SecureJsonData map[string][]byte func (s SecureJsonData) Decrypt() map[string]string { decrypted := make(map[string]string) for key, data := range s { - decrypted[key] = string(util.Decrypt(data, setting.SecretKey)) + decryptedData, err := util.Decrypt(data, setting.SecretKey) + if err != nil { + log.Fatal(4, err.Error()) + } + + decrypted[key] = string(decryptedData) } return decrypted } @@ -18,7 +24,12 @@ func (s SecureJsonData) Decrypt() map[string]string { func GetEncryptedJsonData(sjd map[string]string) SecureJsonData { encrypted := make(SecureJsonData) for key, data := range sjd { - encrypted[key] = util.Encrypt([]byte(data), setting.SecretKey) + encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) + if err != nil { + log.Fatal(4, err.Error()) + } + + encrypted[key] = encryptedData } return encrypted } diff --git a/pkg/log/log.go b/pkg/log/log.go index f69d0f6b9b4..56b2eb50c9f 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -15,6 +15,8 @@ import ( "github.com/go-stack/stack" "github.com/inconshreveable/log15" "github.com/inconshreveable/log15/term" + + "github.com/grafana/grafana/pkg/util" ) var Root log15.Logger @@ -172,7 +174,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { Close() defaultLevelName, _ := getLogLevelFromConfig("log", "info", cfg) - defaultFilters := getFilters(cfg.Section("log").Key("filters").Strings(" ")) + defaultFilters := getFilters(util.SplitString(cfg.Section("log").Key("filters").String())) handlers := make([]log15.Handler, 0) @@ -185,7 +187,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { // Log level. _, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg) - modeFilters := getFilters(sec.Key("filters").Strings(" ")) + modeFilters := getFilters(util.SplitString(sec.Key("filters").String())) format := getLogFormat(sec.Key("format").MustString("")) var handler log15.Handler diff --git a/pkg/models/datasource_cache_test.go b/pkg/models/datasource_cache_test.go index 25fee55529d..5e821ea28c4 100644 --- a/pkg/models/datasource_cache_test.go +++ b/pkg/models/datasource_cache_test.go @@ -54,10 +54,15 @@ func TestDataSourceCache(t *testing.T) { }) ds.JsonData = json + + tlsCaCert, _ := util.Encrypt([]byte(caCert), "password") + tlsClientCert, _ := util.Encrypt([]byte(clientCert), "password") + tlsClientKey, _ := util.Encrypt([]byte(clientKey), "password") + ds.SecureJsonData = map[string][]byte{ - "tlsCACert": util.Encrypt([]byte(caCert), "password"), - "tlsClientCert": util.Encrypt([]byte(clientCert), "password"), - "tlsClientKey": util.Encrypt([]byte(clientKey), "password"), + "tlsCACert": tlsCaCert, + "tlsClientCert": tlsClientCert, + "tlsClientKey": tlsClientKey, } ds.Updated = t.Add(-1 * time.Minute) diff --git a/pkg/models/health.go b/pkg/models/health.go new file mode 100644 index 00000000000..ee206ed2d6f --- /dev/null +++ b/pkg/models/health.go @@ -0,0 +1,3 @@ +package models + +type GetDBHealthQuery struct{} diff --git a/pkg/models/notifications.go b/pkg/models/notifications.go index ad7aed3bc50..089d7c4360d 100644 --- a/pkg/models/notifications.go +++ b/pkg/models/notifications.go @@ -3,6 +3,7 @@ package models import "errors" var ErrInvalidEmailCode = errors.New("Invalid or expired email code") +var ErrSmtpNotEnabled = errors.New("SMTP not configured, check your grafana.ini config file's [smtp] section.") type SendEmailCommand struct { To []string diff --git a/pkg/services/notifications/mailer.go b/pkg/services/notifications/mailer.go index ae1348cbf99..df9f1138b15 100644 --- a/pkg/services/notifications/mailer.go +++ b/pkg/services/notifications/mailer.go @@ -107,7 +107,7 @@ func createDialer() (*gomail.Dialer, error) { func buildEmailMessage(cmd *m.SendEmailCommand) (*Message, error) { if !setting.Smtp.Enabled { - return nil, errors.New("Grafana mailing/smtp options not configured, contact your Grafana admin") + return nil, m.ErrSmtpNotEnabled } var buffer bytes.Buffer diff --git a/pkg/services/sqlstore/health.go b/pkg/services/sqlstore/health.go new file mode 100644 index 00000000000..ca5a8d2d085 --- /dev/null +++ b/pkg/services/sqlstore/health.go @@ -0,0 +1,14 @@ +package sqlstore + +import ( + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetDBHealthQuery) +} + +func GetDBHealthQuery(query *m.GetDBHealthQuery) error { + return x.Ping() +} diff --git a/pkg/services/sqlstore/plugin_setting.go b/pkg/services/sqlstore/plugin_setting.go index 8121b8c7b4b..f52d65cdc94 100644 --- a/pkg/services/sqlstore/plugin_setting.go +++ b/pkg/services/sqlstore/plugin_setting.go @@ -74,7 +74,12 @@ func UpdatePluginSetting(cmd *m.UpdatePluginSettingCmd) error { return err } else { for key, data := range cmd.SecureJsonData { - pluginSetting.SecureJsonData[key] = util.Encrypt([]byte(data), setting.SecretKey) + encryptedData, err := util.Encrypt([]byte(data), setting.SecretKey) + if err != nil { + return err + } + + pluginSetting.SecureJsonData[key] = encryptedData } // add state change event on commit success diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index fe759ec8df9..848dd884323 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -158,6 +158,7 @@ func getEngine() (*xorm.Engine, error) { } else { engine.SetMaxOpenConns(DbCfg.MaxOpenConn) engine.SetMaxIdleConns(DbCfg.MaxIdleConn) + engine.SetLogger(&xorm.DiscardLogger{}) // engine.SetLogger(NewXormLogger(log.LvlInfo, log.New("sqlstore.xorm"))) // engine.ShowSQL = true // engine.ShowInfo = true diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index cacb4547162..8cc0e723712 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -27,6 +27,7 @@ type Scheme string const ( HTTP Scheme = "http" HTTPS Scheme = "https" + SOCKET Scheme = "socket" DEFAULT_HTTP_ADDR string = "0.0.0.0" ) @@ -65,6 +66,7 @@ var ( HttpAddr, HttpPort string SshPort int CertFile, KeyFile string + SocketPath string RouterLogging bool DataProxyLogging bool StaticRootPath string @@ -473,6 +475,10 @@ func NewConfigContext(args *CommandLineArgs) error { CertFile = server.Key("cert_file").String() KeyFile = server.Key("cert_key").String() } + if server.Key("protocol").MustString("http") == "socket" { + Protocol = SOCKET + SocketPath = server.Key("socket").String() + } Domain = server.Key("domain").MustString("localhost") HttpAddr = server.Key("http_addr").MustString(DEFAULT_HTTP_ADDR) @@ -509,7 +515,7 @@ func NewConfigContext(args *CommandLineArgs) error { // read data source proxy white list DataProxyWhiteList = make(map[string]bool) - for _, hostAndIp := range security.Key("data_source_proxy_whitelist").Strings(" ") { + for _, hostAndIp := range util.SplitString(security.Key("data_source_proxy_whitelist").String()) { DataProxyWhiteList[hostAndIp] = true } diff --git a/pkg/social/social.go b/pkg/social/social.go index 29c4b7ecbb4..8e1e30fdf4f 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -8,6 +8,7 @@ import ( "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" ) type BasicUserInfo struct { @@ -53,12 +54,12 @@ func NewOAuthService() { info := &setting.OAuthInfo{ ClientId: sec.Key("client_id").String(), ClientSecret: sec.Key("client_secret").String(), - Scopes: sec.Key("scopes").Strings(" "), + Scopes: util.SplitString(sec.Key("scopes").String()), AuthUrl: sec.Key("auth_url").String(), TokenUrl: sec.Key("token_url").String(), ApiUrl: sec.Key("api_url").String(), Enabled: sec.Key("enabled").MustBool(), - AllowedDomains: sec.Key("allowed_domains").Strings(" "), + AllowedDomains: util.SplitString(sec.Key("allowed_domains").String()), HostedDomain: sec.Key("hosted_domain").String(), AllowSignup: sec.Key("allow_sign_up").MustBool(), Name: sec.Key("name").MustString(name), @@ -92,7 +93,7 @@ func NewOAuthService() { apiUrl: info.ApiUrl, allowSignup: info.AllowSignup, teamIds: sec.Key("team_ids").Ints(","), - allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), + allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), } } @@ -115,7 +116,7 @@ func NewOAuthService() { apiUrl: info.ApiUrl, allowSignup: info.AllowSignup, teamIds: sec.Key("team_ids").Ints(","), - allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), + allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), } } @@ -135,7 +136,7 @@ func NewOAuthService() { Config: &config, url: setting.GrafanaNetUrl, allowSignup: info.AllowSignup, - allowedOrganizations: sec.Key("allowed_organizations").Strings(" "), + allowedOrganizations: util.SplitString(sec.Key("allowed_organizations").String()), } } } diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index 19f374b938b..f271c5d245f 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -34,18 +34,13 @@ func (query *Query) Build(queryContext *tsdb.QueryContext) (string, error) { return "", err } - res = replaceVariable(res, "$timeFilter", query.renderTimeFilter(queryContext)) - res = replaceVariable(res, "$interval", interval.Text) - res = replaceVariable(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10)) - res = replaceVariable(res, "$__interval", interval.Text) + res = strings.Replace(res, "$timeFilter", query.renderTimeFilter(queryContext), -1) + res = strings.Replace(res, "$interval", interval.Text, -1) + res = strings.Replace(res, "$__interval_ms", strconv.FormatInt(interval.Value.Nanoseconds()/int64(time.Millisecond), 10), -1) + res = strings.Replace(res, "$__interval", interval.Text, -1) return res, nil } -func replaceVariable(str string, variable string, value string) string { - count := strings.Count(str, variable) - return strings.Replace(str, variable, value, count) -} - func getDefinedInterval(query *Query, queryContext *tsdb.QueryContext) (*tsdb.Interval, error) { defaultInterval := tsdb.CalculateInterval(queryContext.TimeRange) diff --git a/pkg/util/encryption.go b/pkg/util/encryption.go index 42586ddac8b..2f8b5d7af56 100644 --- a/pkg/util/encryption.go +++ b/pkg/util/encryption.go @@ -5,26 +5,25 @@ import ( "crypto/cipher" "crypto/rand" "crypto/sha256" + "errors" "io" - - "github.com/grafana/grafana/pkg/log" ) const saltLength = 8 -func Decrypt(payload []byte, secret string) []byte { +func Decrypt(payload []byte, secret string) ([]byte, error) { salt := payload[:saltLength] key := encryptionKeyToBytes(secret, string(salt)) block, err := aes.NewCipher(key) if err != nil { - log.Fatal(4, err.Error()) + return nil, err } // The IV needs to be unique, but not secure. Therefore it's common to // include it at the beginning of the ciphertext. if len(payload) < aes.BlockSize { - log.Fatal(4, "payload too short") + return nil, errors.New("payload too short") } iv := payload[saltLength : saltLength+aes.BlockSize] payload = payload[saltLength+aes.BlockSize:] @@ -33,16 +32,16 @@ func Decrypt(payload []byte, secret string) []byte { // XORKeyStream can work in-place if the two arguments are the same. stream.XORKeyStream(payload, payload) - return payload + return payload, nil } -func Encrypt(payload []byte, secret string) []byte { +func Encrypt(payload []byte, secret string) ([]byte, error) { salt := GetRandomString(saltLength) key := encryptionKeyToBytes(secret, salt) block, err := aes.NewCipher(key) if err != nil { - log.Fatal(4, err.Error()) + return nil, err } // The IV needs to be unique, but not secure. Therefore it's common to @@ -51,13 +50,13 @@ func Encrypt(payload []byte, secret string) []byte { copy(ciphertext[:saltLength], []byte(salt)) iv := ciphertext[saltLength : saltLength+aes.BlockSize] if _, err := io.ReadFull(rand.Reader, iv); err != nil { - log.Fatal(4, err.Error()) + return nil, err } stream := cipher.NewCFBEncrypter(block, iv) stream.XORKeyStream(ciphertext[saltLength+aes.BlockSize:], payload) - return ciphertext + return ciphertext, nil } // Key needs to be 32bytes diff --git a/pkg/util/encryption_test.go b/pkg/util/encryption_test.go index 5f1dc18fea3..908931c9516 100644 --- a/pkg/util/encryption_test.go +++ b/pkg/util/encryption_test.go @@ -18,9 +18,11 @@ func TestEncryption(t *testing.T) { }) Convey("When decrypting basic payload", t, func() { - encrypted := Encrypt([]byte("grafana"), "1234") - decrypted := Decrypt(encrypted, "1234") + encrypted, encryptErr := Encrypt([]byte("grafana"), "1234") + decrypted, decryptErr := Decrypt(encrypted, "1234") + So(encryptErr, ShouldBeNil) + So(decryptErr, ShouldBeNil) So(string(decrypted), ShouldEqual, "grafana") }) diff --git a/pkg/util/strings.go b/pkg/util/strings.go index 8598949c2cb..3ccf8b8ce7e 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -1,5 +1,9 @@ package util +import ( + "regexp" +) + func StringsFallback2(val1 string, val2 string) string { return stringsFallback(val1, val2) } @@ -16,3 +20,11 @@ func stringsFallback(vals ...string) string { } return "" } + +func SplitString(str string) []string { + if len(str) == 0 { + return []string{} + } + + return regexp.MustCompile("[, ]+").Split(str, -1) +} diff --git a/pkg/util/strings_test.go b/pkg/util/strings_test.go index c959dfd1d54..ac8653158cc 100644 --- a/pkg/util/strings_test.go +++ b/pkg/util/strings_test.go @@ -13,3 +13,14 @@ func TestStringsUtil(t *testing.T) { So(StringsFallback3("", "", "3"), ShouldEqual, "3") }) } + +func TestSplitString(t *testing.T) { + Convey("Splits strings correctly", t, func() { + So(SplitString(""), ShouldResemble, []string{}) + So(SplitString("test"), ShouldResemble, []string{"test"}) + So(SplitString("test1 test2 test3"), ShouldResemble, []string{"test1", "test2", "test3"}) + So(SplitString("test1,test2,test3"), ShouldResemble, []string{"test1", "test2", "test3"}) + So(SplitString("test1, test2, test3"), ShouldResemble, []string{"test1", "test2", "test3"}) + So(SplitString("test1 , test2 test3"), ShouldResemble, []string{"test1", "test2", "test3"}) + }) +} diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index daf03f8f827..782543787d7 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -35,6 +35,7 @@ export default class TimeSeries { isOutsideRange: boolean; lines: any; + dashes: any; bars: any; points: any; yaxis: any; @@ -61,6 +62,9 @@ export default class TimeSeries { applySeriesOverrides(overrides) { this.lines = {}; + this.dashes = { + dashLength: [] + }; this.points = {}; this.bars = {}; this.yaxis = 1; @@ -74,11 +78,18 @@ export default class TimeSeries { continue; } if (override.lines !== void 0) { this.lines.show = override.lines; } + if (override.dashes !== void 0) { + this.dashes.show = override.dashes; + this.lines.lineWidth = 0; + } if (override.points !== void 0) { this.points.show = override.points; } if (override.bars !== void 0) { this.bars.show = override.bars; } if (override.fill !== void 0) { this.lines.fill = translateFillOption(override.fill); } if (override.stack !== void 0) { this.stack = override.stack; } - if (override.linewidth !== void 0) { this.lines.lineWidth = override.linewidth; } + if (override.linewidth !== void 0) { + this.lines.lineWidth = override.linewidth; + this.dashes.lineWidth = override.linewidth; + } if (override.nullPointMode !== void 0) { this.nullPointMode = override.nullPointMode; } if (override.pointradius !== void 0) { this.points.radius = override.pointradius; } if (override.steppedLine !== void 0) { this.lines.steps = override.steppedLine; } diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index c2422a63c27..e5660e294d9 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -144,6 +144,13 @@ export class AnnotationsSrv { } translateQueryResult(annotation, results) { + // if annotation has snapshotData + // make clone and remove it + if (annotation.snapshotData) { + annotation = angular.copy(annotation); + delete annotation.snapshotData; + } + for (var item of results) { item.source = annotation; item.min = item.time; diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index 85c41354c0d..6b8a58f0b57 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -58,7 +58,7 @@ export class EventManager { } addFlotEvents(annotations, flotOptions) { - if (!this.event || annotations.length === 0) { + if (!this.event && annotations.length === 0) { return; } diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 5acc2bc60b4..8b71e23f467 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -65,7 +65,7 @@
Options
- Name + Name
@@ -87,11 +87,11 @@ label="Hide toggle" tooltip="Hides the annotation query toggle from showing at the top of the dashboard" checked="ctrl.currentAnnotation.hide" - label-class="width-7"> + label-class="width-9">
- +
diff --git a/public/app/features/dashboard/shareSnapshotCtrl.js b/public/app/features/dashboard/shareSnapshotCtrl.js index 361db2d55b4..846dc0f7ed7 100644 --- a/public/app/features/dashboard/shareSnapshotCtrl.js +++ b/public/app/features/dashboard/shareSnapshotCtrl.js @@ -116,6 +116,7 @@ function (angular, _) { return { name: annotation.name, enable: annotation.enable, + iconColor: annotation.iconColor, snapshotData: annotation.snapshotData }; }).value(); diff --git a/public/app/plugins/datasource/influxdb/influx_query.ts b/public/app/plugins/datasource/influxdb/influx_query.ts index c7fc795af53..e2bc0f33d14 100644 --- a/public/app/plugins/datasource/influxdb/influx_query.ts +++ b/public/app/plugins/datasource/influxdb/influx_query.ts @@ -194,7 +194,7 @@ export default class InfluxQuery { } var escapedValues = _.map(value, kbn.regexEscape); - return escapedValues.join('|'); + return '(' + escapedValues.join('|') + ')'; } render(interpolate?) { diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 3cd065719c7..9f1bd948852 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -7,6 +7,7 @@ import moment from 'moment'; import kbn from 'app/core/utils/kbn'; import * as dateMath from 'app/core/utils/datemath'; import PrometheusMetricFindQuery from './metric_find_query'; +import TableModel from 'app/core/table_model'; var durationSplitRegexp = /(\d+)(ms|s|m|h|d|w|M|y)/; @@ -20,7 +21,6 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS this.directUrl = instanceSettings.directUrl; this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; - this.lastErrors = {}; this._request = function(method, url, requestId) { var options: any = { @@ -74,7 +74,7 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS options = _.clone(options); - _.each(options.targets, target => { + for (let target of options.targets) { if (!target.expr || target.hide) { return; } @@ -91,31 +91,33 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS var range = Math.ceil(end - start); target.step = query.step = this.adjustStep(query.step, this.intervalSeconds(options.interval), range); queries.push(query); - }); + } // No valid targets, return the empty result to save a round trip. if (_.isEmpty(queries)) { - var d = $q.defer(); - d.resolve({ data: [] }); - return d.promise; + return $q.when({ data: [] }); } var allQueryPromise = _.map(queries, query => { return this.performTimeSeriesQuery(query, start, end); }); - return $q.all(allQueryPromise).then(function(allResponse) { + return $q.all(allQueryPromise).then(responseList => { var result = []; + var index = 0; - _.each(allResponse, function(response, index) { + _.each(responseList, (response, index) => { if (response.status === 'error') { - self.lastErrors.query = response.error; throw response.error; } - delete self.lastErrors.query; - _.each(response.data.data.result, function(metricData) { - result.push(self.transformMetricData(metricData, activeTargets[index], start, end)); - }); + + if (activeTargets[index].format === "table") { + result.push(self.transformMetricDataToTable(response.data.data.result)); + } else { + for (let metricData of response.data.data.result) { + result.push(self.transformMetricData(metricData, activeTargets[index], start, end)); + } + } }); return { data: result }; @@ -273,6 +275,44 @@ export function PrometheusDatasource(instanceSettings, $q, backendSrv, templateS return { target: metricLabel, datapoints: dps }; }; + this.transformMetricDataToTable = function(series) { + var table = new TableModel(); + var self = this; + var i, j; + + if (series.length === 0) { + return table; + } + + _.each(series, function(series, seriesIndex) { + if (seriesIndex === 0) { + table.columns.push({text: 'Time', type: 'time'}); + _.each(_.keys(series.metric), function(key) { + table.columns.push({text: key}); + }); + table.columns.push({text: 'Value'}); + } + + if (series.values) { + for (i = 0; i < series.values.length; i++) { + var values = series.values[i]; + var reordered = [values[0] * 1000]; + if (series.metric) { + for (var key in series.metric) { + if (series.metric.hasOwnProperty(key)) { + reordered.push(series.metric[key]); + } + } + } + reordered.push(parseFloat(values[1])); + table.rows.push(reordered); + } + } + }); + + return table; + }; + this.createMetricLabel = function(labelData, options) { if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { return this.getOriginalMetricName(labelData); diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html index 98bb359a093..f919800b813 100644 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ b/public/app/plugins/datasource/prometheus/partials/query.editor.html @@ -42,6 +42,10 @@
+ +
+ +
-
+
diff --git a/public/app/plugins/panel/heatmap/axes_editor.ts b/public/app/plugins/panel/heatmap/axes_editor.ts index cd0f465636a..b8fcba871a7 100644 --- a/public/app/plugins/panel/heatmap/axes_editor.ts +++ b/public/app/plugins/panel/heatmap/axes_editor.ts @@ -26,8 +26,8 @@ export class AxesEditorCtrl { }; this.dataFormats = { - 'TS': 'timeseries', - 'TS Pre-bucketed': 'tsbuckets' + 'Time series': 'timeseries', + 'Time series Pre-bucketed': 'tsbuckets' }; } diff --git a/public/app/system.conf.js b/public/app/system.conf.js index f7ed0c5b2d2..ae8e93a726a 100644 --- a/public/app/system.conf.js +++ b/public/app/system.conf.js @@ -31,7 +31,8 @@ System.config({ "jquery.flot.crosshair": "vendor/flot/jquery.flot.crosshair", "jquery.flot.fillbelow": "vendor/flot/jquery.flot.fillbelow", "jquery.flot.gauge": "vendor/flot/jquery.flot.gauge", - "d3": "vendor/d3/d3.js" + "d3": "vendor/d3/d3.js", + "jquery.flot.dashes": "vendor/flot/jquery.flot.dashes" }, packages: { diff --git a/public/dashboards/default.json b/public/dashboards/default.json index 931d4c6f802..b16b5d710d6 100644 --- a/public/dashboards/default.json +++ b/public/dashboards/default.json @@ -82,6 +82,9 @@ "lines": true, "fill": 1, "linewidth": 2, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, "points": false, "pointradius": 5, "bars": false, diff --git a/public/dashboards/template_vars.json b/public/dashboards/template_vars.json index a3c25d00371..f9ce493300c 100644 --- a/public/dashboards/template_vars.json +++ b/public/dashboards/template_vars.json @@ -50,6 +50,9 @@ "lines": true, "fill": 1, "linewidth": 1, + "dashes": false, + "dashLength": 10, + "spaceLength": 10, "points": false, "pointradius": 5, "bars": false, diff --git a/public/emails/alert_notification.html b/public/emails/alert_notification.html index 15668c1924c..ce8354cdff5 100644 --- a/public/emails/alert_notification.html +++ b/public/emails/alert_notification.html @@ -4,9 +4,7 @@ - - + + -
@@ -107,11 +106,11 @@ td[class="stack-column-center"] { - - - + + +
- -
+ +
- -
-
+ + +
+
+ + +
+
- - -
-
+ + +
- - - @@ -200,21 +198,21 @@ text-decoration: underline;
- - - - - - +
- -
+ + + +
+ +
- - - +
+ + - - +
+
{{Subject .Subject "{{.Title}}"}} - - -
- - -
-

{{.Title}}

+ + +
+ + +
+

{{.Title}}

@@ -222,13 +220,13 @@ text-decoration: underline;
- - -
- - -
-

{{.Message}}

+ + +
+ + +
+

{{.Message}}

@@ -237,26 +235,26 @@ text-decoration: underline;
{{if ne .State "ok" }} - - -
-
- - -
-
Metric name
+ + +
+
+ + + - {{range .EvalMatches}} - - + - {{end}} @@ -267,17 +265,17 @@ text-decoration: underline;
+
Metric name
-
Value
+
+
Value
-
{{.Metric}}
+
+
{{.Metric}}
-
{{.Value}}
+
+
{{.Value}}
{{end}} - - -
- - -
+ + +
+ + + @@ -287,25 +285,25 @@ text-decoration: underline;
{{if ne .ImageLink "" }} - Alerting Panel + Alerting Panel {{end}} {{if ne .EmbededImage "" }} - Alerting Panel + Alerting Panel {{end}}
- - -
- - -
- - -
- View your Alert rule + + +
+ + + -
+ + +
+ View your Alert rule
- - -
- Go to the Alerts page + + + +
+ Go to the Alerts page
@@ -323,20 +321,20 @@ text-decoration: underline;
- - -