diff --git a/package.json b/package.json
index d38de899bf5..326c813dc93 100644
--- a/package.json
+++ b/package.json
@@ -76,7 +76,7 @@
"systemjs-builder": "^0.15.34",
"tether": "^1.4.0",
"tether-drop": "https://github.com/torkelo/drop",
- "tslint": "^4.0.2",
+ "tslint": "^4.5.1",
"typescript": "^2.1.4",
"virtual-scroll": "^1.1.1"
}
diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server
index 61e82d4c612..d01778560f7 100755
--- a/packaging/deb/init.d/grafana-server
+++ b/packaging/deb/init.d/grafana-server
@@ -37,14 +37,8 @@ MAX_OPEN_FILES=10000
PID_FILE=/var/run/$NAME.pid
DAEMON=/usr/sbin/$NAME
-
umask 0027
-if [ `id -u` -ne 0 ]; then
- echo "You need root privileges to run this script"
- exit 4
-fi
-
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
@@ -63,9 +57,16 @@ fi
DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}"
+function checkUser() {
+ if [ `id -u` -ne 0 ]; then
+ echo "You need root privileges to run this script"
+ exit 4
+ fi
+}
+
case "$1" in
start)
-
+ checkUser
log_daemon_msg "Starting $DESC"
pid=`pidofproc -p $PID_FILE grafana`
@@ -112,6 +113,7 @@ case "$1" in
log_end_msg $return
;;
stop)
+ checkUser
log_daemon_msg "Stopping $DESC"
if [ -f "$PID_FILE" ]; then
diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server
index cb9bb73de7d..a9e2988bdb7 100755
--- a/packaging/rpm/init.d/grafana-server
+++ b/packaging/rpm/init.d/grafana-server
@@ -36,11 +36,6 @@ MAX_OPEN_FILES=10000
PID_FILE=/var/run/$NAME.pid
DAEMON=/usr/sbin/$NAME
-if [ `id -u` -ne 0 ]; then
- echo "You need root privileges to run this script"
- exit 4
-fi
-
if [ ! -x $DAEMON ]; then
echo "Program not installed or not executable"
exit 5
@@ -70,8 +65,16 @@ function isRunning() {
status -p $PID_FILE $NAME > /dev/null 2>&1
}
+function checkUser() {
+ if [ `id -u` -ne 0 ]; then
+ echo "You need root privileges to run this script"
+ exit 4
+ fi
+}
+
case "$1" in
start)
+ checkUser
isRunning
if [ $? -eq 0 ]; then
echo "Already running."
@@ -115,6 +118,7 @@ case "$1" in
exit $return
;;
stop)
+ checkUser
echo -n "Stopping $DESC: ..."
if [ -f "$PID_FILE" ]; then
diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go
index 48bf6c327ad..a5211cfbec2 100644
--- a/pkg/api/annotations.go
+++ b/pkg/api/annotations.go
@@ -39,12 +39,52 @@ func GetAnnotations(c *middleware.Context) Response {
Text: item.Text,
Metric: item.Metric,
Title: item.Title,
+ PanelId: item.PanelId,
+ RegionId: item.RegionId,
})
}
return Json(200, result)
}
+func PostAnnotation(c *middleware.Context, cmd dtos.PostAnnotationsCmd) Response {
+ repo := annotations.GetRepository()
+
+ item := annotations.Item{
+ OrgId: c.OrgId,
+ DashboardId: cmd.DashboardId,
+ PanelId: cmd.PanelId,
+ Epoch: cmd.Time / 1000,
+ Title: cmd.Title,
+ Text: cmd.Text,
+ CategoryId: cmd.CategoryId,
+ NewState: cmd.FillColor,
+ Type: annotations.EventType,
+ }
+
+ if err := repo.Save(&item); err != nil {
+ return ApiError(500, "Failed to save annotation", err)
+ }
+
+ // handle regions
+ if cmd.IsRegion {
+ item.RegionId = item.Id
+
+ if err := repo.Update(&item); err != nil {
+ return ApiError(500, "Failed set regionId on annotation", err)
+ }
+
+ item.Id = 0
+ item.Epoch = cmd.TimeEnd
+
+ if err := repo.Save(&item); err != nil {
+ return ApiError(500, "Failed save annotation for region end time", err)
+ }
+ }
+
+ return ApiSuccess("Annotation added")
+}
+
func DeleteAnnotations(c *middleware.Context, cmd dtos.DeleteAnnotationsCmd) Response {
repo := annotations.GetRepository()
diff --git a/pkg/api/api.go b/pkg/api/api.go
index 843b68eb915..6dcc900c16f 100644
--- a/pkg/api/api.go
+++ b/pkg/api/api.go
@@ -277,7 +277,10 @@ func (hs *HttpServer) registerRoutes() {
}, reqEditorRole)
r.Get("/annotations", wrap(GetAnnotations))
- r.Post("/annotations/mass-delete", reqOrgAdmin, bind(dtos.DeleteAnnotationsCmd{}), wrap(DeleteAnnotations))
+
+ r.Group("/annotations", func() {
+ r.Post("/", bind(dtos.PostAnnotationsCmd{}), wrap(PostAnnotation))
+ }, reqEditorRole)
// error test
r.Get("/metrics/error", wrap(GenerateError))
diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go
index 45415978ee1..7bf33618261 100644
--- a/pkg/api/dtos/annotations.go
+++ b/pkg/api/dtos/annotations.go
@@ -12,10 +12,24 @@ type Annotation struct {
Title string `json:"title"`
Text string `json:"text"`
Metric string `json:"metric"`
+ RegionId int64 `json:"regionId"`
Data *simplejson.Json `json:"data"`
}
+type PostAnnotationsCmd struct {
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ CategoryId int64 `json:"categoryId"`
+ Time int64 `json:"time"`
+ Title string `json:"title"`
+ Text string `json:"text"`
+
+ FillColor string `json:"fillColor"`
+ IsRegion bool `json:"isRegion"`
+ TimeEnd int64 `json:"timeEnd"`
+}
+
type DeleteAnnotationsCmd struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go
index d9d15bca34b..be9d3f2d4d0 100644
--- a/pkg/services/annotations/annotations.go
+++ b/pkg/services/annotations/annotations.go
@@ -4,6 +4,7 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
type Repository interface {
Save(item *Item) error
+ Update(item *Item) error
Find(query *ItemQuery) ([]*Item, error)
Delete(params *DeleteParams) error
}
@@ -21,6 +22,14 @@ type ItemQuery struct {
Limit int64 `json:"limit"`
}
+type PostParams struct {
+ DashboardId int64 `json:"dashboardId"`
+ PanelId int64 `json:"panelId"`
+ Epoch int64 `json:"epoch"`
+ Title string `json:"title"`
+ Text string `json:"text"`
+}
+
type DeleteParams struct {
AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
@@ -41,6 +50,7 @@ type ItemType string
const (
AlertType ItemType = "alert"
+ EventType ItemType = "event"
)
type Item struct {
@@ -49,6 +59,7 @@ type Item struct {
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
CategoryId int64 `json:"categoryId"`
+ RegionId int64 `json:"regionId"`
Type ItemType `json:"type"`
Title string `json:"title"`
Text string `json:"text"`
diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go
index e219f48d2fe..62a10ee2106 100644
--- a/pkg/services/sqlstore/annotation.go
+++ b/pkg/services/sqlstore/annotation.go
@@ -23,6 +23,17 @@ func (r *SqlAnnotationRepo) Save(item *annotations.Item) error {
})
}
+func (r *SqlAnnotationRepo) Update(item *annotations.Item) error {
+ return inTransaction(func(sess *xorm.Session) error {
+
+ if _, err := sess.Table("annotation").Id(item.Id).Update(item); err != nil {
+ return err
+ }
+
+ return nil
+ })
+}
+
func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.Item, error) {
var sql bytes.Buffer
params := make([]interface{}, 0)
diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go
index 4a7206f9d64..a9343266863 100644
--- a/pkg/services/sqlstore/migrations/annotation_mig.go
+++ b/pkg/services/sqlstore/migrations/annotation_mig.go
@@ -35,7 +35,6 @@ func addAnnotationMig(mg *Migrator) {
}
mg.AddMigration("Drop old annotation table v4", NewDropTableMigration("annotation"))
-
mg.AddMigration("create annotation table v5", NewAddTableMigration(table))
// create indices
@@ -54,4 +53,8 @@ func addAnnotationMig(mg *Migrator) {
{Name: "new_state", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "data", Type: DB_Text, Nullable: false},
}))
+
+ mg.AddMigration("Add column region_id to annotation table", NewAddColumnMigration(table, &Column{
+ Name: "region_id", Type: DB_BigInt, Nullable: true, Default: "0",
+ }))
}
diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go
index 27fba0068d1..fe759ec8df9 100644
--- a/pkg/services/sqlstore/sqlstore.go
+++ b/pkg/services/sqlstore/sqlstore.go
@@ -98,8 +98,8 @@ func SetEngine(engine *xorm.Engine) (err error) {
return fmt.Errorf("Sqlstore::Migration failed err: %v\n", err)
}
+ // Init repo instances
annotations.SetRepository(&SqlAnnotationRepo{})
-
return nil
}
diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts
index 2a64ec487f7..889398d5138 100644
--- a/public/app/core/components/switch.ts
+++ b/public/app/core/components/switch.ts
@@ -9,7 +9,7 @@ import Drop from 'tether-drop';
var template = `
diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts
index 7abc3993e9d..acf0123962b 100644
--- a/public/app/core/services/keybindingSrv.ts
+++ b/public/app/core/services/keybindingSrv.ts
@@ -83,10 +83,6 @@ export class KeybindingSrv {
}
setupDashboardBindings(scope, dashboard) {
- // this.bind('b', () => {
- // dashboard.toggleEditMode();
- // });
-
this.bind('mod+o', () => {
dashboard.graphTooltip = (dashboard.graphTooltip + 1) % 3;
appEvents.emit('graph-hover-clear');
diff --git a/public/app/core/services/popover_srv.ts b/public/app/core/services/popover_srv.ts
index 73249a67b5b..6f61ee742a3 100644
--- a/public/app/core/services/popover_srv.ts
+++ b/public/app/core/services/popover_srv.ts
@@ -7,51 +7,69 @@ import coreModule from 'app/core/core_module';
import Drop from 'tether-drop';
/** @ngInject **/
-function popoverSrv($compile, $rootScope) {
+function popoverSrv($compile, $rootScope, $timeout) {
+ let openDrop = null;
+
+ this.close = function() {
+ if (openDrop) {
+ openDrop.close();
+ }
+ };
this.show = function(options) {
- var popoverScope = _.extend($rootScope.$new(true), options.model);
+ if (openDrop) {
+ openDrop.close();
+ openDrop = null;
+ }
+
+ var scope = _.extend($rootScope.$new(true), options.model);
var drop;
- function destroyDrop() {
- setTimeout(function() {
+ var cleanUp = () => {
+ setTimeout(() => {
+ scope.$destroy();
+
if (drop.tether) {
drop.destroy();
}
- });
- }
- popoverScope.dismiss = function() {
- popoverScope.$destroy();
- destroyDrop();
+ if (options.onClose) {
+ options.onClose();
+ }
+ });
+
+ openDrop = null;
+ };
+
+ scope.dismiss = () => {
+ drop.close();
};
var contentElement = document.createElement('div');
contentElement.innerHTML = options.template;
- $compile(contentElement)(popoverScope);
+ $compile(contentElement)(scope);
- drop = new Drop({
- target: options.element,
- content: contentElement,
- position: options.position,
- classes: 'drop-popover',
- openOn: options.openOn || 'hover',
- hoverCloseDelay: 200,
- tetherOptions: {
- constraints: [{to: 'window', pin: true, attachment: "both"}]
- }
- });
+ $timeout(() => {
+ drop = new Drop({
+ target: options.element,
+ content: contentElement,
+ position: options.position,
+ classes: options.classNames || 'drop-popover',
+ openOn: options.openOn,
+ hoverCloseDelay: 200,
+ tetherOptions: {
+ constraints: [{to: 'scrollParent', attachment: "none both"}]
+ }
+ });
- drop.on('close', () => {
- popoverScope.dismiss({fromDropClose: true});
- destroyDrop();
- if (options.onClose) {
- options.onClose();
- }
- });
+ drop.on('close', () => {
+ cleanUp();
+ });
- setTimeout(() => { drop.open(); }, 10);
+ openDrop = drop;
+ openDrop.open();
+ }, 100);
};
}
diff --git a/public/app/features/all.js b/public/app/features/all.js
index cd7adb49de6..96c28288e8e 100644
--- a/public/app/features/all.js
+++ b/public/app/features/all.js
@@ -1,7 +1,7 @@
define([
'./panellinks/module',
'./dashlinks/module',
- './annotations/annotations_srv',
+ './annotations/all',
'./templating/all',
'./dashboard/all',
'./playlist/all',
diff --git a/public/app/features/annotations/all.ts b/public/app/features/annotations/all.ts
new file mode 100644
index 00000000000..5f195928c7a
--- /dev/null
+++ b/public/app/features/annotations/all.ts
@@ -0,0 +1,12 @@
+
+import {AnnotationsSrv} from './annotations_srv';
+import {eventEditor} from './event_editor';
+import {EventManager} from './event_manager';
+import {AnnotationEvent} from './event';
+
+export {
+ AnnotationsSrv,
+ eventEditor,
+ EventManager,
+ AnnotationEvent,
+};
diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts
index 759e4500c7d..c2422a63c27 100644
--- a/public/app/features/annotations/annotations_srv.ts
+++ b/public/app/features/annotations/annotations_srv.ts
@@ -36,6 +36,18 @@ export class AnnotationsSrv {
// combine the annotations and flatten results
var annotations = _.flattenDeep([results[0], results[1]]);
+ // filter out annotations that do not belong to requesting panel
+ annotations = _.filter(annotations, item => {
+ // shownIn === 1 requires annotation matching panel id
+ if (item.source.showIn === 1) {
+ if (item.panelId && options.panel.id === item.panelId) {
+ return true;
+ }
+ return false;
+ }
+ return true;
+ });
+
// look for alert state for this panel
var alertState = _.find(results[2], {panelId: options.panel.id});
@@ -126,6 +138,11 @@ export class AnnotationsSrv {
return this.globalAnnotationsPromise;
}
+ saveAnnotationEvent(annotation) {
+ this.globalAnnotationsPromise = null;
+ return this.backendSrv.post('/api/annotations', annotation);
+ }
+
translateQueryResult(annotation, results) {
for (var item of results) {
item.source = annotation;
diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts
index cb76045dbec..deb90691d91 100644
--- a/public/app/features/annotations/editor_ctrl.ts
+++ b/public/app/features/annotations/editor_ctrl.ts
@@ -17,9 +17,16 @@ export class AnnotationsEditorCtrl {
name: '',
datasource: null,
iconColor: 'rgba(255, 96, 96, 1)',
- enable: true
+ enable: true,
+ showIn: 0,
+ hide: false,
};
+ showOptions: any = [
+ {text: 'All Panels', value: 0},
+ {text: 'Specific Panels', value: 1},
+ ];
+
/** @ngInject */
constructor(private $scope, private datasourceSrv) {
$scope.ctrl = this;
@@ -44,6 +51,7 @@ export class AnnotationsEditorCtrl {
edit(annotation) {
this.currentAnnotation = annotation;
+ this.currentAnnotation.showIn = this.currentAnnotation.showIn || 0;
this.currentIsNew = false;
this.datasourceChanged();
this.mode = 'edit';
@@ -74,7 +82,7 @@ export class AnnotationsEditorCtrl {
removeAnnotation(annotation) {
var index = _.indexOf(this.annotations, annotation);
this.annotations.splice(index, 1);
- this.$scope.updateSubmenuVisibility();
+ this.$scope.dashboard.updateSubmenuVisibility();
this.$scope.broadcastRefresh();
}
}
diff --git a/public/app/features/annotations/event.ts b/public/app/features/annotations/event.ts
new file mode 100644
index 00000000000..53afbea5b07
--- /dev/null
+++ b/public/app/features/annotations/event.ts
@@ -0,0 +1,10 @@
+
+export class AnnotationEvent {
+ dashboardId: number;
+ panelId: number;
+ time: any;
+ timeEnd: any;
+ isRegion: boolean;
+ title: string;
+ text: string;
+}
diff --git a/public/app/features/annotations/event_editor.ts b/public/app/features/annotations/event_editor.ts
new file mode 100644
index 00000000000..939920e21ad
--- /dev/null
+++ b/public/app/features/annotations/event_editor.ts
@@ -0,0 +1,66 @@
+///