Commit 2df8c649 by bergquist

Merge branch 'master' into alerting_opentsdb

parents 5fbab038 a826591e
...@@ -12,11 +12,15 @@ module.exports = function (grunt) { ...@@ -12,11 +12,15 @@ module.exports = function (grunt) {
platform: process.platform.replace('win32', 'windows'), platform: process.platform.replace('win32', 'windows'),
}; };
if (grunt.option('arch')) {
config.arch = grunt.option('arch');
} else {
config.arch = os.arch();
if (process.platform.match(/^win/)) { if (process.platform.match(/^win/)) {
config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86'; config.arch = process.env.hasOwnProperty('ProgramFiles(x86)') ? 'x64' : 'x86';
} }
}
config.arch = grunt.option('arch') || os.arch();
config.phjs = grunt.option('phjsToRelease'); config.phjs = grunt.option('phjsToRelease');
......
...@@ -25,10 +25,13 @@ install: ...@@ -25,10 +25,13 @@ install:
build_script: build_script:
- go run build.go build - go run build.go build
- grunt release - grunt release
#- 7z a grafana.zip %APPVEYOR_BUILD_FOLDER%\dist\* - go run build.go sha1-dist
- cp dist/* . - cp dist/* .
artifacts: artifacts:
- path: grafana-*windows-ia32.zip - path: grafana-*windows-*.*
#- path: dist/*
name: binzip name: binzip
deploy:
- provider: Environment
name: GrafanaBuildsS3
...@@ -98,6 +98,9 @@ func main() { ...@@ -98,6 +98,9 @@ func main() {
createDebPackages() createDebPackages()
sha1FilesInDist() sha1FilesInDist()
case "sha1-dist":
sha1FilesInDist()
case "latest": case "latest":
makeLatestDistCopies() makeLatestDistCopies()
sha1FilesInDist() sha1FilesInDist()
......
bind-address = "0.0.0.0"
[logging]
level = "debug"
file = "/opt/influxdb/shared/data/influxdb.log" # stdout to log to standard out
[admin]
port = 8083 # binding is disabled if the port isn't set
assets = "/opt/influxdb/current/admin"
[api]
port = 8086 # binding is disabled if the port isn't set
read-timeout = "5s"
[input_plugins]
[input_plugins.graphite]
enabled = true
port = 2004
database = "graphite" # store graphite data in this database
[raft]
port = 8090
dir = "/opt/influxdb/shared/data/raft"
[storage]
dir = "/opt/influxdb/shared/data/db"
# How many requests to potentially buffer in memory. If the buffer gets filled then writes
# will still be logged and once the local storage has caught up (or compacted) the writes
# will be replayed from the WAL
write-buffer-size = 10000
default-engine = "rocksdb"
max-open-shards = 0
point-batch-size = 100
write-batch-size = 5000000
retention-sweep-period = "10m"
[storage.engines.rocksdb]
max-open-files = 1000
lru-cache-size = "200m"
[storage.engines.leveldb]
max-open-files = 1000
lru-cache-size = "200m"
[cluster]
protobuf_port = 8099
protobuf_timeout = "2s" # the write timeout on the protobuf conn any duration parseable by time.ParseDuration
protobuf_heartbeat = "200ms" # the heartbeat interval between the servers. must be parseable by time.ParseDuration
protobuf_min_backoff = "1s" # the minimum backoff after a failed heartbeat attempt
protobuf_max_backoff = "10s" # the maxmimum backoff after a failed heartbeat attempt
write-buffer-size = 10000
ax-response-buffer-size = 100000
oncurrent-shard-query-limit = 10
[sharding]
replication-factor = 1
[sharding.short-term]
duration = "7d"
split = 1
[sharding.long-term]
duration = "30d"
split = 1
# split-random = "/^Hf.*/"
[wal]
dir = "/opt/influxdb/shared/data/wal"
flush-after = 1000 # the number of writes after which wal will be flushed, 0 for flushing on every write
bookmark-after = 1000 # the number of writes after which a bookmark will be created
index-after = 1000
requests-per-logfile = 10000
influxdb: influxdb:
#image: influxdb/influxdb:1.0-alpine
image: influxdb:latest image: influxdb:latest
container_name: influxdb container_name: influxdb
ports: ports:
- "2004:2004" - "2004:2004"
- "8083:8083" - "8083:8083"
- "8086:8086" - "8086:8086"
volumes:
- ./blocks/influxdb/influxdb.conf:/etc/influxdb/influxdb.conf
fake-influxdb-data: fake-influxdb-data:
image: grafana/fake-data-gen image: grafana/fake-data-gen
......
reporting-disabled = false
[meta]
# Where the metadata/raft database is stored
dir = "/var/lib/influxdb/meta"
retention-autocreate = true
# If log messages are printed for the meta service
logging-enabled = true
pprof-enabled = false
# The default duration for leases.
lease-duration = "1m0s"
[data]
# Controls if this node holds time series data shards in the cluster
enabled = true
dir = "/var/lib/influxdb/data"
# These are the WAL settings for the storage engine >= 0.9.3
wal-dir = "/var/lib/influxdb/wal"
wal-logging-enabled = true
[coordinator]
write-timeout = "10s"
max-concurrent-queries = 0
query-timeout = "0"
log-queries-after = "0"
max-select-point = 0
max-select-series = 0
max-select-buckets = 0
[retention]
enabled = true
check-interval = "30m"
[shard-precreation]
enabled = true
check-interval = "10m"
advance-period = "30m"
[monitor]
store-enabled = true # Whether to record statistics internally.
store-database = "_internal" # The destination database for recorded statistics
store-interval = "10s" # The interval at which to record statistics
[admin]
enabled = true
bind-address = ":8083"
https-enabled = false
https-certificate = "/etc/ssl/influxdb.pem"
[http]
enabled = true
bind-address = ":8086"
auth-enabled = true
log-enabled = true
write-tracing = false
pprof-enabled = false
https-enabled = false
https-certificate = "/etc/ssl/influxdb.pem"
### Use a separate private key location.
# https-private-key = ""
max-row-limit = 10000
realm = "InfluxDB"
unix-socket-enabled = false # enable http service over unix domain socket
# bind-socket = "/var/run/influxdb.sock"
[subscriber]
enabled = true
[[graphite]]
enabled = false
[[collectd]]
enabled = false
[[opentsdb]]
enabled = false
[[udp]]
enabled = false
[continuous_queries]
log-enabled = true
enabled = true
# run-interval = "1s" # interval for how often continuous queries will be checked if they need to run
...@@ -252,7 +252,7 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R ...@@ -252,7 +252,7 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
return ApiSuccess("Test notification sent") return ApiSuccess("Test notification sent")
} }
//POST /api/:alertId/pause //POST /api/alerts/:alertId/pause
func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response { func PauseAlert(c *middleware.Context, dto dtos.PauseAlertCommand) Response {
cmd := models.PauseAlertCommand{ cmd := models.PauseAlertCommand{
OrgId: c.OrgId, OrgId: c.OrgId,
......
...@@ -44,3 +44,19 @@ func GetAnnotations(c *middleware.Context) Response { ...@@ -44,3 +44,19 @@ func GetAnnotations(c *middleware.Context) Response {
return Json(200, result) return Json(200, result)
} }
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
err := repo.Delete(&annotations.DeleteParams{
AlertId: cmd.PanelId,
DashboardId: cmd.DashboardId,
PanelId: cmd.PanelId,
})
if err != nil {
return ApiError(500, "Failed to delete annotations", err)
}
return ApiSuccess("Annotations deleted")
}
...@@ -252,7 +252,7 @@ func Register(r *macaron.Macaron) { ...@@ -252,7 +252,7 @@ func Register(r *macaron.Macaron) {
r.Group("/alerts", func() { r.Group("/alerts", func() {
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
r.Post("/:alertId/pause", ValidateOrgAlert, bind(dtos.PauseAlertCommand{}), wrap(PauseAlert)) r.Post("/:alertId/pause", bind(dtos.PauseAlertCommand{}), wrap(PauseAlert), reqEditorRole)
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
r.Get("/", wrap(GetAlerts)) r.Get("/", wrap(GetAlerts))
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard)) r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
...@@ -266,9 +266,10 @@ func Register(r *macaron.Macaron) { ...@@ -266,9 +266,10 @@ func Register(r *macaron.Macaron) {
r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification)) r.Put("/:notificationId", bind(m.UpdateAlertNotificationCommand{}), wrap(UpdateAlertNotification))
r.Get("/:notificationId", wrap(GetAlertNotificationById)) r.Get("/:notificationId", wrap(GetAlertNotificationById))
r.Delete("/:notificationId", wrap(DeleteAlertNotification)) r.Delete("/:notificationId", wrap(DeleteAlertNotification))
}, reqOrgAdmin) }, reqEditorRole)
r.Get("/annotations", wrap(GetAnnotations)) r.Get("/annotations", wrap(GetAnnotations))
r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
// error test // error test
r.Get("/metrics/error", wrap(GenerateError)) r.Get("/metrics/error", wrap(GenerateError))
......
...@@ -15,3 +15,9 @@ type Annotation struct { ...@@ -15,3 +15,9 @@ type Annotation struct {
Data *simplejson.Json `json:"data"` Data *simplejson.Json `json:"data"`
} }
type DeleteAnnotationsCmd struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
}
...@@ -103,8 +103,10 @@ func writePIDFile() { ...@@ -103,8 +103,10 @@ func writePIDFile() {
func listenToSystemSignals(server models.GrafanaServer) { func listenToSystemSignals(server models.GrafanaServer) {
signalChan := make(chan os.Signal, 1) signalChan := make(chan os.Signal, 1)
ignoreChan := make(chan os.Signal, 1)
code := 0 code := 0
signal.Notify(ignoreChan, syscall.SIGHUP)
signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM) signal.Notify(signalChan, os.Interrupt, os.Kill, syscall.SIGTERM)
select { select {
......
...@@ -21,6 +21,7 @@ type SendWebhook struct { ...@@ -21,6 +21,7 @@ type SendWebhook struct {
User string User string
Password string Password string
Body string Body string
HttpMethod string
} }
type SendWebhookSync struct { type SendWebhookSync struct {
...@@ -28,6 +29,7 @@ type SendWebhookSync struct { ...@@ -28,6 +29,7 @@ type SendWebhookSync struct {
User string User string
Password string Password string
Body string Body string
HttpMethod string
} }
type SendResetPasswordEmailCommand struct { type SendResetPasswordEmailCommand struct {
......
...@@ -24,6 +24,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { ...@@ -24,6 +24,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
Url: url, Url: url,
User: model.Settings.Get("user").MustString(), User: model.Settings.Get("user").MustString(),
Password: model.Settings.Get("password").MustString(), Password: model.Settings.Get("password").MustString(),
HttpMethod: model.Settings.Get("httpMethod").MustString("POST"),
log: log.New("alerting.notifier.webhook"), log: log.New("alerting.notifier.webhook"),
}, nil }, nil
} }
...@@ -33,6 +34,7 @@ type WebhookNotifier struct { ...@@ -33,6 +34,7 @@ type WebhookNotifier struct {
Url string Url string
User string User string
Password string Password string
HttpMethod string
log log.Logger log log.Logger
} }
...@@ -63,6 +65,7 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { ...@@ -63,6 +65,7 @@ func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
User: this.User, User: this.User,
Password: this.Password, Password: this.Password,
Body: string(body), Body: string(body),
HttpMethod: this.HttpMethod,
} }
if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
......
...@@ -5,6 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson" ...@@ -5,6 +5,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
type Repository interface { type Repository interface {
Save(item *Item) error Save(item *Item) error
Find(query *ItemQuery) ([]*Item, error) Find(query *ItemQuery) ([]*Item, error)
Delete(params *DeleteParams) error
} }
type ItemQuery struct { type ItemQuery struct {
...@@ -20,6 +21,12 @@ type ItemQuery struct { ...@@ -20,6 +21,12 @@ type ItemQuery struct {
Limit int64 `json:"alertId"` Limit int64 `json:"alertId"`
} }
type DeleteParams struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
}
var repositoryInstance Repository var repositoryInstance Repository
func GetRepository() Repository { func GetRepository() Repository {
......
...@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error { ...@@ -65,6 +65,7 @@ func SendWebhookSync(ctx context.Context, cmd *m.SendWebhookSync) error {
User: cmd.User, User: cmd.User,
Password: cmd.Password, Password: cmd.Password,
Body: cmd.Body, Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
}) })
} }
...@@ -74,6 +75,7 @@ func sendWebhook(cmd *m.SendWebhook) error { ...@@ -74,6 +75,7 @@ func sendWebhook(cmd *m.SendWebhook) error {
User: cmd.User, User: cmd.User,
Password: cmd.Password, Password: cmd.Password,
Body: cmd.Body, Body: cmd.Body,
HttpMethod: cmd.HttpMethod,
}) })
return nil return nil
......
...@@ -19,6 +19,7 @@ type Webhook struct { ...@@ -19,6 +19,7 @@ type Webhook struct {
User string User string
Password string Password string
Body string Body string
HttpMethod string
} }
var webhookQueue chan *Webhook var webhookQueue chan *Webhook
...@@ -44,13 +45,17 @@ func processWebhookQueue() { ...@@ -44,13 +45,17 @@ func processWebhookQueue() {
} }
func sendWebRequestSync(ctx context.Context, webhook *Webhook) error { func sendWebRequestSync(ctx context.Context, webhook *Webhook) error {
webhookLog.Debug("Sending webhook", "url", webhook.Url) webhookLog.Debug("Sending webhook", "url", webhook.Url, "http method", webhook.HttpMethod)
client := &http.Client{ client := &http.Client{
Timeout: time.Duration(10 * time.Second), Timeout: time.Duration(10 * time.Second),
} }
request, err := http.NewRequest(http.MethodPost, webhook.Url, bytes.NewReader([]byte(webhook.Body))) if webhook.HttpMethod == "" {
webhook.HttpMethod = http.MethodPost
}
request, err := http.NewRequest(webhook.HttpMethod, webhook.Url, bytes.NewReader([]byte(webhook.Body)))
if webhook.User != "" && webhook.Password != "" { if webhook.User != "" && webhook.Password != "" {
request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password)) request.Header.Add("Authorization", util.GetBasicAuthHeader(webhook.User, webhook.Password))
} }
......
...@@ -46,13 +46,23 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error { ...@@ -46,13 +46,23 @@ func GetAllAlertQueryHandler(query *m.GetAllAlertsQuery) error {
return nil return nil
} }
func DeleteAlertById(cmd *m.DeleteAlertCommand) error { func deleteAlertByIdInternal(alertId int64, reason string, sess *xorm.Session) error {
return inTransaction(func(sess *xorm.Session) error { sqlog.Debug("Deleting alert", "id", alertId, "reason", reason)
if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", cmd.AlertId); err != nil {
if _, err := sess.Exec("DELETE FROM alert WHERE id = ?", alertId); err != nil {
return err
}
if _, err := sess.Exec("DELETE FROM annotation WHERE alert_id = ?", alertId); err != nil {
return err return err
} }
return nil return nil
}
func DeleteAlertById(cmd *m.DeleteAlertCommand) error {
return inTransaction(func(sess *xorm.Session) error {
return deleteAlertByIdInternal(cmd.AlertId, "DeleteAlertCommand", sess)
}) })
} }
...@@ -110,12 +120,7 @@ func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error { ...@@ -110,12 +120,7 @@ func DeleteAlertDefinition(dashboardId int64, sess *xorm.Session) error {
sess.Where("dashboard_id = ?", dashboardId).Find(&alerts) sess.Where("dashboard_id = ?", dashboardId).Find(&alerts)
for _, alert := range alerts { for _, alert := range alerts {
_, err := sess.Exec("DELETE FROM alert WHERE id = ? ", alert.Id) deleteAlertByIdInternal(alert.Id, "Dashboard deleted", sess)
if err != nil {
return err
}
sqlog.Debug("Alert deleted (due to dashboard deletion)", "name", alert.Name, "id", alert.Id)
} }
return nil return nil
...@@ -195,12 +200,7 @@ func deleteMissingAlerts(alerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xorm ...@@ -195,12 +200,7 @@ func deleteMissingAlerts(alerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xorm
} }
if missing { if missing {
_, err := sess.Exec("DELETE FROM alert WHERE id = ?", missingAlert.Id) deleteAlertByIdInternal(missingAlert.Id, "Removed from dashboard", sess)
if err != nil {
return err
}
sqlog.Debug("Alert deleted", "name", missingAlert.Name, "id", missingAlert.Id)
} }
} }
...@@ -248,7 +248,9 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error { ...@@ -248,7 +248,9 @@ func PauseAlertRule(cmd *m.PauseAlertCommand) error {
return inTransaction(func(sess *xorm.Session) error { return inTransaction(func(sess *xorm.Session) error {
alert := m.Alert{} alert := m.Alert{}
if has, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { has, err := x.Where("id = ? AND org_id=?", cmd.AlertId, cmd.OrgId).Get(&alert)
if err != nil {
return err return err
} else if !has { } else if !has {
return fmt.Errorf("Could not find alert") return fmt.Errorf("Could not find alert")
......
...@@ -84,3 +84,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I ...@@ -84,3 +84,17 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
return items, nil return items, nil
} }
func (r *SqlAnnotationRepo) Delete(params *annotations.DeleteParams) error {
return inTransaction(func(sess *xorm.Session) error {
sql := "DELETE FROM annotation WHERE dashboard_id = ? AND panel_id = ?"
_, err := sess.Exec(sql, params.DashboardId, params.PanelId)
if err != nil {
return err
}
return nil
})
}
...@@ -46,5 +46,6 @@ func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) { ...@@ -46,5 +46,6 @@ func (s *SocialGoogle) UserInfo(client *http.Client) (*BasicUserInfo, error) {
return &BasicUserInfo{ return &BasicUserInfo{
Name: data.Name, Name: data.Name,
Email: data.Email, Email: data.Email,
Login: data.Email,
}, nil }, nil
} }
...@@ -124,10 +124,15 @@ func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) { ...@@ -124,10 +124,15 @@ func (e *InfluxDBExecutor) createRequest(query string) (*http.Request, error) {
req.URL.RawQuery = params.Encode() req.URL.RawQuery = params.Encode()
req.Header.Set("User-Agent", "Grafana") req.Header.Set("User-Agent", "Grafana")
if e.BasicAuth { if e.BasicAuth {
req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword) req.SetBasicAuth(e.BasicAuthUser, e.BasicAuthPassword)
} }
if e.User != "" {
req.SetBasicAuth(e.User, e.Password)
}
glog.Debug("Influxdb request", "url", req.URL.String()) glog.Debug("Influxdb request", "url", req.URL.String())
return req, nil return req, nil
} }
...@@ -84,8 +84,10 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string { ...@@ -84,8 +84,10 @@ func formatLegend(metric pmodel.Metric, query *PrometheusQuery) string {
reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`) reg, _ := regexp.Compile(`\{\{\s*(.+?)\s*\}\}`)
result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte { result := reg.ReplaceAllFunc([]byte(query.LegendFormat), func(in []byte) []byte {
ind := strings.Replace(strings.Replace(string(in), "{{", "", 1), "}}", "", 1) labelName := strings.Replace(string(in), "{{", "", 1)
if val, exists := metric[pmodel.LabelName(ind)]; exists { labelName = strings.Replace(labelName, "}}", "", 1)
labelName = strings.TrimSpace(labelName)
if val, exists := metric[pmodel.LabelName(labelName)]; exists {
return []byte(val) return []byte(val)
} }
......
...@@ -17,7 +17,7 @@ func TestPrometheus(t *testing.T) { ...@@ -17,7 +17,7 @@ func TestPrometheus(t *testing.T) {
} }
query := &PrometheusQuery{ query := &PrometheusQuery{
LegendFormat: "legend {{app}} {{device}} {{broken}}", LegendFormat: "legend {{app}} {{ device }} {{broken}}",
} }
So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}") So(formatLegend(metric, query), ShouldEqual, "legend backend mobile {{broken}}")
......
...@@ -82,8 +82,9 @@ function formatDate(date) { ...@@ -82,8 +82,9 @@ function formatDate(date) {
// now/d // now/d
// if no to <expr> then to now is assumed // if no to <expr> then to now is assumed
export function describeTextRange(expr: any) { export function describeTextRange(expr: any) {
let isLast = (expr.indexOf('+') !== 0);
if (expr.indexOf('now') === -1) { if (expr.indexOf('now') === -1) {
expr = 'now-' + expr; expr = (isLast ? 'now-' : 'now') + expr;
} }
let opt = rangeIndex[expr + ' to now']; let opt = rangeIndex[expr + ' to now'];
...@@ -91,15 +92,20 @@ export function describeTextRange(expr: any) { ...@@ -91,15 +92,20 @@ export function describeTextRange(expr: any) {
return opt; return opt;
} }
if (isLast) {
opt = {from: expr, to: 'now'}; opt = {from: expr, to: 'now'};
} else {
opt = {from: 'now', to: expr};
}
let parts = /^now-(\d+)(\w)/.exec(expr); let parts = /^now([-+])(\d+)(\w)/.exec(expr);
if (parts) { if (parts) {
let unit = parts[2]; let unit = parts[3];
let amount = parseInt(parts[1]); let amount = parseInt(parts[2]);
let span = spans[unit]; let span = spans[unit];
if (span) { if (span) {
opt.display = 'Last ' + amount + ' ' + span.display; opt.display = isLast ? 'Last ' : 'Next ';
opt.display += amount + ' ' + span.display;
opt.section = span.section; opt.section = span.section;
if (amount > 1) { if (amount > 1) {
opt.display += 's'; opt.display += 's';
......
...@@ -352,6 +352,24 @@ export class AlertTabCtrl { ...@@ -352,6 +352,24 @@ export class AlertTabCtrl {
this.evaluatorParamsChanged(); this.evaluatorParamsChanged();
} }
clearHistory() {
appEvents.emit('confirm-modal', {
title: 'Delete Alert History',
text: 'Are you sure you want to remove all history & annotations for this alert?',
icon: 'fa-trash',
yesText: 'Yes',
onConfirm: () => {
this.backendSrv.post('/api/annotations/mass-delete', {
dashboardId: this.panelCtrl.dashboard.id,
panelId: this.panel.id,
}).then(res => {
this.alertHistory = [];
this.panelCtrl.refresh();
});
}
});
}
test() { test() {
this.testing = true; this.testing = true;
......
...@@ -18,7 +18,7 @@ export class AlertNotificationEditCtrl { ...@@ -18,7 +18,7 @@ export class AlertNotificationEditCtrl {
this.model = { this.model = {
type: 'email', type: 'email',
settings: { settings: {
severityFilter: 'none' httpMethod: 'POST'
}, },
isDefault: false isDefault: false
}; };
......
...@@ -125,7 +125,16 @@ ...@@ -125,7 +125,16 @@
</div> </div>
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2"> <div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
<h5 class="section-heading">State history <span class="muted small">(last 50 state changes)</span></h5> <button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i>&nbsp;Clear history</button>
<h5 class="section-heading" style="whitespace: nowrap">
State history <span class="muted small">(last 50 state changes)</span>
</h5>
<div ng-show="ctrl.alertHistory.length === 0">
<br>
<i>No state changes recorded</i>
</div>
<section class="card-section card-list-layout-list"> <section class="card-section card-list-layout-list">
<ol class="card-list" > <ol class="card-list" >
<li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory"> <li class="card-item-wrapper" ng-repeat="ah in ctrl.alertHistory">
......
...@@ -32,18 +32,23 @@ ...@@ -32,18 +32,23 @@
<div class="gf-form-group" ng-if="ctrl.model.type === 'webhook'"> <div class="gf-form-group" ng-if="ctrl.model.type === 'webhook'">
<h3 class="page-heading">Webhook settings</h3> <h3 class="page-heading">Webhook settings</h3>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-6">Url</span> <span class="gf-form-label width-10">Url</span>
<input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input> <input type="text" required class="gf-form-input max-width-26" ng-model="ctrl.model.settings.url"></input>
</div> </div>
<div class="gf-form-inline">
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-6">Username</span> <span class="gf-form-label width-10">Http Method</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.username"></input> <div class="gf-form-select-wrapper width-14">
<select class="gf-form-input" ng-model="ctrl.model.settings.httpMethod" ng-options="t for t in ['POST', 'PUT']">
</select>
</div>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label width-6">Password</span> <span class="gf-form-label width-10">Username</span>
<input type="text" class="gf-form-input max-width-10" ng-model="ctrl.model.settings.password"></input> <input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.username"></input>
</div> </div>
<div class="gf-form">
<span class="gf-form-label width-10">Password</span>
<input type="text" class="gf-form-input max-width-14" ng-model="ctrl.model.settings.password"></input>
</div> </div>
</div> </div>
......
///<reference path="../../headers/common.d.ts" />
import config from 'app/core/config';
import angular from 'angular';
import moment from 'moment';
import _ from 'lodash';
import coreModule from 'app/core/core_module';
export class AlertingSrv {
dashboard: any;
alerts: any[];
init(dashboard, alerts) {
this.dashboard = dashboard;
this.alerts = alerts || [];
}
}
coreModule.service('alertingSrv', AlertingSrv);
define([ define([
'./dashboard_ctrl', './dashboard_ctrl',
'./alerting_srv',
'./dashboardLoaderSrv', './dashboardLoaderSrv',
'./dashnav/dashnav', './dashnav/dashnav',
'./submenu/submenu', './submenu/submenu',
......
...@@ -16,6 +16,7 @@ export class DashboardCtrl { ...@@ -16,6 +16,7 @@ export class DashboardCtrl {
dashboardKeybindings, dashboardKeybindings,
timeSrv, timeSrv,
variableSrv, variableSrv,
alertingSrv,
dashboardSrv, dashboardSrv,
unsavedChangesSrv, unsavedChangesSrv,
dynamicDashboardSrv, dynamicDashboardSrv,
...@@ -43,6 +44,7 @@ export class DashboardCtrl { ...@@ -43,6 +44,7 @@ export class DashboardCtrl {
// init services // init services
timeSrv.init(dashboard); timeSrv.init(dashboard);
alertingSrv.init(dashboard, data.alerts);
// template values service needs to initialize completely before // template values service needs to initialize completely before
// the rest of the dashboard can load // the rest of the dashboard can load
......
...@@ -9,7 +9,7 @@ import {DashboardExporter} from '../export/exporter'; ...@@ -9,7 +9,7 @@ import {DashboardExporter} from '../export/exporter';
export class DashNavCtrl { export class DashNavCtrl {
/** @ngInject */ /** @ngInject */
constructor($scope, $rootScope, alertSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) { constructor($scope, $rootScope, dashboardSrv, $location, playlistSrv, backendSrv, $timeout, datasourceSrv) {
$scope.init = function() { $scope.init = function() {
$scope.onAppEvent('save-dashboard', $scope.saveDashboard); $scope.onAppEvent('save-dashboard', $scope.saveDashboard);
...@@ -71,88 +71,14 @@ export class DashNavCtrl { ...@@ -71,88 +71,14 @@ export class DashNavCtrl {
$scope.makeEditable = function() { $scope.makeEditable = function() {
$scope.dashboard.editable = true; $scope.dashboard.editable = true;
var clone = $scope.dashboard.getSaveModelClone(); return dashboardSrv.saveDashboard({makeEditable: true, overwrite: false}).then(function() {
backendSrv.saveDashboard(clone, {overwrite: false}).then(function(data) {
$scope.dashboard.version = data.version;
$scope.appEvent('dashboard-saved', $scope.dashboard);
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
// force refresh whole page // force refresh whole page
window.location.href = window.location.href; window.location.href = window.location.href;
}, $scope.handleSaveDashError); });
}; };
$scope.saveDashboard = function(options) { $scope.saveDashboard = function(options) {
if ($scope.dashboardMeta.canSave === false) { return dashboardSrv.saveDashboard(options);
return;
}
var clone = $scope.dashboard.getSaveModelClone();
backendSrv.saveDashboard(clone, options).then(function(data) {
$scope.dashboard.version = data.version;
$scope.appEvent('dashboard-saved', $scope.dashboard);
var dashboardUrl = '/dashboard/db/' + data.slug;
if (dashboardUrl !== $location.path()) {
$location.url(dashboardUrl);
}
$scope.appEvent('alert-success', ['Dashboard saved', 'Saved as ' + clone.title]);
}, $scope.handleSaveDashError);
};
$scope.handleSaveDashError = function(err) {
if (err.data && err.data.status === "version-mismatch") {
err.isHandled = true;
$scope.appEvent('confirm-modal', {
title: 'Conflict',
text: 'Someone else has updated this dashboard.',
text2: 'Would you still like to save this dashboard?',
yesText: "Save & Overwrite",
icon: "fa-warning",
onConfirm: function() {
$scope.saveDashboard({overwrite: true});
}
});
}
if (err.data && err.data.status === "name-exists") {
err.isHandled = true;
$scope.appEvent('confirm-modal', {
title: 'Conflict',
text: 'Dashboard with the same name exists.',
text2: 'Would you still like to save this dashboard?',
yesText: "Save & Overwrite",
icon: "fa-warning",
onConfirm: function() {
$scope.saveDashboard({overwrite: true});
}
});
}
if (err.data && err.data.status === "plugin-dashboard") {
err.isHandled = true;
$scope.appEvent('confirm-modal', {
title: 'Plugin Dashboard',
text: err.data.message,
text2: 'Your changes will be lost when you update the plugin. Use Save As to create custom version.',
yesText: "Overwrite",
icon: "fa-warning",
altActionText: "Save As",
onAltAction: function() {
$scope.saveDashboardAs();
},
onConfirm: function() {
$scope.saveDashboard({overwrite: true});
}
});
}
}; };
$scope.deleteDashboard = function() { $scope.deleteDashboard = function() {
...@@ -189,16 +115,7 @@ export class DashNavCtrl { ...@@ -189,16 +115,7 @@ export class DashNavCtrl {
}; };
$scope.saveDashboardAs = function() { $scope.saveDashboardAs = function() {
var newScope = $rootScope.$new(); return dashboardSrv.saveDashboardAs();
newScope.clone = $scope.dashboard.getSaveModelClone();
newScope.clone.editable = true;
newScope.clone.hideControls = false;
$scope.appEvent('show-modal', {
src: 'public/app/features/dashboard/partials/saveDashboardAs.html',
scope: newScope,
modalClass: 'modal--narrow'
});
}; };
$scope.viewJson = function() { $scope.viewJson = function() {
......
...@@ -6,7 +6,7 @@ describe('dashboardSrv', function() { ...@@ -6,7 +6,7 @@ describe('dashboardSrv', function() {
var _dashboardSrv; var _dashboardSrv;
beforeEach(() => { beforeEach(() => {
_dashboardSrv = new DashboardSrv(); _dashboardSrv = new DashboardSrv({}, {}, {});
}); });
describe('when creating new dashboard with defaults only', function() { describe('when creating new dashboard with defaults only', function() {
......
...@@ -358,9 +358,15 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -358,9 +358,15 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
} }
this.getExpandedVariables = function(target, dimensionKey, variable) { this.getExpandedVariables = function(target, dimensionKey, variable) {
/* if the all checkbox is marked we should add all values to the targets */
var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
return _.chain(variable.options) return _.chain(variable.options)
.filter(function(v) { .filter(function(v) {
if (allSelected) {
return v.text !== 'All';
} else {
return v.selected; return v.selected;
}
}) })
.map(function(v) { .map(function(v) {
var t = angular.copy(target); var t = angular.copy(target);
...@@ -369,6 +375,10 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -369,6 +375,10 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
}).value(); }).value();
}; };
this.containsVariable = function (str, variableName) {
return str.indexOf('$' + variableName) !== -1;
};
this.expandTemplateVariable = function(targets, templateSrv) { this.expandTemplateVariable = function(targets, templateSrv) {
var self = this; var self = this;
return _.chain(targets) return _.chain(targets)
...@@ -379,7 +389,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { ...@@ -379,7 +389,7 @@ function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) {
if (dimensionKey) { if (dimensionKey) {
var variable = _.find(templateSrv.variables, function(variable) { var variable = _.find(templateSrv.variables, function(variable) {
return templateSrv.containsVariable(target.dimensions[dimensionKey], variable.name); return self.containsVariable(target.dimensions[dimensionKey], variable.name);
}); });
return self.getExpandedVariables(target, dimensionKey, variable); return self.getExpandedVariables(target, dimensionKey, variable);
} else { } else {
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label width-13">Default Region</label> <label class="gf-form-label width-13">Default Region</label>
<div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon"> <div class="gf-form-select-wrapper max-width-18 gf-form-select-wrapper--has-help-icon">
<select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-west-1', 'us-west-2']"></select> <select class="gf-form-input" ng-model="ctrl.current.jsonData.defaultRegion" ng-options="region for region in ['ap-northeast-1', 'ap-northeast-2', 'ap-southeast-1', 'ap-southeast-2', 'ap-south-1', 'cn-north-1', 'eu-central-1', 'eu-west-1', 'sa-east-1', 'us-east-1', 'us-east-2', 'us-west-1', 'us-west-2']"></select>
<info-popover mode="right-absolute"> <info-popover mode="right-absolute">
Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region. Specify the region, such as for US West (Oregon) use ` us-west-2 ` as the region.
</info-popover> </info-popover>
......
...@@ -478,7 +478,7 @@ ...@@ -478,7 +478,7 @@
"steppedLine": false, "steppedLine": false,
"targets": [ "targets": [
{ {
"expr": "prometheus_evaluator_duration_milliseconds{quantile!=\"0.01\", quantile!=\"0.05\"}", "expr": "prometheus_evaluator_duration_seconds{quantile!=\"0.01\", quantile!=\"0.05\"}",
"interval": "", "interval": "",
"intervalFactor": 2, "intervalFactor": 2,
"legendFormat": "{{quantile}}", "legendFormat": "{{quantile}}",
......
...@@ -392,17 +392,21 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) { ...@@ -392,17 +392,21 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
position: 'BOTTOM', position: 'BOTTOM',
markerSize: 5, markerSize: 5,
}; };
types['$__ok'] = { types['$__ok'] = {
color: 'rgba(11, 237, 50, 1)', color: 'rgba(11, 237, 50, 1)',
position: 'BOTTOM', position: 'BOTTOM',
markerSize: 5, markerSize: 5,
}; };
types['$__nodata'] = {
types['$__no_data'] = {
color: 'rgba(150, 150, 150, 1)', color: 'rgba(150, 150, 150, 1)',
position: 'BOTTOM', position: 'BOTTOM',
markerSize: 5, markerSize: 5,
}; };
types['$__execution_error'] = ['$__no_data'];
for (var i = 0; i < annotations.length; i++) { for (var i = 0; i < annotations.length; i++) {
var item = annotations[i]; var item = annotations[i];
if (item.newState) { if (item.newState) {
......
...@@ -149,8 +149,6 @@ function ($, _) { ...@@ -149,8 +149,6 @@ function ($, _) {
seriesHtml = ''; seriesHtml = '';
absoluteTime = dashboard.formatDate(seriesHoverInfo.time, tooltipFormat);
// Dynamically reorder the hovercard for the current time point if the // Dynamically reorder the hovercard for the current time point if the
// option is enabled, sort by yaxis by default. // option is enabled, sort by yaxis by default.
if (panel.tooltip.sort === 2) { if (panel.tooltip.sort === 2) {
...@@ -161,13 +159,14 @@ function ($, _) { ...@@ -161,13 +159,14 @@ function ($, _) {
seriesHoverInfo.sort(function(a, b) { seriesHoverInfo.sort(function(a, b) {
return a.value - b.value; return a.value - b.value;
}); });
} } else {
else {
seriesHoverInfo.sort(function(a, b) { seriesHoverInfo.sort(function(a, b) {
return a.yaxis - b.yaxis; return a.yaxis - b.yaxis;
}); });
} }
var distance, time;
for (i = 0; i < seriesHoverInfo.length; i++) { for (i = 0; i < seriesHoverInfo.length; i++) {
hoverInfo = seriesHoverInfo[i]; hoverInfo = seriesHoverInfo[i];
...@@ -175,6 +174,11 @@ function ($, _) { ...@@ -175,6 +174,11 @@ function ($, _) {
continue; continue;
} }
if (! distance || hoverInfo.distance < distance) {
distance = hoverInfo.distance;
time = hoverInfo.time;
}
var highlightClass = ''; var highlightClass = '';
if (item && i === item.seriesIndex) { if (item && i === item.seriesIndex) {
highlightClass = 'graph-tooltip-list-item--highlight'; highlightClass = 'graph-tooltip-list-item--highlight';
...@@ -190,6 +194,7 @@ function ($, _) { ...@@ -190,6 +194,7 @@ function ($, _) {
plot.highlight(hoverInfo.index, hoverInfo.hoverIndex); plot.highlight(hoverInfo.index, hoverInfo.hoverIndex);
} }
absoluteTime = dashboard.formatDate(time, tooltipFormat);
self.showTooltip(absoluteTime, seriesHtml, pos); self.showTooltip(absoluteTime, seriesHtml, pos);
} }
// single series tooltip // single series tooltip
......
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
flex-direction: row; flex-direction: row;
} }
.edit-tab-content {
flex-grow: 1;
}
.edit-sidemenu-aside { .edit-sidemenu-aside {
width: 16rem; width: 16rem;
} }
......
...@@ -31,6 +31,13 @@ describe("rangeUtil", () => { ...@@ -31,6 +31,13 @@ describe("rangeUtil", () => {
expect(info.from).to.be('now-13h') expect(info.from).to.be('now-13h')
}); });
it('should handle non default future amount', () => {
var info = rangeUtil.describeTextRange('+3h');
expect(info.display).to.be('Next 3 hours')
expect(info.from).to.be('now')
expect(info.to).to.be('now+3h')
});
it('should handle now/d', () => { it('should handle now/d', () => {
var info = rangeUtil.describeTextRange('now/d'); var info = rangeUtil.describeTextRange('now/d');
expect(info.display).to.be('Today so far'); expect(info.display).to.be('Today so far');
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment