mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(annotations): added support to show grafana stored annotations in graphs, #5982
This commit is contained in:
parent
c769148701
commit
d60bd77658
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/middleware"
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
"github.com/grafana/grafana/pkg/services/annotations"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func ValidateOrgAlert(c *middleware.Context) {
|
func ValidateOrgAlert(c *middleware.Context) {
|
||||||
@ -231,42 +230,6 @@ func NotificationTest(c *middleware.Context, dto dtos.NotificationTestCommand) R
|
|||||||
return ApiSuccess("Test notification sent")
|
return ApiSuccess("Test notification sent")
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlertHistory(c *middleware.Context) Response {
|
|
||||||
alertId, err := getAlertIdForRequest(c)
|
|
||||||
if err != nil {
|
|
||||||
return ApiError(400, "Invalid request", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
query := &annotations.ItemQuery{
|
|
||||||
AlertId: alertId,
|
|
||||||
Type: annotations.AlertType,
|
|
||||||
OrgId: c.OrgId,
|
|
||||||
Limit: c.QueryInt64("limit"),
|
|
||||||
}
|
|
||||||
|
|
||||||
repo := annotations.GetRepository()
|
|
||||||
|
|
||||||
items, err := repo.Find(query)
|
|
||||||
if err != nil {
|
|
||||||
return ApiError(500, "Failed to get history for alert", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result []dtos.AlertHistory
|
|
||||||
for _, item := range items {
|
|
||||||
result = append(result, dtos.AlertHistory{
|
|
||||||
AlertId: item.AlertId,
|
|
||||||
Timestamp: item.Timestamp,
|
|
||||||
Data: item.Data,
|
|
||||||
NewState: item.NewState,
|
|
||||||
Text: item.Text,
|
|
||||||
Metric: item.Metric,
|
|
||||||
Title: item.Title,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Json(200, result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAlertIdForRequest(c *middleware.Context) (int64, error) {
|
func getAlertIdForRequest(c *middleware.Context) (int64, error) {
|
||||||
alertId := c.QueryInt64("alertId")
|
alertId := c.QueryInt64("alertId")
|
||||||
panelId := c.QueryInt64("panelId")
|
panelId := c.QueryInt64("panelId")
|
||||||
|
42
pkg/api/annotations.go
Normal file
42
pkg/api/annotations.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
|
"github.com/grafana/grafana/pkg/middleware"
|
||||||
|
"github.com/grafana/grafana/pkg/services/annotations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetAnnotations(c *middleware.Context) Response {
|
||||||
|
|
||||||
|
query := &annotations.ItemQuery{
|
||||||
|
From: c.QueryInt64("from") / 1000,
|
||||||
|
To: c.QueryInt64("to") / 1000,
|
||||||
|
Type: annotations.ItemType(c.Query("type")),
|
||||||
|
OrgId: c.OrgId,
|
||||||
|
Limit: c.QueryInt64("limit"),
|
||||||
|
}
|
||||||
|
|
||||||
|
repo := annotations.GetRepository()
|
||||||
|
|
||||||
|
items, err := repo.Find(query)
|
||||||
|
if err != nil {
|
||||||
|
return ApiError(500, "Failed to get annotations", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]dtos.Annotation, 0)
|
||||||
|
|
||||||
|
for _, item := range items {
|
||||||
|
result = append(result, dtos.Annotation{
|
||||||
|
AlertId: item.AlertId,
|
||||||
|
Time: item.Epoch * 1000,
|
||||||
|
Data: item.Data,
|
||||||
|
NewState: item.NewState,
|
||||||
|
PrevState: item.PrevState,
|
||||||
|
Text: item.Text,
|
||||||
|
Metric: item.Metric,
|
||||||
|
Title: item.Title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return Json(200, result)
|
||||||
|
}
|
@ -254,8 +254,6 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Get("/", wrap(GetAlerts))
|
r.Get("/", wrap(GetAlerts))
|
||||||
})
|
})
|
||||||
|
|
||||||
r.Get("/alert-history", wrap(GetAlertHistory))
|
|
||||||
|
|
||||||
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
r.Get("/alert-notifications", wrap(GetAlertNotifications))
|
||||||
|
|
||||||
r.Group("/alert-notifications", func() {
|
r.Group("/alert-notifications", func() {
|
||||||
@ -266,6 +264,8 @@ func Register(r *macaron.Macaron) {
|
|||||||
r.Delete("/:notificationId", wrap(DeleteAlertNotification))
|
r.Delete("/:notificationId", wrap(DeleteAlertNotification))
|
||||||
}, reqOrgAdmin)
|
}, reqOrgAdmin)
|
||||||
|
|
||||||
|
r.Get("/annotations", wrap(GetAnnotations))
|
||||||
|
|
||||||
// error test
|
// error test
|
||||||
r.Get("/metrics/error", wrap(GenerateError))
|
r.Get("/metrics/error", wrap(GenerateError))
|
||||||
|
|
||||||
|
@ -54,17 +54,6 @@ type EvalMatch struct {
|
|||||||
Value float64 `json:"value"`
|
Value float64 `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertHistory struct {
|
|
||||||
AlertId int64 `json:"alertId"`
|
|
||||||
NewState string `json:"newState"`
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Text string `json:"text"`
|
|
||||||
Metric string `json:"metric"`
|
|
||||||
|
|
||||||
Data *simplejson.Json `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NotificationTestCommand struct {
|
type NotificationTestCommand struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
15
pkg/api/dtos/annotations.go
Normal file
15
pkg/api/dtos/annotations.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package dtos
|
||||||
|
|
||||||
|
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
|
||||||
|
type Annotation struct {
|
||||||
|
AlertId int64 `json:"alertId"`
|
||||||
|
NewState string `json:"newState"`
|
||||||
|
PrevState string `json:"prevState"`
|
||||||
|
Time int64 `json:"time"`
|
||||||
|
Title string `json:"title"`
|
||||||
|
Text string `json:"text"`
|
||||||
|
Metric string `json:"metric"`
|
||||||
|
|
||||||
|
Data *simplejson.Json `json:"data"`
|
||||||
|
}
|
@ -105,6 +105,7 @@ func getFrontendSettingsMap(c *middleware.Context) (map[string]interface{}, erro
|
|||||||
grafanaDatasourceMeta, _ := plugins.DataSources["grafana"]
|
grafanaDatasourceMeta, _ := plugins.DataSources["grafana"]
|
||||||
datasources["-- Grafana --"] = map[string]interface{}{
|
datasources["-- Grafana --"] = map[string]interface{}{
|
||||||
"type": "grafana",
|
"type": "grafana",
|
||||||
|
"name": "-- Grafana --",
|
||||||
"meta": grafanaDatasourceMeta,
|
"meta": grafanaDatasourceMeta,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,7 +66,6 @@ func (c *EvalContext) GetStateModel() *StateDescription {
|
|||||||
default:
|
default:
|
||||||
panic("Unknown rule state " + c.Rule.State)
|
panic("Unknown rule state " + c.Rule.State)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *EvalContext) GetDurationMs() float64 {
|
func (a *EvalContext) GetDurationMs() float64 {
|
||||||
|
@ -73,7 +73,7 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
|
|||||||
Text: ctx.GetStateModel().Text,
|
Text: ctx.GetStateModel().Text,
|
||||||
NewState: string(ctx.Rule.State),
|
NewState: string(ctx.Rule.State),
|
||||||
PrevState: string(oldState),
|
PrevState: string(oldState),
|
||||||
Timestamp: time.Now(),
|
Epoch: time.Now().Unix(),
|
||||||
Data: annotationData,
|
Data: annotationData,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,10 +1,6 @@
|
|||||||
package annotations
|
package annotations
|
||||||
|
|
||||||
import (
|
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Save(item *Item) error
|
Save(item *Item) error
|
||||||
@ -13,6 +9,8 @@ type Repository interface {
|
|||||||
|
|
||||||
type ItemQuery struct {
|
type ItemQuery struct {
|
||||||
OrgId int64 `json:"orgId"`
|
OrgId int64 `json:"orgId"`
|
||||||
|
From int64 `json:"from"`
|
||||||
|
To int64 `json:"from"`
|
||||||
Type ItemType `json:"type"`
|
Type ItemType `json:"type"`
|
||||||
AlertId int64 `json:"alertId"`
|
AlertId int64 `json:"alertId"`
|
||||||
|
|
||||||
@ -46,7 +44,7 @@ type Item struct {
|
|||||||
UserId int64 `json:"userId"`
|
UserId int64 `json:"userId"`
|
||||||
PrevState string `json:"prevState"`
|
PrevState string `json:"prevState"`
|
||||||
NewState string `json:"newState"`
|
NewState string `json:"newState"`
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Epoch int64 `json:"epoch"`
|
||||||
|
|
||||||
Data *simplejson.Json `json:"data"`
|
Data *simplejson.Json `json:"data"`
|
||||||
}
|
}
|
||||||
|
@ -38,6 +38,9 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
params = append(params, query.AlertId)
|
params = append(params, query.AlertId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sql.WriteString(` AND epoch BETWEEN ? AND ?`)
|
||||||
|
params = append(params, query.From, query.To)
|
||||||
|
|
||||||
if query.Type != "" {
|
if query.Type != "" {
|
||||||
sql.WriteString(` AND type = ?`)
|
sql.WriteString(` AND type = ?`)
|
||||||
params = append(params, string(query.Type))
|
params = append(params, string(query.Type))
|
||||||
@ -47,7 +50,7 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
|
|||||||
query.Limit = 10
|
query.Limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.WriteString(fmt.Sprintf("ORDER BY timestamp DESC LIMIT %v", query.Limit))
|
sql.WriteString(fmt.Sprintf("ORDER BY epoch DESC LIMIT %v", query.Limit))
|
||||||
|
|
||||||
items := make([]*annotations.Item, 0)
|
items := make([]*annotations.Item, 0)
|
||||||
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
if err := x.Sql(sql.String(), params...).Find(&items); err != nil {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func addAnnotationMig(mg *Migrator) {
|
func addAnnotationMig(mg *Migrator) {
|
||||||
|
|
||||||
table := Table{
|
table := Table{
|
||||||
Name: "annotation",
|
Name: "annotation",
|
||||||
Columns: []*Column{
|
Columns: []*Column{
|
||||||
@ -19,20 +20,22 @@ func addAnnotationMig(mg *Migrator) {
|
|||||||
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
{Name: "prev_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||||
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
|
||||||
{Name: "data", Type: DB_Text, Nullable: false},
|
{Name: "data", Type: DB_Text, Nullable: false},
|
||||||
{Name: "timestamp", Type: DB_DateTime, Nullable: false},
|
{Name: "epoch", Type: DB_BigInt, Nullable: false},
|
||||||
},
|
},
|
||||||
Indices: []*Index{
|
Indices: []*Index{
|
||||||
{Cols: []string{"org_id", "alert_id"}, Type: IndexType},
|
{Cols: []string{"org_id", "alert_id"}, Type: IndexType},
|
||||||
{Cols: []string{"org_id", "type"}, Type: IndexType},
|
{Cols: []string{"org_id", "type"}, Type: IndexType},
|
||||||
{Cols: []string{"timestamp"}, Type: IndexType},
|
{Cols: []string{"epoch"}, Type: IndexType},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
mg.AddMigration("create annotation table v1", NewAddTableMigration(table))
|
mg.AddMigration("Drop old annotation table v2", NewDropTableMigration("annotation"))
|
||||||
|
|
||||||
|
mg.AddMigration("create annotation table v3", NewAddTableMigration(table))
|
||||||
|
|
||||||
// create indices
|
// create indices
|
||||||
mg.AddMigration("add index annotation org_id & alert_id ", NewAddIndexMigration(table, table.Indices[0]))
|
mg.AddMigration("add index annotation org_id & alert_id v2", NewAddIndexMigration(table, table.Indices[0]))
|
||||||
|
|
||||||
mg.AddMigration("add index annotation org_id & type", NewAddIndexMigration(table, table.Indices[1]))
|
mg.AddMigration("add index annotation org_id & type v2", NewAddIndexMigration(table, table.Indices[1]))
|
||||||
mg.AddMigration("add index annotation timestamp", NewAddIndexMigration(table, table.Indices[2]))
|
mg.AddMigration("add index annotation epoch", NewAddIndexMigration(table, table.Indices[2]))
|
||||||
}
|
}
|
||||||
|
@ -136,12 +136,12 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $
|
|||||||
}
|
}
|
||||||
// Annotations
|
// Annotations
|
||||||
case "annotations-query-ctrl": {
|
case "annotations-query-ctrl": {
|
||||||
return System.import(scope.currentDatasource.meta.module).then(function(dsModule) {
|
return System.import(scope.ctrl.currentDatasource.meta.module).then(function(dsModule) {
|
||||||
return {
|
return {
|
||||||
baseUrl: scope.currentDatasource.meta.baseUrl,
|
baseUrl: scope.ctrl.currentDatasource.meta.baseUrl,
|
||||||
name: 'annotations-query-ctrl-' + scope.currentDatasource.meta.id,
|
name: 'annotations-query-ctrl-' + scope.ctrl.currentDatasource.meta.id,
|
||||||
bindings: {annotation: "=", datasource: "="},
|
bindings: {annotation: "=", datasource: "="},
|
||||||
attrs: {"annotation": "currentAnnotation", datasource: "currentDatasource"},
|
attrs: {"annotation": "ctrl.currentAnnotation", datasource: "ctrl.currentDatasource"},
|
||||||
Component: dsModule.AnnotationsQueryCtrl,
|
Component: dsModule.AnnotationsQueryCtrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -80,7 +80,7 @@ export class AlertTabCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getAlertHistory() {
|
getAlertHistory() {
|
||||||
this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}`).then(res => {
|
this.backendSrv.get(`/api/alert-history?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50`).then(res => {
|
||||||
this.alertHistory = _.map(res, ah => {
|
this.alertHistory = _.map(res, ah => {
|
||||||
ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
|
ah.time = moment(ah.timestamp).format('MMM D, YYYY HH:mm:ss');
|
||||||
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
|
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||||
<a ng-click="ctrl.changeTabIndex(2)">Alert History</a>
|
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a ng-click="ctrl.delete()">Delete</a>
|
<a ng-click="ctrl.delete()">Delete</a>
|
||||||
@ -136,7 +136,7 @@
|
|||||||
</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">Alert history</h5>
|
<h5 class="section-heading">State history <span class="muted small">(last 50 state changes)</span></h5>
|
||||||
<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">
|
||||||
|
@ -1,76 +0,0 @@
|
|||||||
define([
|
|
||||||
'angular',
|
|
||||||
'lodash',
|
|
||||||
'jquery'
|
|
||||||
],
|
|
||||||
function (angular, _, $) {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
var module = angular.module('grafana.controllers');
|
|
||||||
|
|
||||||
module.controller('AnnotationsEditorCtrl', function($scope, datasourceSrv) {
|
|
||||||
var annotationDefaults = {
|
|
||||||
name: '',
|
|
||||||
datasource: null,
|
|
||||||
iconColor: 'rgba(255, 96, 96, 1)',
|
|
||||||
enable: true
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.init = function() {
|
|
||||||
$scope.mode = 'list';
|
|
||||||
$scope.datasources = datasourceSrv.getAnnotationSources();
|
|
||||||
$scope.annotations = $scope.dashboard.annotations.list;
|
|
||||||
$scope.reset();
|
|
||||||
|
|
||||||
$scope.$watch('mode', function(newVal) {
|
|
||||||
if (newVal === 'new') { $scope.reset(); }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.datasourceChanged = function() {
|
|
||||||
return datasourceSrv.get($scope.currentAnnotation.datasource).then(function(ds) {
|
|
||||||
$scope.currentDatasource = ds;
|
|
||||||
$scope.currentAnnotation.datasource = $scope.currentAnnotation.datasource;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.edit = function(annotation) {
|
|
||||||
$scope.currentAnnotation = annotation;
|
|
||||||
$scope.currentIsNew = false;
|
|
||||||
$scope.datasourceChanged();
|
|
||||||
$scope.mode = 'edit';
|
|
||||||
|
|
||||||
$(".tooltip.in").remove();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.reset = function() {
|
|
||||||
$scope.currentAnnotation = angular.copy(annotationDefaults);
|
|
||||||
$scope.currentAnnotation.datasource = $scope.datasources[0].name;
|
|
||||||
$scope.currentIsNew = true;
|
|
||||||
$scope.datasourceChanged();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.update = function() {
|
|
||||||
$scope.reset();
|
|
||||||
$scope.mode = 'list';
|
|
||||||
$scope.broadcastRefresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.add = function() {
|
|
||||||
$scope.annotations.push($scope.currentAnnotation);
|
|
||||||
$scope.reset();
|
|
||||||
$scope.mode = 'list';
|
|
||||||
$scope.updateSubmenuVisibility();
|
|
||||||
$scope.broadcastRefresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
$scope.removeAnnotation = function(annotation) {
|
|
||||||
var index = _.indexOf($scope.annotations, annotation);
|
|
||||||
$scope.annotations.splice(index, 1);
|
|
||||||
$scope.updateSubmenuVisibility();
|
|
||||||
$scope.broadcastRefresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
82
public/app/features/annotations/editor_ctrl.ts
Normal file
82
public/app/features/annotations/editor_ctrl.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
///<reference path="../../headers/common.d.ts" />
|
||||||
|
|
||||||
|
import angular from 'angular';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import config from 'app/core/config';
|
||||||
|
import $ from 'jquery';
|
||||||
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
|
export class AnnotationsEditorCtrl {
|
||||||
|
mode: any;
|
||||||
|
datasources: any;
|
||||||
|
annotations: any;
|
||||||
|
currentAnnotation: any;
|
||||||
|
currentDatasource: any;
|
||||||
|
currentIsNew: any;
|
||||||
|
|
||||||
|
annotationDefaults: any = {
|
||||||
|
name: '',
|
||||||
|
datasource: null,
|
||||||
|
iconColor: 'rgba(255, 96, 96, 1)',
|
||||||
|
enable: true
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(private $scope, private datasourceSrv) {
|
||||||
|
$scope.ctrl = this;
|
||||||
|
|
||||||
|
this.mode = 'list';
|
||||||
|
this.datasources = datasourceSrv.getAnnotationSources();
|
||||||
|
this.annotations = $scope.dashboard.annotations.list;
|
||||||
|
this.reset();
|
||||||
|
|
||||||
|
$scope.$watch('mode', newVal => {
|
||||||
|
if (newVal === 'new') {
|
||||||
|
this.reset();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
datasourceChanged() {
|
||||||
|
return this.datasourceSrv.get(this.currentAnnotation.datasource).then(ds => {
|
||||||
|
this.currentDatasource = ds;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
edit(annotation) {
|
||||||
|
this.currentAnnotation = annotation;
|
||||||
|
this.currentIsNew = false;
|
||||||
|
this.datasourceChanged();
|
||||||
|
this.mode = 'edit';
|
||||||
|
$(".tooltip.in").remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.currentAnnotation = angular.copy(this.annotationDefaults);
|
||||||
|
this.currentAnnotation.datasource = this.datasources[0].name;
|
||||||
|
this.currentIsNew = true;
|
||||||
|
this.datasourceChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
this.reset();
|
||||||
|
this.mode = 'list';
|
||||||
|
this.$scope.broadcastRefresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
add() {
|
||||||
|
this.annotations.push(this.currentAnnotation);
|
||||||
|
this.reset();
|
||||||
|
this.mode = 'list';
|
||||||
|
this.$scope.updateSubmenuVisibility();
|
||||||
|
this.$scope.broadcastRefresh();
|
||||||
|
};
|
||||||
|
|
||||||
|
removeAnnotation(annotation) {
|
||||||
|
var index = _.indexOf(this.annotations, annotation);
|
||||||
|
this.annotations.splice(index, 1);
|
||||||
|
this.$scope.updateSubmenuVisibility();
|
||||||
|
this.$scope.broadcastRefresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coreModule.controller('AnnotationsEditorCtrl', AnnotationsEditorCtrl);
|
@ -1,4 +1,4 @@
|
|||||||
<div ng-controller="AnnotationsEditorCtrl" ng-init="init()">
|
<div ng-controller="AnnotationsEditorCtrl">
|
||||||
<div class="tabbed-view-header">
|
<div class="tabbed-view-header">
|
||||||
<h2 class="tabbed-view-title">
|
<h2 class="tabbed-view-title">
|
||||||
Annotations
|
Annotations
|
||||||
@ -6,16 +6,16 @@
|
|||||||
|
|
||||||
<ul class="gf-tabs">
|
<ul class="gf-tabs">
|
||||||
<li class="gf-tabs-item" >
|
<li class="gf-tabs-item" >
|
||||||
<a class="gf-tabs-link" ng-click="mode = 'list';" ng-class="{active: mode === 'list'}">
|
<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
|
||||||
List
|
List
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="gf-tabs-item" ng-show="mode === 'edit'">
|
<li class="gf-tabs-item" ng-show="ctrl.mode === 'edit'">
|
||||||
<a class="gf-tabs-link" ng-class="{active: mode === 'edit'}">
|
<a class="gf-tabs-link" ng-class="{active: ctrl.mode === 'edit'}">
|
||||||
{{currentAnnotation.name}}
|
{{currentAnnotation.name}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="gf-tabs-item" ng-show="mode === 'new'">
|
<li class="gf-tabs-item" ng-show="ctrl.mode === 'new'">
|
||||||
<span class="active gf-tabs-link">New</span>
|
<span class="active gf-tabs-link">New</span>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
@ -26,18 +26,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="tabbed-view-body">
|
<div class="tabbed-view-body">
|
||||||
<div class="editor-row row" ng-if="mode === 'list'">
|
<div class="editor-row row" ng-if="ctrl.mode === 'list'">
|
||||||
<div ng-if="annotations.length === 0">
|
<div ng-if="ctrl.annotations.length === 0">
|
||||||
<em>No annotations defined</em>
|
<em>No annotations defined</em>
|
||||||
</div>
|
</div>
|
||||||
<table class="grafana-options-table">
|
<table class="grafana-options-table">
|
||||||
<tr ng-repeat="annotation in annotations">
|
<tr ng-repeat="annotation in ctrl.annotations">
|
||||||
<td style="width:90%">
|
<td style="width:90%">
|
||||||
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
|
<i class="fa fa-bolt" style="color:{{annotation.iconColor}}"></i>
|
||||||
{{annotation.name}}
|
{{annotation.name}}
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index-1)" ng-hide="$first" class="pointer fa fa-arrow-up"></i></td>
|
||||||
<td style="width: 1%"><i ng-click="_.move(annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
|
||||||
|
|
||||||
<td style="width: 1%">
|
<td style="width: 1%">
|
||||||
<a ng-click="edit(annotation)" class="btn btn-inverse btn-mini">
|
<a ng-click="edit(annotation)" class="btn btn-inverse btn-mini">
|
||||||
@ -46,7 +46,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td style="width: 1%">
|
<td style="width: 1%">
|
||||||
<a ng-click="removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
<a ng-click="ctrl.removeAnnotation(annotation)" class="btn btn-danger btn-mini">
|
||||||
<i class="fa fa-remove"></i>
|
<i class="fa fa-remove"></i>
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
@ -54,43 +54,43 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form" ng-show="mode === 'list'">
|
<div class="gf-form" ng-show="ctrl.mode === 'list'">
|
||||||
<div class="gf-form-button-row">
|
<div class="gf-form-button-row">
|
||||||
<a type="button" class="btn gf-form-button btn-success" ng-click="mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
<a type="button" class="btn gf-form-button btn-success" ng-click="ctrl.mode = 'new';"><i class="fa fa-plus" ></i> New</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="annotations-basic-settings" ng-if="mode === 'edit' || mode === 'new'">
|
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||||
<div class="gf-form-group">
|
<div class="gf-form-group">
|
||||||
<div class="gf-form-inline">
|
<div class="gf-form-inline">
|
||||||
<div class="gf-form gf-size-max-xxl">
|
<div class="gf-form gf-size-max-xxl">
|
||||||
<span class="gf-form-label">Name</span>
|
<span class="gf-form-label">Name</span>
|
||||||
<input type="text" class="gf-form-input" ng-model='currentAnnotation.name' placeholder="name"></input>
|
<input type="text" class="gf-form-input" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<span class="gf-form-label max-width-10">Datasource</span>
|
<span class="gf-form-label max-width-10">Datasource</span>
|
||||||
<div class="gf-form-select-wrapper">
|
<div class="gf-form-select-wrapper">
|
||||||
<select class="gf-form-input gf-size-auto" ng-model="currentAnnotation.datasource" ng-options="f.name as f.name for f in datasources" ng-change="datasourceChanged()"></select>
|
<select class="gf-form-input gf-size-auto" ng-model="ctrl.currentAnnotation.datasource" ng-options="f.name as f.name for f in ctrl.datasources" ng-change="ctrl.datasourceChanged()"></select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<label class="gf-form-label">
|
<label class="gf-form-label">
|
||||||
<span>Color</span>
|
<span>Color</span>
|
||||||
</label>
|
</label>
|
||||||
<spectrum-picker class="gf-form-input" ng-model="currentAnnotation.iconColor"></spectrum-picker>
|
<spectrum-picker class="gf-form-input" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<rebuild-on-change property="currentDatasource">
|
<rebuild-on-change property="ctrl.currentDatasource">
|
||||||
<plugin-component type="annotations-query-ctrl">
|
<plugin-component type="annotations-query-ctrl">
|
||||||
</plugin-component>
|
</plugin-component>
|
||||||
</rebuild-on-change>
|
</rebuild-on-change>
|
||||||
|
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
<div class="gf-form-button-row p-y-0">
|
<div class="gf-form-button-row p-y-0">
|
||||||
<button ng-show="mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="add()">Add</button>
|
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||||
<button ng-show="mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="update()">Update</button>
|
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -12,6 +12,21 @@ class GrafanaDatasource {
|
|||||||
maxDataPoints: options.maxDataPoints
|
maxDataPoints: options.maxDataPoints
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
annotationQuery(options) {
|
||||||
|
return this.backendSrv.get('/api/annotations', {
|
||||||
|
from: options.range.from.valueOf(),
|
||||||
|
to: options.range.to.valueOf(),
|
||||||
|
limit: options.limit,
|
||||||
|
type: options.type,
|
||||||
|
}).then(data => {
|
||||||
|
return data.map(item => {
|
||||||
|
item.annotation = options.annotation;
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export {GrafanaDatasource};
|
export {GrafanaDatasource};
|
||||||
|
@ -8,9 +8,22 @@ class GrafanaQueryCtrl extends QueryCtrl {
|
|||||||
static templateUrl = 'partials/query.editor.html';
|
static templateUrl = 'partials/query.editor.html';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class GrafanaAnnotationsQueryCtrl {
|
||||||
|
annotation: any;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.annotation.type = this.annotation.type || 'alert';
|
||||||
|
this.annotation.limit = this.annotation.limit || 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
GrafanaDatasource,
|
GrafanaDatasource,
|
||||||
GrafanaDatasource as Datasource,
|
GrafanaDatasource as Datasource,
|
||||||
GrafanaQueryCtrl as QueryCtrl,
|
GrafanaQueryCtrl as QueryCtrl,
|
||||||
|
GrafanaAnnotationsQueryCtrl as AnnotationsQueryCtrl,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<div class="gf-form-group">
|
||||||
|
<h6>Filters</h6>
|
||||||
|
<div class="gf-form-inline">
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-7">Type</span>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Alert', value: 'alert'}]">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="gf-form">
|
||||||
|
<span class="gf-form-label width-7">Max limit</span>
|
||||||
|
<div class="gf-form-select-wrapper">
|
||||||
|
<select class="gf-form-input" ng-model="ctrl.annotation.limit" ng-options="f for f in [10,50,100,200,300,500,1000,2000]">
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -4,5 +4,6 @@
|
|||||||
"id": "grafana",
|
"id": "grafana",
|
||||||
|
|
||||||
"builtIn": true,
|
"builtIn": true,
|
||||||
|
"annotations": true,
|
||||||
"metrics": true
|
"metrics": true
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user