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 @@ +/// + +import _ from 'lodash'; +import moment from 'moment'; +import {coreModule} from 'app/core/core'; +import {MetricsPanelCtrl} from 'app/plugins/sdk'; +import {AnnotationEvent} from './event'; + +export class EventEditorCtrl { + panelCtrl: MetricsPanelCtrl; + event: AnnotationEvent; + timeRange: {from: number, to: number}; + form: any; + close: any; + + /** @ngInject **/ + constructor(private annotationsSrv) { + this.event.panelId = this.panelCtrl.panel.id; + this.event.dashboardId = this.panelCtrl.dashboard.id; + } + + save() { + if (!this.form.$valid) { + return; + } + + let saveModel = _.cloneDeep(this.event); + saveModel.time = saveModel.time.valueOf(); + saveModel.timeEnd = 0; + + if (saveModel.isRegion) { + saveModel.timeEnd = saveModel.timeEnd.valueOf(); + + if (saveModel.timeEnd < saveModel.time) { + console.log('invalid time'); + return; + } + } + + this.annotationsSrv.saveAnnotationEvent(saveModel).then(() => { + this.panelCtrl.refresh(); + this.close(); + }); + } + + timeChanged() { + this.panelCtrl.render(); + } +} + +export function eventEditor() { + return { + restrict: 'E', + controller: EventEditorCtrl, + bindToController: true, + controllerAs: 'ctrl', + templateUrl: 'public/app/features/annotations/partials/event_editor.html', + scope: { + "panelCtrl": "=", + "event": "=", + "close": "&", + } + }; +} + +coreModule.directive('eventEditor', eventEditor); diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts new file mode 100644 index 00000000000..85c41354c0d --- /dev/null +++ b/public/app/features/annotations/event_manager.ts @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import moment from 'moment'; +import {MetricsPanelCtrl} from 'app/plugins/sdk'; +import {AnnotationEvent} from './event'; + +export class EventManager { + event: AnnotationEvent; + + constructor(private panelCtrl: MetricsPanelCtrl, private elem, private popoverSrv) { + } + + editorClosed() { + console.log('editorClosed'); + this.event = null; + this.panelCtrl.render(); + } + + updateTime(range) { + let newEvent = true; + + if (this.event) { + newEvent = false; + } else { + // init new event + this.event = new AnnotationEvent(); + this.event.dashboardId = this.panelCtrl.dashboard.id; + this.event.panelId = this.panelCtrl.panel.id; + } + + // update time + this.event.time = moment(range.from); + this.event.isRegion = false; + if (range.to) { + this.event.timeEnd = moment(range.to); + this.event.isRegion = true; + } + + // newEvent means the editor is not visible + if (!newEvent) { + this.panelCtrl.render(); + return; + } + + this.popoverSrv.show({ + element: this.elem[0], + classNames: 'drop-popover drop-popover--form', + position: 'bottom center', + openOn: null, + template: '', + onClose: this.editorClosed.bind(this), + model: { + event: this.event, + panelCtrl: this.panelCtrl, + }, + }); + + this.panelCtrl.render(); + } + + addFlotEvents(annotations, flotOptions) { + if (!this.event || annotations.length === 0) { + return; + } + + var types = { + '$__alerting': { + color: 'rgba(237, 46, 24, 1)', + position: 'BOTTOM', + markerSize: 5, + }, + '$__ok': { + color: 'rgba(11, 237, 50, 1)', + position: 'BOTTOM', + markerSize: 5, + }, + '$__no_data': { + color: 'rgba(150, 150, 150, 1)', + position: 'BOTTOM', + markerSize: 5, + }, + }; + + if (this.event) { + annotations = [ + { + min: this.event.time.valueOf(), + title: this.event.title, + text: this.event.text, + eventType: '$__alerting', + } + ]; + } else { + // annotations from query + for (var i = 0; i < annotations.length; i++) { + var item = annotations[i]; + if (item.newState) { + item.eventType = '$__' + item.newState; + continue; + } + + if (!types[item.source.name]) { + types[item.source.name] = { + color: item.source.iconColor, + position: 'BOTTOM', + markerSize: 5, + }; + } + } + } + + flotOptions.events = { + levels: _.keys(types).length + 1, + data: annotations, + types: types, + }; + } +} diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 0bfc8bb2028..5acc2bc60b4 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -7,16 +7,16 @@ @@ -62,37 +62,53 @@
-
-
- Name - -
-
- Datasource -
- +
Options
+
+
+ Name + +
+
+ Data source +
+ +
-
- - +
+
+ + + + + + + + +
+
+ + +
-
- - - - +
Query
+ + + + -
-
- - +
+
+ + +
-
diff --git a/public/app/features/annotations/partials/event_editor.html b/public/app/features/annotations/partials/event_editor.html new file mode 100644 index 00000000000..6e44b6f768d --- /dev/null +++ b/public/app/features/annotations/partials/event_editor.html @@ -0,0 +1,38 @@ + +
Add annotation
+ +
+
+
+ Title + +
+ +
+
+ Time + +
+
+ +
+
+ Start + +
+
+ End + +
+
+
+ Description + +
+ +
+ + Cancel +
+
+
diff --git a/public/app/features/dashboard/model.ts b/public/app/features/dashboard/model.ts index e31a6c1afd0..e62c98b8598 100644 --- a/public/app/features/dashboard/model.ts +++ b/public/app/features/dashboard/model.ts @@ -193,32 +193,22 @@ export class DashboardModel { }); } - toggleEditMode() { - if (!this.meta.canEdit) { - console.log('Not allowed to edit dashboard'); - return; - } - - this.editMode = !this.editMode; - this.updateSubmenuVisibility(); - this.events.emit('edit-mode-changed', this.editMode); - } - setPanelFocus(id) { this.meta.focusPanelId = id; } updateSubmenuVisibility() { - if (this.editMode) { - this.meta.submenuEnabled = true; - return; - } + this.meta.submenuEnabled = (() => { + if (this.links.length > 0) { return true; } - var visibleVars = _.filter(this.templating.list, function(template) { - return template.hide !== 2; - }); + var visibleVars = _.filter(this.templating.list, variable => variable.hide !== 2); + if (visibleVars.length > 0) { return true; } - this.meta.submenuEnabled = visibleVars.length > 0 || this.annotations.list.length > 0 || this.links.length > 0; + var visibleAnnotations = _.filter(this.annotations.list, annotation => annotation.hide !== true); + if (visibleAnnotations.length > 0) { return true; } + + return false; + })(); } getPanelInfoById(panelId) { diff --git a/public/app/features/dashboard/partials/addAnnotationModal.html b/public/app/features/dashboard/partials/addAnnotationModal.html new file mode 100644 index 00000000000..f55f888375f --- /dev/null +++ b/public/app/features/dashboard/partials/addAnnotationModal.html @@ -0,0 +1,65 @@ + +
diff --git a/public/app/features/dashboard/specs/dashboard_model_specs.ts b/public/app/features/dashboard/specs/dashboard_model_specs.ts index c7d85b8a190..1c7415d342e 100644 --- a/public/app/features/dashboard/specs/dashboard_model_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_model_specs.ts @@ -364,4 +364,85 @@ describe('DashboardModel', function() { }); }); + describe('updateSubmenuVisibility with empty lists', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({}); + model.updateSubmenuVisibility(); + }); + + it('should not enable submmenu', function() { + expect(model.meta.submenuEnabled).to.be(false); + }); + }); + + describe('updateSubmenuVisibility with annotation', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + annotations: { + list: [{}] + } + }); + model.updateSubmenuVisibility(); + }); + + it('should enable submmenu', function() { + expect(model.meta.submenuEnabled).to.be(true); + }); + }); + + describe('updateSubmenuVisibility with template var', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + templating: { + list: [{}] + } + }); + model.updateSubmenuVisibility(); + }); + + it('should enable submmenu', function() { + expect(model.meta.submenuEnabled).to.be(true); + }); + }); + + describe('updateSubmenuVisibility with hidden template var', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + templating: { + list: [{hide: 2}] + } + }); + model.updateSubmenuVisibility(); + }); + + it('should not enable submmenu', function() { + expect(model.meta.submenuEnabled).to.be(false); + }); + }); + + describe('updateSubmenuVisibility with hidden annotation toggle', function() { + var model; + + beforeEach(function() { + model = new DashboardModel({ + annotations: { + list: [{hide: true}] + } + }); + model.updateSubmenuVisibility(); + }); + + it('should not enable submmenu', function() { + expect(model.meta.submenuEnabled).to.be(false); + }); + }); + }); diff --git a/public/app/features/dashboard/submenu/submenu.html b/public/app/features/dashboard/submenu/submenu.html index 3e09fe4425e..ce3c61f1cc3 100644 --- a/public/app/features/dashboard/submenu/submenu.html +++ b/public/app/features/dashboard/submenu/submenu.html @@ -1,4 +1,4 @@ -