mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge remote-tracking branch 'grafana/master' into influx-db-query2
This commit is contained in:
commit
8c838cff08
@ -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"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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"`
|
||||
|
@ -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"`
|
||||
|
@ -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)
|
||||
|
@ -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",
|
||||
}))
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ import Drop from 'tether-drop';
|
||||
var template = `
|
||||
<label for="check-{{ctrl.id}}" class="gf-form-label {{ctrl.labelClass}} pointer">
|
||||
{{ctrl.label}}
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip">
|
||||
<info-popover mode="right-normal" ng-if="ctrl.tooltip" position="top center">
|
||||
{{ctrl.tooltip}}
|
||||
</info-popover>
|
||||
</label>
|
||||
|
@ -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');
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
define([
|
||||
'./panellinks/module',
|
||||
'./dashlinks/module',
|
||||
'./annotations/annotations_srv',
|
||||
'./annotations/all',
|
||||
'./templating/all',
|
||||
'./dashboard/all',
|
||||
'./playlist/all',
|
||||
|
12
public/app/features/annotations/all.ts
Normal file
12
public/app/features/annotations/all.ts
Normal file
@ -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,
|
||||
};
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
10
public/app/features/annotations/event.ts
Normal file
10
public/app/features/annotations/event.ts
Normal file
@ -0,0 +1,10 @@
|
||||
|
||||
export class AnnotationEvent {
|
||||
dashboardId: number;
|
||||
panelId: number;
|
||||
time: any;
|
||||
timeEnd: any;
|
||||
isRegion: boolean;
|
||||
title: string;
|
||||
text: string;
|
||||
}
|
66
public/app/features/annotations/event_editor.ts
Normal file
66
public/app/features/annotations/event_editor.ts
Normal file
@ -0,0 +1,66 @@
|
||||
///<reference path="../../headers/common.d.ts" />
|
||||
|
||||
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);
|
117
public/app/features/annotations/event_manager.ts
Normal file
117
public/app/features/annotations/event_manager.ts
Normal file
@ -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: '<event-editor panel-ctrl="panelCtrl" event="event" close="dismiss()"></event-editor>',
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
@ -7,16 +7,16 @@
|
||||
<ul class="gf-tabs">
|
||||
<li class="gf-tabs-item" >
|
||||
<a class="gf-tabs-link" ng-click="ctrl.mode = 'list';" ng-class="{active: ctrl.mode === 'list'}">
|
||||
List
|
||||
Queries
|
||||
</a>
|
||||
</li>
|
||||
<li class="gf-tabs-item" ng-show="ctrl.mode === 'edit'">
|
||||
<a class="gf-tabs-link" ng-class="{active: ctrl.mode === 'edit'}">
|
||||
{{currentAnnotation.name}}
|
||||
Edit Query
|
||||
</a>
|
||||
</li>
|
||||
<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 Query</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@ -62,37 +62,53 @@
|
||||
|
||||
<div class="annotations-basic-settings" ng-if="ctrl.mode === 'edit' || ctrl.mode === 'new'">
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form gf-size-max-xxl">
|
||||
<span class="gf-form-label">Name</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label max-width-10">Datasource</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<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>
|
||||
<h5 class="section-heading">Options</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Name</span>
|
||||
<input type="text" class="gf-form-input width-12" ng-model='ctrl.currentAnnotation.name' placeholder="name"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Data source</span>
|
||||
<div class="gf-form-select-wrapper width-12">
|
||||
<select class="gf-form-input" 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 class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<span>Color</span>
|
||||
</label>
|
||||
<spectrum-picker class="gf-form-input" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<!-- <div class="gf-form"> -->
|
||||
<!-- <span class="gf-form-label width-7">Show in</span> -->
|
||||
<!-- <div class="gf-form-select-wrapper width-12"> -->
|
||||
<!-- <select class="gf-form-input" ng-model="ctrl.currentAnnotation.showIn" ng-options="f.value as f.text for f in ctrl.showOptions"></select> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<gf-form-switch class="gf-form"
|
||||
label="Hide toggle"
|
||||
tooltip="Hides the annotation query toggle from showing at the top of the dashboard"
|
||||
checked="ctrl.currentAnnotation.hide"
|
||||
label-class="width-7">
|
||||
</gf-form-switch>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-7">Color</label>
|
||||
<spectrum-picker class="gf-form-input width-3" ng-model="ctrl.currentAnnotation.iconColor"></spectrum-picker>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<rebuild-on-change property="ctrl.currentDatasource">
|
||||
<plugin-component type="annotations-query-ctrl">
|
||||
</plugin-component>
|
||||
</rebuild-on-change>
|
||||
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</button>
|
||||
<button ng-show="ctrl.mode === 'edit'" type="button" class="btn btn-success pull-left" ng-click="ctrl.update()">Update</button>
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-button-row p-y-0">
|
||||
<button ng-show="ctrl.mode === 'new'" type="button" class="btn gf-form-button btn-success" ng-click="ctrl.add()">Add</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>
|
||||
|
38
public/app/features/annotations/partials/event_editor.html
Normal file
38
public/app/features/annotations/partials/event_editor.html
Normal file
@ -0,0 +1,38 @@
|
||||
|
||||
<h5 class="section-heading text-center">Add annotation</h5>
|
||||
|
||||
<form name="ctrl.form" class="text-center">
|
||||
<div style="display: inline-block">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Title</span>
|
||||
<input type="text" ng-model="ctrl.event.title" class="gf-form-input max-width-20" required>
|
||||
</div>
|
||||
<!-- single event -->
|
||||
<div ng-if="!ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Time</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<!-- region event -->
|
||||
<div ng-if="ctrl.event.isRegion">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">Start</span>
|
||||
<input type="text" ng-model="ctrl.event.time" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-7">End</span>
|
||||
<input type="text" ng-model="ctrl.event.timeEnd" class="gf-form-input max-width-20" input-datetime required ng-change="ctrl.timeChanged()">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-7">Description</span>
|
||||
<textarea class="gf-form-input width-20" rows="3" ng-model="ctrl.event.text" placeholder="Event description"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button type="submit" class="btn gf-form-btn btn-success" ng-click="ctrl.save()">Save</button>
|
||||
<a class="btn-text" ng-click="ctrl.close();">Cancel</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
@ -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) {
|
||||
|
@ -0,0 +1,65 @@
|
||||
<div class="modal-body" ng-controller="AddAnnotationModalCtrl">
|
||||
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
Add Annotation
|
||||
</h2>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.close()">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal-content">
|
||||
<div class="share-modal-body">
|
||||
<div class="share-modal-header">
|
||||
|
||||
<div class="share-modal-big-icon">
|
||||
<i class="fa fa-tag"></i>
|
||||
</div>
|
||||
|
||||
<div class="share-modal-content">
|
||||
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<p class="share-modal-info-text">
|
||||
Add annotation details.
|
||||
</p>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Title</span>
|
||||
<input type="text" ng-model="ctrl.annotation.title" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8" ng-if="!ctrl.annotation.timeTo">Time</span>
|
||||
<span class="gf-form-label width-8" ng-if="ctrl.annotation.timeTo">Time Start</span>
|
||||
<input type="text" ng-model="ctrl.annotation.time" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
<div class="gf-form" ng-if="ctrl.annotation.timeTo">
|
||||
<span class="gf-form-label width-8">Time Stop</span>
|
||||
<input type="text" ng-model="ctrl.annotation.timeTo" class="gf-form-input max-width-20">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h6>Description</h6>
|
||||
</div>
|
||||
<div class="gf-form-group share-modal-options">
|
||||
<div class="gf-form">
|
||||
<textarea rows="3" class="gf-form-input width-27" ng-model="ctrl.annotation.text"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn gf-form-btn width-10 btn-success" ng-click="ctrl.addAnnotation()">
|
||||
<i class="fa fa-pencil"></i>
|
||||
Add Annotation
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
<div class="submenu-controls gf-form-query">
|
||||
<div class="submenu-controls">
|
||||
|
||||
<div ng-repeat="variable in ctrl.variables" ng-hide="variable.hide === 2" class="submenu-item gf-form-inline">
|
||||
<div class="gf-form">
|
||||
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.dashboard.annotations.list.length > 0">
|
||||
<div ng-repeat="annotation in ctrl.dashboard.annotations.list" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
|
||||
<div ng-repeat="annotation in ctrl.dashboard.annotations.list" ng-hide="annotation.hide" class="submenu-item" ng-class="{'annotation-disabled': !annotation.enable}">
|
||||
<gf-form-switch class="gf-form" label="{{annotation.name}}" checked="annotation.enable" on-change="ctrl.annotationStateChanged()"></gf-form-switch>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -13,7 +13,7 @@
|
||||
<dash-row class="dash-row" ng-repeat="row in dashboard.rows" row="row" dashboard="dashboard">
|
||||
</dash-row>
|
||||
|
||||
<div ng-show='dashboardMeta.canEdit' class="add-row-panel-hint">
|
||||
<div ng-show='dashboard.meta.canEdit && !dashboard.meta.fullscreen' class="add-row-panel-hint">
|
||||
<div class="span12" style="text-align:left;">
|
||||
<span style="margin-left: 12px;" ng-click="addRowDefault()" class="pointer btn btn-inverse btn-small">
|
||||
<span><i class="fa fa-plus"></i> ADD ROW</span>
|
||||
|
@ -46,7 +46,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label width-13">Custom Metrics namespace</label>
|
||||
<label class="gf-form-label width-13">Custom Metrics</label>
|
||||
<input type="text" class="gf-form-input max-width-18" ng-model='ctrl.current.jsonData.customMetricsNamespaces' placeholder="Namespace1,Namespace2"></input>
|
||||
<info-popover mode="right-absolute">
|
||||
Namespaces of Custom Metrics
|
||||
|
@ -3,9 +3,11 @@
|
||||
<span class="gf-form-label width-14">Index name</span>
|
||||
<input type="text" class="gf-form-input max-width-20" ng-model='ctrl.annotation.index' placeholder="events-*"></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-14">Search query (lucene) <tip>Use [[filterName]] in query to replace part of the query with a filter value</tip></span>
|
||||
<input type="text" class="gf-form-input max-width-20" ng-model='ctrl.annotation.query' placeholder="tags:deploy"></input>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query'
|
||||
placeholder="Elasticsearch lucene query"></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -33,4 +35,4 @@
|
||||
<input type="text" class="gf-form-input max-width-16" ng-model='ctrl.annotation.textField' placeholder=""></input>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,11 +1,10 @@
|
||||
|
||||
<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 class="gf-form-input" ng-model="ctrl.annotation.type" ng-options="f.value as f.text for f in [{text: 'Event', value: 'event'}, {text: 'Alert', value: 'alert'}]">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,13 @@
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-13">Graphite metrics query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder=""></input>
|
||||
<span class="gf-form-label width-12">Graphite query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.target' placeholder="Example: statsd.application.counters.*.count"></input>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Or</h5>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-13">Or Graphite events query</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.tags' placeholder=""></input>
|
||||
<span class="gf-form-label width-12">Graphite events tags</span>
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.tags' placeholder="Example: event_tag_name"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||
|
||||
<div class="gf-form" ng-show="ctrl.target.textEditor">
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.refresh()"></input>
|
||||
<input type="text" class="gf-form-input" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
|
||||
</div>
|
||||
|
||||
<div ng-hide="ctrl.target.textEditor">
|
||||
|
@ -28,7 +28,6 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
toggleEditorMode() {
|
||||
this.target.textEditor = !this.target.textEditor;
|
||||
this.parseTarget();
|
||||
}
|
||||
|
||||
@ -55,7 +54,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
try {
|
||||
this.parseTargeRecursive(astNode, null, 0);
|
||||
this.parseTargetRecursive(astNode, null, 0);
|
||||
} catch (err) {
|
||||
console.log('error parsing target:', err.message);
|
||||
this.error = err.message;
|
||||
@ -72,7 +71,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
func.params[index] = value;
|
||||
}
|
||||
|
||||
parseTargeRecursive(astNode, func, index) {
|
||||
parseTargetRecursive(astNode, func, index) {
|
||||
if (astNode === null) {
|
||||
return null;
|
||||
}
|
||||
@ -81,7 +80,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
case 'function':
|
||||
var innerFunc = gfunc.createFuncInstance(astNode.name, { withDefaultParams: false });
|
||||
_.each(astNode.params, (param, index) => {
|
||||
this.parseTargeRecursive(param, innerFunc, index);
|
||||
this.parseTargetRecursive(param, innerFunc, index);
|
||||
});
|
||||
|
||||
innerFunc.updateText();
|
||||
@ -209,30 +208,61 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
targetTextChanged() {
|
||||
this.parseTarget();
|
||||
this.panelCtrl.refresh();
|
||||
this.updateModelTarget();
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
updateModelTarget() {
|
||||
// render query
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length);
|
||||
this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath);
|
||||
if (!this.target.textEditor) {
|
||||
var metricPath = this.getSegmentPathUpTo(this.segments.length);
|
||||
this.target.target = _.reduce(this.functions, this.wrapFunction, metricPath);
|
||||
}
|
||||
|
||||
this.updateRenderedTarget(this.target);
|
||||
|
||||
// loop through other queries and update targetFull as needed
|
||||
for (const target of this.panelCtrl.panel.targets || []) {
|
||||
if (target.refId !== this.target.refId) {
|
||||
this.updateRenderedTarget(target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateRenderedTarget(target) {
|
||||
// render nested query
|
||||
var targetsByRefId = _.keyBy(this.panelCtrl.panel.targets, 'refId');
|
||||
|
||||
// no references to self
|
||||
delete targetsByRefId[target.refId];
|
||||
|
||||
var nestedSeriesRefRegex = /\#([A-Z])/g;
|
||||
var targetWithNestedQueries = this.target.target.replace(nestedSeriesRefRegex, (match, g1) => {
|
||||
var target = targetsByRefId[g1];
|
||||
if (!target) {
|
||||
return match;
|
||||
var targetWithNestedQueries = target.target;
|
||||
|
||||
// Keep interpolating until there are no query references
|
||||
// The reason for the loop is that the referenced query might contain another reference to another query
|
||||
while (targetWithNestedQueries.match(nestedSeriesRefRegex)) {
|
||||
var updated = targetWithNestedQueries.replace(nestedSeriesRefRegex, (match, g1) => {
|
||||
var t = targetsByRefId[g1];
|
||||
if (!t) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// no circular references
|
||||
delete targetsByRefId[g1];
|
||||
return t.target;
|
||||
});
|
||||
|
||||
if (updated === targetWithNestedQueries) {
|
||||
break;
|
||||
}
|
||||
|
||||
return target.targetFull || target.target;
|
||||
});
|
||||
targetWithNestedQueries = updated;
|
||||
}
|
||||
|
||||
delete this.target.targetFull;
|
||||
if (this.target.target !== targetWithNestedQueries) {
|
||||
this.target.targetFull = targetWithNestedQueries;
|
||||
delete target.targetFull;
|
||||
if (target.target !== targetWithNestedQueries) {
|
||||
target.targetFull = targetWithNestedQueries;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,4 +186,24 @@ describe('GraphiteQueryCtrl', function() {
|
||||
expect(ctx.ctrl.target.targetFull).to.be('scaleToSeconds(nested.query.count)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when updating target used in other query', function() {
|
||||
beforeEach(function() {
|
||||
ctx.ctrl.target.target = 'metrics.a.count';
|
||||
ctx.ctrl.target.refId = 'A';
|
||||
ctx.ctrl.datasource.metricFindQuery = sinon.stub().returns(ctx.$q.when([{expandable: false}]));
|
||||
ctx.ctrl.parseTarget();
|
||||
|
||||
ctx.ctrl.panelCtrl.panel.targets = [
|
||||
ctx.ctrl.target, {target: 'sumSeries(#A)', refId: 'B'}
|
||||
];
|
||||
|
||||
ctx.ctrl.updateModelTarget();
|
||||
});
|
||||
|
||||
it('targetFull of other query should update', function() {
|
||||
expect(ctx.ctrl.panel.targets[1].targetFull).to.be('sumSeries(metrics.a.count)');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@ -1,12 +1,11 @@
|
||||
|
||||
<h5 class="section-heading">Query</h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<input type="text" class="gf-form-input" ng-model='ctrl.annotation.query' placeholder="select text from events where $timeFilter"></input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h5 class="section-heading">Column mappings <tip>If your influxdb query returns more than one column you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
|
||||
<h5 class="section-heading">Field mappings <tip>If your influxdb query returns more than one field you need to specify the column names below. An annotation event is composed of a title, tags, and an additional text field.</tip></h5>
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
|
@ -17,9 +17,10 @@ import {tickStep} from 'app/core/utils/ticks';
|
||||
import {appEvents, coreModule} from 'app/core/core';
|
||||
import GraphTooltip from './graph_tooltip';
|
||||
import {ThresholdManager} from './threshold_manager';
|
||||
import {EventManager} from 'app/features/annotations/all';
|
||||
import {convertValuesToHistogram, getSeriesValues} from './histogram';
|
||||
|
||||
coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
coreModule.directive('grafanaGraph', function($rootScope, timeSrv, popoverSrv) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
template: '',
|
||||
@ -27,13 +28,14 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
var ctrl = scope.ctrl;
|
||||
var dashboard = ctrl.dashboard;
|
||||
var panel = ctrl.panel;
|
||||
var annotations = [];
|
||||
var data;
|
||||
var annotations;
|
||||
var plot;
|
||||
var sortedSeries;
|
||||
var legendSideLastValue = null;
|
||||
var rootScope = scope.$root;
|
||||
var panelWidth = 0;
|
||||
var eventManager = new EventManager(ctrl, elem, popoverSrv);
|
||||
var thresholdManager = new ThresholdManager(ctrl);
|
||||
var tooltip = new GraphTooltip(elem, dashboard, scope, function() {
|
||||
return sortedSeries;
|
||||
@ -54,7 +56,7 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
annotations = ctrl.annotations;
|
||||
annotations = ctrl.annotations || [];
|
||||
render_panel();
|
||||
});
|
||||
|
||||
@ -328,8 +330,8 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
}
|
||||
}
|
||||
|
||||
thresholdManager.addPlotOptions(options, panel);
|
||||
addAnnotations(options);
|
||||
thresholdManager.addFlotOptions(options, panel);
|
||||
eventManager.addFlotEvents(annotations, options);
|
||||
configureAxisOptions(data, options);
|
||||
|
||||
sortedSeries = _.sortBy(data, function(series) { return series.zindex; });
|
||||
@ -461,56 +463,6 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
};
|
||||
}
|
||||
|
||||
function addAnnotations(options) {
|
||||
if (!annotations || annotations.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var types = {};
|
||||
types['$__alerting'] = {
|
||||
color: 'rgba(237, 46, 24, 1)',
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
|
||||
types['$__ok'] = {
|
||||
color: 'rgba(11, 237, 50, 1)',
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
|
||||
types['$__no_data'] = {
|
||||
color: 'rgba(150, 150, 150, 1)',
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
|
||||
types['$__execution_error'] = ['$__no_data'];
|
||||
|
||||
for (var i = 0; i < annotations.length; i++) {
|
||||
var item = annotations[i];
|
||||
if (item.newState) {
|
||||
console.log(item.newState);
|
||||
item.eventType = '$__' + item.newState;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!types[item.source.name]) {
|
||||
types[item.source.name] = {
|
||||
color: item.source.iconColor,
|
||||
position: 'BOTTOM',
|
||||
markerSize: 5,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
options.events = {
|
||||
levels: _.keys(types).length + 1,
|
||||
data: annotations,
|
||||
types: types,
|
||||
};
|
||||
}
|
||||
|
||||
function configureAxisOptions(data, options) {
|
||||
var defaults = {
|
||||
position: 'left',
|
||||
@ -639,12 +591,30 @@ coreModule.directive('grafanaGraph', function($rootScope, timeSrv) {
|
||||
}
|
||||
|
||||
elem.bind("plotselected", function (event, ranges) {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
if (ranges.ctrlKey || ranges.metaKey) {
|
||||
// scope.$apply(() => {
|
||||
// eventManager.updateTime(ranges.xaxis);
|
||||
// });
|
||||
} else {
|
||||
scope.$apply(function() {
|
||||
timeSrv.setTime({
|
||||
from : moment.utc(ranges.xaxis.from),
|
||||
to : moment.utc(ranges.xaxis.to),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
elem.bind("plotclick", function (event, pos, item) {
|
||||
if (pos.ctrlKey || pos.metaKey || eventManager.event) {
|
||||
// Skip if range selected (added in "plotselected" event handler)
|
||||
let isRangeSelection = pos.x !== pos.x1;
|
||||
if (!isRangeSelection) {
|
||||
// scope.$apply(() => {
|
||||
// eventManager.updateTime({from: pos.x, to: null});
|
||||
// });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on('$destroy', function() {
|
||||
|
@ -48,6 +48,7 @@ function (angular, _, $) {
|
||||
element: el[0],
|
||||
position: 'bottom center',
|
||||
template: '<gf-color-picker></gf-color-picker>',
|
||||
openOn: 'hover',
|
||||
model: {
|
||||
series: series,
|
||||
toggleAxis: function() {
|
||||
|
@ -21,7 +21,7 @@ describe('ThresholdManager', function() {
|
||||
ctx.setup = function(thresholds) {
|
||||
ctx.panel.thresholds = thresholds;
|
||||
var manager = new ThresholdManager(ctx.panelCtrl);
|
||||
manager.addPlotOptions(ctx.options, ctx.panel);
|
||||
manager.addFlotOptions(ctx.options, ctx.panel);
|
||||
};
|
||||
|
||||
func(ctx);
|
||||
|
@ -158,7 +158,7 @@ export class ThresholdManager {
|
||||
this.needsCleanup = true;
|
||||
}
|
||||
|
||||
addPlotOptions(options, panel) {
|
||||
addFlotOptions(options, panel) {
|
||||
if (!panel.thresholds || panel.thresholds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
@ -251,8 +251,9 @@ $infoText: $blue-dark;
|
||||
$infoBackground: $blue-dark;
|
||||
|
||||
// popover
|
||||
$popover-bg: $dark-4;
|
||||
$popover-color: $text-color;
|
||||
$popover-bg: $panel-bg;
|
||||
$popover-color: $text-color;
|
||||
$popover-border-color: $gray-1;
|
||||
|
||||
$popover-help-bg: $btn-secondary-bg;
|
||||
$popover-help-color: $text-color;
|
||||
|
@ -278,6 +278,7 @@ $infoBorder: transparent;
|
||||
// popover
|
||||
$popover-bg: $gray-5;
|
||||
$popover-color: $text-color;
|
||||
$popover-border-color: $gray-3;
|
||||
|
||||
$popover-help-bg: $blue-dark;
|
||||
$popover-help-color: $gray-6;
|
||||
|
@ -1,11 +1,18 @@
|
||||
$popover-arrow-size: 0.7rem;
|
||||
$color: inherit;
|
||||
$backgroundColor: $btn-secondary-bg;
|
||||
$color: $text-color;
|
||||
$useDropShadow: false;
|
||||
$attachmentOffset: 0%;
|
||||
$easing: cubic-bezier(0, 0, 0.265, 1.00);
|
||||
|
||||
@include drop-theme("error", $errorBackground, $popover-color);
|
||||
@include drop-theme("popover", $popover-bg, $popover-color, $popover-border-color);
|
||||
@include drop-theme("help", $popover-help-bg, $popover-help-color);
|
||||
|
||||
@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
|
||||
.drop-element {
|
||||
z-index: 10000;
|
||||
position: absolute;
|
||||
@ -44,11 +51,8 @@ $easing: cubic-bezier(0, 0, 0.265, 1.00);
|
||||
}
|
||||
}
|
||||
|
||||
@include drop-theme("help", $popover-help-bg, $popover-help-color);
|
||||
@include drop-theme("error", $errorBackground, $popover-color);
|
||||
@include drop-theme("popover", $popover-bg, $popover-color);
|
||||
|
||||
@include drop-animation-scale("drop", "help", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
@include drop-animation-scale("drop", "error", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
@include drop-animation-scale("drop", "popover", $attachmentOffset: $attachmentOffset, $easing: $easing);
|
||||
|
||||
.drop-element.drop-popover--form {
|
||||
.drop-content {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,6 @@
|
||||
|
||||
.modal-content {
|
||||
padding: $spacer*2;
|
||||
min-height: $spacer*15;
|
||||
}
|
||||
|
||||
// Remove bottom margin if need be
|
||||
|
@ -1,4 +1,10 @@
|
||||
.submenu-controls {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
align-items: flex-start;
|
||||
|
||||
margin: 0 $panel-margin ($panel-margin*2) $panel-margin;
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
|
||||
@mixin drop-theme($themeName, $theme-bg, $theme-color) {
|
||||
@mixin drop-theme($themeName, $theme-bg, $theme-color, $border-color: $theme-bg) {
|
||||
.drop-element.drop-#{$themeName} {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@ -14,6 +14,11 @@
|
||||
font-size: $font-size-sm;
|
||||
word-wrap: break-word;
|
||||
max-width: 20rem;
|
||||
border: 1px solid $border-color;
|
||||
|
||||
@if $theme-bg != $border-color {
|
||||
box-shadow: 0 0 15px $border-color;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
@ -43,7 +48,7 @@
|
||||
top: 100%;
|
||||
left: 50%;
|
||||
margin-left: - $popover-arrow-size;
|
||||
border-top-color: $theme-bg;
|
||||
border-top-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -54,7 +59,7 @@
|
||||
bottom: 100%;
|
||||
left: 50%;
|
||||
margin-left: - $popover-arrow-size;
|
||||
border-bottom-color: $theme-bg;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -65,7 +70,7 @@
|
||||
left: 100%;
|
||||
top: 50%;
|
||||
margin-top: - $popover-arrow-size;
|
||||
border-left-color: $theme-bg;
|
||||
border-left-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -76,7 +81,7 @@
|
||||
right: 100%;
|
||||
top: 50%;
|
||||
margin-top: - $popover-arrow-size;
|
||||
border-right-color: $theme-bg;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -95,7 +100,7 @@
|
||||
&:before {
|
||||
bottom: 100%;
|
||||
left: $popover-arrow-size;
|
||||
border-bottom-color: $theme-bg;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +110,7 @@
|
||||
&:before {
|
||||
bottom: 100%;
|
||||
right: $popover-arrow-size;
|
||||
border-bottom-color: $theme-bg;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -115,7 +120,7 @@
|
||||
&:before {
|
||||
top: 100%;
|
||||
left: $popover-arrow-size;
|
||||
border-top-color: $theme-bg;
|
||||
border-top-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,7 +130,7 @@
|
||||
&:before {
|
||||
top: 100%;
|
||||
right: $popover-arrow-size;
|
||||
border-top-color: $theme-bg;
|
||||
border-top-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -136,7 +141,7 @@
|
||||
&:before {
|
||||
bottom: 100%;
|
||||
left: $popover-arrow-size;
|
||||
border-bottom-color: $theme-bg;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,7 +151,7 @@
|
||||
&:before {
|
||||
bottom: 100%;
|
||||
right: $popover-arrow-size;
|
||||
border-bottom-color: $theme-bg;
|
||||
border-bottom-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -156,7 +161,7 @@
|
||||
&:before {
|
||||
top: 100%;
|
||||
left: $popover-arrow-size;
|
||||
border-top-color: $theme-bg;
|
||||
border-top-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +171,7 @@
|
||||
&:before {
|
||||
top: 100%;
|
||||
right: $popover-arrow-size;
|
||||
border-top-color: $theme-bg;
|
||||
border-top-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +182,7 @@
|
||||
&:before {
|
||||
top: $popover-arrow-size;
|
||||
left: 100%;
|
||||
border-left-color: $theme-bg;
|
||||
border-left-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -187,7 +192,7 @@
|
||||
&:before {
|
||||
top: $popover-arrow-size;
|
||||
right: 100%;
|
||||
border-right-color: $theme-bg;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -197,7 +202,7 @@
|
||||
&:before {
|
||||
bottom: $popover-arrow-size;
|
||||
left: 100%;
|
||||
border-left-color: $theme-bg;
|
||||
border-left-color: $border-color;
|
||||
}
|
||||
}
|
||||
|
||||
@ -207,7 +212,7 @@
|
||||
&:before {
|
||||
bottom: $popover-arrow-size;
|
||||
right: 100%;
|
||||
border-right-color: $theme-bg;
|
||||
border-right-color: $border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
4
public/vendor/flot/jquery.flot.js
vendored
4
public/vendor/flot/jquery.flot.js
vendored
@ -2972,6 +2972,10 @@ Licensed under the MIT license.
|
||||
pos.pageX = event.pageX;
|
||||
pos.pageY = event.pageY;
|
||||
|
||||
// Add ctrlKey and metaKey to event
|
||||
pos.ctrlKey = event.ctrlKey;
|
||||
pos.metaKey = event.metaKey;
|
||||
|
||||
var item = findNearbyItem(canvasX, canvasY, seriesFilter);
|
||||
|
||||
if (item) {
|
||||
|
8
public/vendor/flot/jquery.flot.selection.js
vendored
8
public/vendor/flot/jquery.flot.selection.js
vendored
@ -145,7 +145,7 @@ The plugin allso adds the following methods to the plot object:
|
||||
updateSelection(e);
|
||||
|
||||
if (selectionIsSane())
|
||||
triggerSelectedEvent();
|
||||
triggerSelectedEvent(e);
|
||||
else {
|
||||
// this counts as a clear
|
||||
plot.getPlaceholder().trigger("plotunselected", [ ]);
|
||||
@ -180,9 +180,13 @@ The plugin allso adds the following methods to the plot object:
|
||||
return r;
|
||||
}
|
||||
|
||||
function triggerSelectedEvent() {
|
||||
function triggerSelectedEvent(event) {
|
||||
var r = getSelection();
|
||||
|
||||
// Add ctrlKey and metaKey to event
|
||||
r.ctrlKey = event.ctrlKey;
|
||||
r.metaKey = event.metaKey;
|
||||
|
||||
plot.getPlaceholder().trigger("plotselected", [ r ]);
|
||||
|
||||
// backwards-compat stuff, to be removed in future
|
||||
|
@ -1,7 +1,8 @@
|
||||
module.exports = function(config) {
|
||||
module.exports = function(config, grunt) {
|
||||
'use strict'
|
||||
return {
|
||||
tslint : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json",
|
||||
tslintfile : "node ./node_modules/tslint/lib/tslint-cli.js -c tslint.json --project ./tsconfig.json <%= tslint.source.files.src %>",
|
||||
tscompile: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics",
|
||||
tswatch: "node ./node_modules/typescript/lib/tsc.js -p tsconfig.json --diagnostics --watch",
|
||||
};
|
||||
|
11
tasks/options/tslint.js
Normal file
11
tasks/options/tslint.js
Normal file
@ -0,0 +1,11 @@
|
||||
module.exports = function(config, grunt) {
|
||||
'use strict'
|
||||
// dummy to avoid template compile error
|
||||
return {
|
||||
source: {
|
||||
files: {
|
||||
src: ""
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
@ -8,6 +8,10 @@ module.exports = function(config, grunt) {
|
||||
var lastTime;
|
||||
|
||||
grunt.registerTask('watch', function() {
|
||||
if (!grunt.option('skip-ts-compile')) {
|
||||
grunt.log.writeln('We recommoned starting with: grunt watch --force --skip-ts-compile')
|
||||
grunt.log.writeln('Then do incremental typescript builds with: grunt exec:tswatch')
|
||||
}
|
||||
|
||||
done = this.async();
|
||||
lastTime = new Date().getTime();
|
||||
@ -58,7 +62,15 @@ module.exports = function(config, grunt) {
|
||||
newPath = filepath.replace(/^public/, 'public_gen');
|
||||
grunt.log.writeln('Copying to ' + newPath);
|
||||
grunt.file.copy(filepath, newPath);
|
||||
grunt.task.run('exec:tslint');
|
||||
|
||||
if (grunt.option('skip-ts-compile')) {
|
||||
grunt.log.writeln('Skipping ts compile, run grunt exec:tswatch to start typescript watcher')
|
||||
} else {
|
||||
grunt.task.run('exec:tscompile');
|
||||
}
|
||||
|
||||
grunt.config('tslint.source.files.src', filepath);
|
||||
grunt.task.run('exec:tslintfile');
|
||||
}
|
||||
|
||||
done();
|
||||
|
0
tasks/tslint.js
Normal file
0
tasks/tslint.js
Normal file
Loading…
Reference in New Issue
Block a user