Merge remote-tracking branch 'grafana/master' into influx-db-query2

This commit is contained in:
ryan 2017-04-18 09:24:20 -07:00
commit 8c838cff08
51 changed files with 822 additions and 228 deletions

View File

@ -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"
}

View File

@ -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

View File

@ -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

View File

@ -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()

View File

@ -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))

View File

@ -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"`

View File

@ -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"`

View File

@ -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)

View File

@ -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",
}))
}

View File

@ -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
}

View File

@ -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>

View File

@ -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');

View File

@ -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);
};
}

View File

@ -1,7 +1,7 @@
define([
'./panellinks/module',
'./dashlinks/module',
'./annotations/annotations_srv',
'./annotations/all',
'./templating/all',
'./dashboard/all',
'./playlist/all',

View 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,
};

View File

@ -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;

View File

@ -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();
}
}

View File

@ -0,0 +1,10 @@
export class AnnotationEvent {
dashboardId: number;
panelId: number;
time: any;
timeEnd: any;
isRegion: boolean;
title: string;
text: string;
}

View 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);

View 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,
};
}
}

View File

@ -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&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label width&#45;7">Show in</span> -->
<!-- <div class="gf&#45;form&#45;select&#45;wrapper width&#45;12"> -->
<!-- <select class="gf&#45;form&#45;input" ng&#45;model="ctrl.currentAnnotation.showIn" ng&#45;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>

View 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>

View File

@ -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) {

View File

@ -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>

View File

@ -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);
});
});
});

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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;
}
}

View File

@ -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)');
});
});
});

View File

@ -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">

View File

@ -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() {

View File

@ -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() {

View File

@ -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);

View File

@ -158,7 +158,7 @@ export class ThresholdManager {
this.needsCleanup = true;
}
addPlotOptions(options, panel) {
addFlotOptions(options, panel) {
if (!panel.thresholds || panel.thresholds.length === 0) {
return;
}

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}

View File

@ -67,7 +67,6 @@
.modal-content {
padding: $spacer*2;
min-height: $spacer*15;
}
// Remove bottom margin if need be

View File

@ -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;
}

View File

@ -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;
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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
View File

@ -0,0 +1,11 @@
module.exports = function(config, grunt) {
'use strict'
// dummy to avoid template compile error
return {
source: {
files: {
src: ""
}
}
};
};

View File

@ -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
View File