Merge branch 'issue-6566' of https://github.com/benrubson/grafana into benrubson-issue-6566

This commit is contained in:
Torkel Ödegaard 2016-11-21 09:39:35 +01:00
commit 8d80812601
80 changed files with 824 additions and 1318 deletions

View File

@ -5,6 +5,13 @@
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
* **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
* **Cloudwatch**: Fixed cloudwatch datasource requesting to many datapoints, [#6544](https://github.com/grafana/grafana/issues/6544)
* **UX**: Panel Drop zone visible after duplicating panel, and when entering fullscreen/edit view, [#6598](https://github.com/grafana/grafana/issues/6598)
* **Templating**: Newly added variable was not visible directly only after dashboard reload, [#6622](https://github.com/grafana/grafana/issues/6622)
### Enhancements
* **Singlestat**: Support repeated template variables in prefix/postfix [#6595](https://github.com/grafana/grafana/issues/6595)
* **Templating**: Don't persist variable options with refresh option [#6586](https://github.com/grafana/grafana/issues/6586)
* **Alerting**: Add ability to have OR conditions (and mixing AND & OR) [#6579](https://github.com/grafana/grafana/issues/6579)
# 4.0-beta1 (2016-11-09)

View File

@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the
[external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
config file.
This is not an optional requirement, you can get slack and email notifications without setting this up.
This is an optional requirement, you can get slack and email notifications without setting this up.

View File

@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot
contain any template variables. Currently we only support `AND` operator between conditions.
contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
For example, we have 3 conditions in the following order:
`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`.

View File

@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses
that is how angular directives needs to be named in order to match an element with name `<metric-query-editor-graphite />`.
You also specify the query controller here instead of in the query.editor.html partial like before.
### query.editor.html
This partial needs to be updated, remove the `np-repeat` this is done in the outer partial now,m the query.editor.html
should only render a single query. Take a look at the Graphite or InfluxDB partials for `query.editor.html` for reference.
You should also add a `tight-form-item` with `{{target.refId}}`, all queries needs to be assigned a letter (`refId`).
These query reference letters are going to be utilized in a later feature.

View File

@ -85,6 +85,34 @@ page_keywords: grafana, admin, http, api, documentation, orgs, organisation
}
}
## Create Organisation
`POST /api/org`
**Example Request**:
POST /api/org HTTP/1.1
Accept: application/json
Content-Type: application/json
Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
{
"name":"New Org."
}
**Example Response**:
HTTP/1.1 200
Content-Type: application/json
{
"orgId":"1",
"message":"Organization created"
}
## Update current Organisation
`PUT /api/org`

View File

@ -141,6 +141,18 @@ those options.
- [OpenTSDB]({{< relref "datasources/opentsdb.md" >}})
- [Prometheus]({{< relref "datasources/prometheus.md" >}})
### Server side image rendering
Server side image (png) rendering is a feature that is optional but very useful when sharing visualizations,
for example in alert notifications.
If the image is missing text make sure you have font packages installed.
```
yum install fontconfig
yum install freetype*
yum install urw-fonts
```
## Installing from binary tar file

View File

@ -119,7 +119,8 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
res := backendCmd.Result
dtoRes := &dtos.AlertTestResult{
Firing: res.Firing,
Firing: res.Firing,
ConditionEvals: res.ConditionEvals,
}
if res.Error != nil {

View File

@ -307,4 +307,5 @@ func Register(r *macaron.Macaron) {
InitAppPluginRoutes(r)
r.NotFound(NotFoundHandler)
}

View File

@ -35,11 +35,12 @@ type AlertTestCommand struct {
}
type AlertTestResult struct {
Firing bool `json:"firing"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
EvalMatches []*EvalMatch `json:"matches,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"`
Firing bool `json:"firing"`
ConditionEvals string `json:"conditionEvals"`
TimeMs string `json:"timeMs"`
Error string `json:"error,omitempty"`
EvalMatches []*EvalMatch `json:"matches,omitempty"`
Logs []*AlertTestResultLog `json:"logs,omitempty"`
}
type AlertTestResultLog struct {

View File

@ -96,7 +96,7 @@ func OAuthLogin(ctx *middleware.Context) {
}
sslcli := &http.Client{Transport: tr}
oauthCtx = context.TODO()
oauthCtx = context.Background()
oauthCtx = context.WithValue(oauthCtx, oauth2.HTTPClient, sslcli)
}
@ -106,6 +106,8 @@ func OAuthLogin(ctx *middleware.Context) {
ctx.Handle(500, "login.OAuthLogin(NewTransportWithCode)", err)
return
}
// token.TokenType was defaulting to "bearer", which is out of spec, so we explicitly set to "Bearer"
token.TokenType = "Bearer"
ctx.Logger.Debug("OAuthLogin Got token")

View File

@ -187,6 +187,7 @@ func (ctx *Context) Handle(status int, title string, err error) {
}
ctx.Data["Title"] = title
ctx.Data["AppSubUrl"] = setting.AppSubUrl
ctx.HTML(status, strconv.Itoa(status))
}

View File

@ -19,53 +19,14 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"runtime"
"gopkg.in/macaron.v1"
"github.com/go-macaron/inject"
"github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
)
const (
panicHtml = `<html>
<head><title>PANIC: %s</title>
<meta charset="utf-8" />
<style type="text/css">
html, body {
font-family: "Roboto", sans-serif;
color: #333333;
background-color: #ea5343;
margin: 0px;
}
h1 {
color: #d04526;
background-color: #ffffff;
padding: 20px;
border-bottom: 1px dashed #2b3848;
}
pre {
margin: 20px;
padding: 20px;
border: 2px solid #2b3848;
background-color: #ffffff;
white-space: pre-wrap; /* css-3 */
white-space: -moz-pre-wrap; /* Mozilla, since 1999 */
white-space: -pre-wrap; /* Opera 4-6 */
white-space: -o-pre-wrap; /* Opera 7 */
word-wrap: break-word; /* Internet Explorer 5.5+ */
}
</style>
</head><body>
<h1>PANIC</h1>
<pre style="font-weight: bold;">%s</pre>
<pre>%s</pre>
</body>
</html>`
)
var (
dunno = []byte("???")
centerDot = []byte("·")
@ -151,21 +112,34 @@ func Recovery() macaron.Handler {
panicLogger.Error("Request error", "error", err, "stack", string(stack))
// Lookup the current responsewriter
val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
res := val.Interface().(http.ResponseWriter)
c.Data["Title"] = "Server Error"
c.Data["AppSubUrl"] = setting.AppSubUrl
if theErr, ok := err.(error); ok {
c.Data["Title"] = theErr.Error()
}
// respond with panic message while in development mode
var body []byte
if setting.Env == setting.DEV {
res.Header().Set("Content-Type", "text/html")
body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
c.Data["ErrorMsg"] = string(stack)
}
res.WriteHeader(http.StatusInternalServerError)
if nil != body {
res.Write(body)
}
c.HTML(500, "500")
// // Lookup the current responsewriter
// val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil)))
// res := val.Interface().(http.ResponseWriter)
//
// // respond with panic message while in development mode
// var body []byte
// if setting.Env == setting.DEV {
// res.Header().Set("Content-Type", "text/html")
// body = []byte(fmt.Sprintf(panicHtml, err, err, stack))
// }
//
// res.WriteHeader(http.StatusInternalServerError)
// if nil != body {
// res.Write(body)
// }
}
}()

View File

@ -23,6 +23,7 @@ type QueryCondition struct {
Query AlertQuery
Reducer QueryReducer
Evaluator AlertEvaluator
Operator string
HandleRequest tsdb.HandleRequestFunc
}
@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio
return &alerting.ConditionResult{
Firing: evalMatchCount > 0,
NoDataFound: emptySerieCount == len(seriesList),
Operator: c.Operator,
EvalMatches: matches,
}, nil
}
@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro
if err != nil {
return nil, err
}
condition.Evaluator = evaluator
operatorJson := model.Get("operator")
operator := operatorJson.Get("type").MustString("and")
condition.Operator = operator
return &condition, nil
}

View File

@ -17,7 +17,7 @@ type EvalContext struct {
EvalMatches []*EvalMatch
Logs []*ResultLogEntry
Error error
Description string
ConditionEvals string
StartTime time.Time
EndTime time.Time
Rule *Rule

View File

@ -1,6 +1,8 @@
package alerting
import (
"strconv"
"strings"
"time"
"github.com/grafana/grafana/pkg/log"
@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler {
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
firing := true
for _, condition := range context.Rule.Conditions {
conditionEvals := ""
for i := 0; i < len(context.Rule.Conditions); i++ {
condition := context.Rule.Conditions[i]
cr, err := condition.Eval(context)
if err != nil {
context.Error = err
@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break
}
// break if result has not triggered yet
if cr.Firing == false {
firing = false
break
// calculating Firing based on operator
if cr.Operator == "or" {
firing = firing || cr.Firing
} else {
firing = firing && cr.Firing
}
if i > 0 {
conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]"
} else {
conditionEvals = strconv.FormatBool(firing)
}
context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...)
}
context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing)
context.Firing = firing
context.EndTime = time.Now()
elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond

View File

@ -8,12 +8,13 @@ import (
)
type conditionStub struct {
firing bool
matches []*EvalMatch
firing bool
operator string
matches []*EvalMatch
}
func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) {
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil
return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil
}
func TestAlertingExecutor(t *testing.T) {
@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) {
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
})
Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false},
&conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if any of the condition is passing with OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[true OR false] = true")
})
Convey("Show return false if any of the condition is failing with AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[true AND false] = false")
})
Convey("Show return true if one condition is failing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true")
})
Convey("Show return false if one condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false")
})
Convey("Show return false if a condition is failing with nested AND operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "and"},
&conditionStub{firing: true, operator: "and"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false")
})
Convey("Show return true if a condition is passing with nested OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: true, operator: "and"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: true, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
})
})
}

View File

@ -24,6 +24,7 @@ type Notifier interface {
type ConditionResult struct {
Firing bool
NoDataFound bool
Operator string
EvalMatches []*EvalMatch
}

View File

@ -26,11 +26,32 @@ type Rule struct {
}
type ValidationError struct {
Reason string
Reason string
Err error
Alertid int64
DashboardId int64
PanelId int64
}
func (e ValidationError) Error() string {
return e.Reason
extraInfo := ""
if e.Alertid != 0 {
extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid)
}
if e.PanelId != 0 {
extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId)
}
if e.DashboardId != 0 {
extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId)
}
if e.Err != nil {
return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo)
}
return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo)
}
var (
@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v)
if id, err := jsonModel.Get("id").Int64(); err != nil {
return nil, ValidationError{Reason: "Invalid notification schema"}
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
model.Notifications = append(model.Notifications, id)
}
@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
conditionModel := simplejson.NewFromAny(condition)
conditionType := conditionModel.Get("type").MustString()
if factory, exist := conditionFactories[conditionType]; !exist {
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType}
return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
if queryCondition, err := factory(conditionModel, index); err != nil {
return nil, err
return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
} else {
model.Conditions = append(model.Conditions, queryCondition)
}

View File

@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) {
offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i)
job.Offset = int64(math.Floor(float64(offset) / 1000))
if job.Offset == 0 { //zero offset causes division with 0 panics.
job.Offset = 1
}
jobs[rule.Id] = job
}

View File

@ -2,7 +2,6 @@ package influxdb
import (
"fmt"
"strconv"
"strings"
"regexp"
@ -58,13 +57,12 @@ func (query *Query) renderTags() []string {
}
textValue := ""
numericValue, err := strconv.ParseFloat(tag.Value, 64)
// quote value unless regex or number
if tag.Operator == "=~" || tag.Operator == "!~" {
textValue = tag.Value
} else if err == nil {
textValue = fmt.Sprintf("%v", numericValue)
} else if tag.Operator == "<" || tag.Operator == ">" {
textValue = tag.Value
} else {
textValue = fmt.Sprintf("'%s'", tag.Value)
}

View File

@ -106,13 +106,19 @@ func TestInfluxdbQueryBuilder(t *testing.T) {
Convey("can render number tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001`)
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`)
})
Convey("can render number tags with decimals", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}}
Convey("can render numbers less then condition tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: "<", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001.1`)
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`)
})
Convey("can render number greather then condition tags", func() {
query := &Query{Tags: []*Tag{&Tag{Operator: ">", Value: "10001", Key: "key"}}}
So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`)
})
Convey("can render string tags", func() {

View File

@ -40,7 +40,6 @@ export class GrafanaApp {
init() {
var app = angular.module('grafana', []);
app.constant('grafanaVersion', "@grafanaVersion@");
moment.locale(config.bootData.user.locale);

View File

@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) {
}
}
// mouse and keyboard is user activity
body.mousemove(userActivityDetected);
body.keydown(userActivityDetected);
setInterval(checkForInActiveUser, 1000);
// treat tab change as activity
document.addEventListener('visibilitychange', userActivityDetected);
// check every 2 seconds
setInterval(checkForInActiveUser, 2000);
appEvents.on('toggle-view-mode', () => {
lastActivity = 0;

View File

@ -6,7 +6,6 @@ import "./directives/dash_class";
import "./directives/confirm_click";
import "./directives/dash_edit_link";
import "./directives/dropdown_typeahead";
import "./directives/grafana_version_check";
import "./directives/metric_segment";
import "./directives/misc";
import "./directives/ng_model_on_blur";

View File

@ -1,31 +0,0 @@
define([
'../core_module',
],
function (coreModule) {
'use strict';
coreModule.default.directive('grafanaVersionCheck', function($http, contextSrv) {
return {
restrict: 'A',
link: function(scope, elem) {
if (contextSrv.version === 'master') {
return;
}
$http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' })
.then(function(response) {
if (!response.data || !response.data.version) {
return;
}
if (contextSrv.version !== response.data.version) {
elem.append('<i class="icon-info-sign"></i> ' +
'<a href="http://grafana.org/download" target="_blank"> ' +
'New version available: ' + response.data.version +
'</a>');
}
});
}
};
});
});

View File

@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) {
if (ctrl.$isEmpty(modelValue)) {
return true;
}
if (viewValue.indexOf('$') === 0) {
if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) {
return true; // allow template variable
}
var info = rangeUtil.describeTextRange(viewValue);

View File

@ -420,11 +420,11 @@ function($, _, moment) {
kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps');
kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps');
kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1);
kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bits', 1);
kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1);
kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2);
kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2);
kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2);
kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3);
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bits', 3);
kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3);
// Throughput
kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops');

View File

@ -28,6 +28,11 @@ var evalFunctions = [
{text: 'HAS NO VALUE' , value: 'no_value'}
];
var evalOperators = [
{text: 'OR', value: 'or'},
{text: 'AND', value: 'and'},
];
var reducerTypes = [
{text: 'avg()', value: 'avg'},
{text: 'min()', value: 'min'},
@ -116,6 +121,7 @@ export default {
getStateDisplayModel: getStateDisplayModel,
conditionTypes: conditionTypes,
evalFunctions: evalFunctions,
evalOperators: evalOperators,
noDataModes: noDataModes,
executionErrorModes: executionErrorModes,
reducerTypes: reducerTypes,

View File

@ -18,6 +18,7 @@ export class AlertTabCtrl {
alert: any;
conditionModels: any;
evalFunctions: any;
evalOperators: any;
noDataModes: any;
executionErrorModes: any;
addNotificationSegment;
@ -41,6 +42,7 @@ export class AlertTabCtrl {
this.$scope.ctrl = this;
this.subTabIndex = 0;
this.evalFunctions = alertDef.evalFunctions;
this.evalOperators = alertDef.evalOperators;
this.conditionTypes = alertDef.conditionTypes;
this.noDataModes = alertDef.noDataModes;
this.executionErrorModes = alertDef.executionErrorModes;
@ -194,6 +196,7 @@ export class AlertTabCtrl {
query: {params: ['A', '5m', 'now']},
reducer: {type: 'avg', params: []},
evaluator: {type: 'gt', params: [null]},
operator: {type: 'and'},
};
}
@ -250,6 +253,7 @@ export class AlertTabCtrl {
cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
cm.reducerPart = alertDef.createReducerPart(source.reducer);
cm.evaluator = source.evaluator;
cm.operator = source.operator;
return cm;
}

View File

@ -38,23 +38,23 @@
<h5 class="section-heading">Conditions</h5>
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
<div class="gf-form">
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
<query-part-editor class="gf-form-label query-part width-5" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div>
<div class="gf-form">
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
<query-part-editor class="gf-form-label query-part width-10" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
</query-part-editor>
</div>
<div class="gf-form">
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-7" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div>
<div class="gf-form">
<label class="gf-form-label">

View File

@ -52,7 +52,7 @@ export class DashboardCtrl {
.catch($scope.onInitFailed.bind(this, 'Templating init failed', false))
// continue
.finally(function() {
dynamicDashboardSrv.init(dashboard, variableSrv);
dynamicDashboardSrv.init(dashboard);
dynamicDashboardSrv.process();
unsavedChangesSrv.init(dashboard, $scope);

View File

@ -12,12 +12,12 @@ export class DynamicDashboardSrv {
dashboard: any;
variables: any;
init(dashboard, variableSrv) {
init(dashboard) {
this.dashboard = dashboard;
this.variables = variableSrv.variables;
this.variables = dashboard.templating.list;
}
process(options) {
process(options?) {
if (this.dashboard.snapshot || this.variables.length === 0) {
return;
}
@ -31,6 +31,8 @@ export class DynamicDashboardSrv {
// cleanup scopedVars
for (i = 0; i < this.dashboard.rows.length; i++) {
row = this.dashboard.rows[i];
delete row.scopedVars;
for (j = 0; j < row.panels.length; j++) {
delete row.panels[j].scopedVars;
}
@ -64,6 +66,8 @@ export class DynamicDashboardSrv {
j = j - 1;
}
}
row.panelSpanChanged();
}
}

View File

@ -17,9 +17,7 @@ export class DashExportCtrl {
constructor(private backendSrv, dashboardSrv, datasourceSrv, $scope) {
this.exporter = new DashboardExporter(datasourceSrv);
var current = dashboardSrv.getCurrent().getSaveModelClone();
this.exporter.makeExportable(current).then(dash => {
this.exporter.makeExportable(dashboardSrv.getCurrent()).then(dash => {
$scope.$apply(() => {
this.dash = dash;
});

View File

@ -11,19 +11,40 @@ export class DashboardExporter {
constructor(private datasourceSrv) {
}
makeExportable(dash) {
makeExportable(dashboard) {
var dynSrv = new DynamicDashboardSrv();
dynSrv.init(dash, {variables: dash.templating.list});
// clean up repeated rows and panels,
// this is done on the live real dashboard instance, not on a clone
// so we need to undo this
// this is pretty hacky and needs to be changed
dynSrv.init(dashboard);
dynSrv.process({cleanUpOnly: true});
dash.id = null;
var saveModel = dashboard.getSaveModelClone();
saveModel.id = null;
// undo repeat cleanup
dynSrv.process();
var inputs = [];
var requires = {};
var datasources = {};
var promises = [];
var variableLookup: any = {};
for (let variable of saveModel.templating.list) {
variableLookup[variable.name] = variable;
}
var templateizeDatasourceUsage = obj => {
// ignore data source properties that contain a variable
if (obj.datasource && obj.datasource.indexOf('$') === 0) {
if (variableLookup[obj.datasource.substring(1)]){
return;
}
}
promises.push(this.datasourceSrv.get(obj.datasource).then(ds => {
if (ds.meta.builtIn) {
return;
@ -50,7 +71,7 @@ export class DashboardExporter {
};
// check up panel data sources
for (let row of dash.rows) {
for (let row of saveModel.rows) {
for (let panel of row.panels) {
if (panel.datasource !== undefined) {
templateizeDatasourceUsage(panel);
@ -77,7 +98,7 @@ export class DashboardExporter {
}
// templatize template vars
for (let variable of dash.templating.list) {
for (let variable of saveModel.templating.list) {
if (variable.type === 'query') {
templateizeDatasourceUsage(variable);
variable.options = [];
@ -87,7 +108,7 @@ export class DashboardExporter {
}
// templatize annotations vars
for (let annotationDef of dash.annotations.list) {
for (let annotationDef of saveModel.annotations.list) {
templateizeDatasourceUsage(annotationDef);
}
@ -105,7 +126,7 @@ export class DashboardExporter {
});
// templatize constants
for (let variable of dash.templating.list) {
for (let variable of saveModel.templating.list) {
if (variable.type === 'constant') {
var refName = 'VAR_' + variable.name.replace(' ', '_').toUpperCase();
inputs.push({
@ -133,7 +154,7 @@ export class DashboardExporter {
newObj["__inputs"] = inputs;
newObj["__requires"] = requires;
_.defaults(newObj, dash);
_.defaults(newObj, saveModel);
return newObj;
}).catch(err => {

View File

@ -98,12 +98,14 @@ export class DashboardModel {
var events = this.events;
var meta = this.meta;
var rows = this.rows;
var variables = this.templating.list;
delete this.events;
delete this.meta;
// prepare save model
this.rows = _.map(this.rows, row => row.getSaveModel());
events.emit('prepare-save-model');
this.rows = _.map(rows, row => row.getSaveModel());
this.templating.list = _.map(variables, variable => variable.getSaveModel ? variable.getSaveModel() : variable);
var copy = $.extend(true, {}, this);
@ -111,6 +113,8 @@ export class DashboardModel {
this.events = events;
this.meta = meta;
this.rows = rows;
this.templating.list = variables;
return copy;
}
@ -233,7 +237,6 @@ export class DashboardModel {
}
duplicatePanel(panel, row) {
var rowIndex = _.indexOf(this.rows, row);
var newPanel = angular.copy(panel);
newPanel.id = this.getNextPanelId();
@ -241,9 +244,9 @@ export class DashboardModel {
delete newPanel.repeatIteration;
delete newPanel.repeatPanelId;
delete newPanel.scopedVars;
delete newPanel.alert;
var currentRow = this.rows[rowIndex];
currentRow.panels.push(newPanel);
row.addPanel(newPanel);
return newPanel;
}

View File

@ -1,282 +0,0 @@
<topnav title="Alerting" subnav="false">
<ul class="nav">
<li class="active" ><a href="global-alerts">Global Alerts</a></li>
</ul>
</topnav>
<div class="page-container">
<div class="page-wide">
<h1>Global alerts</h1>
<div class="filter-controls-filters">
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item">Filters:</li>
<li class="tight-form-item">Alert State</li>
<li><!-- <value-select-dropdown></value-select-dropdown> --></li>
<li class="tight-form-item">Dashboards</li>
<li><!-- <value-select-dropdown></value-select-dropdown> --></li>
<li class="tight-form-item">
<a class="pointer">
<i class="fa fa-pencil"></i>
</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
<ul class="filter-controls-actions">
<li>
<div class="dropdown">
<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
<input class="cr1" id="state-enabled" type="checkbox">
<label for="state-enabled" class="cr1"></label> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a>All</a></li>
</ul>
</div>
</li>
<li>
<div class="dropdown">
<button class="btn btn-inverse dropdown-toggle" data-toggle="dropdown">
Bulk Actions &nbsp; <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a>Update notifications</a></li>
</ul>
</div>
</li>
<li>
<button class="btn btn-inverse" data-toggle="dropdown">
<i class="fa fa-fw fa-th-large"></i> New Dashboard from selected
</button>
</li>
<li>
<span class="filter-controls-actions-selected">2 selected, showing 6 of 6 total</span>
</li>
</ul>
<ul class="filter-list">
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert1" type="checkbox">
<label for="alert1" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Prod CPU Data Writes</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert1.expanded = !alert1.expanded">
<i class="fa fa-angle-right" ng-show="!alert1.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert1.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod CPU Data Writes</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 19 hours
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert1.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert2" type="checkbox" checked>
<label for="alert2" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Insanely Super Duper Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">client side full page load</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert2.expanded = !alert2.expanded">
<i class="fa fa-angle-right" ng-show="!alert2.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert2.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod DB Reads</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state warn">Warn</span> for 1 hour
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert2.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert3" type="checkbox" checked>
<label for="alert3" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Mildly Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Memory/CPU</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert3.expanded = !alert3.expanded">
<i class="fa fa-angle-right" ng-show="!alert3.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert3.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Prod CPU Data Writes</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state critical">Online</span> for 10 minutes
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert3.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert4" type="checkbox">
<label for="alert4" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Super Sekret</a></span>
<span class="filter-list-card-link">Panel: <a href="">Stacked lines</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert4.expanded = !alert4.expanded">
<i class="fa fa-angle-right" ng-show="!alert4.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert4.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">Critical Thing</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 5 weeks
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert4.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
<li>
<ul class="filter-list-card">
<li class="filter-list-card-select">
<input class="cr1" id="alert5" type="checkbox">
<label for="alert5" class="cr1"></label>
</li>
<li>
<div class="filter-list-card-controls">
<div class="filter-list-card-links">
<span class="filter-list-card-link"><i class="fa fa-fw fa-th-large"></i>: <a href="">OpSec Public</a></span>
<span class="filter-list-card-link">Panel: <a href="">More Critical Thing</a></span>
</div>
<div class="filter-list-card-config">
<a href="#"><i class="fa fa-cog"></i></a>
</div>
<div class="filter-list-card-expand" ng-click="alert5.expanded = !alert5.expanded">
<i class="fa fa-angle-right" ng-show="!alert5.expanded"></i>
<i class="fa fa-angle-down" ng-show="alert5.expanded"></i>
</div>
</div>
<span class="filter-list-card-title">More Critical Thing</span>
<span class="filter-list-card-status">
<span class="filter-list-card-state online">Online</span> for 2 months
</span>
</li>
</ul>
<div class="filter-list-card-details" ng-show="alert5.expanded">
<h5 class="filter-list-card-details-heading">Alert query <a>configure alerting</a></h5>
<div class="tight-form last">
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">A</li>
<li class="tight-form-item">apps</li>
<li class="tight-form-item"><i class="fa fa-asterisk"><i></i></i></li>
<li class="tight-form-item">fakesite</li>
<li class="tight-form-item">counters</li>
<li class="tight-form-item">requests</li>
<li class="tight-form-item">count</li>
<li class="tight-form-item">scaleToSeconds(1)</li>
<li class="tight-form-item">aliasByNode(2)</li>
</ul>
<div class="clearfix"></div>
</div>
</div>
</li>
</ul>
</div>
</div>

View File

@ -5,7 +5,7 @@
<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" ng-blur="ctrl.panelSearchBlur()"></input>
<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>

View File

@ -45,12 +45,6 @@ export class AddPanelCtrl {
}
}
panelSearchBlur() {
// this.$timeout(() => {
// this.rowCtrl.dropView = 0;
// }, 400);
}
moveSelection(direction) {
var max = this.panelHits.length;
var newIndex = this.activeIndex + direction;

View File

@ -19,7 +19,6 @@ export class DashRowCtrl {
if (this.row.isNew) {
this.dropView = 1;
delete this.row.isNew;
}
}
@ -35,8 +34,8 @@ export class DashRowCtrl {
title: config.new_panel_title,
type: panelId,
id: this.dashboard.getNextPanelId(),
isNew: true,
},
isNew: true,
};
} else {
dragObject = this.dashboard.getPanelInfoById(panelId);
@ -65,7 +64,7 @@ export class DashRowCtrl {
this.row.panels.push(dragObject.panel);
// if not new remove from source row
if (!dragObject.isNew) {
if (!dragObject.panel.isNew) {
dragObject.row.removePanel(dragObject.panel, false);
}
}

View File

@ -33,7 +33,11 @@ export class DashboardRow {
}
getSaveModel() {
this.model = {};
assignModelProperties(this.model, this, this.defaults);
// remove properties that dont server persisted purpose
delete this.model.isNew;
return this.model;
}

View File

@ -62,7 +62,9 @@ describe('dashboardSrv', function() {
it('duplicate panel should try to add it to same row', function() {
var panel = { span: 4, attr: '123', id: 10 };
dashboard.rows = [{ panels: [panel] }];
dashboard.addEmptyRow();
dashboard.rows[0].addPanel(panel);
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[0].span).to.be(4);
@ -73,7 +75,9 @@ describe('dashboardSrv', function() {
it('duplicate panel should remove repeat data', function() {
var panel = { span: 4, attr: '123', id: 10, repeat: 'asd', scopedVars: { test: 'asd' }};
dashboard.rows = [{ panels: [panel] }];
dashboard.addEmptyRow();
dashboard.rows[0].addPanel(panel);
dashboard.duplicatePanel(panel, dashboard.rows[0]);
expect(dashboard.rows[0].panels[1].repeat).to.be(undefined);

View File

@ -20,7 +20,6 @@ function dynamicDashScenario(desc, func) {
beforeEach(angularMocks.inject(function(dashboardSrv) {
ctx.dashboardSrv = dashboardSrv;
ctx.variableSrv = {};
var model = {
rows: [],
@ -29,9 +28,8 @@ function dynamicDashScenario(desc, func) {
setupFunc(model);
ctx.dash = ctx.dashboardSrv.create(model);
ctx.variableSrv.variables = ctx.dash.templating.list;
ctx.dynamicDashboardSrv = new DynamicDashboardSrv();
ctx.dynamicDashboardSrv.init(ctx.dash, ctx.variableSrv);
ctx.dynamicDashboardSrv.init(ctx.dash);
ctx.dynamicDashboardSrv.process();
ctx.rows = ctx.dash.rows;
}));

View File

@ -34,6 +34,14 @@ describe('given dashboard with repeated panels', function() {
options: []
});
dash.templating.list.push({
name: 'ds',
type: 'datasource',
query: 'testdb',
current: {value: 'prod', text: 'prod'},
options: []
});
dash.annotations.list.push({
name: 'logs',
datasource: 'gfdb',
@ -49,6 +57,7 @@ describe('given dashboard with repeated panels', function() {
datasource: '-- Mixed --',
targets: [{datasource: 'other'}],
},
{id: 5, datasource: '$ds'},
]
});
@ -87,7 +96,7 @@ describe('given dashboard with repeated panels', function() {
});
it('exported dashboard should not contain repeated panels', function() {
expect(exported.rows[0].panels.length).to.be(2);
expect(exported.rows[0].panels.length).to.be(3);
});
it('exported dashboard should not contain repeated rows', function() {

View File

@ -54,6 +54,12 @@ export class PanelCtrl {
this.events.emit('panel-teardown');
this.events.removeAllListeners();
});
// we should do something interesting
// with newly added panels
if (this.panel.isNew) {
delete this.panel.isNew;
}
}
init() {
@ -188,6 +194,9 @@ export class PanelCtrl {
duplicate() {
this.dashboard.duplicatePanel(this.panel, this.row);
this.$timeout(() => {
this.$scope.$root.$broadcast('render');
});
}
updateColumnSpan(span) {

View File

@ -68,8 +68,8 @@ module.directive('grafanaPanel', function($rootScope) {
// the reason for handling these classes this way is for performance
// limit the watchers on panels etc
var transparentLastState;
var lastHasAlertRule;
var transparentLastState = false;
var lastHasAlertRule = false;
var lastAlertState;
var hasAlertRule;
var lastHeight = 0;
@ -91,6 +91,12 @@ module.directive('grafanaPanel', function($rootScope) {
lastHeight = ctrl.containerHeight;
}
// set initial transparency
if (ctrl.panel.transparent) {
transparentLastState = true;
panelContainer.addClass('panel-transparent', true);
}
ctrl.events.on('render', () => {
if (lastHeight !== ctrl.containerHeight) {
panelContainer.css({minHeight: ctrl.containerHeight});

View File

@ -57,59 +57,3 @@
</div>
</div>
<div class="tight-form" ng-if="false">
<ul class="tight-form-list pull-right">
<li ng-show="ctrl.error" class="tight-form-item">
<a bs-tooltip="ctrl.error" style="color: rgb(229, 189, 28)" role="menuitem">
<i class="fa fa-warning"></i>
</a>
</li>
<li class="tight-form-item small" ng-show="ctrl.target.datasource">
<em>{{ctrl.target.datasource}}</em>
</li>
<li class="tight-form-item" ng-if="ctrl.toggleEditorMode">
<a class="pointer" tabindex="1" ng-click="ctrl.toggleEditorMode()">
<i class="fa fa-pencil"></i>
</a>
</li>
<li class="tight-form-item">
<div class="dropdown">
<a class="pointer dropdown-toggle" data-toggle="dropdown" tabindex="1">
<i class="fa fa-bars"></i>
</a>
<ul class="dropdown-menu pull-right" role="menu">
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.duplicateQuery()">Duplicate</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(-1)">Move up</a>
</li>
<li role="menuitem">
<a tabindex="1" ng-click="ctrl.moveQuery(1)">Move down</a>
</li>
</ul>
</div>
</li>
<li class="tight-form-item last">
<a class="pointer" tabindex="1" ng-click="ctrl.removeQuery(target)">
<i class="fa fa-trash"></i>
</a>
</li>
</ul>
<ul class="tight-form-list">
<li class="tight-form-item" style="min-width: 15px; text-align: center">
{{ctrl.target.refId}}
</li>
<li>
<a class="tight-form-item" ng-click="ctrl.toggleHideQuery()" role="menuitem">
<i class="fa fa-eye"></i>
</a>
</li>
</ul>
<ul class="tight-form-list" ng-transclude>
</ul>
<div class="clearfix"></div>
</div>

View File

@ -26,7 +26,7 @@ export class AdhocVariable implements Variable {
return Promise.resolve();
}
getModel() {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}

View File

@ -24,7 +24,7 @@ export class ConstantVariable implements Variable {
assignModelProperties(this, model, this.defaults);
}
getModel() {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}

View File

@ -34,7 +34,7 @@ export class CustomVariable implements Variable {
return this.variableSrv.setOptionAsCurrent(this, option);
}
getModel() {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}

View File

@ -30,8 +30,11 @@ export class DatasourceVariable implements Variable {
this.refresh = 1;
}
getModel() {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
// dont persist options
this.model.options = [];
return this.model;
}

View File

@ -34,7 +34,7 @@ export class IntervalVariable implements Variable {
this.refresh = 2;
}
getModel() {
getSaveModel() {
assignModelProperties(this.model, this, this.defaults);
return this.model;
}

View File

@ -136,7 +136,7 @@
<div ng-if="current.type === 'custom'" class="gf-form-group">
<h5 class="section-heading">Custom Options</h5>
<div class="gf-form">
<span class="gf-form-label width-13">Values separated by comma</span>
<span class="gf-form-label width-14">Values separated by comma</span>
<input type="text" class="gf-form-input" ng-model='current.query' ng-blur="runQuery()" placeholder="1, 10, 20, myvalue" required></input>
</div>
</div>

View File

@ -47,9 +47,15 @@ export class QueryVariable implements Variable {
assignModelProperties(this, model, this.defaults);
}
getModel() {
getSaveModel() {
// copy back model properties to model
assignModelProperties(this.model, this, this.defaults);
// remove options
if (this.refresh !== 0) {
this.model.options = [];
}
return this.model;
}

View File

@ -25,7 +25,7 @@ describe('QueryVariable', function() {
variable.regex = 'asd';
variable.sort = 50;
var model = variable.getModel();
var model = variable.getSaveModel();
expect(model.options.length).to.be(1);
expect(model.options[0].text).to.be('test');
expect(model.datasource).to.be('google');
@ -33,7 +33,14 @@ describe('QueryVariable', function() {
expect(model.sort).to.be(50);
});
});
it('if refresh != 0 then remove options in presisted mode', () => {
var variable = new QueryVariable({}, null, null, null, null);
variable.options = [{text: 'test'}];
variable.refresh = 1;
var model = variable.getSaveModel();
expect(model.options.length).to.be(0);
});
});
});

View File

@ -10,7 +10,7 @@ export interface Variable {
dependsOn(variable);
setValueFromUrl(urlValue);
getValueForUrl();
getModel();
getSaveModel();
}
export var variableTypes = {};

View File

@ -20,12 +20,9 @@ export class VariableSrv {
this.dashboard = dashboard;
// create working class models representing variables
this.variables = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this));
this.templateSrv.init(this.variables);
// register event to sync back to persisted model
this.dashboard.events.on('prepare-save-model', this.syncToDashboardModel.bind(this));
// init variables
for (let variable of this.variables) {
variable.initLock = this.$q.defer();
@ -99,12 +96,6 @@ export class VariableSrv {
return variable;
}
syncToDashboardModel() {
this.dashboard.templating.list = this.variables.map(variable => {
return variable.getModel();
});
}
updateOptions(variable) {
return variable.updateOptions();
}

View File

@ -1,3 +0,0 @@
<li ng-class="{active: active, disabled: disabled}">
<a href ng-click="select()" tab-heading-transclude>{{heading}}</a>
</li>

View File

@ -1,11 +0,0 @@
<div>
<ul class="nav nav-tabs" ng-class="{'nav-stacked': vertical, 'nav-justified': justified}" ng-transclude>
</ul>
<div class="tab-content">
<div class="tab-pane"
ng-repeat="tab in tabs"
ng-class="{active: tab.active}"
tab-content-transclude="tab">
</div>
</div>
</div>

View File

@ -1,11 +1,12 @@
<navbar title="404" icon="fa fa-fw fa-question" title-url="/">
</navbar>
<div class="row-fluid" style="margin-top: 100px;">
<div class="span2"></div>
<div class="page-container">
<div class="grafana-info-box span8 text-center">
<h3>Page not found (404)</h3>
</div>
<div class="span2"></div>
<div class="page-header">
<h1>
Page not found (404)
</h1>
</div>
</div>

View File

@ -1,5 +1,5 @@
{
"revision": 5,
"revision": 6,
"title": "TestData - Graph Panel Last 1h",
"tags": [
"grafana-test"
@ -7,8 +7,48 @@
"style": "dark",
"timezone": "browser",
"editable": true,
"hideControls": false,
"sharedCrosshair": false,
"hideControls": false,
"time": {
"from": "2016-11-16T16:59:38.294Z",
"to": "2016-11-16T17:09:01.532Z"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 4,
"links": [],
"gnetId": null,
"rows": [
{
"collapse": false,
@ -238,7 +278,13 @@
]
}
],
"title": "New row"
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
},
{
"collapse": false,
@ -332,7 +378,13 @@
"type": "text"
}
],
"title": "New row"
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
},
{
"collapse": false,
@ -371,7 +423,7 @@
"yaxis": 2
}
],
"span": 7.99561403508772,
"span": 8,
"stack": false,
"steppedLine": false,
"targets": [
@ -432,12 +484,18 @@
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4.00438596491228,
"span": 4,
"title": "",
"type": "text"
}
],
"title": "New row"
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
},
{
"collapse": false,
@ -545,7 +603,7 @@
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"span": 3,
"span": 4,
"stack": false,
"steppedLine": false,
"targets": [
@ -592,6 +650,31 @@
}
]
},
{
"content": "Should be a long line connecting the null region in the `connected` mode, and in zero it should just be a line with zero value at the null points. ",
"editable": true,
"error": false,
"id": 13,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
}
],
"title": "New row",
"showTitle": false,
"titleSize": "h6",
"isNew": false,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null
},
{
"isNew": false,
"title": "Dashboard Row",
"panels": [
{
"aliasColors": {},
"bars": false,
@ -624,7 +707,7 @@
"zindex": -3
}
],
"span": 5,
"span": 8,
"stack": true,
"steppedLine": false,
"targets": [
@ -687,49 +770,149 @@
"show": true
}
]
},
{
"content": "Stacking values on top of nulls, should treat the null values as zero. ",
"editable": true,
"error": false,
"id": 14,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
}
],
"title": "New row"
"showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
},
{
"isNew": false,
"title": "Dashboard Row",
"panels": [
{
"aliasColors": {},
"bars": false,
"datasource": "Grafana TestData",
"editable": true,
"error": false,
"fill": 1,
"id": 12,
"isNew": true,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 2,
"links": [],
"nullPointMode": "null",
"percentage": false,
"pointradius": 5,
"points": false,
"renderer": "flot",
"seriesOverrides": [
{
"alias": "B-series",
"zindex": -3
}
],
"span": 8,
"stack": true,
"steppedLine": false,
"targets": [
{
"hide": false,
"refId": "B",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": "",
"alias": ""
},
{
"alias": "",
"hide": false,
"refId": "A",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": ""
},
{
"alias": "",
"hide": false,
"refId": "C",
"scenarioId": "csv_metric_values",
"stringInput": "1,20,40,null,null,null,null,null,null,100,10,10,20,30,40,10",
"target": ""
}
],
"thresholds": [],
"timeFrom": null,
"timeShift": null,
"title": "Stacking all series null segment",
"tooltip": {
"msResolution": false,
"shared": true,
"sort": 0,
"value_type": "cumulative"
},
"type": "graph",
"xaxis": {
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
]
},
{
"content": "Stacking when all values are null should leave a gap in the graph",
"editable": true,
"error": false,
"id": 15,
"isNew": true,
"links": [],
"mode": "markdown",
"span": 4,
"title": "",
"type": "text"
}
],
"showTitle": false,
"titleSize": "h6",
"height": 250,
"repeat": null,
"repeatRowId": null,
"repeatIteration": null,
"collapse": false
}
],
"time": {
"from": "now-1h",
"to": "now"
},
"timepicker": {
"refresh_intervals": [
"5s",
"10s",
"30s",
"1m",
"5m",
"15m",
"30m",
"1h",
"2h",
"1d"
],
"time_options": [
"5m",
"15m",
"1h",
"6h",
"12h",
"24h",
"2d",
"7d",
"30d"
]
},
"templating": {
"list": []
},
"annotations": {
"list": []
},
"refresh": false,
"schemaVersion": 13,
"version": 13,
"links": [],
"gnetId": null
]
}

View File

@ -9,7 +9,7 @@
"name": "Grafana Project",
"url": "http://grafana.org"
},
"version": "1.0.14",
"version": "1.0.15",
"updated": "2016-09-26"
},

View File

@ -37,7 +37,8 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
query.statistics = target.statistics;
var period = this._getPeriod(target, query, options, start, end);
var now = Math.round(Date.now() / 1000);
var period = this._getPeriod(target, query, options, start, end, now);
target.period = period;
query.period = period;
@ -67,11 +68,19 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
});
};
this._getPeriod = function(target, query, options, start, end) {
this._getPeriod = function(target, query, options, start, end, now) {
var period;
var range = end - start;
if (!target.period) {
var daySec = 60 * 60 * 24;
var periodUnit = 60;
if (now - start > (daySec * 15)) { // until 63 days ago
periodUnit = period = 60 * 5;
} else if (now - start > (daySec * 63)) { // until 455 days ago
periodUnit = period = 60 * 60;
} else if (now - start > (daySec * 455)) { // over 455 days, should return error, but try to long period
periodUnit = period = 60 * 60;
} else if (!target.period) {
period = (query.namespace === 'AWS/EC2') ? 300 : 60;
} else if (/^\d+$/.test(target.period)) {
period = parseInt(target.period, 10);
@ -82,7 +91,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
period = 60;
}
if (range / period >= 1440) {
period = Math.ceil(range / 1440 / 60) * 60;
period = Math.ceil(range / 1440 / periodUnit) * periodUnit;
}
return period;

View File

@ -22,7 +22,7 @@ function ($) {
var len = series.datapoints.points.length;
for (var j = initial; j < len; j += ps) {
// Special case of a non stepped line, highlight the very last point just before a null point
if ((series.datapoints.points[initial] != null && series.datapoints.points[j] == null && ! series.lines.steps)
if ((!series.lines.steps && series.datapoints.points[initial] != null && series.datapoints.points[j] == null)
//normal case
|| series.datapoints.points[j] > posX) {
return Math.max(j - ps, 0)/ps;
@ -195,7 +195,7 @@ function ($) {
}
var highlightClass = '';
if (item && i === item.seriesIndex) {
if (item && hoverInfo.index === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight';
}

View File

@ -208,11 +208,8 @@ class SingleStatCtrl extends MetricsPanelCtrl {
}
// Add $__name variable for using in prefix or postfix
data.scopedVars = {
__name: {
value: this.series[0].label
}
};
data.scopedVars = _.extend({}, this.panel.scopedVars);
data.scopedVars["__name"] = {value: this.series[0].label};
}
// check value to text mappings if its enabled
@ -526,7 +523,7 @@ class SingleStatCtrl extends MetricsPanelCtrl {
elem.toggleClass('pointer', panel.links.length > 0);
if (panel.links.length > 0) {
linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], panel.scopedVars);
linkInfo = linkSrv.getPanelLinkAnchorInfo(panel.links[0], data.scopedVars);
} else {
linkInfo = null;
}

View File

@ -50,11 +50,9 @@
@import "components/tagsinput";
@import "components/tables_lists";
@import "components/search";
@import "components/tightform";
@import "components/gf-form";
@import "components/sidemenu";
@import "components/navbar";
@import "components/gfbox";
@import "components/timepicker";
@import "components/filter-controls";
@import "components/filter-list";

View File

@ -1,69 +0,0 @@
.gf-box {
margin: 10px 5px;
background-color: $page-bg;
position: relative;
border: 1px solid $tight-form-func-bg;
}
.gf-box-no-margin {
margin: 0;
}
.gf-box-header-close-btn {
float: right;
padding: 0;
margin: 0;
background-color: transparent;
border: none;
padding: 8px;
i {
font-size: 120%;
}
color: $text-color;
&:hover {
color: $white;
}
}
.gf-box-header-save-btn {
padding: 7px 0;
float: right;
color: $gray-2;
font-style: italic;
}
.gf-box-body {
padding: 20px;
min-height: 150px;
}
.gf-box-footer {
overflow: hidden;
}
.gf-box-header {
border-bottom: 1px solid $tight-form-func-bg;
overflow: hidden;
background-color: $tight-form-bg;
.tabs {
float: left;
}
.nav {
margin: 0;
}
}
.gf-box-title {
padding-right: 20px;
padding-left: 10px;
float: left;
color: $link-color;
font-size: 18px;
font-weight: normal;
line-height: 38px;
margin: 0;
.fa {
padding: 0 8px 0 5px;
color: $text-color;
}
}

View File

@ -87,7 +87,7 @@
}
// temp hack
.modal-body, .gf-box {
.modal-body {
.nav-tabs {
border-bottom: none;
}

View File

@ -67,3 +67,82 @@
}
}
.grafana-metric-options {
margin-top: 25px;
}
.tight-form-func {
background: $tight-form-func-bg;
&.show-function-controls {
padding-top: 5px;
min-width: 100px;
text-align: center;
}
}
input[type="text"].tight-form-func-param {
background: transparent;
border: none;
margin: 0;
padding: 0;
}
.tight-form-func-controls {
display: none;
text-align: center;
.fa-arrow-left {
float: left;
position: relative;
top: 2px;
}
.fa-arrow-right {
float: right;
position: relative;
top: 2px;
}
.fa-remove {
margin-left: 10px;
}
}
.grafana-metric-options {
margin-top: 25px;
}
.tight-form-func {
background: $tight-form-func-bg;
&.show-function-controls {
padding-top: 5px;
min-width: 100px;
text-align: center;
}
}
input[type="text"].tight-form-func-param {
background: transparent;
border: none;
margin: 0;
padding: 0;
}
.tight-form-func-controls {
display: none;
text-align: center;
.fa-arrow-left {
float: left;
position: relative;
top: 2px;
}
.fa-arrow-right {
float: right;
position: relative;
top: 2px;
}
.fa-remove {
margin-left: 10px;
}
}

View File

@ -74,12 +74,11 @@
.add-panel-panels-scroll {
width: 100%;
overflow: auto;
-ms-overflow-style: none;
&::-webkit-scrollbar {
display: none
}
-ms-overflow-style: none;
}
.add-panel-panels {

View File

@ -6,6 +6,8 @@
}
.shortcut-table {
margin-bottom: $spacer;
.shortcut-table-category-header {
font-weight: normal;
font-size: $font-size-h6;
@ -26,8 +28,6 @@
text-align: right;
color: $text-color;
}
margin-bottom: $spacer;
}
.shortcut-table-key {

View File

@ -7,11 +7,12 @@
}
.annotation-segment {
padding: 8px 7px;
label.cr1 {
margin-left: 5px;
margin-top: 3px;
}
padding: 8px 7px;
}
.submenu-item {
@ -31,14 +32,14 @@
.variable-value-link {
padding-right: 10px;
.label-tag {
margin: 0 5px;
}
padding: 8px 7px;
box-sizing: content-box;
display: inline-block;
color: $text-color;
.label-tag {
margin: 0 5px;
}
}
.variable-link-wrapper {

View File

@ -38,10 +38,10 @@
background-color: transparent;
border: none;
padding: ($tabs-padding-top + $tabs-top-margin) $spacer $tabs-padding-bottom;
color: $text-color;
i {
font-size: 120%;
}
color: $text-color;
&:hover {
color: $white;
}

View File

@ -1,235 +0,0 @@
.tight-form {
border-top: 1px solid $tight-form-border;
border-left: 1px solid $tight-form-border;
border-right: 1px solid $tight-form-border;
background: $tight-form-bg;
&.last {
border-bottom: 1px solid $tight-form-border;
}
&.borderless {
background: transparent;
border: none;
}
.checkbox-label {
display: inline;
padding-right: 4px;
margin-bottom: 0;
cursor: pointer;
}
}
.tight-form-container-no-item-borders {
border: 1px solid $tight-form-border;
border-bottom: none;
.tight-form, .tight-form-item, [type="text"].tight-form-input, [type="text"].tight-form-clear-input {
border: none;
}
}
.spaced-form {
.tight-form {
margin: 7px 0;
}
}
.borderless {
.tight-form-item,
.tight-form-input {
border: none;
}
}
.tight-form-container {
border-bottom: 1px solid $tight-form-border;
}
.tight-form-btn {
padding: 7px 12px;
}
.tight-form-list {
list-style: none;
margin: 0;
>li {
float: left;
}
}
.tight-form-flex-wrapper {
display: flex;
flex-direction: row;
float: none !important;
}
.grafana-metric-options {
margin-top: 25px;
}
.tight-form-item {
padding: 8px 7px;
box-sizing: content-box;
display: inline-block;
font-weight: normal;
border-right: 1px solid $tight-form-border;
display: inline-block;
color: $text-color;
.has-open-function & {
padding-top: 25px;
}
.tight-form-disabled & {
color: $link-color-disabled;
a {
color: $link-color-disabled;
}
}
&:hover, &:focus {
text-decoration: none;
}
&a:hover {
background: $tight-form-func-bg;
}
&.last {
border-right: none;
}
}
.tight-form-item-icon {
i {
width: 15px;
text-align: center;
display: inline-block;
}
}
.tight-form-func {
background: $tight-form-func-bg;
&.show-function-controls {
padding-top: 5px;
min-width: 100px;
text-align: center;
}
}
input[type="text"].tight-form-func-param {
background: transparent;
border: none;
margin: 0;
padding: 0;
}
input[type="text"].tight-form-clear-input {
padding: 8px 7px;
border: none;
margin: 0px;
background: transparent;
border-radius: 0;
border-right: 1px solid $tight-form-border;
}
[type="text"],
[type="email"],
[type="number"],
[type="password"] {
&.tight-form-input {
background-color: $input-bg;
border: none;
border-right: 1px solid $tight-form-border;
margin: 0px;
border-radius: 0;
padding: 8px 6px;
height: 100%;
box-sizing: border-box;
&.last {
border-right: none;
}
}
}
input[type="checkbox"].tight-form-checkbox {
margin: 0;
}
.tight-form-textarea {
height: 200px;
margin: 0;
box-sizing: border-box;
}
select.tight-form-input {
border: none;
border-right: 1px solid $tight-form-border;
background-color: $input-bg;
margin: 0px;
border-radius: 0;
height: 36px;
padding: 9px 3px;
&.last {
border-right: none;
}
}
.tight-form-func-controls {
display: none;
text-align: center;
.fa-arrow-left {
float: left;
position: relative;
top: 2px;
}
.fa-arrow-right {
float: right;
position: relative;
top: 2px;
}
.fa-remove {
margin-left: 10px;
}
}
.tight-form-radio {
input[type="radio"] {
margin: 0;
}
label {
display: inline;
}
}
.tight-form-section {
margin-bottom: 20px;
margin-right: 40px;
vertical-align: top;
display: inline-block;
.tight-form {
margin-left: 20px;
}
}
.tight-form-align {
padding-left: 66px;
}
.tight-form-item-large { width: 115px; }
.tight-form-item-xlarge { width: 150px; }
.tight-form-item-xxlarge { width: 200px; }
.tight-form-input.tight-form-item-xxlarge {
width: 215px;
}
.tight-form-inner-box {
margin: 20px 0 20px 148px;
display: inline-block;
}

View File

@ -65,15 +65,17 @@
}
.gf-timepicker-component {
margin-bottom: 10px;
padding: $spacer/2 0 $spacer 0;
td {
padding: 1px;
}
button.btn-sm {
@include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl);
font-size: $font-size-sm;
background-image: none;
border: none;
padding: 6px 10px;
padding: 5px 11px;
color: $text-color;
&.active span {
color: $blue;

View File

@ -62,12 +62,6 @@
.admin-page {
max-width: 800px;
margin-left: 10px;
.gf-box {
margin-top: 0;
}
.gf-box-body {
min-height: 0;
}
h2 {
margin-left: 15px;
margin-bottom: 0px;

View File

@ -61,7 +61,6 @@
}
&--ok {
box-shadow: 0 0 5px rgba(0,200,0,10.8);
.panel-alert-icon:before {
color: $online;
content: "\e611";

View File

@ -172,6 +172,12 @@ div.flot-text {
}
}
.panel-in-fullscreen {
.panel-drop-zone {
display: none !important;
}
}
.panel-time-info {
font-weight: bold;
float: right;

View File

@ -5,8 +5,8 @@
* Version: 0.13.4 - 2015-09-03
* License: MIT
*/
angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker","ui.bootstrap.tabs"]);
angular.module("ui.bootstrap.tpls", ["template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html","template/tabs/tab.html","template/tabs/tabset.html"]);
angular.module("ui.bootstrap", ["ui.bootstrap.tpls","ui.bootstrap.position","ui.bootstrap.dateparser","ui.bootstrap.datepicker"]);
angular.module("ui.bootstrap.tpls", ["template/datepicker/datepicker.html","template/datepicker/day.html","template/datepicker/month.html","template/datepicker/popup.html","template/datepicker/year.html"]);
angular.module('ui.bootstrap.position', [])
/**
@ -1180,302 +1180,6 @@ function($compile, $parse, $document, $rootScope, $position, dateFilter, datePar
});
/**
* @ngdoc overview
* @name ui.bootstrap.tabs
*
* @description
* AngularJS version of the tabs directive.
*/
angular.module('ui.bootstrap.tabs', [])
.controller('TabsetController', ['$scope', function TabsetCtrl($scope) {
var ctrl = this,
tabs = ctrl.tabs = $scope.tabs = [];
ctrl.select = function(selectedTab) {
angular.forEach(tabs, function(tab) {
if (tab.active && tab !== selectedTab) {
tab.active = false;
tab.onDeselect();
selectedTab.selectCalled = false;
}
});
selectedTab.active = true;
// only call select if it has not already been called
if (!selectedTab.selectCalled) {
selectedTab.onSelect();
selectedTab.selectCalled = true;
}
};
ctrl.addTab = function addTab(tab) {
tabs.push(tab);
// we can't run the select function on the first tab
// since that would select it twice
if (tabs.length === 1 && tab.active !== false) {
tab.active = true;
} else if (tab.active) {
ctrl.select(tab);
} else {
tab.active = false;
}
};
ctrl.removeTab = function removeTab(tab) {
var index = tabs.indexOf(tab);
//Select a new tab if the tab to be removed is selected and not destroyed
if (tab.active && tabs.length > 1 && !destroyed) {
//If this is the last tab, select the previous tab. else, the next tab.
var newActiveIndex = index == tabs.length - 1 ? index - 1 : index + 1;
ctrl.select(tabs[newActiveIndex]);
}
tabs.splice(index, 1);
};
var destroyed;
$scope.$on('$destroy', function() {
destroyed = true;
});
}])
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tabset
* @restrict EA
*
* @description
* Tabset is the outer container for the tabs directive
*
* @param {boolean=} vertical Whether or not to use vertical styling for the tabs.
* @param {boolean=} justified Whether or not to use justified styling for the tabs.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<tabset>
<tab heading="Tab 1"><b>First</b> Content!</tab>
<tab heading="Tab 2"><i>Second</i> Content!</tab>
</tabset>
<hr />
<tabset vertical="true">
<tab heading="Vertical Tab 1"><b>First</b> Vertical Content!</tab>
<tab heading="Vertical Tab 2"><i>Second</i> Vertical Content!</tab>
</tabset>
<tabset justified="true">
<tab heading="Justified Tab 1"><b>First</b> Justified Content!</tab>
<tab heading="Justified Tab 2"><i>Second</i> Justified Content!</tab>
</tabset>
</file>
</example>
*/
.directive('tabset', function() {
return {
restrict: 'EA',
transclude: true,
replace: true,
scope: {
type: '@'
},
controller: 'TabsetController',
templateUrl: 'template/tabs/tabset.html',
link: function(scope, element, attrs) {
scope.vertical = angular.isDefined(attrs.vertical) ? scope.$parent.$eval(attrs.vertical) : false;
scope.justified = angular.isDefined(attrs.justified) ? scope.$parent.$eval(attrs.justified) : false;
}
};
})
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tab
* @restrict EA
*
* @param {string=} heading The visible heading, or title, of the tab. Set HTML headings with {@link ui.bootstrap.tabs.directive:tabHeading tabHeading}.
* @param {string=} select An expression to evaluate when the tab is selected.
* @param {boolean=} active A binding, telling whether or not this tab is selected.
* @param {boolean=} disabled A binding, telling whether or not this tab is disabled.
*
* @description
* Creates a tab with a heading and content. Must be placed within a {@link ui.bootstrap.tabs.directive:tabset tabset}.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<div ng-controller="TabsDemoCtrl">
<button class="btn btn-small" ng-click="items[0].active = true">
Select item 1, using active binding
</button>
<button class="btn btn-small" ng-click="items[1].disabled = !items[1].disabled">
Enable/disable item 2, using disabled binding
</button>
<br />
<tabset>
<tab heading="Tab 1">First Tab</tab>
<tab select="alertMe()">
<tab-heading><i class="icon-bell"></i> Alert me!</tab-heading>
Second Tab, with alert callback and html heading!
</tab>
<tab ng-repeat="item in items"
heading="{{item.title}}"
disabled="item.disabled"
active="item.active">
{{item.content}}
</tab>
</tabset>
</div>
</file>
<file name="script.js">
function TabsDemoCtrl($scope) {
$scope.items = [
{ title:"Dynamic Title 1", content:"Dynamic Item 0" },
{ title:"Dynamic Title 2", content:"Dynamic Item 1", disabled: true }
];
$scope.alertMe = function() {
setTimeout(function() {
alert("You've selected the alert tab!");
});
};
};
</file>
</example>
*/
/**
* @ngdoc directive
* @name ui.bootstrap.tabs.directive:tabHeading
* @restrict EA
*
* @description
* Creates an HTML heading for a {@link ui.bootstrap.tabs.directive:tab tab}. Must be placed as a child of a tab element.
*
* @example
<example module="ui.bootstrap">
<file name="index.html">
<tabset>
<tab>
<tab-heading><b>HTML</b> in my titles?!</tab-heading>
And some content, too!
</tab>
<tab>
<tab-heading><i class="icon-heart"></i> Icon heading?!?</tab-heading>
That's right.
</tab>
</tabset>
</file>
</example>
*/
.directive('tab', ['$parse', '$log', function($parse, $log) {
return {
require: '^tabset',
restrict: 'EA',
replace: true,
templateUrl: 'template/tabs/tab.html',
transclude: true,
scope: {
active: '=?',
heading: '@',
onSelect: '&select', //This callback is called in contentHeadingTransclude
//once it inserts the tab's content into the dom
onDeselect: '&deselect'
},
controller: function() {
//Empty controller so other directives can require being 'under' a tab
},
link: function(scope, elm, attrs, tabsetCtrl, transclude) {
scope.$watch('active', function(active) {
if (active) {
tabsetCtrl.select(scope);
}
});
scope.disabled = false;
if (attrs.disable) {
scope.$parent.$watch($parse(attrs.disable), function(value) {
scope.disabled = !! value;
});
}
// Deprecation support of "disabled" parameter
// fix(tab): IE9 disabled attr renders grey text on enabled tab #2677
// This code is duplicated from the lines above to make it easy to remove once
// the feature has been completely deprecated
if (attrs.disabled) {
$log.warn('Use of "disabled" attribute has been deprecated, please use "disable"');
scope.$parent.$watch($parse(attrs.disabled), function(value) {
scope.disabled = !! value;
});
}
scope.select = function() {
if (!scope.disabled) {
scope.active = true;
}
};
tabsetCtrl.addTab(scope);
scope.$on('$destroy', function() {
tabsetCtrl.removeTab(scope);
});
//We need to transclude later, once the content container is ready.
//when this link happens, we're inside a tab heading.
scope.$transcludeFn = transclude;
}
};
}])
.directive('tabHeadingTransclude', function() {
return {
restrict: 'A',
require: '^tab',
link: function(scope, elm, attrs, tabCtrl) {
scope.$watch('headingElement', function updateHeadingElement(heading) {
if (heading) {
elm.html('');
elm.append(heading);
}
});
}
};
})
.directive('tabContentTransclude', function() {
return {
restrict: 'A',
require: '^tabset',
link: function(scope, elm, attrs) {
var tab = scope.$eval(attrs.tabContentTransclude);
//Now our tab is ready to be transcluded: both the tab heading area
//and the tab content area are loaded. Transclude 'em both.
tab.$transcludeFn(tab.$parent, function(contents) {
angular.forEach(contents, function(node) {
if (isTabHeading(node)) {
//Let tabHeadingTransclude know.
tab.headingElement = node;
} else {
elm.append(node);
}
});
});
}
};
function isTabHeading(node) {
return node.tagName && (
node.hasAttribute('tab-heading') ||
node.hasAttribute('data-tab-heading') ||
node.hasAttribute('x-tab-heading') ||
node.tagName.toLowerCase() === 'tab-heading' ||
node.tagName.toLowerCase() === 'data-tab-heading' ||
node.tagName.toLowerCase() === 'x-tab-heading'
);
}
});
angular.module("template/datepicker/datepicker.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("template/datepicker/datepicker.html",
"<div ng-switch=\"datepickerMode\" role=\"application\" ng-keydown=\"keydown($event)\">\n" +
@ -1568,25 +1272,3 @@ angular.module("template/datepicker/year.html", []).run(["$templateCache", funct
"");
}]);
angular.module("template/tabs/tab.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("template/tabs/tab.html",
"<li ng-class=\"{active: active, disabled: disabled}\">\n" +
" <a href ng-click=\"select()\" tab-heading-transclude>{{heading}}</a>\n" +
"</li>\n" +
"");
}]);
angular.module("template/tabs/tabset.html", []).run(["$templateCache", function($templateCache) {
$templateCache.put("template/tabs/tabset.html",
"<div>\n" +
" <ul class=\"nav nav-{{type || 'tabs'}} nav-tabs-alt\" ng-class=\"{'nav-stacked': vertical, 'nav-justified': justified}\" ng-transclude></ul>\n" +
" <div class=\"tab-content\">\n" +
" <div class=\"tab-pane\" \n" +
" ng-repeat=\"tab in tabs\" \n" +
" ng-class=\"{active: tab.active}\"\n" +
" tab-content-transclude=\"tab\">\n" +
" </div>\n" +
" </div>\n" +
"</div>\n" +
"");
}]);

View File

@ -72,52 +72,49 @@ charts or filled areas).
horizontal = s.bars.horizontal,
withbottom = ps > 2 && (horizontal ? datapoints.format[2].x : datapoints.format[2].y),
withsteps = withlines && s.lines.steps,
fromgap = true,
keyOffset = horizontal ? 1 : 0,
accumulateOffset = horizontal ? 0 : 1,
i = 0, j = 0, l, m;
while (true) {
// browse all points from the current series and from the previous series
if (i >= points.length && j >= otherpoints.length)
break;
// newpoints will replace current series with
// as many points as different timestamps we have in the 2 (current & previous) series
l = newpoints.length;
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
if (i < points.length && px == null) {
// let's ignore null points from current series, nothing to do with them
i += ps;
}
else if (j < otherpoints.length && qx == null) {
// let's ignore null points from previous series, nothing to do with them
j += otherps;
}
else if (i >= points.length) {
// no more points in the current series, simply take the remaining points
// from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
bottom = qy;
j += otherps;
}
else if (j >= otherpoints.length) {
// no more points in the previous series, of course let's take
// the remaining points from the current series
if (i < points.length && points[i] == null) {
// copy gaps
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else if (i >= points.length) {
// take the remaining points from the previous series
for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
if (withbottom)
newpoints[l + 2] = otherpoints[j + accumulateOffset];
j += otherps;
}
else if (j >= otherpoints.length) {
// take the remaining points from the current series
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
i += ps;
}
else if (j < otherpoints.length && otherpoints[j] == null) {
// ignore point
j += otherps;
}
else {
// next available points from current and previous series have the same timestamp
// cases where we actually got two points
px = points[i + keyOffset];
py = points[i + accumulateOffset];
qx = otherpoints[j + keyOffset];
qy = otherpoints[j + accumulateOffset];
bottom = 0;
if (px == qx) {
// so take the point from the current series and skip the previous' one
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
@ -127,23 +124,27 @@ charts or filled areas).
i += ps;
j += otherps;
}
// next available point with the smallest timestamp is from the previous series
else if (px > qx) {
// so take the point from the previous series so that next series will correctly stack
for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
// we might be able to interpolate
if (i > 0 && points[i - ps] != null)
newpoints[l + accumulateOffset] += py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
bottom = qy;
// take the point from the previous series so that next series will correctly stack
if (i == 0) {
for (m = 0; m < ps; ++m)
newpoints.push(otherpoints[j + m]);
bottom = qy;
}
// we got past point below, might need to
// insert interpolated extra point
if (i > 0 && points[i - ps] != null) {
intery = py + (points[i - ps + accumulateOffset] - py) * (qx - px) / (points[i - ps + keyOffset] - px);
newpoints.push(qx);
newpoints.push(intery + qy);
for (m = 2; m < ps; ++m)
newpoints.push(points[i + m]);
bottom = qy;
}
j += otherps;
}
// (px < qx) next available point with the smallest timestamp is from the current series
else {
// so of course let's take the point from the current series
else { // px < qx
for (m = 0; m < ps; ++m)
newpoints.push(points[i + m]);
@ -156,10 +157,22 @@ charts or filled areas).
i += ps;
}
}
if (l != newpoints.length && withbottom)
newpoints[l + 2] = bottom;
fromgap = false;
if (l != newpoints.length && withbottom)
newpoints[l + 2] = bottom;
}
// maintain the line steps invariant
if (withsteps && l != newpoints.length && l > 0
&& newpoints[l] != null
&& newpoints[l] != newpoints[l - ps]
&& newpoints[l + 1] != newpoints[l - ps + 1]) {
for (m = 0; m < ps; ++m)
newpoints[l + ps + m] = newpoints[l + m];
newpoints[l + 1] = newpoints[l - ps + 1];
}
}
datapoints.points = newpoints;

View File

@ -5,28 +5,32 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width">
<title>Grafana</title>
<title>Grafana - Error</title>
<link href='[[.AppSubUrl]]/public/css/fonts.min.css' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css">
<link rel="stylesheet" href="[[.AppSubUrl]]/public/css/grafana.dark.min.css" title="Dark">
<link rel="icon" type="image/png" href="[[.AppSubUrl]]/public/img/fav32.png">
<base href="[[.AppSubUrl]]/" />
</head>
<body>
<div class="gf-box" style="margin: 200px auto 0 auto; width: 500px;">
<div class="gf-box-header">
<span class="gf-box-title">
<div class="page-container">
<div class="page-header">
<h1>
Server side error :(
</span>
</h1>
</div>
<div class="gf-box-body">
<h4>[[.Title]]</h4>
[[.ErrorMsg]]
</div>
<h4>[[.Title]]</h4>
<pre>[[.ErrorMsg]]</pre>
</div>
</body>
</body>
</html>