Compare commits

...

33 Commits

Author SHA1 Message Date
bergquist
cac8b976d9 changes version to 4.6.1 2017-11-01 11:27:12 +01:00
Torkel Ödegaard
4366d2f281 fix: panel view now wraps, no scrolling required, fixes #9746
(cherry picked from commit 894951c7d4)
2017-11-01 11:25:50 +01:00
Torkel Ödegaard
c80995b067 plugins: fix for loading external plugins behind auth proxy, fixes #9509
(cherry picked from commit e19b4a9291)
2017-11-01 10:49:01 +01:00
Alexander Zobnin
ebaf8620ea fix: color picker bug at series overrides page, #9715 (#9738)
(cherry picked from commit 948a5259a2)
2017-10-31 13:54:47 +01:00
bergquist
64d8ae9dcb tech: switch to golang 1.9.2
(cherry picked from commit 1be476b5f5)
2017-10-31 12:39:31 +01:00
bergquist
989bf2067c tech: add missing include 2017-10-29 21:16:34 +01:00
bergquist
baeea98473 save as should only delete threshold for panels with alerts
closes #9681
2017-10-29 19:34:41 +01:00
Torkel Ödegaard
dce8522575 fix: graphite annotation tooltip included undefined, fixes #9707
(cherry picked from commit 43d45f9fae)
2017-10-28 13:00:26 +02:00
Torkel Ödegaard
c47f670a58 build: updated version to v4.6.0 2017-10-26 12:15:29 +02:00
Torkel Ödegaard
fb50cec096 plugins: added backward compatible path for rxjs
(cherry picked from commit 01f16ece3e)
2017-10-26 12:08:25 +02:00
Torkel Ödegaard
eb9bdb09b3 ux: updated singlestat default colors
(cherry picked from commit bf680acae5)
2017-10-26 12:00:08 +02:00
Torkel Ödegaard
002ac79124 prometheus: fixed unsaved changes warning when changing time range due to step option on query model was changed in datasource.query code, fixes #9675
(cherry picked from commit 74fcb2494a)
2017-10-26 11:48:00 +02:00
Torkel Ödegaard
bd11b01aaa fix: firefox can now create region annotations, fixes #9638
(cherry picked from commit 0c2aa91e61)
2017-10-26 10:58:31 +02:00
bergquist
d7dd7e3c81 alerting: only editors can pause rules
closes #9640
2017-10-24 11:01:55 +02:00
Torkel Ödegaard
ea78d13b54 fix: another fix for playlist view state, #9639
(cherry picked from commit 7861c27557)
2017-10-24 10:59:23 +02:00
Patrick O'Carroll
83c5853900 fix: fixed playlist controls and view state, fixes #9639
(cherry picked from commit 8afb84a5e5)
2017-10-24 10:48:48 +02:00
bergquist
0f0be4e6e3 prom: adds pre built grafana dashboard
(cherry picked from commit 28af20ff813fd1e3d734ac38163fac0d8ea1b522)
2017-10-24 10:35:45 +02:00
Daniel Lee
f308a25589 bump version for publish_testing.sh 2017-10-23 16:30:15 +02:00
Daniel Lee
9a91a882b4 update version to 4.6.0-beta3 2017-10-23 15:22:06 +02:00
Daniel Lee
6ad6131aaf plugins: expose dashboard impression store
(cherry picked from commit 90ef877e6e)
2017-10-23 15:20:29 +02:00
Sven Klemm
1f10928450 modify $__timeGroup macro so it can be used in select clause (#9527)
* modify $__timeGroup macro so it can be used in select clause

* fix $__interval_ms for postgres datasource

* use $__timeGroup macro in documentation

* fix annotation template query
remove title since its no longer used and add tags instead

* change __timeFilter macro to work on postgresql < 8.1 and redshift

(cherry picked from commit b2d880c6de)
2017-10-23 14:13:38 +02:00
Daniel Lee
0cd0aa19d6 plugins: fixes path issue on Windows
When loading a plugin and setting the path, an extra backslash sneaks
when running on Windows. Fixes #9597

(cherry picked from commit 7863a0417c)
2017-10-23 13:06:09 +02:00
bergquist
c2f2a43197 prometheus: enable gzip for /metrics endpoint
closes #9464

(cherry picked from commit 139f077453)
2017-10-23 09:37:41 +02:00
Torkel Ödegaard
7fe1ac5fe7 fix: fixed save to file button in export modal, fixes #9586
(cherry picked from commit 1e61eae9f4)
2017-10-19 12:54:24 +02:00
bergquist
aec448f7c6 mysql: add usage stats for mysql 2017-10-19 10:31:30 +02:00
Daniel Lee
2ce36c8670 pluginloader: esModule true for systemjs config
Supports importing a module's contents with the
'* as module' syntax. The latest version of SystemJS turns
it off per default which broke several plugins.

(cherry picked from commit 7cbb4020e9)
2017-10-19 09:04:22 +02:00
Alexander Zobnin
fb35d839c1 Fix heatmap Y axis rendering (#9580)
* heatmap: fix Y axis rendering, #9576

* heatmap: fix color legend rendering

(cherry picked from commit 92c67e8c83)
2017-10-18 15:23:12 +02:00
Mitsuhiro Tanda
c8f5d39d97 fix vector range
(cherry picked from commit 8a43d4e25c)
2017-10-18 14:07:44 +02:00
bergquist
34bc19359d prometheus: add builtin template variable as range vectors
add $__interval and $__interval_ms as range vectors to
prometheus editor

(cherry picked from commit c2c5f529f3)
2017-10-18 14:05:43 +02:00
Torkel Ödegaard
3dcae78126 fix: fixed prometheus step issue that caused browser crash, fixes #9575
(cherry picked from commit 8d68bd6bb9)
2017-10-18 13:29:02 +02:00
Torkel Ödegaard
054c7a154a fix: getting started panel and mark adding data source as done, fixes #9568
(cherry picked from commit 039fc2964a)
2017-10-18 12:04:52 +02:00
Alexander Zobnin
6f3d61f4d2 Fixes for annotations API (#9577)
* annotations: throw error if no text specified and set default time to Now() if empty, #9571

* annotations: fix saving graphite event with empty string tags

* docs: add /api/annotations/graphite endpoint docs, #9571

(cherry picked from commit 74e90d01ec)
2017-10-18 10:13:51 +02:00
bergquist
305f8c10e9 bump packagecloud script 2017-10-17 14:16:40 +02:00
49 changed files with 1382 additions and 118 deletions

View File

@@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana
environment:
nodejs_version: "6"
GOPATH: c:\gopath
GOVERSION: 1.9.1
GOVERSION: 1.9.2
install:
- rmdir c:\go /s /q

View File

@@ -9,7 +9,7 @@ machine:
GOPATH: "/home/ubuntu/.go_workspace"
ORG_PATH: "github.com/grafana"
REPO_PATH: "${ORG_PATH}/grafana"
GODIST: "go1.9.1.linux-amd64.tar.gz"
GODIST: "go1.9.2.linux-amd64.tar.gz"
post:
- mkdir -p ~/download
- mkdir -p ~/docker

View File

@@ -48,7 +48,7 @@ Macro example | Description
*$__timeFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name. For example, *dateColumn > to_timestamp(1494410783) AND dateColumn < to_timestamp(1494497183)*
*$__timeFrom()* | Will be replaced by the start of the currently active time selection. For example, *to_timestamp(1494410783)*
*$__timeTo()* | Will be replaced by the end of the currently active time selection. For example, *to_timestamp(1494497183)*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int*
*$__timeGroup(dateColumn,'5m')* | Will be replaced by an expression usable in GROUP BY clause. For example, *(extract(epoch from "dateColumn")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)*
*$__unixEpochFilter(dateColumn)* | Will be replaced by a time range filter using the specified column name with times represented as unix timestamp. For example, *dateColumn > 1494410783 AND dateColumn < 1494497183*
*$__unixEpochFrom()* | Will be replaced by the start of the currently active time selection as unix timestamp. For example, *1494410783*
*$__unixEpochTo()* | Will be replaced by the end of the currently active time selection as unix timestamp. For example, *1494497183*
@@ -94,26 +94,26 @@ Example with `metric` column
```sql
SELECT
min(time_date_time) as time,
$__timeGroup(time_date_time,'5m') as time,
min(value_double),
'min' as metric
FROM test_data
WHERE $__timeFilter(time_date_time)
GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int
ORDER BY time asc
GROUP BY time
ORDER BY time
```
Example with multiple columns:
```sql
SELECT
min(time_date_time) as time,
$__timeGroup(time_date_time,'5m') as time,
min(value_double) as min_value,
max(value_double) as max_value
FROM test_data
WHERE $__timeFilter(time_date_time)
GROUP BY metric1, (extract(epoch from time_date_time)/extract(epoch from $__interval::interval))::int
ORDER BY time asc
GROUP BY time
ORDER BY time
```
## Templating

View File

@@ -120,6 +120,37 @@ Content-Type: application/json
PUT /api/annotations/1141 HTTP/1.1
Accept: application/json
Content-Type: application/json
{
"time":1507037197339,
"isRegion":true,
"timeEnd":1507180805056,
"text":"Annotation Description",
"tags":["tag3","tag4","tag5"]
}
```
## Delete Annotation By Id
`DELETE /api/annotation/:id`
Deletes the annotation that matches the specified id.
**Example Request**:
```http
DELETE /api/annotation/1 HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
```
**Example Response**:
```http
HTTP/1.1 200
Content-Type: application/json
```
## Delete Annotation By RegionId

View File

@@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple
## Dependencies
- [Go 1.9.1](https://golang.org/dl/)
- [Go 1.9.2](https://golang.org/dl/)
- [NodeJS LTS](https://nodejs.org/download/)
- [Git](https://git-scm.com/downloads)

View File

@@ -4,7 +4,7 @@
"company": "Grafana Labs"
},
"name": "grafana",
"version": "4.6.0-beta2",
"version": "4.6.1",
"repository": {
"type": "git",
"url": "http://github.com/grafana/grafana.git"

View File

@@ -1,6 +1,6 @@
#! /usr/bin/env bash
deb_ver=4.6.0-beta1
rpm_ver=4.6.0-beta1
deb_ver=4.6.0-beta3
rpm_ver=4.6.0-beta3
wget https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana_${deb_ver}_amd64.deb

View File

@@ -1,7 +1,6 @@
package api
import (
"fmt"
"strings"
"time"
@@ -41,9 +40,22 @@ func GetAnnotations(c *middleware.Context) Response {
return Json(200, items)
}
type CreateAnnotationError struct {
message string
}
func (e *CreateAnnotationError) Error() string {
return e.message
}
func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
repo := annotations.GetRepository()
if cmd.Text == "" {
err := &CreateAnnotationError{"text field should not be empty"}
return ApiError(500, "Failed to save annotation", err)
}
item := annotations.Item{
OrgId: c.OrgId,
UserId: c.UserId,
@@ -55,6 +67,10 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
Tags: cmd.Tags,
}
if item.Epoch == 0 {
item.Epoch = time.Now().Unix()
}
if err := repo.Save(&item); err != nil {
return ApiError(500, "Failed to save annotation", err)
}
@@ -82,21 +98,22 @@ func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response
return ApiSuccess("Annotation added")
}
type GraphiteAnnotationError struct {
message string
}
func (e *GraphiteAnnotationError) Error() string {
return e.message
}
func formatGraphiteAnnotation(what string, data string) string {
return fmt.Sprintf("%s\n%s", what, data)
text := what
if data != "" {
text = text + "\n" + data
}
return text
}
func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotationsCmd) Response {
repo := annotations.GetRepository()
if cmd.What == "" {
err := &CreateAnnotationError{"what field should not be empty"}
return ApiError(500, "Failed to save Graphite annotation", err)
}
if cmd.When == 0 {
cmd.When = time.Now().Unix()
}
@@ -106,18 +123,22 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
var tagsArray []string
switch tags := cmd.Tags.(type) {
case string:
tagsArray = strings.Split(tags, " ")
if tags != "" {
tagsArray = strings.Split(tags, " ")
} else {
tagsArray = []string{}
}
case []interface{}:
for _, t := range tags {
if tagStr, ok := t.(string); ok {
tagsArray = append(tagsArray, tagStr)
} else {
err := &GraphiteAnnotationError{"tag should be a string"}
err := &CreateAnnotationError{"tag should be a string"}
return ApiError(500, "Failed to save Graphite annotation", err)
}
}
default:
err := &GraphiteAnnotationError{"unsupported tags format"}
err := &CreateAnnotationError{"unsupported tags format"}
return ApiError(500, "Failed to save Graphite annotation", err)
}
@@ -133,7 +154,7 @@ func PostGraphiteAnnotation(c *middleware.Context, cmd dtos.PostGraphiteAnnotati
return ApiError(500, "Failed to save Graphite annotation", err)
}
return ApiSuccess("Graphite Annotation added")
return ApiSuccess("Graphite annotation added")
}
func UpdateAnnotation(c *middleware.Context, cmd dtos.UpdateAnnotationsCmd) Response {

View File

@@ -267,7 +267,7 @@ func (hs *HttpServer) registerRoutes() {
apiRoute.Group("/alerts", func(alertsRoute RouteRegister) {
alertsRoute.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
alertsRoute.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
alertsRoute.Post("/:alertId/pause", reqEditorRole, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert))
alertsRoute.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
alertsRoute.Get("/", wrap(GetAlerts))
alertsRoute.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))

View File

@@ -188,9 +188,8 @@ func (hs *HttpServer) metricsEndpoint(ctx *macaron.Context) {
return
}
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{
DisableCompression: true,
}).ServeHTTP(ctx.Resp, ctx.Req.Request)
promhttp.HandlerFor(prometheus.DefaultGatherer, promhttp.HandlerOpts{}).
ServeHTTP(ctx.Resp, ctx.Req.Request)
}
func (hs *HttpServer) healthHandler(ctx *macaron.Context) {

View File

@@ -21,6 +21,10 @@ func Gziper() macaron.Handler {
return
}
if strings.HasPrefix(requestPath, "/metrics") {
return
}
ctx.Invoke(macaronGziper)
}
}

View File

@@ -18,6 +18,7 @@ const (
DS_KAIROSDB = "kairosdb"
DS_PROMETHEUS = "prometheus"
DS_POSTGRES = "postgres"
DS_MYSQL = "mysql"
DS_ACCESS_DIRECT = "direct"
DS_ACCESS_PROXY = "proxy"
)
@@ -64,6 +65,7 @@ var knownDatasourcePlugins map[string]bool = map[string]bool{
DS_PROMETHEUS: true,
DS_OPENTSDB: true,
DS_POSTGRES: true,
DS_MYSQL: true,
"opennms": true,
"druid": true,
"dalmatinerdb": true,

View File

@@ -40,7 +40,7 @@ func getPluginLogoUrl(pluginType, path, baseUrl string) string {
}
func (fp *FrontendPluginBase) setPathsBasedOnApp(app *AppPlugin) {
appSubPath := strings.Replace(fp.PluginDir, app.PluginDir, "", 1)
appSubPath := strings.Replace(strings.Replace(fp.PluginDir, app.PluginDir, "", 1), "\\", "/", 1)
fp.IncludedInAppId = app.Id
fp.BaseUrl = app.BaseUrl

View File

@@ -0,0 +1,34 @@
package plugins
import (
"testing"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey"
)
func TestFrontendPlugin(t *testing.T) {
Convey("When setting paths based on App on Windows", t, func() {
setting.StaticRootPath = "c:\\grafana\\public"
fp := &FrontendPluginBase{
PluginBase: PluginBase{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata\\datasource",
BaseUrl: "fpbase",
},
}
app := &AppPlugin{
FrontendPluginBase: FrontendPluginBase{
PluginBase: PluginBase{
PluginDir: "c:\\grafana\\public\\app\\plugins\\app\\testdata",
Id: "testdata",
BaseUrl: "public/app/plugins/app/testdata",
},
},
}
fp.setPathsBasedOnApp(app)
So(fp.Module, ShouldEqual, "app/plugins/app/testdata/datasource/module")
})
}

View File

@@ -74,7 +74,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)
}
return fmt.Sprintf("%s >= to_timestamp(%d) AND %s <= to_timestamp(%d)", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), args[0], uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
return fmt.Sprintf("extract(epoch from %s) BETWEEN %d AND %d", args[0], uint64(m.TimeRange.GetFromAsMsEpoch()/1000), uint64(m.TimeRange.GetToAsMsEpoch()/1000)), nil
case "__timeFrom":
return fmt.Sprintf("to_timestamp(%d)", uint64(m.TimeRange.GetFromAsMsEpoch()/1000)), nil
case "__timeTo":
@@ -83,7 +83,7 @@ func (m *PostgresMacroEngine) evaluateMacro(name string, args []string) (string,
if len(args) < 2 {
return "", fmt.Errorf("macro %v needs time column and interval", name)
}
return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int", args[0], args[1]), nil
return fmt.Sprintf("(extract(epoch from \"%s\")/extract(epoch from %s::interval))::int*extract(epoch from %s::interval)", args[0], args[1], args[1]), nil
case "__unixEpochFilter":
if len(args) == 0 {
return "", fmt.Errorf("missing time column argument for macro %v", name)

View File

@@ -30,7 +30,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(timeRange, "WHERE $__timeFilter(time_column)")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "WHERE time_column >= to_timestamp(18446744066914186738) AND time_column <= to_timestamp(18446744066914187038)")
So(sql, ShouldEqual, "WHERE extract(epoch from time_column) BETWEEN 18446744066914186738 AND 18446744066914187038")
})
Convey("interpolate __timeFrom function", func() {
@@ -45,7 +45,7 @@ func TestMacroEngine(t *testing.T) {
sql, err := engine.Interpolate(timeRange, "GROUP BY $__timeGroup(time_column,'5m')")
So(err, ShouldBeNil)
So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int")
So(sql, ShouldEqual, "GROUP BY (extract(epoch from \"time_column\")/extract(epoch from '5m'::interval))::int*extract(epoch from '5m'::interval)")
})
Convey("interpolate __timeTo function", func() {

View File

@@ -43,7 +43,7 @@ export class SeriesColorPicker extends React.Component<IProps, any> {
render() {
return (
<div className="graph-legend-popover">
{this.props.series && this.renderAxisSelection()}
{this.props.series.yaxis && this.renderAxisSelection()}
<ColorPickerPopover color={this.props.series.color} onColorSelect={this.onColorChange} />
</div>
);

View File

@@ -31,8 +31,8 @@ function (_, $, coreModule) {
}
});
$scope.$watch('playlistSrv', function(newValue) {
elem.toggleClass('playlist-active', _.isObject(newValue));
$scope.$watch('playlistSrv.isPlaying', function(newValue) {
elem.toggleClass('playlist-active', newValue === true);
});
}
};

View File

@@ -7,14 +7,15 @@ export class DeltaCtrl {
observer: any;
/** @ngInject */
constructor($rootScope) {
const waitForCompile = function(mutations) {
constructor(private $rootScope) {
const waitForCompile = (mutations) => {
if (mutations.length === 1) {
this.$rootScope.appEvent('json-diff-ready');
}
};
this.observer = new MutationObserver(waitForCompile.bind(this));
this.observer = new MutationObserver(waitForCompile);
const observerConfig = {
attributes: true,

View File

@@ -39,7 +39,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv,
text = text + '<br />' + event.text;
}
} else if (title) {
text = title + '<br />' + text;
text = title + '<br />' + (_.isString(text) ? text : '');
title = '';
}

View File

@@ -20,10 +20,12 @@ export class DashboardCtrl {
dynamicDashboardSrv,
dashboardViewStateSrv,
contextSrv,
playlistSrv,
alertSrv,
$timeout) {
$scope.editor = { index: 0 };
$scope.playlistSrv = playlistSrv;
var resizeEventTimeout;

View File

@@ -19,6 +19,7 @@ export class DashNavCtrl {
private $location,
private backendSrv,
private contextSrv,
public playlistSrv,
navModelSrv) {
this.navModel = navModelSrv.getDashboardNav(this.dashboard, this);

View File

@@ -1,8 +1,7 @@
///<reference path="../../../headers/common.d.ts" />
import angular from 'angular';
import coreModule from 'app/core/core_module';
import {saveAs} from 'file-saver';
import coreModule from 'app/core/core_module';
import {DashboardExporter} from './exporter';
export class DashExportCtrl {
@@ -22,9 +21,8 @@ export class DashExportCtrl {
}
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');
var blob = new Blob([angular.toJson(this.dash, true)], {type: 'application/json;charset=utf-8'});
saveAs(blob, this.dash.title + '-' + new Date().getTime() + '.json');
}
saveJson() {
@@ -44,7 +42,7 @@ export function dashExportDirective() {
controller: DashExportCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {dismiss: "&"}
scope: {dismiss: '&'},
};
}

View File

@@ -35,7 +35,6 @@ export class DashboardModel {
gnetId: any;
meta: any;
events: any;
editMode: boolean;
constructor(data, meta?) {
if (!data) {

View File

@@ -3,25 +3,21 @@
<i class="fa fa-remove"></i>
</a>
<div class="gf-form-inline dash-row-add-panel-form">
<div class="gf-form">
<input type="text" class="gf-form-input max-width-14" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter"></input>
</div>
</div>
<div class="gf-form width-10">
<input type="text" class="gf-form-input width-10" ng-model='ctrl.panelSearch' give-focus='true' ng-keydown="ctrl.keyDown($event)" ng-change="ctrl.panelSearchChanged()" placeholder="panel search filter"></input>
</div>
<div class="add-panel-panels-scroll">
<div class="add-panel-panels">
<div class="add-panel-item"
ng-repeat="panel in ctrl.panelHits"
ng-class="{active: $index === ctrl.activeIndex}"
ng-click="ctrl.addPanel(panel)"
ui-draggable="true"
drag="panel.id"
title="{{panel.name}}">
<img class="add-panel-item-img" ng-src="{{panel.info.logos.small}}"></img>
<div class="add-panel-item-name">{{panel.name}}</div>
</div>
</div>
</div>
<div class="add-panel-panels">
<div class="add-panel-item"
ng-repeat="panel in ctrl.panelHits"
ng-class="{active: $index === ctrl.activeIndex}"
ng-click="ctrl.addPanel(panel)"
ui-draggable="true"
drag="panel.id"
title="{{panel.name}}">
<img class="add-panel-item-img" ng-src="{{panel.info.logos.small}}"></img>
<div class="add-panel-item-name">{{panel.name}}</div>
</div>
</div>
</div>

View File

@@ -49,7 +49,10 @@ export class SaveDashboardAsModalCtrl {
if (dashboard.id > 0) {
this.clone.rows.forEach(row => {
row.panels.forEach(panel => {
delete panel.thresholds;
if (panel.type === "graph" && panel.alert) {
delete panel.thresholds;
}
delete panel.alert;
});
});

View File

@@ -0,0 +1,67 @@
import {SaveDashboardAsModalCtrl} from '../save_as_modal';
import {describe, it, expect} from 'test/lib/common';
describe('saving dashboard as', () => {
function scenario(name, panel, verify) {
describe(name, () => {
var json = {
title: "name",
rows: [ { panels: [
panel
]}]
};
var mockDashboardSrv = {
getCurrent: function() {
return {
id: 5,
getSaveModelClone: function() {
return json;
}
};
}
};
var ctrl = new SaveDashboardAsModalCtrl(mockDashboardSrv);
var ctx: any = {
clone: ctrl.clone,
ctrl: ctrl,
panel: {}
};
for (let row of ctrl.clone.rows) {
for (let panel of row.panels) {
ctx.panel = panel;
}
}
it("verify", () => {
verify(ctx);
});
});
}
scenario("default values", {}, (ctx) => {
var clone = ctx.clone;
expect(clone.id).toBe(null);
expect(clone.title).toBe("name Copy");
expect(clone.editable).toBe(true);
expect(clone.hideControls).toBe(false);
});
var graphPanel = { id: 1, type: "graph", alert: { rule: 1}, thresholds: { value: 3000} };
scenario("should remove alert from graph panel", graphPanel , (ctx) => {
expect(ctx.panel.alert).toBe(undefined);
});
scenario("should remove threshold from graph panel", graphPanel, (ctx) => {
expect(ctx.panel.thresholds).toBe(undefined);
});
scenario("singlestat should keep threshold", { id: 1, type: "singlestat", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
scenario("table should keep threshold", { id: 1, type: "table", thresholds: { value: 3000} }, (ctx) => {
expect(ctx.panel.thresholds).not.toBe(undefined);
});
});

View File

@@ -73,7 +73,6 @@ function(angular, _) {
dash.time = 0;
dash.refresh = 0;
dash.schemaVersion = 0;
dash.editMode = false;
// filter row and panels properties that should be ignored
dash.rows = _.filter(dash.rows, function(row) {

View File

@@ -154,7 +154,6 @@ function (angular, _, $, config) {
ctrl.editMode = false;
ctrl.fullscreen = false;
ctrl.dashboard.editMode = this.oldDashboardEditMode;
this.$scope.appEvent('panel-fullscreen-exit', {panelId: ctrl.panel.id});
@@ -176,10 +175,8 @@ function (angular, _, $, config) {
ctrl.editMode = this.state.edit && this.dashboard.meta.canEdit;
ctrl.fullscreen = true;
this.oldDashboardEditMode = this.dashboard.editMode;
this.oldTimeRange = ctrl.range;
this.fullscreenPanel = panelScope;
this.dashboard.editMode = false;
$(window).scrollTop(0);

View File

@@ -13,7 +13,8 @@ import * as datemath from 'app/core/utils/datemath';
import * as fileExport from 'app/core/utils/file_export';
import * as flatten from 'app/core/utils/flatten';
import * as ticks from 'app/core/utils/ticks';
import builtInPlugins from './buit_in_plugins';
import {impressions} from 'app/features/dashboard/impression_store';
import builtInPlugins from './built_in_plugins';
import d3 from 'vendor/d3/d3';
// rxjs
@@ -38,6 +39,12 @@ System.config({
text: 'vendor/plugin-text/text.js',
css: 'vendor/plugin-css/css.js'
},
meta: {
'*': {
esModule: true,
authorization: true,
}
}
});
// add cache busting
@@ -60,9 +67,20 @@ exposeToPlugin('lodash', _);
exposeToPlugin('moment', moment);
exposeToPlugin('jquery', jquery);
exposeToPlugin('angular', angular);
exposeToPlugin('d3', d3);
exposeToPlugin('rxjs/Subject', Subject);
exposeToPlugin('rxjs/Observable', Observable);
exposeToPlugin('d3', d3);
// backward compatible path
exposeToPlugin('vendor/npm/rxjs/Rx', {
Subject: Subject,
Observable: Observable
});
exposeToPlugin('app/features/dashboard/impression_store', {
impressions: impressions,
__esModule: true
});
exposeToPlugin('app/plugins/sdk', sdk);
exposeToPlugin('app/core/utils/datemath', datemath);

View File

@@ -75,6 +75,12 @@ export class ElasticDatasource {
return this.request('POST', url, data).then(function(results) {
results.data.$$config = results.config;
return results.data;
}).catch(err => {
if (err.data && err.data.error) {
throw {message: 'Elasticsearch error: ' + err.data.error.reason, error: err.data.error};
}
throw err;
});
}

View File

@@ -20,6 +20,10 @@ export class PostgresDatasource {
return '\'' + value + '\'';
}
if (typeof value === 'number') {
return value.toString();
}
var quotedValues = _.map(value, function(val) {
return '\'' + val + '\'';
});

View File

@@ -16,8 +16,8 @@ class PostgresConfigCtrl {
const defaultQuery = `SELECT
extract(epoch from time_column) AS time,
title_column as title,
description_column as text
text_column as text,
tags_column as tags
FROM
metric_table
WHERE

View File

@@ -8,7 +8,7 @@ export class PromCompleter {
labelNameCache: any;
labelValueCache: any;
identifierRegexps = [/[\[\]a-zA-Z_0-9=]/];
identifierRegexps = [/\[/, /[a-zA-Z0-9_:]/];
constructor(private datasource: PrometheusDatasource) {
this.labelQueryCache = {};
@@ -73,13 +73,15 @@ export class PromCompleter {
});
}
if (prefix === '[') {
if (token.type === 'paren.lparen' && token.value === '[') {
var vectors = [];
for (let unit of ['s', 'm', 'h']) {
for (let value of [1,5,10,30]) {
vectors.push({caption: value+unit, value: '['+value+unit, meta: 'range vector'});
}
}
vectors.push({caption: '$__interval', value: '[$__interval', meta: 'range vector'});
vectors.push({caption: '$__interval_ms', value: '[$__interval_ms', meta: 'range vector'});
callback(null, vectors);
return;
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,3 @@
///<reference path="../../../headers/common.d.ts" />
import _ from 'lodash';
import kbn from 'app/core/utils/kbn';
@@ -122,7 +120,7 @@ export class PrometheusDatasource {
} else {
for (let metricData of response.data.data.result) {
if (response.data.data.resultType === 'matrix') {
result.push(self.transformMetricData(metricData, activeTargets[index], start, end));
result.push(self.transformMetricData(metricData, activeTargets[index], start, end, queries[index].step));
} else if (response.data.data.resultType === 'vector') {
result.push(self.transformInstantMetricData(metricData, activeTargets[index]));
}
@@ -144,7 +142,6 @@ export class PrometheusDatasource {
var intervalFactor = target.intervalFactor || 1;
// Adjust the interval to take into account any specified minimum and interval factor plus Prometheus limits
var adjustedInterval = this.adjustInterval(interval, minInterval, range, intervalFactor);
var scopedVars = options.scopedVars;
// If the interval was adjusted, make a shallow copy of scopedVars with updated interval vars
if (interval !== adjustedInterval) {
@@ -154,7 +151,7 @@ export class PrometheusDatasource {
"__interval_ms": {text: interval * 1000, value: interval * 1000},
});
}
target.step = query.step = interval;
query.step = interval;
// Only replace vars in expression after having (possibly) updated interval vars
query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr);
@@ -168,7 +165,7 @@ export class PrometheusDatasource {
if (interval !== 0 && range / intervalFactor / interval > 11000) {
interval = Math.ceil(range / intervalFactor / 11000);
}
return Math.max(interval * intervalFactor, minInterval);
return Math.max(interval * intervalFactor, minInterval, 1);
}
performTimeSeriesQuery(query, start, end) {
@@ -272,13 +269,13 @@ export class PrometheusDatasource {
});
}
transformMetricData(md, options, start, end) {
transformMetricData(md, options, start, end, step) {
var dps = [],
metricLabel = null;
metricLabel = this.createMetricLabel(md.metric, options);
var stepMs = parseInt(options.step) * 1000;
var stepMs = step * 1000;
var baseTimestamp = start * 1000;
for (let value of md.values) {
var dp_value = parseFloat(value[1]);

View File

@@ -4,7 +4,8 @@
"id": "prometheus",
"includes": [
{"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"}
{"type": "dashboard", "name": "Prometheus Stats", "path": "dashboards/prometheus_stats.json"},
{"type": "dashboard", "name": "Grafana Stats", "path": "dashboards/grafana_stats.json"}
],
"metrics": true,

View File

@@ -44,12 +44,18 @@ describe('Prometheus editor completer', function() {
describe('When inside brackets', () => {
it('Should return range vectors', () => {
const session = getSessionStub({
currentToken: {},
tokens: [],
line: '',
currentToken: {type: 'paren.lparen', value: '[', index: 2, start: 9},
tokens: [
{type: 'identifier', value: 'node_cpu'},
{type: 'paren.lparen', value: '['}
],
line: 'node_cpu[',
});
completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => {
expect(res[0]).to.eql({caption: '1s', value: '[1s', meta: 'range vector'});
return completer.getCompletions(editor, session, {row: 0, column: 10}, '[', (s, res) => {
expect(res[0].caption).to.eql('1s');
expect(res[0].value).to.eql('[1s');
expect(res[0].meta).to.eql('range vector');
});
});
});

View File

@@ -294,6 +294,20 @@ describe('PrometheusDatasource', function() {
ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation();
});
it('step should never go below 1', function() {
var query = {
// 6 hour range
range: { from: moment(1508318768202), to: moment(1508318770118) },
targets: [{expr: 'test'}],
interval: '100ms'
};
var urlExpected = 'proxied/api/v1/query_range?query=test&start=1508318769&end=1508318771&step=1';
ctx.$httpBackend.expect('GET', urlExpected).respond(response);
ctx.ds.query(query);
ctx.$httpBackend.verifyNoOutstandingExpectation();
});
it('should be auto interval when greater than min interval', function() {
var query = {
// 6 hour range

View File

@@ -34,7 +34,7 @@ class GettingStartedPanelCtrl extends PanelCtrl {
check: () => {
return $q.when(
datasourceSrv.getMetricSources().filter(item => {
return item.meta.builtIn === false;
return item.meta.builtIn !== true;
}).length > 0
);
}

View File

@@ -29,7 +29,7 @@ define([
$scope.setOverride = function(item, subItem) {
// handle color overrides
if (item.propertyName === 'color') {
$scope.openColorSelector();
$scope.openColorSelector($scope.override['color']);
return;
}
@@ -52,15 +52,17 @@ define([
$scope.ctrl.render();
};
$scope.openColorSelector = function() {
$scope.openColorSelector = function(color) {
var fakeSeries = {color: color};
popoverSrv.show({
element: $element.find(".dropdown")[0],
position: 'top center',
openOn: 'click',
template: '<series-color-picker onColorChange="colorSelected" />',
template: '<series-color-picker series="series" onColorChange="colorSelected" />',
model: {
autoClose: true,
colorSelected: $scope.colorSelected,
series: fakeSeries
},
onClose: function() {
$scope.ctrl.render();

View File

@@ -152,7 +152,7 @@ function drawLegendValues(elem, colorScale, rangeFrom, rangeTo, maxValue, minVal
.tickSize(2);
let colorRect = legendElem.find(":first-child");
let posY = colorRect.height() + 2;
let posY = getSvgElemHeight(legendElem) + 2;
let posX = getSvgElemX(colorRect);
d3.select(legendElem.get(0)).append("g")
@@ -256,7 +256,16 @@ function getOpacityScale(options, maxValue, minValue = 0) {
function getSvgElemX(elem) {
let svgElem = elem.get(0);
if (svgElem && svgElem.x && svgElem.x.baseVal) {
return elem.get(0).x.baseVal.value;
return svgElem.x.baseVal.value;
} else {
return 0;
}
}
function getSvgElemHeight(elem) {
let svgElem = elem.get(0);
if (svgElem && svgElem.height && svgElem.height.baseVal) {
return svgElem.height.baseVal.value;
} else {
return 0;
}

View File

@@ -71,9 +71,8 @@ export default function link(scope, elem, attrs, ctrl) {
function getYAxisWidth(elem) {
let axis_text = elem.selectAll(".axis-y text").nodes();
let max_text_width = _.max(_.map(axis_text, text => {
let el = $(text);
// Use JQuery outerWidth() to compute full element width
return el.outerWidth();
// Use SVG getBBox method
return text.getBBox().width;
}));
return max_text_width;

View File

@@ -66,7 +66,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
thresholds: '',
colorBackground: false,
colorValue: false,
colors: ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
colors: ["#299c46", "rgba(237, 129, 40, 0.89)", "#d44a3a"],
sparkline: {
show: false,
full: false,

View File

@@ -61,6 +61,7 @@
margin: 0 $panel-margin $panel-margin*2 $panel-margin;
padding: $panel-margin*2;
display: flex;
flex-direction: row;
}
.dash-row-dropview-close {
@@ -71,19 +72,10 @@
height: 20px;
}
.add-panel-panels-scroll {
width: 100%;
overflow: auto;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none
}
}
.add-panel-panels {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
.add-panel-item {

View File

@@ -2962,8 +2962,11 @@ Licensed under the MIT license.
}
function onClick(e) {
triggerClickHoverEvent("plotclick", e,
function (s) { return s["clickable"] != false; });
if (plot.isSelecting) {
return;
}
triggerClickHoverEvent("plotclick", e, function (s) { return s["clickable"] != false; });
}
// trigger click or hover event (they send the same parameters

View File

@@ -152,6 +152,10 @@ The plugin allso adds the following methods to the plot object:
plot.getPlaceholder().trigger("plotselecting", [ null ]);
}
setTimeout(function() {
plot.isSelecting = false;
}, 10);
return false;
}
@@ -218,6 +222,7 @@ The plugin allso adds the following methods to the plot object:
setSelectionPos(selection.second, pos);
if (selectionIsSane()) {
plot.isSelecting = true;
selection.show = true;
plot.triggerRedrawOverlay();
}

View File

@@ -21,7 +21,7 @@ RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A170311380
RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \
yum install -y nodejs --nogpgcheck
ENV GOLANG_VERSION 1.9.1
ENV GOLANG_VERSION 1.9.2
RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \
yum install -y yarn --nogpgcheck && \